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
fff1d548
Commit
fff1d548
authored
Feb 12, 2026
by
yangjianbo
Browse files
feat(log): 落地统一日志底座与系统日志运维能力
parent
a5f29019
Changes
48
Show whitespace changes
Inline
Side-by-side
backend/internal/service/wire.go
View file @
fff1d548
...
...
@@ -6,6 +6,7 @@ import (
"time"
"github.com/Wei-Shaw/sub2api/internal/config"
"github.com/Wei-Shaw/sub2api/internal/pkg/logger"
"github.com/google/wire"
"github.com/redis/go-redis/v9"
)
...
...
@@ -193,6 +194,13 @@ func ProvideOpsCleanupService(
return
svc
}
func
ProvideOpsSystemLogSink
(
opsRepo
OpsRepository
)
*
OpsSystemLogSink
{
sink
:=
NewOpsSystemLogSink
(
opsRepo
)
sink
.
Start
()
logger
.
SetSink
(
sink
)
return
sink
}
// ProvideSoraMediaStorage 初始化 Sora 媒体存储
func
ProvideSoraMediaStorage
(
cfg
*
config
.
Config
)
*
SoraMediaStorage
{
return
NewSoraMediaStorage
(
cfg
)
...
...
@@ -268,6 +276,7 @@ var ProviderSet = wire.NewSet(
NewAccountUsageService
,
NewAccountTestService
,
NewSettingService
,
ProvideOpsSystemLogSink
,
NewOpsService
,
ProvideOpsMetricsCollector
,
ProvideOpsAggregationService
,
...
...
backend/migrations/054_ops_system_logs.sql
0 → 100644
View file @
fff1d548
-- 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 @
fff1d548
...
...
@@ -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 @
fff1d548
...
...
@@ -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
:
"
json"
# 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 直连配置
...
...
frontend/src/api/admin/ops.ts
View file @
fff1d548
...
...
@@ -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 @
fff1d548
...
...
@@ -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/OpsSystemLogTable.vue
0 → 100644
View file @
fff1d548
<
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
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 divide-y divide-gray-200 dark:divide-dark-700"
>
<thead
class=
"bg-gray-50 dark:bg-dark-900"
>
<tr>
<th
class=
"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>
<th
class=
"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>
<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"
>
{{
row
.
component
||
'
-
'
}}
</td>
<td
class=
"max-w-[680px] px-3 py-2 text-xs text-gray-700 dark:text-gray-300"
>
{{
row
.
message
}}
</td>
<td
class=
"px-3 py-2 text-xs text-gray-600 dark:text-gray-400"
>
<div>
req:
{{
row
.
request_id
||
'
-
'
}}
</div>
<div>
client:
{{
row
.
client_request_id
||
'
-
'
}}
</div>
<div>
user:
{{
row
.
user_id
||
'
-
'
}}
/ acc:
{{
row
.
account_id
||
'
-
'
}}
</div>
<div>
{{
row
.
platform
||
'
-
'
}}
/
{{
row
.
model
||
'
-
'
}}
</div>
</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 @
fff1d548
...
...
@@ -17,5 +17,8 @@ export type {
OpsMetricThresholds
,
OpsAdvancedSettings
,
OpsDataRetentionSettings
,
OpsAggregationSettings
OpsAggregationSettings
,
OpsRuntimeLogConfig
,
OpsSystemLog
,
OpsSystemLogSinkHealth
}
from
'
@/api/admin/ops
'
Prev
1
2
3
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