Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
陈曦
sub2api
Commits
64236361
Commit
64236361
authored
Feb 12, 2026
by
yangjianbo
Browse files
Merge branch 'test' into dev
parents
2d6066f9
b6aaee01
Changes
92
Show whitespace changes
Inline
Side-by-side
backend/migrations/054_ops_system_logs.sql
0 → 100644
View file @
64236361
-- 054_ops_system_logs.sql
-- 统一日志索引表与清理审计表
CREATE
TABLE
IF
NOT
EXISTS
ops_system_logs
(
id
BIGSERIAL
PRIMARY
KEY
,
created_at
TIMESTAMPTZ
NOT
NULL
DEFAULT
NOW
(),
level
VARCHAR
(
16
)
NOT
NULL
,
component
VARCHAR
(
128
)
NOT
NULL
DEFAULT
''
,
message
TEXT
NOT
NULL
,
request_id
VARCHAR
(
128
),
client_request_id
VARCHAR
(
128
),
user_id
BIGINT
,
account_id
BIGINT
,
platform
VARCHAR
(
32
),
model
VARCHAR
(
128
),
extra
JSONB
NOT
NULL
DEFAULT
'{}'
::
jsonb
);
CREATE
INDEX
IF
NOT
EXISTS
idx_ops_system_logs_created_at_id
ON
ops_system_logs
(
created_at
DESC
,
id
DESC
);
CREATE
INDEX
IF
NOT
EXISTS
idx_ops_system_logs_level_created_at
ON
ops_system_logs
(
level
,
created_at
DESC
);
CREATE
INDEX
IF
NOT
EXISTS
idx_ops_system_logs_component_created_at
ON
ops_system_logs
(
component
,
created_at
DESC
);
CREATE
INDEX
IF
NOT
EXISTS
idx_ops_system_logs_request_id
ON
ops_system_logs
(
request_id
);
CREATE
INDEX
IF
NOT
EXISTS
idx_ops_system_logs_client_request_id
ON
ops_system_logs
(
client_request_id
);
CREATE
INDEX
IF
NOT
EXISTS
idx_ops_system_logs_user_id_created_at
ON
ops_system_logs
(
user_id
,
created_at
DESC
);
CREATE
INDEX
IF
NOT
EXISTS
idx_ops_system_logs_account_id_created_at
ON
ops_system_logs
(
account_id
,
created_at
DESC
);
CREATE
INDEX
IF
NOT
EXISTS
idx_ops_system_logs_platform_model_created_at
ON
ops_system_logs
(
platform
,
model
,
created_at
DESC
);
CREATE
INDEX
IF
NOT
EXISTS
idx_ops_system_logs_message_search
ON
ops_system_logs
USING
GIN
(
to_tsvector
(
'simple'
,
COALESCE
(
message
,
''
)));
CREATE
TABLE
IF
NOT
EXISTS
ops_system_log_cleanup_audits
(
id
BIGSERIAL
PRIMARY
KEY
,
created_at
TIMESTAMPTZ
NOT
NULL
DEFAULT
NOW
(),
operator_id
BIGINT
NOT
NULL
,
conditions
JSONB
NOT
NULL
DEFAULT
'{}'
::
jsonb
,
deleted_rows
BIGINT
NOT
NULL
DEFAULT
0
);
CREATE
INDEX
IF
NOT
EXISTS
idx_ops_system_log_cleanup_audits_created_at
ON
ops_system_log_cleanup_audits
(
created_at
DESC
,
id
DESC
);
deploy/.env.example
View file @
64236361
...
...
@@ -20,6 +20,52 @@ SERVER_PORT=8080
# Server mode: release or debug
SERVER_MODE=release
# -----------------------------------------------------------------------------
# Logging Configuration
# 日志配置
# -----------------------------------------------------------------------------
# 日志级别:debug/info/warn/error
LOG_LEVEL=info
# 日志格式:json/console
LOG_FORMAT=json
# 每条日志附带的 service 字段
LOG_SERVICE_NAME=sub2api
# 每条日志附带的 env 字段
LOG_ENV=production
# 是否输出调用方位置信息
LOG_CALLER=true
# 堆栈输出阈值:none/error/fatal
LOG_STACKTRACE_LEVEL=error
# 输出开关(建议容器内保持双输出)
# 是否输出到 stdout/stderr
LOG_OUTPUT_TO_STDOUT=true
# 是否输出到文件
LOG_OUTPUT_TO_FILE=true
# 日志文件路径(留空自动推导):
# - 设置 DATA_DIR:${DATA_DIR}/logs/sub2api.log
# - 未设置 DATA_DIR:/app/data/logs/sub2api.log
LOG_OUTPUT_FILE_PATH=
# 滚动配置
# 单文件最大体积(MB)
LOG_ROTATION_MAX_SIZE_MB=100
# 保留历史文件数量(0 表示不限制)
LOG_ROTATION_MAX_BACKUPS=10
# 历史日志保留天数(0 表示不限制)
LOG_ROTATION_MAX_AGE_DAYS=7
# 是否压缩历史日志
LOG_ROTATION_COMPRESS=true
# 滚动文件时间戳是否使用本地时间
LOG_ROTATION_LOCAL_TIME=true
# 采样配置(高频重复日志降噪)
LOG_SAMPLING_ENABLED=false
# 每秒前 N 条日志不采样
LOG_SAMPLING_INITIAL=100
# 之后每 N 条保留 1 条
LOG_SAMPLING_THEREAFTER=100
# Global max request body size in bytes (default: 100MB)
# 全局最大请求体大小(字节,默认 100MB)
# Applies to all requests, especially important for h2c first request memory protection
...
...
deploy/config.example.yaml
View file @
64236361
...
...
@@ -286,6 +286,70 @@ gateway:
# profile_2:
# name: "Custom Profile 2"
# =============================================================================
# Logging Configuration
# 日志配置
# =============================================================================
log
:
# Log level: debug/info/warn/error
# 日志级别:debug/info/warn/error
level
:
"
info"
# Log format: json/console
# 日志格式:json/console
format
:
"
console"
# Service name field written into each log line
# 每条日志都会附带 service 字段
service_name
:
"
sub2api"
# Environment field written into each log line
# 每条日志都会附带 env 字段
env
:
"
production"
# Include caller information
# 是否输出调用方位置信息
caller
:
true
# Stacktrace threshold: none/error/fatal
# 堆栈输出阈值:none/error/fatal
stacktrace_level
:
"
error"
output
:
# Keep stdout/stderr output for container log collection
# 保持标准输出用于容器日志采集
to_stdout
:
true
# Enable file output (default path auto-derived)
# 启用文件输出(默认路径自动推导)
to_file
:
true
# Empty means:
# - DATA_DIR set: {{DATA_DIR}}/logs/sub2api.log
# - otherwise: /app/data/logs/sub2api.log
# 留空时:
# - 设置 DATA_DIR:{{DATA_DIR}}/logs/sub2api.log
# - 否则:/app/data/logs/sub2api.log
file_path
:
"
"
rotation
:
# Max file size before rotation (MB)
# 单文件滚动阈值(MB)
max_size_mb
:
100
# Number of rotated files to keep (0 means unlimited)
# 保留历史文件数量(0 表示不限制)
max_backups
:
10
# Number of days to keep old log files (0 means unlimited)
# 历史日志保留天数(0 表示不限制)
max_age_days
:
7
# Compress rotated files
# 是否压缩历史日志
compress
:
true
# Use local time for timestamp in rotated filename
# 滚动文件名时间戳使用本地时区
local_time
:
true
sampling
:
# Enable zap sampler (reduce high-frequency repetitive logs)
# 启用 zap 采样(减少高频重复日志)
enabled
:
false
# Number of first entries per second to always log
# 每秒无采样保留的前 N 条日志
initial
:
100
# Thereafter keep 1 out of N entries per second
# 之后每 N 条保留 1 条
thereafter
:
100
# =============================================================================
# Sora Direct Client Configuration
# Sora 直连配置
...
...
deploy/docker-compose-aicodex.yml
View file @
64236361
...
...
@@ -162,6 +162,10 @@ services:
volumes
:
-
postgres_data:/var/lib/postgresql/data
environment
:
# postgres:18-alpine 默认 PGDATA=/var/lib/postgresql/18/docker(位于镜像声明的匿名卷 /var/lib/postgresql 内)。
# 若不显式设置 PGDATA,则即使挂载了 postgres_data 到 /var/lib/postgresql/data,数据也不会落盘到该命名卷,
# docker compose down/up 后会触发 initdb 重新初始化,导致用户/密码等数据丢失。
-
PGDATA=/var/lib/postgresql/data
-
POSTGRES_USER=${POSTGRES_USER:-sub2api}
-
POSTGRES_PASSWORD=${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required}
-
POSTGRES_DB=${POSTGRES_DB:-sub2api}
...
...
deploy/docker-compose-test.yml
View file @
64236361
...
...
@@ -142,6 +142,10 @@ services:
volumes
:
-
postgres_data:/var/lib/postgresql/data
environment
:
# postgres:18-alpine 默认 PGDATA=/var/lib/postgresql/18/docker(位于镜像声明的匿名卷 /var/lib/postgresql 内)。
# 若不显式设置 PGDATA,则即使挂载了 postgres_data 到 /var/lib/postgresql/data,数据也不会落盘到该命名卷,
# docker compose down/up 后会触发 initdb 重新初始化,导致用户/密码等数据丢失。
-
PGDATA=/var/lib/postgresql/data
-
POSTGRES_USER=${POSTGRES_USER:-sub2api}
-
POSTGRES_PASSWORD=${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required}
-
POSTGRES_DB=${POSTGRES_DB:-sub2api}
...
...
deploy/docker-compose.yml
View file @
64236361
...
...
@@ -166,6 +166,10 @@ services:
volumes
:
-
postgres_data:/var/lib/postgresql/data
environment
:
# postgres:18-alpine 默认 PGDATA=/var/lib/postgresql/18/docker(位于镜像声明的匿名卷 /var/lib/postgresql 内)。
# 若不显式设置 PGDATA,则即使挂载了 postgres_data 到 /var/lib/postgresql/data,数据也不会落盘到该命名卷,
# docker compose down/up 后会触发 initdb 重新初始化,导致用户/密码等数据丢失。
-
PGDATA=/var/lib/postgresql/data
-
POSTGRES_USER=${POSTGRES_USER:-sub2api}
-
POSTGRES_PASSWORD=${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required}
-
POSTGRES_DB=${POSTGRES_DB:-sub2api}
...
...
frontend/src/api/admin/ops.ts
View file @
64236361
...
...
@@ -850,6 +850,77 @@ export interface OpsAggregationSettings {
aggregation_enabled
:
boolean
}
export
interface
OpsRuntimeLogConfig
{
level
:
'
debug
'
|
'
info
'
|
'
warn
'
|
'
error
'
enable_sampling
:
boolean
sampling_initial
:
number
sampling_thereafter
:
number
caller
:
boolean
stacktrace_level
:
'
none
'
|
'
error
'
|
'
fatal
'
retention_days
:
number
source
?:
string
updated_at
?:
string
updated_by_user_id
?:
number
}
export
interface
OpsSystemLog
{
id
:
number
created_at
:
string
level
:
string
component
:
string
message
:
string
request_id
?:
string
client_request_id
?:
string
user_id
?:
number
|
null
account_id
?:
number
|
null
platform
?:
string
model
?:
string
extra
?:
Record
<
string
,
any
>
}
export
type
OpsSystemLogListResponse
=
PaginatedResponse
<
OpsSystemLog
>
export
interface
OpsSystemLogQuery
{
page
?:
number
page_size
?:
number
time_range
?:
'
5m
'
|
'
30m
'
|
'
1h
'
|
'
6h
'
|
'
24h
'
|
'
7d
'
|
'
30d
'
start_time
?:
string
end_time
?:
string
level
?:
string
component
?:
string
request_id
?:
string
client_request_id
?:
string
user_id
?:
number
|
null
account_id
?:
number
|
null
platform
?:
string
model
?:
string
q
?:
string
}
export
interface
OpsSystemLogCleanupRequest
{
start_time
?:
string
end_time
?:
string
level
?:
string
component
?:
string
request_id
?:
string
client_request_id
?:
string
user_id
?:
number
|
null
account_id
?:
number
|
null
platform
?:
string
model
?:
string
q
?:
string
}
export
interface
OpsSystemLogSinkHealth
{
queue_depth
:
number
queue_capacity
:
number
dropped_count
:
number
write_failed_count
:
number
written_count
:
number
avg_write_delay_ms
:
number
last_error
?:
string
}
export
interface
OpsErrorLog
{
id
:
number
created_at
:
string
...
...
@@ -1205,6 +1276,36 @@ export async function updateAlertRuntimeSettings(config: OpsAlertRuntimeSettings
return
data
}
export
async
function
getRuntimeLogConfig
():
Promise
<
OpsRuntimeLogConfig
>
{
const
{
data
}
=
await
apiClient
.
get
<
OpsRuntimeLogConfig
>
(
'
/admin/ops/runtime/logging
'
)
return
data
}
export
async
function
updateRuntimeLogConfig
(
config
:
OpsRuntimeLogConfig
):
Promise
<
OpsRuntimeLogConfig
>
{
const
{
data
}
=
await
apiClient
.
put
<
OpsRuntimeLogConfig
>
(
'
/admin/ops/runtime/logging
'
,
config
)
return
data
}
export
async
function
resetRuntimeLogConfig
():
Promise
<
OpsRuntimeLogConfig
>
{
const
{
data
}
=
await
apiClient
.
post
<
OpsRuntimeLogConfig
>
(
'
/admin/ops/runtime/logging/reset
'
)
return
data
}
export
async
function
listSystemLogs
(
params
:
OpsSystemLogQuery
):
Promise
<
OpsSystemLogListResponse
>
{
const
{
data
}
=
await
apiClient
.
get
<
OpsSystemLogListResponse
>
(
'
/admin/ops/system-logs
'
,
{
params
})
return
data
}
export
async
function
cleanupSystemLogs
(
payload
:
OpsSystemLogCleanupRequest
):
Promise
<
{
deleted
:
number
}
>
{
const
{
data
}
=
await
apiClient
.
post
<
{
deleted
:
number
}
>
(
'
/admin/ops/system-logs/cleanup
'
,
payload
)
return
data
}
export
async
function
getSystemLogSinkHealth
():
Promise
<
OpsSystemLogSinkHealth
>
{
const
{
data
}
=
await
apiClient
.
get
<
OpsSystemLogSinkHealth
>
(
'
/admin/ops/system-logs/health
'
)
return
data
}
// Advanced settings (DB-backed)
export
async
function
getAdvancedSettings
():
Promise
<
OpsAdvancedSettings
>
{
const
{
data
}
=
await
apiClient
.
get
<
OpsAdvancedSettings
>
(
'
/admin/ops/advanced-settings
'
)
...
...
@@ -1272,10 +1373,16 @@ export const opsAPI = {
updateEmailNotificationConfig
,
getAlertRuntimeSettings
,
updateAlertRuntimeSettings
,
getRuntimeLogConfig
,
updateRuntimeLogConfig
,
resetRuntimeLogConfig
,
getAdvancedSettings
,
updateAdvancedSettings
,
getMetricThresholds
,
updateMetricThresholds
updateMetricThresholds
,
listSystemLogs
,
cleanupSystemLogs
,
getSystemLogSinkHealth
}
export
default
opsAPI
frontend/src/views/admin/ops/OpsDashboard.vue
View file @
64236361
...
...
@@ -96,6 +96,13 @@
<!-- Alert Events -->
<OpsAlertEventsCard
v-if=
"opsEnabled && !(loading && !hasLoadedOnce)"
/>
<!-- System Logs -->
<OpsSystemLogTable
v-if=
"opsEnabled && !(loading && !hasLoadedOnce)"
:platform-filter=
"platform"
:refresh-token=
"dashboardRefreshToken"
/>
<!-- Settings Dialog (hidden in fullscreen mode) -->
<template
v-if=
"!isFullscreen"
>
<OpsSettingsDialog
:show=
"showSettingsDialog"
@
close=
"showSettingsDialog = false"
@
saved=
"onSettingsSaved"
/>
...
...
@@ -158,6 +165,7 @@ import OpsThroughputTrendChart from './components/OpsThroughputTrendChart.vue'
import
OpsSwitchRateTrendChart
from
'
./components/OpsSwitchRateTrendChart.vue
'
import
OpsAlertEventsCard
from
'
./components/OpsAlertEventsCard.vue
'
import
OpsOpenAITokenStatsCard
from
'
./components/OpsOpenAITokenStatsCard.vue
'
import
OpsSystemLogTable
from
'
./components/OpsSystemLogTable.vue
'
import
OpsRequestDetailsModal
,
{
type
OpsRequestDetailsPreset
}
from
'
./components/OpsRequestDetailsModal.vue
'
import
OpsSettingsDialog
from
'
./components/OpsSettingsDialog.vue
'
import
OpsAlertRulesCard
from
'
./components/OpsAlertRulesCard.vue
'
...
...
frontend/src/views/admin/ops/components/OpsOpenAITokenStatsCard.vue
View file @
64236361
...
...
@@ -130,8 +130,7 @@ watch(
next
.
viewMode
!==
prev
.
viewMode
||
next
.
pageSize
!==
prev
.
pageSize
||
next
.
platform
!==
prev
.
platform
||
next
.
groupId
!==
prev
.
groupId
||
next
.
refreshToken
!==
prev
.
refreshToken
next
.
groupId
!==
prev
.
groupId
if
(
next
.
viewMode
===
'
pagination
'
&&
filtersChanged
&&
next
.
page
!==
1
)
{
page
.
value
=
1
...
...
frontend/src/views/admin/ops/components/OpsSystemLogTable.vue
0 → 100644
View file @
64236361
<
script
setup
lang=
"ts"
>
import
{
computed
,
onMounted
,
reactive
,
ref
,
watch
}
from
'
vue
'
import
{
opsAPI
,
type
OpsRuntimeLogConfig
,
type
OpsSystemLog
,
type
OpsSystemLogSinkHealth
}
from
'
@/api/admin/ops
'
import
Pagination
from
'
@/components/common/Pagination.vue
'
import
{
useAppStore
}
from
'
@/stores
'
const
appStore
=
useAppStore
()
const
props
=
withDefaults
(
defineProps
<
{
platformFilter
?:
string
refreshToken
?:
number
}
>
(),
{
platformFilter
:
''
,
refreshToken
:
0
})
const
loading
=
ref
(
false
)
const
logs
=
ref
<
OpsSystemLog
[]
>
([])
const
total
=
ref
(
0
)
const
page
=
ref
(
1
)
const
pageSize
=
ref
(
20
)
const
health
=
ref
<
OpsSystemLogSinkHealth
>
({
queue_depth
:
0
,
queue_capacity
:
0
,
dropped_count
:
0
,
write_failed_count
:
0
,
written_count
:
0
,
avg_write_delay_ms
:
0
})
const
runtimeLoading
=
ref
(
false
)
const
runtimeSaving
=
ref
(
false
)
const
runtimeConfig
=
reactive
<
OpsRuntimeLogConfig
>
({
level
:
'
info
'
,
enable_sampling
:
false
,
sampling_initial
:
100
,
sampling_thereafter
:
100
,
caller
:
true
,
stacktrace_level
:
'
error
'
,
retention_days
:
30
})
const
filters
=
reactive
({
time_range
:
'
1h
'
as
'
5m
'
|
'
30m
'
|
'
1h
'
|
'
6h
'
|
'
24h
'
|
'
7d
'
|
'
30d
'
,
start_time
:
''
,
end_time
:
''
,
level
:
''
,
component
:
''
,
request_id
:
''
,
client_request_id
:
''
,
user_id
:
''
,
account_id
:
''
,
platform
:
''
,
model
:
''
,
q
:
''
})
const
levelBadgeClass
=
(
level
:
string
)
=>
{
const
v
=
String
(
level
||
''
).
toLowerCase
()
if
(
v
===
'
error
'
||
v
===
'
fatal
'
)
return
'
bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-300
'
if
(
v
===
'
warn
'
||
v
===
'
warning
'
)
return
'
bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300
'
if
(
v
===
'
debug
'
)
return
'
bg-slate-100 text-slate-700 dark:bg-slate-800 dark:text-slate-300
'
return
'
bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300
'
}
const
formatTime
=
(
value
:
string
)
=>
{
if
(
!
value
)
return
'
-
'
const
d
=
new
Date
(
value
)
if
(
Number
.
isNaN
(
d
.
getTime
()))
return
value
return
d
.
toLocaleString
()
}
const
getExtraString
=
(
extra
:
Record
<
string
,
any
>
|
undefined
,
key
:
string
)
=>
{
if
(
!
extra
)
return
''
const
v
=
extra
[
key
]
if
(
v
==
null
)
return
''
if
(
typeof
v
===
'
string
'
)
return
v
.
trim
()
if
(
typeof
v
===
'
number
'
||
typeof
v
===
'
boolean
'
)
return
String
(
v
)
return
''
}
const
formatSystemLogDetail
=
(
row
:
OpsSystemLog
)
=>
{
const
parts
:
string
[]
=
[]
const
msg
=
String
(
row
.
message
||
''
).
trim
()
if
(
msg
)
parts
.
push
(
msg
)
const
extra
=
row
.
extra
||
{}
const
statusCode
=
getExtraString
(
extra
,
'
status_code
'
)
const
latencyMs
=
getExtraString
(
extra
,
'
latency_ms
'
)
const
method
=
getExtraString
(
extra
,
'
method
'
)
const
path
=
getExtraString
(
extra
,
'
path
'
)
const
clientIP
=
getExtraString
(
extra
,
'
client_ip
'
)
const
protocol
=
getExtraString
(
extra
,
'
protocol
'
)
const
accessParts
:
string
[]
=
[]
if
(
statusCode
)
accessParts
.
push
(
`status=
${
statusCode
}
`
)
if
(
latencyMs
)
accessParts
.
push
(
`latency_ms=
${
latencyMs
}
`
)
if
(
method
)
accessParts
.
push
(
`method=
${
method
}
`
)
if
(
path
)
accessParts
.
push
(
`path=
${
path
}
`
)
if
(
clientIP
)
accessParts
.
push
(
`ip=
${
clientIP
}
`
)
if
(
protocol
)
accessParts
.
push
(
`proto=
${
protocol
}
`
)
if
(
accessParts
.
length
>
0
)
parts
.
push
(
accessParts
.
join
(
'
'
))
const
corrParts
:
string
[]
=
[]
if
(
row
.
request_id
)
corrParts
.
push
(
`req=
${
row
.
request_id
}
`
)
if
(
row
.
client_request_id
)
corrParts
.
push
(
`client_req=
${
row
.
client_request_id
}
`
)
if
(
row
.
user_id
!=
null
)
corrParts
.
push
(
`user=
${
row
.
user_id
}
`
)
if
(
row
.
account_id
!=
null
)
corrParts
.
push
(
`acc=
${
row
.
account_id
}
`
)
if
(
row
.
platform
)
corrParts
.
push
(
`platform=
${
row
.
platform
}
`
)
if
(
row
.
model
)
corrParts
.
push
(
`model=
${
row
.
model
}
`
)
if
(
corrParts
.
length
>
0
)
parts
.
push
(
corrParts
.
join
(
'
'
))
const
errors
=
getExtraString
(
extra
,
'
errors
'
)
if
(
errors
)
parts
.
push
(
`errors=
${
errors
}
`
)
const
err
=
getExtraString
(
extra
,
'
err
'
)
||
getExtraString
(
extra
,
'
error
'
)
if
(
err
)
parts
.
push
(
`error=
${
err
}
`
)
// 用空格拼接,交给 CSS 自动换行,尽量“填满再换行”。
return
parts
.
join
(
'
'
)
}
const
toRFC3339
=
(
value
:
string
)
=>
{
if
(
!
value
)
return
undefined
const
d
=
new
Date
(
value
)
if
(
Number
.
isNaN
(
d
.
getTime
()))
return
undefined
return
d
.
toISOString
()
}
const
buildQuery
=
()
=>
{
const
query
:
Record
<
string
,
any
>
=
{
page
:
page
.
value
,
page_size
:
pageSize
.
value
,
time_range
:
filters
.
time_range
}
if
(
filters
.
time_range
===
'
30d
'
)
{
query
.
time_range
=
'
30d
'
}
if
(
filters
.
start_time
)
query
.
start_time
=
toRFC3339
(
filters
.
start_time
)
if
(
filters
.
end_time
)
query
.
end_time
=
toRFC3339
(
filters
.
end_time
)
if
(
filters
.
level
.
trim
())
query
.
level
=
filters
.
level
.
trim
()
if
(
filters
.
component
.
trim
())
query
.
component
=
filters
.
component
.
trim
()
if
(
filters
.
request_id
.
trim
())
query
.
request_id
=
filters
.
request_id
.
trim
()
if
(
filters
.
client_request_id
.
trim
())
query
.
client_request_id
=
filters
.
client_request_id
.
trim
()
if
(
filters
.
user_id
.
trim
())
{
const
v
=
Number
.
parseInt
(
filters
.
user_id
.
trim
(),
10
)
if
(
Number
.
isFinite
(
v
)
&&
v
>
0
)
query
.
user_id
=
v
}
if
(
filters
.
account_id
.
trim
())
{
const
v
=
Number
.
parseInt
(
filters
.
account_id
.
trim
(),
10
)
if
(
Number
.
isFinite
(
v
)
&&
v
>
0
)
query
.
account_id
=
v
}
if
(
filters
.
platform
.
trim
())
query
.
platform
=
filters
.
platform
.
trim
()
if
(
filters
.
model
.
trim
())
query
.
model
=
filters
.
model
.
trim
()
if
(
filters
.
q
.
trim
())
query
.
q
=
filters
.
q
.
trim
()
return
query
}
const
fetchLogs
=
async
()
=>
{
loading
.
value
=
true
try
{
const
res
=
await
opsAPI
.
listSystemLogs
(
buildQuery
())
logs
.
value
=
res
.
items
||
[]
total
.
value
=
res
.
total
||
0
}
catch
(
err
:
any
)
{
console
.
error
(
'
[OpsSystemLogTable] Failed to fetch logs
'
,
err
)
appStore
.
showError
(
err
?.
response
?.
data
?.
detail
||
'
系统日志加载失败
'
)
}
finally
{
loading
.
value
=
false
}
}
const
fetchHealth
=
async
()
=>
{
try
{
health
.
value
=
await
opsAPI
.
getSystemLogSinkHealth
()
}
catch
{
// 忽略健康数据读取失败,不影响主流程。
}
}
const
loadRuntimeConfig
=
async
()
=>
{
runtimeLoading
.
value
=
true
try
{
const
cfg
=
await
opsAPI
.
getRuntimeLogConfig
()
runtimeConfig
.
level
=
cfg
.
level
runtimeConfig
.
enable_sampling
=
cfg
.
enable_sampling
runtimeConfig
.
sampling_initial
=
cfg
.
sampling_initial
runtimeConfig
.
sampling_thereafter
=
cfg
.
sampling_thereafter
runtimeConfig
.
caller
=
cfg
.
caller
runtimeConfig
.
stacktrace_level
=
cfg
.
stacktrace_level
runtimeConfig
.
retention_days
=
cfg
.
retention_days
}
catch
(
err
:
any
)
{
console
.
error
(
'
[OpsSystemLogTable] Failed to load runtime log config
'
,
err
)
}
finally
{
runtimeLoading
.
value
=
false
}
}
const
saveRuntimeConfig
=
async
()
=>
{
runtimeSaving
.
value
=
true
try
{
const
saved
=
await
opsAPI
.
updateRuntimeLogConfig
({
...
runtimeConfig
})
runtimeConfig
.
level
=
saved
.
level
runtimeConfig
.
enable_sampling
=
saved
.
enable_sampling
runtimeConfig
.
sampling_initial
=
saved
.
sampling_initial
runtimeConfig
.
sampling_thereafter
=
saved
.
sampling_thereafter
runtimeConfig
.
caller
=
saved
.
caller
runtimeConfig
.
stacktrace_level
=
saved
.
stacktrace_level
runtimeConfig
.
retention_days
=
saved
.
retention_days
appStore
.
showSuccess
(
'
日志运行时配置已生效
'
)
}
catch
(
err
:
any
)
{
console
.
error
(
'
[OpsSystemLogTable] Failed to save runtime log config
'
,
err
)
appStore
.
showError
(
err
?.
response
?.
data
?.
detail
||
'
保存日志配置失败
'
)
}
finally
{
runtimeSaving
.
value
=
false
}
}
const
resetRuntimeConfig
=
async
()
=>
{
const
ok
=
window
.
confirm
(
'
确认回滚为启动配置(env/yaml)并立即生效?
'
)
if
(
!
ok
)
return
runtimeSaving
.
value
=
true
try
{
const
saved
=
await
opsAPI
.
resetRuntimeLogConfig
()
runtimeConfig
.
level
=
saved
.
level
runtimeConfig
.
enable_sampling
=
saved
.
enable_sampling
runtimeConfig
.
sampling_initial
=
saved
.
sampling_initial
runtimeConfig
.
sampling_thereafter
=
saved
.
sampling_thereafter
runtimeConfig
.
caller
=
saved
.
caller
runtimeConfig
.
stacktrace_level
=
saved
.
stacktrace_level
runtimeConfig
.
retention_days
=
saved
.
retention_days
appStore
.
showSuccess
(
'
已回滚到启动日志配置
'
)
await
fetchHealth
()
}
catch
(
err
:
any
)
{
console
.
error
(
'
[OpsSystemLogTable] Failed to reset runtime log config
'
,
err
)
appStore
.
showError
(
err
?.
response
?.
data
?.
detail
||
'
回滚日志配置失败
'
)
}
finally
{
runtimeSaving
.
value
=
false
}
}
const
cleanupCurrentFilter
=
async
()
=>
{
const
ok
=
window
.
confirm
(
'
确认按当前筛选条件清理系统日志?该操作不可撤销。
'
)
if
(
!
ok
)
return
try
{
const
payload
=
{
start_time
:
toRFC3339
(
filters
.
start_time
),
end_time
:
toRFC3339
(
filters
.
end_time
),
level
:
filters
.
level
.
trim
()
||
undefined
,
component
:
filters
.
component
.
trim
()
||
undefined
,
request_id
:
filters
.
request_id
.
trim
()
||
undefined
,
client_request_id
:
filters
.
client_request_id
.
trim
()
||
undefined
,
user_id
:
filters
.
user_id
.
trim
()
?
Number
.
parseInt
(
filters
.
user_id
.
trim
(),
10
)
:
undefined
,
account_id
:
filters
.
account_id
.
trim
()
?
Number
.
parseInt
(
filters
.
account_id
.
trim
(),
10
)
:
undefined
,
platform
:
filters
.
platform
.
trim
()
||
undefined
,
model
:
filters
.
model
.
trim
()
||
undefined
,
q
:
filters
.
q
.
trim
()
||
undefined
}
const
res
=
await
opsAPI
.
cleanupSystemLogs
(
payload
)
appStore
.
showSuccess
(
`清理完成,删除
${
res
.
deleted
||
0
}
条日志`
)
page
.
value
=
1
await
Promise
.
all
([
fetchLogs
(),
fetchHealth
()])
}
catch
(
err
:
any
)
{
console
.
error
(
'
[OpsSystemLogTable] Failed to cleanup logs
'
,
err
)
appStore
.
showError
(
err
?.
response
?.
data
?.
detail
||
'
清理系统日志失败
'
)
}
}
const
resetFilters
=
()
=>
{
filters
.
time_range
=
'
1h
'
filters
.
start_time
=
''
filters
.
end_time
=
''
filters
.
level
=
''
filters
.
component
=
''
filters
.
request_id
=
''
filters
.
client_request_id
=
''
filters
.
user_id
=
''
filters
.
account_id
=
''
filters
.
platform
=
props
.
platformFilter
||
''
filters
.
model
=
''
filters
.
q
=
''
page
.
value
=
1
fetchLogs
()
}
watch
(()
=>
props
.
platformFilter
,
(
v
)
=>
{
if
(
v
&&
!
filters
.
platform
)
{
filters
.
platform
=
v
page
.
value
=
1
fetchLogs
()
}
})
watch
(()
=>
props
.
refreshToken
,
()
=>
{
fetchLogs
()
fetchHealth
()
})
const
onPageChange
=
(
next
:
number
)
=>
{
page
.
value
=
next
fetchLogs
()
}
const
onPageSizeChange
=
(
next
:
number
)
=>
{
pageSize
.
value
=
next
page
.
value
=
1
fetchLogs
()
}
const
applyFilters
=
()
=>
{
page
.
value
=
1
fetchLogs
()
}
const
hasData
=
computed
(()
=>
logs
.
value
.
length
>
0
)
onMounted
(
async
()
=>
{
if
(
props
.
platformFilter
)
{
filters
.
platform
=
props
.
platformFilter
}
await
Promise
.
all
([
fetchLogs
(),
fetchHealth
(),
loadRuntimeConfig
()])
})
</
script
>
<
template
>
<section
class=
"rounded-2xl border border-gray-200 bg-white p-4 shadow-sm dark:border-dark-700 dark:bg-dark-900/60"
>
<div
class=
"mb-4 flex flex-wrap items-center justify-between gap-3"
>
<div>
<h3
class=
"text-sm font-bold text-gray-900 dark:text-white"
>
系统日志
</h3>
<p
class=
"mt-1 text-xs text-gray-500 dark:text-gray-400"
>
默认按最新时间倒序,支持筛选搜索与按条件清理。
</p>
</div>
<div
class=
"flex flex-wrap items-center gap-2 text-xs"
>
<span
class=
"rounded-md bg-gray-100 px-2 py-1 text-gray-700 dark:bg-dark-700 dark:text-gray-200"
>
队列
{{
health
.
queue_depth
}}
/
{{
health
.
queue_capacity
}}
</span>
<span
class=
"rounded-md bg-gray-100 px-2 py-1 text-gray-700 dark:bg-dark-700 dark:text-gray-200"
>
写入
{{
health
.
written_count
}}
</span>
<span
class=
"rounded-md bg-amber-100 px-2 py-1 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300"
>
丢弃
{{
health
.
dropped_count
}}
</span>
<span
class=
"rounded-md bg-red-100 px-2 py-1 text-red-700 dark:bg-red-900/30 dark:text-red-300"
>
失败
{{
health
.
write_failed_count
}}
</span>
</div>
</div>
<div
class=
"mb-4 rounded-xl border border-gray-200 bg-gray-50 p-3 dark:border-dark-700 dark:bg-dark-800/70"
>
<div
class=
"mb-2 flex items-center justify-between"
>
<div
class=
"text-xs font-semibold text-gray-700 dark:text-gray-200"
>
运行时日志配置(实时生效)
</div>
<span
v-if=
"runtimeLoading"
class=
"text-xs text-gray-500"
>
加载中...
</span>
</div>
<div
class=
"grid grid-cols-1 gap-3 md:grid-cols-6"
>
<label
class=
"text-xs text-gray-600 dark:text-gray-300"
>
级别
<select
v-model=
"runtimeConfig.level"
class=
"input mt-1"
>
<option
value=
"debug"
>
debug
</option>
<option
value=
"info"
>
info
</option>
<option
value=
"warn"
>
warn
</option>
<option
value=
"error"
>
error
</option>
</select>
</label>
<label
class=
"text-xs text-gray-600 dark:text-gray-300"
>
堆栈阈值
<select
v-model=
"runtimeConfig.stacktrace_level"
class=
"input mt-1"
>
<option
value=
"none"
>
none
</option>
<option
value=
"error"
>
error
</option>
<option
value=
"fatal"
>
fatal
</option>
</select>
</label>
<label
class=
"text-xs text-gray-600 dark:text-gray-300"
>
采样初始
<input
v-model.number=
"runtimeConfig.sampling_initial"
type=
"number"
min=
"1"
class=
"input mt-1"
/>
</label>
<label
class=
"text-xs text-gray-600 dark:text-gray-300"
>
采样后续
<input
v-model.number=
"runtimeConfig.sampling_thereafter"
type=
"number"
min=
"1"
class=
"input mt-1"
/>
</label>
<label
class=
"text-xs text-gray-600 dark:text-gray-300"
>
保留天数
<input
v-model.number=
"runtimeConfig.retention_days"
type=
"number"
min=
"1"
max=
"3650"
class=
"input mt-1"
/>
</label>
<div
class=
"flex items-end gap-2"
>
<label
class=
"inline-flex items-center gap-2 text-xs text-gray-600 dark:text-gray-300"
>
<input
v-model=
"runtimeConfig.caller"
type=
"checkbox"
/>
caller
</label>
<label
class=
"inline-flex items-center gap-2 text-xs text-gray-600 dark:text-gray-300"
>
<input
v-model=
"runtimeConfig.enable_sampling"
type=
"checkbox"
/>
sampling
</label>
<button
type=
"button"
class=
"btn btn-primary btn-sm"
:disabled=
"runtimeSaving"
@
click=
"saveRuntimeConfig"
>
{{
runtimeSaving
?
'
保存中...
'
:
'
保存并生效
'
}}
</button>
<button
type=
"button"
class=
"btn btn-secondary btn-sm"
:disabled=
"runtimeSaving"
@
click=
"resetRuntimeConfig"
>
回滚默认值
</button>
</div>
</div>
<p
v-if=
"health.last_error"
class=
"mt-2 text-xs text-red-600 dark:text-red-400"
>
最近写入错误:
{{
health
.
last_error
}}
</p>
</div>
<div
class=
"mb-4 grid grid-cols-1 gap-3 md:grid-cols-5"
>
<label
class=
"text-xs text-gray-600 dark:text-gray-300"
>
时间范围
<select
v-model=
"filters.time_range"
class=
"input mt-1"
>
<option
value=
"5m"
>
5m
</option>
<option
value=
"30m"
>
30m
</option>
<option
value=
"1h"
>
1h
</option>
<option
value=
"6h"
>
6h
</option>
<option
value=
"24h"
>
24h
</option>
<option
value=
"7d"
>
7d
</option>
<option
value=
"30d"
>
30d
</option>
</select>
</label>
<label
class=
"text-xs text-gray-600 dark:text-gray-300"
>
开始时间(可选)
<input
v-model=
"filters.start_time"
type=
"datetime-local"
class=
"input mt-1"
/>
</label>
<label
class=
"text-xs text-gray-600 dark:text-gray-300"
>
结束时间(可选)
<input
v-model=
"filters.end_time"
type=
"datetime-local"
class=
"input mt-1"
/>
</label>
<label
class=
"text-xs text-gray-600 dark:text-gray-300"
>
级别
<select
v-model=
"filters.level"
class=
"input mt-1"
>
<option
value=
""
>
全部
</option>
<option
value=
"debug"
>
debug
</option>
<option
value=
"info"
>
info
</option>
<option
value=
"warn"
>
warn
</option>
<option
value=
"error"
>
error
</option>
</select>
</label>
<label
class=
"text-xs text-gray-600 dark:text-gray-300"
>
组件
<input
v-model=
"filters.component"
type=
"text"
class=
"input mt-1"
placeholder=
"如 http.access"
/>
</label>
<label
class=
"text-xs text-gray-600 dark:text-gray-300"
>
request_id
<input
v-model=
"filters.request_id"
type=
"text"
class=
"input mt-1"
/>
</label>
<label
class=
"text-xs text-gray-600 dark:text-gray-300"
>
client_request_id
<input
v-model=
"filters.client_request_id"
type=
"text"
class=
"input mt-1"
/>
</label>
<label
class=
"text-xs text-gray-600 dark:text-gray-300"
>
user_id
<input
v-model=
"filters.user_id"
type=
"text"
class=
"input mt-1"
/>
</label>
<label
class=
"text-xs text-gray-600 dark:text-gray-300"
>
account_id
<input
v-model=
"filters.account_id"
type=
"text"
class=
"input mt-1"
/>
</label>
<label
class=
"text-xs text-gray-600 dark:text-gray-300"
>
平台
<input
v-model=
"filters.platform"
type=
"text"
class=
"input mt-1"
/>
</label>
<label
class=
"text-xs text-gray-600 dark:text-gray-300"
>
模型
<input
v-model=
"filters.model"
type=
"text"
class=
"input mt-1"
/>
</label>
<label
class=
"text-xs text-gray-600 dark:text-gray-300"
>
关键词
<input
v-model=
"filters.q"
type=
"text"
class=
"input mt-1"
placeholder=
"消息/request_id"
/>
</label>
</div>
<div
class=
"mb-3 flex flex-wrap gap-2"
>
<button
type=
"button"
class=
"btn btn-primary btn-sm"
@
click=
"applyFilters"
>
查询
</button>
<button
type=
"button"
class=
"btn btn-secondary btn-sm"
@
click=
"resetFilters"
>
重置
</button>
<button
type=
"button"
class=
"btn btn-danger btn-sm"
@
click=
"cleanupCurrentFilter"
>
按当前筛选清理
</button>
<button
type=
"button"
class=
"btn btn-secondary btn-sm"
@
click=
"fetchHealth"
>
刷新健康指标
</button>
</div>
<div
class=
"overflow-hidden rounded-xl border border-gray-200 dark:border-dark-700"
>
<div
v-if=
"loading"
class=
"px-4 py-8 text-center text-sm text-gray-500"
>
加载中...
</div>
<div
v-else-if=
"!hasData"
class=
"px-4 py-8 text-center text-sm text-gray-500"
>
暂无系统日志
</div>
<div
v-else
class=
"overflow-auto"
>
<table
class=
"min-w-full table-fixed divide-y divide-gray-200 dark:divide-dark-700"
>
<thead
class=
"bg-gray-50 dark:bg-dark-900"
>
<tr>
<th
class=
"w-[170px] px-3 py-2 text-left text-[11px] font-semibold text-gray-500"
>
时间
</th>
<th
class=
"w-[80px] px-3 py-2 text-left text-[11px] font-semibold text-gray-500"
>
级别
</th>
<th
class=
"px-3 py-2 text-left text-[11px] font-semibold text-gray-500"
>
日志详细信息
</th>
</tr>
</thead>
<tbody
class=
"divide-y divide-gray-100 dark:divide-dark-800"
>
<tr
v-for=
"row in logs"
:key=
"row.id"
class=
"align-top"
>
<td
class=
"px-3 py-2 text-xs text-gray-700 dark:text-gray-300"
>
{{
formatTime
(
row
.
created_at
)
}}
</td>
<td
class=
"px-3 py-2 text-xs"
>
<span
class=
"inline-flex rounded-full px-2 py-0.5 font-semibold"
:class=
"levelBadgeClass(row.level)"
>
{{
row
.
level
}}
</span>
</td>
<td
class=
"px-3 py-2 text-xs text-gray-700 dark:text-gray-300 whitespace-normal break-all"
>
{{
formatSystemLogDetail
(
row
)
}}
</td>
</tr>
</tbody>
</table>
</div>
<Pagination
:total=
"total"
:page=
"page"
:page-size=
"pageSize"
:page-size-options=
"[10, 20, 50, 100, 200]"
@
update:page=
"onPageChange"
@
update:page-size=
"onPageSizeChange"
/>
</div>
</section>
</
template
>
frontend/src/views/admin/ops/types.ts
View file @
64236361
...
...
@@ -17,5 +17,8 @@ export type {
OpsMetricThresholds
,
OpsAdvancedSettings
,
OpsDataRetentionSettings
,
OpsAggregationSettings
OpsAggregationSettings
,
OpsRuntimeLogConfig
,
OpsSystemLog
,
OpsSystemLogSinkHealth
}
from
'
@/api/admin/ops
'
logging_audit_20260212.md
0 → 100644
View file @
64236361
# 日志专项审计与整理(2026-02-12)
## 1. 全量扫描结论
-
扫描范围:
`backend/`
+
`frontend/`
-
日志相关调用总量(粗统计):约
`4100`
处
-
后端标准库日志(
`log.Printf/Println/Fatal*`
):
`808`
处(本轮整改后剩余
`269`
处)
-
前端
`console.*`
:
`180`
处
关键观察:
1.
后端大量业务日志仍走标准库
`log`
,在当前初始化流程里会被统一当作
`INFO`
输出,导致“错误/告警等级失真”。
2.
网关关键链路(OpenAI/Gemini/Sora)原有日志以格式化字符串为主,上下文字段(
`request_id/user_id/group_id/model/account_id`
)不完整,排障时需要人工拼接上下文。
3.
Token 刷新服务同时混用
`log`
与
`slog`
,同类事件日志风格不一致,不利于检索与聚合。
4.
前端
`console.error/warn`
使用量高,缺少统一封装,生产环境噪音和敏感信息泄漏风险较高。
## 2. 本次已落地整改
### 2.1 全局层(后端标准库日志分级修复)
-
修改:
`backend/internal/pkg/logger/logger.go`
-
结果:
1.
替换原
`zap.RedirectStdLogAt(..., INFO)`
机制,改为自定义
`stdlog bridge`
。
2.
对标准库日志自动推断等级(
`DEBUG/WARN/ERROR/INFO`
),并打上
`legacy_stdlog=true`
标记。
3.
规范化消息文本(去换行、压缩空白),提升可读性和检索稳定性。
4.
调整初始化顺序:先桥接
`slog`
,再桥接
`stdlog`
,避免
`slog.SetDefault`
覆盖标准库桥接。
5.
新增
`logger.LegacyPrintf(component, format, ...args)`
,用于后端历史
`printf`
日志的平滑迁移,自动推断等级并打
`legacy_printf=true`
标记。
### 2.2 核心请求链路结构化改造
-
新增:
`backend/internal/handler/logging.go`
-
统一提供请求级 logger 获取入口,继承中间件注入的
`request_id`
上下文。
-
改造文件:
-
`backend/internal/handler/gateway_handler.go`
-
`backend/internal/handler/openai_gateway_handler.go`
-
`backend/internal/handler/gemini_v1beta_handler.go`
-
`backend/internal/handler/sora_gateway_handler.go`
-
`backend/internal/service/antigravity_gateway_service.go`
-
`backend/internal/service/gateway_service.go`
-
`backend/internal/service/gemini_oauth_service.go`
-
`backend/internal/service/auth_service.go`
-
`backend/internal/setup/setup.go`
-
`backend/internal/service/usage_cleanup_service.go`
-
`backend/internal/service/pricing_service.go`
-
`backend/internal/repository/account_repo.go`
-
`backend/internal/service/openai_gateway_service.go`
-
`backend/internal/service/scheduler_snapshot_service.go`
-
`backend/internal/service/gemini_messages_compat_service.go`
-
`backend/internal/service/dashboard_aggregation_service.go`
-
`backend/internal/service/billing_cache_service.go`
-
`backend/internal/repository/claude_oauth_service.go`
-
`backend/internal/service/admin_service.go`
-
`backend/internal/handler/admin/ops_ws_handler.go`
-
改造内容:
1.
把关键日志从字符串拼接改为结构化字段。
2.
统一带上
`component/user_id/api_key_id/group_id/model/account_id`
等字段。
3.
按语义拆分等级:
-
预期业务拒绝(如账单校验失败、队列满)使用
`Info`
-
降级路径/可恢复异常(如抢槽失败、粘性会话绑定失败)使用
`Warn`
-
真正故障(如转发失败、使用量记录失败)使用
`Error`
4.
新增请求完成日志(
`*.request_completed`
)用于链路闭环追踪。
5.
对高密度
`log.Printf`
完成批量迁移到
`logger.LegacyPrintf`
(本轮累计 511 处),并统一组件字段:
-
`component=service.antigravity_gateway`
-
`component=service.gateway`
-
`component=service.gemini_oauth`
-
`component=service.auth`
-
`component=setup`
-
`component=service.usage_cleanup`
-
`component=service.pricing`
-
`component=repository.account`
-
`component=service.openai_gateway`
-
`component=service.scheduler_snapshot`
-
`component=service.gemini_messages_compat`
-
`component=service.dashboard_aggregation`
-
`component=service.billing_cache`
-
`component=repository.claude_oauth`
-
`component=service.admin`
-
`component=handler.admin.ops_ws`
6.
OpenAI 透传断流相关两条关键告警统一回到新日志系统输出(
`service.openai_gateway`
),并通过兼容逻辑保证测试环境可捕获。
### 2.3 后台任务日志统一
-
改造:
`backend/internal/service/token_refresh_service.go`
-
结果:
1.
统一改为
`slog`
结构化输出。
2.
`retry/cycle/account`
等事件改为字段化日志,便于按账号和批次检索。
3.
对“无实际刷新活动”的周期日志降级到
`Debug`
,减少噪音。
### 2.4 测试保障
-
新增:
`backend/internal/pkg/logger/stdlog_bridge_test.go`
-
覆盖标准库日志等级推断、消息标准化、输出路由行为。
-
已验证:
-
`go test ./internal/pkg/logger ./internal/handler ./internal/service`
通过。
## 3. 仍需继续整改(建议下一批)
### 3.1 后端剩余 `std log` 高密度区域(优先级 P1)
建议优先处理以下文件(调用量高):
1.
`backend/internal/service/usage_cleanup_service.go`
(26)
2.
`backend/internal/service/pricing_service.go`
(26)
3.
`backend/internal/repository/account_repo.go`
(24)
4.
`backend/internal/service/openai_gateway_service.go`
(23)
5.
`backend/internal/service/scheduler_snapshot_service.go`
(20)
(以上已完成。当前 Top 5 已变为:
`backend/cmd/server/main.go`
、
`backend/internal/service/openai_tool_corrector.go`
、
`backend/internal/service/email_queue_service.go`
、
`backend/internal/config/config.go`
、
`backend/internal/service/ops_cleanup_service.go`
)
目标:逐步替换为结构化日志,减少对
`legacy_stdlog`
兼容桥接的依赖。
### 3.2 前端日志治理(优先级 P1)
建议新增统一前端日志工具(如
`src/utils/logger.ts`
)并分三步替换:
1.
`console.error/warn/debug/log`
全部收敛到统一 API;
2.
生产环境默认降噪(仅保留关键告警/错误);
3.
统一字段(模块名、请求ID、用户ID、路由、错误码)并避免打印敏感数据。
### 3.3 日志规范与门禁(优先级 P2)
建议补充:
1.
日志规范文档(等级定义、字段最小集、脱敏要求);
2.
CI 检查规则:限制新增裸
`log.Printf`
/
`console.*`
;
3.
面向运营告警的事件白名单(例如
`*.forward_failed`
、
`*.retry_exhausted*`
)。
## 4. 本次整理后可直接使用的检索建议
1.
过滤历史兼容日志:
`legacy_stdlog=true`
2.
网关入口故障:
`component=handler.* AND level in (WARN,ERROR)`
3.
请求闭环:按
`request_id`
+
`*.request_completed`
+
`*.forward_failed`
4.
token 刷新故障:
`component=*token_refresh* AND (retry_attempt_failed OR set_error_status_failed)`
Prev
1
2
3
4
5
Next
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment