feat(observability): enrich error request traces

Expose request, client, model, transport, and account facts in operations errors and surface them in the error table and detail view.

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Zyy
2026-08-31 10:57:53 +08:00
co-authored by Codex
parent 4d53ef6068
commit 56451601fb
13 changed files with 315 additions and 46 deletions
@@ -135,6 +135,7 @@ pub struct OpsErrorFilter {
pub provider_kind: Option<String>,
pub provider_account_ref: Option<String>,
pub operation: Option<String>,
pub endpoint: Option<String>,
pub model: Option<String>,
pub transport: Option<String>,
pub attempt_index: Option<u32>,
@@ -762,6 +763,10 @@ pub struct OpsError {
pub client_api_key_ref: Option<String>,
pub component: String,
pub operation: String,
pub protocol: Option<String>,
pub client_transport: Option<String>,
pub requested_model_id: Option<String>,
pub service_tier: Option<String>,
pub endpoint: Option<String>,
pub provider_kind: Option<String>,
pub provider_account_ref: Option<String>,
@@ -779,6 +784,13 @@ pub struct OpsError {
pub latency_ms: Option<u64>,
pub message: String,
pub occurrence_count: u32,
pub client_ip: Option<String>,
pub user_agent: Option<String>,
pub reasoning_effort: Option<String>,
pub reasoning_preset: Option<String>,
pub request_kind: Option<String>,
pub subagent_kind: Option<String>,
pub compact: Option<bool>,
pub occurred_at: DateTime<Utc>,
pub stable_sort_id: String,
}
@@ -531,24 +531,45 @@ pub(crate) fn ops_error_view(error: domain::OpsError) -> OpsErrorView {
.or(error.provider_account_name.as_ref())
.or(error.provider_account_ref.as_ref())
.cloned();
let model = error
.upstream_model_id
.clone()
.or_else(|| error.requested_model_id.clone());
OpsErrorView {
id: error.event_id,
request_id: error.request_id,
client_api_key_id: error.client_api_key_ref,
kind: error.component.clone(),
operation: error.operation,
protocol: error.protocol,
client_transport: error.client_transport,
provider: error.provider_kind,
authentication_kind: error.provider_account_authentication_kind,
account_id: error.provider_account_ref,
account_name: error.provider_account_name,
account_email: error.provider_account_email,
route: error.endpoint.unwrap_or_else(|| "—".to_owned()),
model: error.upstream_model_id,
model,
requested_model: error.requested_model_id,
upstream_model: error.upstream_model_id,
service_tier: error.service_tier,
client_status_code: error.client_status_code.map(i64::from),
upstream_status_code: error.upstream_status_code.map(i64::from),
transport: error.upstream_transport,
attempt_index: error.attempt_index,
failure_class: error.failure_kind,
provider_error_code: error.provider_error_code,
occurrence_count: error.occurrence_count,
response_id: error.client_response_id,
upstream_request_id: error.upstream_request_id,
latency_ms: error.latency_ms,
client_ip: error.client_ip,
user_agent: error.user_agent,
reasoning_effort: error.reasoning_effort,
reasoning_preset: error.reasoning_preset,
request_kind: error.request_kind,
subagent_kind: error.subagent_kind,
compact: error.compact,
message: error.message,
metadata: OpsErrorMetadataView {
source: error.source,
@@ -351,7 +351,8 @@ pub(crate) fn ops_command(query: &OpsQuery) -> Result<domain::OpsErrorQuery, Wir
request_id: non_empty(query.request_id.clone()),
provider_kind: non_empty(query.provider.clone()),
provider_account_ref: non_empty(query.account_id.clone()),
operation: non_empty(query.route.clone()).or_else(|| non_empty(query.kind.clone())),
operation: non_empty(query.kind.clone()),
endpoint: non_empty(query.route.clone()),
model: non_empty(query.model.clone()),
transport: non_empty(query.transport.clone()),
attempt_index: parse_attempt_index(query.attempt_index)?,
@@ -758,19 +758,36 @@ pub struct OpsErrorView {
pub request_id: Option<String>,
pub client_api_key_id: Option<String>,
pub kind: String,
pub operation: String,
pub protocol: Option<String>,
pub client_transport: Option<String>,
pub provider: Option<String>,
pub authentication_kind: Option<String>,
pub account_id: Option<String>,
pub account_name: Option<String>,
pub account_email: Option<String>,
pub route: String,
pub model: Option<String>,
pub requested_model: Option<String>,
pub upstream_model: Option<String>,
pub service_tier: Option<String>,
pub client_status_code: Option<i64>,
pub upstream_status_code: Option<i64>,
pub transport: Option<String>,
pub attempt_index: Option<u32>,
pub failure_class: String,
pub provider_error_code: Option<String>,
pub occurrence_count: u32,
pub response_id: Option<String>,
pub upstream_request_id: Option<String>,
pub latency_ms: Option<u64>,
pub client_ip: Option<String>,
pub user_agent: Option<String>,
pub reasoning_effort: Option<String>,
pub reasoning_preset: Option<String>,
pub request_kind: Option<String>,
pub subagent_kind: Option<String>,
pub compact: Option<bool>,
pub message: String,
pub metadata: OpsErrorMetadataView,
pub created_at: DateTime<Utc>,
@@ -539,6 +539,10 @@ async fn ops_errors_should_keep_account_label_and_authentication_contract() {
client_api_key_ref: Some("key_err".to_owned()),
component: "model_request".to_owned(),
operation: "responses".to_owned(),
protocol: Some("openai".to_owned()),
client_transport: Some("http_sse".to_owned()),
requested_model_id: Some("gpt-5.4".to_owned()),
service_tier: Some("priority".to_owned()),
endpoint: Some("/v1/responses".to_owned()),
provider_kind: Some("openai".to_owned()),
provider_account_ref: Some("acct_err".to_owned()),
@@ -556,6 +560,13 @@ async fn ops_errors_should_keep_account_label_and_authentication_contract() {
latency_ms: Some(90),
message: "snapshot error".to_owned(),
occurrence_count: 1,
client_ip: Some("203.0.113.8".to_owned()),
user_agent: Some("codex-cli/0.144.0".to_owned()),
reasoning_effort: Some("medium".to_owned()),
reasoning_preset: None,
request_kind: Some("root".to_owned()),
subagent_kind: None,
compact: Some(false),
occurred_at: Utc::now(),
stable_sort_id: "model_request:req_err".to_owned(),
});
@@ -585,6 +596,11 @@ async fn ops_errors_should_keep_account_label_and_authentication_contract() {
"accountLabel": value["data"]["items"][0]["metadata"]["accountLabel"],
"clientStatusCode": value["data"]["items"][0]["clientStatusCode"],
"route": value["data"]["items"][0]["route"],
"requestedModel": value["data"]["items"][0]["requestedModel"],
"clientTransport": value["data"]["items"][0]["clientTransport"],
"clientIp": value["data"]["items"][0]["clientIp"],
"userAgent": value["data"]["items"][0]["userAgent"],
"providerErrorCode": value["data"]["items"][0]["providerErrorCode"],
}),
serde_json::json!({
"provider": "openai",
@@ -594,6 +610,11 @@ async fn ops_errors_should_keep_account_label_and_authentication_contract() {
"accountLabel": "err@example.invalid",
"clientStatusCode": 502,
"route": "/v1/responses",
"requestedModel": "gpt-5.4",
"clientTransport": "http_sse",
"clientIp": "203.0.113.8",
"userAgent": "codex-cli/0.144.0",
"providerErrorCode": "upstream",
})
);
}
@@ -51,6 +51,7 @@ pub(crate) fn store_ops_error_filter(
provider_account_ref: filter.provider_account_ref,
provider_kind: filter.provider_kind,
operation: filter.operation,
endpoint: filter.endpoint,
model: filter.model,
transport: filter.transport,
attempt_index: filter.attempt_index,
@@ -700,6 +701,10 @@ pub(crate) fn admin_ops_error(error: OpsErrorRecord) -> admin_observability::Ops
client_api_key_ref: error.client_api_key_ref,
component: error.component,
operation: error.operation,
protocol: error.protocol,
client_transport: error.client_transport,
requested_model_id: error.requested_model_id,
service_tier: error.service_tier,
endpoint: error.endpoint,
provider_kind: error.provider_kind,
provider_account_ref: error.provider_account_ref,
@@ -717,6 +722,13 @@ pub(crate) fn admin_ops_error(error: OpsErrorRecord) -> admin_observability::Ops
latency_ms: error.latency_ms,
message: error.message,
occurrence_count: error.occurrence_count,
client_ip: error.client_ip,
user_agent: error.user_agent,
reasoning_effort: error.reasoning_effort,
reasoning_preset: error.reasoning_preset,
request_kind: error.request_kind,
subagent_kind: error.subagent_kind,
compact: error.compact,
occurred_at: error.occurred_at,
stable_sort_id: error.stable_sort_id,
}
@@ -866,6 +878,10 @@ pub(crate) fn ops_error_from_row(row: &sqlx::postgres::PgRow) -> StoreResult<Ops
client_api_key_ref: get(row, "client_api_key_ref")?,
component: get(row, "component")?,
operation: get(row, "operation")?,
protocol: get(row, "protocol")?,
client_transport: get(row, "client_transport")?,
requested_model_id: get(row, "requested_model_id")?,
service_tier: get(row, "service_tier")?,
endpoint: get(row, "endpoint")?,
provider_kind: get(row, "provider_kind")?,
provider_account_ref: get(row, "provider_account_ref")?,
@@ -883,6 +899,13 @@ pub(crate) fn ops_error_from_row(row: &sqlx::postgres::PgRow) -> StoreResult<Ops
latency_ms: optional_unsigned(row, "latency_ms")?,
message: get(row, "message")?,
occurrence_count: to_u32(get(row, "occurrence_count")?)?,
client_ip: get(row, "client_ip")?,
user_agent: get(row, "user_agent")?,
reasoning_effort: get(row, "reasoning_effort")?,
reasoning_preset: get(row, "reasoning_preset")?,
request_kind: get(row, "request_kind")?,
subagent_kind: get(row, "subagent_kind")?,
compact: get(row, "compact")?,
occurred_at: get(row, "occurred_at")?,
stable_sort_id: get(row, "stable_sort_id")?,
})
@@ -126,6 +126,7 @@ pub struct OpsErrorFilter {
pub provider_account_ref: Option<String>,
pub provider_kind: Option<String>,
pub operation: Option<String>,
pub endpoint: Option<String>,
pub model: Option<String>,
pub transport: Option<String>,
pub attempt_index: Option<u32>,
@@ -147,6 +148,7 @@ impl OpsErrorFilter {
),
(self.provider_kind.as_deref(), "provider filter"),
(self.operation.as_deref(), "operation filter"),
(self.endpoint.as_deref(), "endpoint filter"),
(self.model.as_deref(), "model filter"),
(self.transport.as_deref(), "transport filter"),
(
@@ -694,6 +696,10 @@ pub struct OpsErrorRecord {
pub client_api_key_ref: Option<String>,
pub component: String,
pub operation: String,
pub protocol: Option<String>,
pub client_transport: Option<String>,
pub requested_model_id: Option<String>,
pub service_tier: Option<String>,
pub endpoint: Option<String>,
pub provider_kind: Option<String>,
pub provider_account_ref: Option<String>,
@@ -711,6 +717,13 @@ pub struct OpsErrorRecord {
pub latency_ms: Option<u64>,
pub message: String,
pub occurrence_count: u32,
pub client_ip: Option<String>,
pub user_agent: Option<String>,
pub reasoning_effort: Option<String>,
pub reasoning_preset: Option<String>,
pub request_kind: Option<String>,
pub subagent_kind: Option<String>,
pub compact: Option<bool>,
pub occurred_at: DateTime<Utc>,
pub stable_sort_id: String,
}
@@ -6,6 +6,7 @@ const REQUEST_ERROR_SELECT: &str = "select 'model_request'::text as source,
mr.id as event_id, mr.id as request_id,
nullif(mr.attempt_count, 0) as attempt_index,
mr.client_api_key_ref, 'model_request'::text as component, mr.operation,
mr.protocol, mr.client_transport, mr.requested_model_id, mr.service_tier,
mr.endpoint, mr.provider_kind, mr.provider_account_ref,
mr.provider_account_name_snapshot as provider_account_name,
mr.provider_account_email_snapshot as provider_account_email,
@@ -17,7 +18,11 @@ const REQUEST_ERROR_SELECT: &str = "select 'model_request'::text as source,
mr.provider_error_code, mr.client_response_id, mr.upstream_request_id,
mr.latency_ms,
coalesce(mr.error_message, mr.error_kind, 'request failed') as message,
1::integer as occurrence_count, mr.completed_at as occurred_at,
1::integer as occurrence_count,
mr.client_ip::text as client_ip, mr.user_agent,
mr.reasoning_effort, mr.reasoning_preset, mr.request_kind,
mr.subagent_kind, mr.compact,
mr.completed_at as occurred_at,
'model_request:' || mr.id as stable_sort_id
from model_requests mr
where mr.outcome = 'failed'";
@@ -25,7 +30,8 @@ where mr.outcome = 'failed'";
const OPS_EVENT_SELECT: &str = "select 'ops_event'::text as source,
oe.id as event_id, oe.model_request_id as request_id, oe.attempt_index,
mr.client_api_key_ref, oe.component, oe.operation,
null::text as endpoint, oe.provider_kind, oe.provider_account_ref,
mr.protocol, mr.client_transport, mr.requested_model_id, mr.service_tier,
mr.endpoint, oe.provider_kind, oe.provider_account_ref,
oe.provider_account_name_snapshot as provider_account_name,
oe.provider_account_email_snapshot as provider_account_email,
oe.provider_account_authentication_kind_snapshot
@@ -33,7 +39,11 @@ const OPS_EVENT_SELECT: &str = "select 'ops_event'::text as source,
oe.upstream_model_id, null::text as upstream_transport, oe.failure_kind,
null::integer as client_status_code, oe.status_code as upstream_status_code,
oe.provider_error_code, mr.client_response_id, oe.upstream_request_id,
oe.latency_ms, oe.message, oe.occurrence_count, oe.created_at as occurred_at,
oe.latency_ms, oe.message, oe.occurrence_count,
mr.client_ip::text as client_ip, mr.user_agent,
mr.reasoning_effort, mr.reasoning_preset, mr.request_kind,
mr.subagent_kind, mr.compact,
oe.created_at as occurred_at,
'ops_event:' || oe.id as stable_sort_id
from ops_events oe
left join model_requests mr on mr.id = oe.model_request_id
@@ -107,6 +117,7 @@ fn push_request_error_predicates(
("mr.provider_account_ref", &filter.provider_account_ref),
("mr.provider_kind", &filter.provider_kind),
("mr.operation", &filter.operation),
("mr.endpoint", &filter.endpoint),
("mr.upstream_transport", &filter.transport),
("mr.upstream_request_id", &filter.upstream_request_id),
] {
@@ -153,6 +164,7 @@ fn push_ops_event_predicates(
("oe.provider_account_ref", &filter.provider_account_ref),
("oe.provider_kind", &filter.provider_kind),
("oe.operation", &filter.operation),
("mr.endpoint", &filter.endpoint),
("oe.upstream_request_id", &filter.upstream_request_id),
("oe.failure_kind", &filter.failure_kind),
] {
@@ -761,6 +761,8 @@ async fn admin_observability_adapter_preserves_utc_queries_metrics_costs_and_det
request_id: Some("req_observe_failed".to_owned()),
provider_kind: Some("openai".to_owned()),
provider_account_ref: Some("acct_observe".to_owned()),
operation: Some("responses".to_owned()),
endpoint: Some("/v1/responses".to_owned()),
model: Some("upstream-model".to_owned()),
failure_kind: Some("rate_limited".to_owned()),
status_code: Some(429),
@@ -1191,6 +1193,15 @@ async fn observability_queries_preserve_request_account_cost_and_diagnostic_fact
.expect("request error");
assert_eq!(request_error.client_status_code, Some(502));
assert_eq!(request_error.upstream_status_code, Some(429));
assert_eq!(request_error.client_ip.as_deref(), Some("203.0.113.9"));
assert_eq!(
request_error.user_agent.as_deref(),
Some("codex-cli/0.144.0")
);
assert_eq!(
request_error.requested_model_id.as_deref(),
Some("public-model")
);
let attempt_error = errors
.items
.iter()
@@ -1198,6 +1209,12 @@ async fn observability_queries_preserve_request_account_cost_and_diagnostic_fact
.expect("attempt error");
assert_eq!(attempt_error.client_status_code, None);
assert_eq!(attempt_error.upstream_status_code, Some(429));
assert_eq!(attempt_error.endpoint.as_deref(), Some("/v1/responses"));
assert_eq!(attempt_error.client_ip.as_deref(), Some("203.0.113.9"));
assert_eq!(
attempt_error.user_agent.as_deref(),
Some("codex-cli/0.144.0")
);
database.close().await;
}
@@ -1286,7 +1303,7 @@ async fn seed_observability_facts(
sqlx::query(
"insert into model_requests (
id, client_api_key_ref, config_revision, protocol, operation, endpoint,
client_transport, requested_model_id,
client_transport, requested_model_id, service_tier,
provider_kind, provider_account_id,
provider_account_ref, upstream_model_id, upstream_transport, attempt_count,
provider_account_name_snapshot, provider_account_email_snapshot,
@@ -1295,16 +1312,20 @@ async fn seed_observability_facts(
error_kind, provider_error_code, error_message, retry_after_ms,
input_tokens, cached_tokens, image_generation_requested,
image_generation_succeeded, cost_source, latency_ms,
client_ip, user_agent, reasoning_effort, reasoning_preset,
request_kind, subagent_kind, compact,
started_at, deadline_at, completed_at,
routing_scope, routing_group_refs, routing_group_names_snapshot
) values (
'req_observe_failed', 'key_observe', 1, 'openai', 'responses', '/v1/responses',
'http_sse', 'public-model', 'openai', 'acct_observe',
'http_sse', 'public-model', 'priority', 'openai', 'acct_observe',
'acct_observe', 'upstream-model',
'http_sse', 2,
'primary', 'account@example.invalid', 'oauth',
'sent', 'failed', 502, 429, 'rate_limited', 'rate_limit',
'upstream limited', 1000, 0, 0, true, false, 'unavailable', 700,
'203.0.113.9', 'codex-cli/0.144.0', 'medium', null,
'root', null, false,
$1 - interval '10 minutes', $1 + interval '20 minutes', $1 - interval '9 minutes',
'all', '{}'::text[], '[]'::jsonb
)",
+17
View File
@@ -211,19 +211,36 @@ export interface OpsError {
requestId: string | null
clientApiKeyId: string | null
kind: string
operation: string
protocol: string | null
clientTransport: string | null
provider: string | null
authenticationKind: string | null
accountId: string | null
accountName: string | null
accountEmail: string | null
route: string
model: string | null
requestedModel: string | null
upstreamModel: string | null
serviceTier: string | null
clientStatusCode: number | null
upstreamStatusCode: number | null
transport: string | null
attemptIndex: number | null
failureClass: string
providerErrorCode: string | null
occurrenceCount: number
responseId: string | null
upstreamRequestId: string | null
latencyMs: number | null
clientIp: string | null
userAgent: string | null
reasoningEffort: string | null
reasoningPreset: string | null
requestKind: string | null
subagentKind: string | null
compact: boolean | null
message: string
metadata: OpsErrorMetadata
createdAt: string
@@ -6,6 +6,7 @@ import BaseModal from '@/components/base/BaseModal/index.vue'
import { displayValue } from '../utils/detail'
import { presentOpsError } from '../utils/opsErrorPresentation'
import UsageDetailCodePanel from './UsageDetailCodePanel.vue'
import UsageDetailFieldGrid from './UsageDetailFieldGrid.vue'
import UsageStatusCodeBadge from './UsageStatusCodeBadge.vue'
const props = defineProps<{
@@ -16,23 +17,52 @@ const open = defineModel<boolean>({ default: false })
const presentation = computed(() => props.record ? presentOpsError(props.record) : null)
const fields = computed(() => [
{ label: '账号', value: props.record?.metadata.accountLabel },
{ label: '时间', value: props.record?.createdAtDisplay },
const panelClass = 'min-w-0 rounded-cp-card bg-cp-fill-quaternary px-4 py-3.5'
const panelTitleClass = 'm-0 text-cp-sm leading-none font-heavy text-cp-text-secondary'
const accountLabel = computed(() => props.record?.accountEmail
|| props.record?.accountName
|| props.record?.metadata.accountLabel
|| props.record?.accountId)
const requestFields = computed(() => [
{ label: '客户端 IP', value: props.record?.clientIp, mono: true },
{ label: 'User-Agent', value: props.record?.userAgent, mono: true, wrap: true },
{ label: '协议', value: props.record?.protocol, mono: true },
{ label: '客户端传输', value: props.record?.clientTransport, mono: true },
{ label: '客户端 Key ID', value: props.record?.clientApiKeyId, mono: true },
{ label: '请求 ID', value: props.record?.requestId, mono: true },
{ label: '响应 ID', value: props.record?.responseId, mono: true },
{ label: '上游请求 ID', value: props.record?.upstreamRequestId, mono: true },
])
const routeFields = computed(() => [
{ label: '端点', value: props.record?.route, mono: true },
{ label: '请求模型', value: props.record?.requestedModel, mono: true },
{ label: '上游模型', value: props.record?.upstreamModel, mono: true },
{ label: '记录模型', value: props.record?.model, mono: true },
{ label: '推理强度', value: props.record?.reasoningEffort, mono: true },
{ label: '推理预设', value: props.record?.reasoningPreset, mono: true },
{ label: '请求类型', value: props.record?.requestKind, mono: true },
{ label: '子代理类型', value: props.record?.subagentKind, mono: true },
{ label: '压缩请求', value: booleanDisplay(props.record?.compact) },
{ label: '服务档位', value: props.record?.serviceTier, mono: true },
{ label: '上游传输', value: props.record?.transport, mono: true },
{ label: '尝试序号', value: props.record?.attemptIndex },
])
const diagnosticFields = computed(() => [
{ label: '账号', value: accountLabel.value, mono: true },
{ label: '时间', value: props.record?.createdAtDisplay, mono: true },
{ label: '记录来源', value: presentation.value?.sourceLabel },
{ label: '组件', value: presentation.value?.componentLabel },
{ label: '操作/触发', value: props.record?.operation, mono: true },
{ label: '失败分类', value: presentation.value?.failureClassLabel },
{ label: '平台/类型', value: providerKindLabel(props.record) },
{ label: '端点', value: props.record?.route },
{ label: '模型', value: props.record?.model },
{ label: '账号 ID', value: props.record?.accountId },
{ label: '客户端 Key ID', value: props.record?.clientApiKeyId },
{ label: '传输方式', value: props.record?.transport },
{ label: '尝试序号', value: props.record?.attemptIndex },
{ label: '耗时', value: latencyDisplay(props.record?.latencyMs) },
{ label: '请求 ID', value: props.record?.requestId, wide: true },
{ label: '响应 ID', value: props.record?.responseId, wide: true },
{ label: '上游请求 ID', value: props.record?.upstreamRequestId, wide: true },
{ label: '账号 ID', value: props.record?.accountId, mono: true },
{ label: 'Provider 错误码', value: props.record?.providerErrorCode, mono: true },
{ label: '聚合次数', value: props.record?.occurrenceCount },
{ label: '耗时', value: latencyDisplay(props.record?.latencyMs), mono: true },
])
const metadataText = computed(() => {
@@ -46,6 +76,14 @@ function latencyDisplay(value: unknown) {
return typeof value === 'number' ? `${value} ms` : '—'
}
function booleanDisplay(value: boolean | null | undefined) {
if (value === true)
return '是'
if (value === false)
return '否'
return '—'
}
function providerKindLabel(record: OpsError | null) {
if (!record?.provider)
return '—'
@@ -59,7 +97,7 @@ function providerKindLabel(record: OpsError | null) {
<BaseModal
v-model="open"
title="错误明细"
description="状态码、失败分类与诊断信息"
description="请求来源、链路上下文与原始诊断"
tone="danger"
size="xl"
>
@@ -79,34 +117,37 @@ function providerKindLabel(record: OpsError | null) {
</p>
</section>
<dl
class="mt-3 grid grid-cols-1 gap-3 rounded-cp-card bg-cp-fill-quaternary px-4 py-3.5 sm:grid-cols-2"
>
<div
v-for="field in fields"
:key="field.label"
class="min-w-0"
:class="field.wide ? 'sm:col-span-2' : undefined"
>
<dt class="text-cp-xs leading-none font-bold text-cp-text-quaternary">
{{ field.label }}
</dt>
<dd
class="mt-1.5 mb-0 truncate font-mono text-cp-sm leading-normal font-emphasis text-cp-text"
:title="displayValue(field.value)"
>
{{ displayValue(field.value) }}
</dd>
</div>
</dl>
<div class="mt-3 grid min-w-0 gap-3 lg:grid-cols-2">
<section :class="panelClass">
<h3 :class="panelTitleClass">
请求追踪
</h3>
<UsageDetailFieldGrid :items="requestFields" />
</section>
<section class="mt-3 rounded-cp-card bg-cp-fill-quaternary px-4 py-3.5">
<section :class="panelClass">
<h3 :class="panelTitleClass">
模型与链路
</h3>
<UsageDetailFieldGrid :items="routeFields" />
</section>
</div>
<section class="mt-3" :class="panelClass">
<h3 :class="panelTitleClass">
错误事件
</h3>
<UsageDetailFieldGrid :items="diagnosticFields" />
</section>
<section class="mt-3" :class="panelClass">
<UsageDetailCodePanel title="原始诊断" max-height="260px" :content="record.message" />
</section>
<section
v-if="metadataText"
class="mt-3 rounded-cp-card bg-cp-fill-quaternary px-4 py-3.5"
class="mt-3"
:class="panelClass"
>
<UsageDetailCodePanel title="Metadata" max-height="260px" :content="metadataText" />
</section>
@@ -8,11 +8,13 @@ import BaseIconButton from '@/components/base/BaseIconButton.vue'
import BaseInput from '@/components/base/BaseInput.vue'
import BaseTablePagination from '@/components/base/BaseTable/BaseTablePagination.vue'
import BaseTable from '@/components/base/BaseTable/index.vue'
import ProviderIconGroup from '@/components/ProviderIconGroup.vue'
import { useOpsErrorsTable } from '../composables/useOpsErrorsTable'
import { opsErrorColumns } from '../constants'
import { presentOpsError } from '../utils/opsErrorPresentation'
import OpsErrorDetailModal from './OpsErrorDetailModal.vue'
import UsageStatusCodeBadge from './UsageStatusCodeBadge.vue'
import UsageTransportBadge from './UsageTransportBadge.vue'
const props = defineProps<{
timeRangeParams: UsageTimeRangeParams
@@ -46,6 +48,30 @@ function failureClassText(record: OpsError) {
function errorSummary(record: OpsError) {
return presentOpsError(record).summary
}
function accountText(record: OpsError) {
return record.accountEmail
|| record.accountName
|| record.metadata.accountLabel
|| record.accountId
|| '—'
}
function modelText(record: OpsError) {
return record.requestedModel || record.model || record.upstreamModel || '—'
}
function modelTitle(record: OpsError) {
const requested = record.requestedModel
const upstream = record.upstreamModel
return requested && upstream && requested !== upstream
? `${requested} → ${upstream}`
: modelText(record)
}
function reasoningText(record: OpsError) {
return record.reasoningPreset || record.reasoningEffort || '—'
}
</script>
<template>
@@ -111,6 +137,12 @@ function errorSummary(record: OpsError) {
{{ failureClassText(row) }}
</span>
</template>
<template #provider="{ row }">
<ProviderIconGroup
:provider="String(row.provider || '')"
:authentication-kind="row.authenticationKind"
/>
</template>
<template #message="{ row }">
<span class="block max-w-full truncate text-cp-sm font-emphasis text-cp-text" :title="errorSummary(row)">
{{ errorSummary(row) }}
@@ -119,9 +151,41 @@ function errorSummary(record: OpsError) {
<template #accountId="{ row }">
<span
class="block max-w-full truncate font-mono text-cp-sm font-bold text-cp-text"
:title="row.accountId || ''"
:title="accountText(row)"
>
{{ row.metadata.accountLabel || '—' }}
{{ accountText(row) }}
</span>
</template>
<template #model="{ row }">
<span
class="block max-w-full truncate font-mono text-cp-sm font-bold text-cp-text"
:title="modelTitle(row)"
>
{{ modelText(row) }}
</span>
</template>
<template #reasoningEffort="{ row }">
<span class="text-cp-sm font-bold text-cp-text-secondary">
{{ reasoningText(row) }}
</span>
</template>
<template #transport="{ row }">
<UsageTransportBadge :transport="row.transport" />
</template>
<template #clientTransport="{ row }">
<UsageTransportBadge :transport="row.clientTransport" />
</template>
<template #clientIp="{ row }">
<span
class="inline-flex h-6 max-w-full items-center rounded-full bg-cp-blue-bg px-2.5 font-mono text-cp-sm leading-none font-bold text-cp-blue-text-on-bg"
:title="row.clientIp || '—'"
>
<span class="min-w-0 truncate">{{ row.clientIp || '—' }}</span>
</span>
</template>
<template #userAgent="{ row }">
<span class="block max-w-full wrap-break-word whitespace-normal font-mono text-cp-sm leading-[1.4] font-emphasis text-cp-text-secondary">
{{ row.userAgent || '—' }}
</span>
</template>
<template #actions="{ row }">
+7 -1
View File
@@ -35,13 +35,19 @@ export const usageRecordColumns = defineTableColumns<UsageDisplayRecord>([
export const opsErrorColumns = defineTableColumns<OpsErrorRow>([
{ key: 'accountId', label: '账号', kind: 'mono', size: '3xl' },
{ key: 'provider', label: '平台/类型', kind: 'custom', size: 'sm' },
{ key: 'createdAtDisplay', label: '时间', kind: 'datetime' },
{ key: 'upstreamStatusCode', label: '上游状态', kind: 'status', size: 'sm' },
{ key: 'clientStatusCode', label: '客户端状态', kind: 'status', size: 'md' },
{ key: 'failureClass', label: '失败分类', kind: 'mono' },
{ key: 'kind', label: '事件', kind: 'mono' },
{ key: 'route', label: '端点', kind: 'mono' },
{ key: 'model', label: '模型', kind: 'mono' },
{ key: 'model', label: '模型', kind: 'custom', size: 'xl' },
{ key: 'reasoningEffort', label: '推理强度', kind: 'custom' },
{ key: 'transport', label: '上游', kind: 'custom', size: 'md' },
{ key: 'clientTransport', label: '接入', kind: 'custom', size: 'md' },
{ key: 'clientIp', label: 'IP', kind: 'custom', size: '3xl' },
{ key: 'userAgent', label: 'User-Agent', kind: 'custom', size: '4xl' },
{ key: 'requestId', label: '请求 ID', kind: 'mono', size: '2xl' },
{ key: 'message', label: '错误摘要', kind: 'custom', size: '3xl' },
{ key: 'actions', label: '操作', kind: 'actions', size: 'sm' },