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
eefab159
Commit
eefab159
authored
Mar 15, 2026
by
Ethan0x0000
Browse files
feat: 完善使用记录端点可观测性与分布统计
将入站、上游与路径三类端点分布统一到使用记录页的一致化卡片交互中,并补齐端点元数据与统计链路,提升排障与流量分析效率。
parent
6da5fa01
Changes
24
Hide whitespace changes
Inline
Side-by-side
frontend/src/i18n/locales/zh.ts
View file @
eefab159
...
...
@@ -723,6 +723,13 @@ export default {
preparingExport
:
'
正在准备导出...
'
,
model
:
'
模型
'
,
reasoningEffort
:
'
推理强度
'
,
endpoint
:
'
端点
'
,
endpointDistribution
:
'
端点分布
'
,
inbound
:
'
入站
'
,
upstream
:
'
上游
'
,
path
:
'
路径
'
,
inboundEndpoint
:
'
入站端点
'
,
upstreamEndpoint
:
'
上游端点
'
,
type
:
'
类型
'
,
tokens
:
'
Token
'
,
cost
:
'
费用
'
,
...
...
frontend/src/types/index.ts
View file @
eefab159
...
...
@@ -962,6 +962,8 @@ export interface UsageLog {
model
:
string
service_tier
?:
string
|
null
reasoning_effort
?:
string
|
null
inbound_endpoint
?:
string
|
null
upstream_endpoint
?:
string
|
null
group_id
:
number
|
null
subscription_id
:
number
|
null
...
...
@@ -1168,6 +1170,14 @@ export interface ModelStat {
actual_cost
:
number
// 实际扣除
}
export
interface
EndpointStat
{
endpoint
:
string
requests
:
number
total_tokens
:
number
cost
:
number
actual_cost
:
number
}
export
interface
GroupStat
{
group_id
:
number
group_name
:
string
...
...
@@ -1362,6 +1372,8 @@ export interface AccountUsageStatsResponse {
history
:
AccountUsageHistory
[]
summary
:
AccountUsageSummary
models
:
ModelStat
[]
endpoints
:
EndpointStat
[]
upstream_endpoints
:
EndpointStat
[]
}
// ==================== User Attribute Types ====================
...
...
frontend/src/views/admin/UsageView.vue
View file @
eefab159
...
...
@@ -26,7 +26,20 @@
:show-metric-toggle=
"true"
/>
</div>
<TokenUsageTrend
:trend-data=
"trendData"
:loading=
"chartsLoading"
/>
<div
class=
"grid grid-cols-1 gap-6 lg:grid-cols-2"
>
<EndpointDistributionChart
v-model:source=
"endpointDistributionSource"
v-model:metric=
"endpointDistributionMetric"
:endpoint-stats=
"inboundEndpointStats"
:upstream-endpoint-stats=
"upstreamEndpointStats"
:endpoint-path-stats=
"endpointPathStats"
:loading=
"endpointStatsLoading"
:show-source-toggle=
"true"
:show-metric-toggle=
"true"
:title=
"t('usage.endpointDistribution')"
/>
<TokenUsageTrend
:trend-data=
"trendData"
:loading=
"chartsLoading"
/>
</div>
</div>
<UsageFilters
v-model=
"filters"
v-model:startDate=
"startDate"
v-model:endDate=
"endDate"
:exporting=
"exporting"
@
change=
"applyFilters"
@
refresh=
"refreshData"
@
reset=
"resetFilters"
@
cleanup=
"openCleanupDialog"
@
export=
"exportToExcel"
>
<template
#after-reset
>
...
...
@@ -99,19 +112,28 @@ import UsageTable from '@/components/admin/usage/UsageTable.vue'; import UsageEx
import
UsageCleanupDialog
from
'
@/components/admin/usage/UsageCleanupDialog.vue
'
import
UserBalanceHistoryModal
from
'
@/components/admin/user/UserBalanceHistoryModal.vue
'
import
ModelDistributionChart
from
'
@/components/charts/ModelDistributionChart.vue
'
;
import
GroupDistributionChart
from
'
@/components/charts/GroupDistributionChart.vue
'
;
import
TokenUsageTrend
from
'
@/components/charts/TokenUsageTrend.vue
'
import
EndpointDistributionChart
from
'
@/components/charts/EndpointDistributionChart.vue
'
import
Icon
from
'
@/components/icons/Icon.vue
'
import
type
{
AdminUsageLog
,
TrendDataPoint
,
ModelStat
,
GroupStat
,
AdminUser
}
from
'
@/types
'
;
import
type
{
AdminUsageStatsResponse
,
AdminUsageQueryParams
}
from
'
@/api/admin/usage
'
import
type
{
AdminUsageLog
,
TrendDataPoint
,
ModelStat
,
GroupStat
,
EndpointStat
,
AdminUser
}
from
'
@/types
'
;
import
type
{
AdminUsageStatsResponse
,
AdminUsageQueryParams
}
from
'
@/api/admin/usage
'
const
{
t
}
=
useI18n
()
const
appStore
=
useAppStore
()
type
DistributionMetric
=
'
tokens
'
|
'
actual_cost
'
type
EndpointSource
=
'
inbound
'
|
'
upstream
'
|
'
path
'
const
route
=
useRoute
()
const
usageStats
=
ref
<
AdminUsageStatsResponse
|
null
>
(
null
);
const
usageLogs
=
ref
<
AdminUsageLog
[]
>
([]);
const
loading
=
ref
(
false
);
const
exporting
=
ref
(
false
)
const
trendData
=
ref
<
TrendDataPoint
[]
>
([]);
const
modelStats
=
ref
<
ModelStat
[]
>
([]);
const
groupStats
=
ref
<
GroupStat
[]
>
([]);
const
chartsLoading
=
ref
(
false
);
const
granularity
=
ref
<
'
day
'
|
'
hour
'
>
(
'
day
'
)
const
modelDistributionMetric
=
ref
<
DistributionMetric
>
(
'
tokens
'
)
const
groupDistributionMetric
=
ref
<
DistributionMetric
>
(
'
tokens
'
)
const
endpointDistributionMetric
=
ref
<
DistributionMetric
>
(
'
tokens
'
)
const
endpointDistributionSource
=
ref
<
EndpointSource
>
(
'
inbound
'
)
const
inboundEndpointStats
=
ref
<
EndpointStat
[]
>
([])
const
upstreamEndpointStats
=
ref
<
EndpointStat
[]
>
([])
const
endpointPathStats
=
ref
<
EndpointStat
[]
>
([])
const
endpointStatsLoading
=
ref
(
false
)
let
abortController
:
AbortController
|
null
=
null
;
let
exportAbortController
:
AbortController
|
null
=
null
let
chartReqSeq
=
0
let
statsReqSeq
=
0
const
exportProgress
=
reactive
({
show
:
false
,
progress
:
0
,
current
:
0
,
total
:
0
,
estimatedTime
:
''
})
const
cleanupDialogVisible
=
ref
(
false
)
// Balance history modal state
...
...
@@ -183,13 +205,25 @@ const loadLogs = async () => {
}
catch
(
error
:
any
)
{
if
(
error
?.
name
!==
'
AbortError
'
)
console
.
error
(
'
Failed to load usage logs:
'
,
error
)
}
finally
{
if
(
abortController
===
c
)
loading
.
value
=
false
}
}
const
loadStats
=
async
()
=>
{
const
seq
=
++
statsReqSeq
endpointStatsLoading
.
value
=
true
try
{
const
requestType
=
filters
.
value
.
request_type
const
legacyStream
=
requestType
?
requestTypeToLegacyStream
(
requestType
)
:
filters
.
value
.
stream
const
s
=
await
adminAPI
.
usage
.
getStats
({
...
filters
.
value
,
stream
:
legacyStream
===
null
?
undefined
:
legacyStream
})
if
(
seq
!==
statsReqSeq
)
return
usageStats
.
value
=
s
inboundEndpointStats
.
value
=
s
.
endpoints
||
[]
upstreamEndpointStats
.
value
=
s
.
upstream_endpoints
||
[]
endpointPathStats
.
value
=
s
.
endpoint_paths
||
[]
}
catch
(
error
)
{
if
(
seq
!==
statsReqSeq
)
return
console
.
error
(
'
Failed to load usage stats:
'
,
error
)
inboundEndpointStats
.
value
=
[]
upstreamEndpointStats
.
value
=
[]
endpointPathStats
.
value
=
[]
}
finally
{
if
(
seq
===
statsReqSeq
)
endpointStatsLoading
.
value
=
false
}
}
const
loadChartData
=
async
()
=>
{
...
...
@@ -246,6 +280,7 @@ const exportToExcel = async () => {
const
headers
=
[
t
(
'
usage.time
'
),
t
(
'
admin.usage.user
'
),
t
(
'
usage.apiKeyFilter
'
),
t
(
'
admin.usage.account
'
),
t
(
'
usage.model
'
),
t
(
'
usage.reasoningEffort
'
),
t
(
'
admin.usage.group
'
),
t
(
'
usage.inboundEndpoint
'
),
t
(
'
usage.upstreamEndpoint
'
),
t
(
'
usage.type
'
),
t
(
'
admin.usage.inputTokens
'
),
t
(
'
admin.usage.outputTokens
'
),
t
(
'
admin.usage.cacheReadTokens
'
),
t
(
'
admin.usage.cacheCreationTokens
'
),
...
...
@@ -263,7 +298,8 @@ const exportToExcel = async () => {
if
(
c
.
signal
.
aborted
)
break
;
if
(
p
===
1
)
{
total
=
res
.
total
;
exportProgress
.
total
=
total
}
const
rows
=
(
res
.
items
||
[]).
map
((
log
:
AdminUsageLog
)
=>
[
log
.
created_at
,
log
.
user
?.
email
||
''
,
log
.
api_key
?.
name
||
''
,
log
.
account
?.
name
||
''
,
log
.
model
,
formatReasoningEffort
(
log
.
reasoning_effort
),
log
.
group
?.
name
||
''
,
getRequestTypeLabel
(
log
),
formatReasoningEffort
(
log
.
reasoning_effort
),
log
.
group
?.
name
||
''
,
log
.
inbound_endpoint
||
''
,
log
.
upstream_endpoint
||
''
,
getRequestTypeLabel
(
log
),
log
.
input_tokens
,
log
.
output_tokens
,
log
.
cache_read_tokens
,
log
.
cache_creation_tokens
,
log
.
input_cost
?.
toFixed
(
6
)
||
'
0.000000
'
,
log
.
output_cost
?.
toFixed
(
6
)
||
'
0.000000
'
,
log
.
cache_read_cost
?.
toFixed
(
6
)
||
'
0.000000
'
,
log
.
cache_creation_cost
?.
toFixed
(
6
)
||
'
0.000000
'
,
...
...
@@ -301,6 +337,7 @@ const allColumns = computed(() => [
{
key
:
'
account
'
,
label
:
t
(
'
admin.usage.account
'
),
sortable
:
false
},
{
key
:
'
model
'
,
label
:
t
(
'
usage.model
'
),
sortable
:
true
},
{
key
:
'
reasoning_effort
'
,
label
:
t
(
'
usage.reasoningEffort
'
),
sortable
:
false
},
{
key
:
'
endpoint
'
,
label
:
t
(
'
usage.endpoint
'
),
sortable
:
false
},
{
key
:
'
group
'
,
label
:
t
(
'
admin.usage.group
'
),
sortable
:
false
},
{
key
:
'
stream
'
,
label
:
t
(
'
usage.type
'
),
sortable
:
false
},
{
key
:
'
tokens
'
,
label
:
t
(
'
usage.tokens
'
),
sortable
:
false
},
...
...
@@ -343,12 +380,18 @@ const loadSavedColumns = () => {
try
{
const
saved
=
localStorage
.
getItem
(
HIDDEN_COLUMNS_KEY
)
if
(
saved
)
{
(
JSON
.
parse
(
saved
)
as
string
[]).
forEach
(
key
=>
hiddenColumns
.
add
(
key
))
(
JSON
.
parse
(
saved
)
as
string
[]).
forEach
((
key
)
=>
{
hiddenColumns
.
add
(
key
)
})
}
else
{
DEFAULT_HIDDEN_COLUMNS
.
forEach
(
key
=>
hiddenColumns
.
add
(
key
))
DEFAULT_HIDDEN_COLUMNS
.
forEach
((
key
)
=>
{
hiddenColumns
.
add
(
key
)
})
}
}
catch
{
DEFAULT_HIDDEN_COLUMNS
.
forEach
(
key
=>
hiddenColumns
.
add
(
key
))
DEFAULT_HIDDEN_COLUMNS
.
forEach
((
key
)
=>
{
hiddenColumns
.
add
(
key
)
})
}
}
...
...
frontend/src/views/user/UsageView.vue
View file @
eefab159
...
...
@@ -166,6 +166,12 @@
</span>
</
template
>
<
template
#cell-endpoint=
"{ row }"
>
<span
class=
"text-sm text-gray-600 dark:text-gray-300 block max-w-[320px] whitespace-normal break-all"
>
{{
formatUsageEndpoints
(
row
)
}}
</span>
</
template
>
<
template
#cell-stream=
"{ row }"
>
<span
class=
"inline-flex items-center rounded px-2 py-0.5 text-xs font-medium"
...
...
@@ -516,6 +522,7 @@ const columns = computed<Column[]>(() => [
{
key
:
'
api_key
'
,
label
:
t
(
'
usage.apiKeyFilter
'
),
sortable
:
false
},
{
key
:
'
model
'
,
label
:
t
(
'
usage.model
'
),
sortable
:
true
},
{
key
:
'
reasoning_effort
'
,
label
:
t
(
'
usage.reasoningEffort
'
),
sortable
:
false
},
{
key
:
'
endpoint
'
,
label
:
t
(
'
usage.endpoint
'
),
sortable
:
false
},
{
key
:
'
stream
'
,
label
:
t
(
'
usage.type
'
),
sortable
:
false
},
{
key
:
'
tokens
'
,
label
:
t
(
'
usage.tokens
'
),
sortable
:
false
},
{
key
:
'
cost
'
,
label
:
t
(
'
usage.cost
'
),
sortable
:
false
},
...
...
@@ -615,6 +622,11 @@ const getRequestTypeExportText = (log: UsageLog): string => {
return
'
Unknown
'
}
const
formatUsageEndpoints
=
(
log
:
UsageLog
):
string
=>
{
const
inbound
=
log
.
inbound_endpoint
?.
trim
()
return
inbound
||
'
-
'
}
const
formatTokens
=
(
value
:
number
):
string
=>
{
if
(
value
>=
1
_000_000_000
)
{
return
`
${(
value
/
1
_000_000_000
).
toFixed
(
2
)}
B`
...
...
@@ -789,6 +801,7 @@ const exportToCSV = async () => {
'
API Key Name
'
,
'
Model
'
,
'
Reasoning Effort
'
,
'
Inbound Endpoint
'
,
'
Type
'
,
'
Input Tokens
'
,
'
Output Tokens
'
,
...
...
@@ -806,6 +819,7 @@ const exportToCSV = async () => {
log
.
api_key
?.
name
||
''
,
log
.
model
,
formatReasoningEffort
(
log
.
reasoning_effort
),
log
.
inbound_endpoint
||
''
,
getRequestTypeExportText
(
log
),
log
.
input_tokens
,
log
.
output_tokens
,
...
...
Prev
1
2
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