diff --git a/backend/crates/gateway-admin/src/model/observability.rs b/backend/crates/gateway-admin/src/model/observability.rs index 270e07957..15a014b72 100644 --- a/backend/crates/gateway-admin/src/model/observability.rs +++ b/backend/crates/gateway-admin/src/model/observability.rs @@ -135,13 +135,11 @@ pub struct OpsErrorFilter { pub provider_kind: Option, pub provider_account_ref: Option, pub operation: Option, - pub endpoint: Option, pub model: Option, pub transport: Option, pub attempt_index: Option, pub response_id: Option, pub upstream_request_id: Option, - pub failure_kind: Option, pub status_code: Option, pub search: Option, } diff --git a/backend/crates/gateway-api/src/admin/observability/query.rs b/backend/crates/gateway-api/src/admin/observability/query.rs index 40fa1abba..3b0acc118 100644 --- a/backend/crates/gateway-api/src/admin/observability/query.rs +++ b/backend/crates/gateway-api/src/admin/observability/query.rs @@ -107,14 +107,12 @@ pub struct OpsQuery { pub provider: Option, pub request_id: Option, pub account_id: Option, - pub route: Option, pub model: Option, pub status_code: Option, pub client_status_code: Option, pub upstream_status_code: Option, pub transport: Option, pub attempt_index: Option, - pub failure_class: Option, pub response_id: Option, pub upstream_request_id: Option, pub search: Option, @@ -352,13 +350,11 @@ pub(crate) fn ops_command(query: &OpsQuery) -> Result(json!({"route": "/v1/responses"})).is_err()); + assert!(serde_json::from_value::(json!({"failureClass": "rate_limited"})).is_err()); +} + #[test] fn diagnostics_query_should_keep_wire_dimension_name() { let query: DiagnosticsQuery = diff --git a/backend/crates/gateway-store/src/postgres/observability/mapping.rs b/backend/crates/gateway-store/src/postgres/observability/mapping.rs index 233d572f4..0376e56f3 100644 --- a/backend/crates/gateway-store/src/postgres/observability/mapping.rs +++ b/backend/crates/gateway-store/src/postgres/observability/mapping.rs @@ -51,13 +51,11 @@ 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, response_id: filter.response_id, upstream_request_id: filter.upstream_request_id, - failure_kind: filter.failure_kind, status_code: filter.status_code, search: filter.search, } diff --git a/backend/crates/gateway-store/src/postgres/observability/model.rs b/backend/crates/gateway-store/src/postgres/observability/model.rs index 04bd73d62..bc509ae97 100644 --- a/backend/crates/gateway-store/src/postgres/observability/model.rs +++ b/backend/crates/gateway-store/src/postgres/observability/model.rs @@ -126,13 +126,11 @@ pub struct OpsErrorFilter { pub provider_account_ref: Option, pub provider_kind: Option, pub operation: Option, - pub endpoint: Option, pub model: Option, pub transport: Option, pub attempt_index: Option, pub response_id: Option, pub upstream_request_id: Option, - pub failure_kind: Option, pub status_code: Option, pub search: Option, } @@ -148,14 +146,12 @@ 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"), ( self.upstream_request_id.as_deref(), "upstream request ID filter", ), - (self.failure_kind.as_deref(), "failure kind filter"), ] { validate_optional_text(value, MAX_FILTER_BYTES, field)?; } diff --git a/backend/crates/gateway-store/src/postgres/observability/queries/ops.rs b/backend/crates/gateway-store/src/postgres/observability/queries/ops.rs index 1f0e34418..c95f891ec 100644 --- a/backend/crates/gateway-store/src/postgres/observability/queries/ops.rs +++ b/backend/crates/gateway-store/src/postgres/observability/queries/ops.rs @@ -120,16 +120,11 @@ 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), ] { push_text_equality(statement, column, value); } - if let Some(value) = &filter.failure_kind { - statement.push(" and coalesce(mr.error_kind, 'failed') = "); - statement.push_bind(value.clone()); - } push_response_id_filter(statement, "mr.client_response_id", filter); push_text_equality(statement, "mr.upstream_model_id", &filter.model); if let Some(index) = filter.attempt_index { @@ -167,9 +162,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), ] { push_text_equality(statement, column, value); } diff --git a/backend/crates/gateway-store/tests/postgres/observability/mod.rs b/backend/crates/gateway-store/tests/postgres/observability/mod.rs index f409c1461..947d12383 100644 --- a/backend/crates/gateway-store/tests/postgres/observability/mod.rs +++ b/backend/crates/gateway-store/tests/postgres/observability/mod.rs @@ -762,9 +762,7 @@ async fn admin_observability_adapter_preserves_utc_queries_metrics_costs_and_det 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), search: Some("req_observe_failed".to_owned()), ..admin_observability::OpsErrorFilter::default() diff --git a/frontend/src/api/modules/usage.ts b/frontend/src/api/modules/usage.ts index 412c3a555..aa17d0824 100644 --- a/frontend/src/api/modules/usage.ts +++ b/frontend/src/api/modules/usage.ts @@ -420,10 +420,7 @@ interface PageQuery { type UsagePageQuery = UsageRangeQuery & PageQuery -type OpsErrorPageQuery = UsagePageQuery & { - failureClass?: string - route?: string -} +type OpsErrorPageQuery = UsagePageQuery interface UsageDetailQuery { id: string diff --git a/frontend/src/views/usage/components/OpsErrorDetailModal.vue b/frontend/src/views/usage/components/OpsErrorDetailModal.vue index a94f5b98f..153fb386a 100644 --- a/frontend/src/views/usage/components/OpsErrorDetailModal.vue +++ b/frontend/src/views/usage/components/OpsErrorDetailModal.vue @@ -2,12 +2,18 @@ import type { OpsError } from '@/api' import { computed } from 'vue' +import BaseButton from '@/components/base/BaseButton.vue' 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' + +interface DetailField { + label: string + value: unknown + mono?: boolean + wrap?: boolean + fullWidth?: boolean +} const props = defineProps<{ record: OpsError | null @@ -15,8 +21,6 @@ const props = defineProps<{ const open = defineModel({ default: false }) -const presentation = computed(() => props.record ? presentOpsError(props.record) : null) - 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' @@ -25,72 +29,56 @@ const accountLabel = computed(() => props.record?.accountEmail || 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 errorFields = computed(() => visibleFields([ + { label: 'Provider 错误码', value: props.record?.providerErrorCode, mono: true }, + { label: '失败分类', value: props.record?.failureClass, mono: true }, + { label: '发送状态', value: props.record?.upstreamSendState, mono: true }, + { label: '客户端状态', value: props.record?.clientStatusCode, mono: true }, + { label: '上游状态', value: props.record?.upstreamStatusCode, mono: true }, + { label: '耗时(ms)', value: props.record?.latencyMs, mono: true }, +])) -const routeFields = computed(() => [ +const requestFields = computed(() => visibleFields([ + { label: '账号', value: accountLabel.value, mono: true }, + { label: '时间', value: props.record?.createdAtDisplay, mono: true }, + { label: '请求 ID', value: props.record?.requestId, mono: true }, + { label: '客户端 Key ID', value: props.record?.clientApiKeyId, mono: true }, + { label: '客户端 IP', value: props.record?.clientIp, mono: true }, + { label: '协议', value: props.record?.protocol, mono: true }, + { label: 'User-Agent', value: props.record?.userAgent, mono: true, wrap: true, fullWidth: true }, +])) + +const routeFields = computed(() => visibleFields([ { label: '端点', value: props.record?.route, mono: true }, + { label: 'Provider', value: props.record?.provider, mono: true }, + { label: '认证类型', value: props.record?.authenticationKind, mono: true }, + { label: '账号 ID', value: props.record?.accountId, 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?.clientTransport, mono: true }, + { label: '上游传输', value: props.record?.transport, mono: true }, + { label: '服务档位', value: props.record?.serviceTier, 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?.compact, mono: true }, +])) + +const eventFields = computed(() => visibleFields([ + { label: '记录来源', value: props.record?.metadata.source, mono: true }, + { label: '组件', value: props.record?.metadata.component || props.record?.kind, mono: true }, + { label: '事件类型', value: props.record?.kind, mono: true }, + { label: '操作', value: props.record?.operation, 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: props.record?.upstreamSendState, mono: true }, - { label: '平台/类型', value: providerKindLabel(props.record) }, - { 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 }, -]) + { label: '响应 ID', value: props.record?.responseId, mono: true }, + { label: '上游请求 ID', value: props.record?.upstreamRequestId, mono: true }, +])) -const metadataText = computed(() => { - const metadata = props.record?.metadata - if (!metadata || (typeof metadata === 'object' && Object.keys(metadata).length === 0)) - return '' - return JSON.stringify(metadata, null, 2) -}) - -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 '—' - return record.authenticationKind - ? `${record.provider} · ${record.authenticationKind}` - : record.provider +function visibleFields(items: DetailField[]) { + return items.filter(({ value }) => value !== null && value !== undefined && value !== '') } @@ -98,68 +86,57 @@ function providerKindLabel(record: OpsError | null) { -
- -
- -
- -
- -
- -
+
diff --git a/frontend/src/views/usage/components/OpsErrorPanel.vue b/frontend/src/views/usage/components/OpsErrorPanel.vue index 2d4f53e71..b3f80fd14 100644 --- a/frontend/src/views/usage/components/OpsErrorPanel.vue +++ b/frontend/src/views/usage/components/OpsErrorPanel.vue @@ -11,10 +11,7 @@ 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 @@ -25,8 +22,6 @@ const { refreshing, records, searchQuery, - failureClass, - route, pagination, handlePageChange, handlePageSizeChange, @@ -36,29 +31,27 @@ const { const selectedRecord = shallowRef(null) const detailOpen = shallowRef(false) +const upstreamSendStateLabels: Record = { + sent: '已发送', + not_sent: '未发送', + ambiguous: '状态不明', +} + function showDetail(record: OpsError) { selectedRecord.value = record detailOpen.value = true } -function failureClassText(record: OpsError) { - return presentOpsError(record).failureClassLabel -} - -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 || '—' + return record.requestedModel || record.model || record.upstreamModel || '未记录' } function modelTitle(record: OpsError) { @@ -69,8 +62,10 @@ function modelTitle(record: OpsError) { : modelText(record) } -function reasoningText(record: OpsError) { - return record.reasoningPreset || record.reasoningEffort || '—' +function upstreamSendStateText(value: string | null | undefined) { + if (!value) + return '未记录' + return upstreamSendStateLabels[value] ?? value } @@ -79,22 +74,18 @@ function reasoningText(record: OpsError) {
-
- - - - - -
+ + +
@@ -120,23 +111,8 @@ function reasoningText(record: OpsError) { :columns="opsErrorColumns" :rows="records" :loading="loading" - empty-text="暂无错误明细" + empty-text="当前时段没有错误" > - - - + - - - - - diff --git a/frontend/src/views/usage/components/UsageDetailFieldGrid.vue b/frontend/src/views/usage/components/UsageDetailFieldGrid.vue index 5d6e3fc26..22f5c8356 100644 --- a/frontend/src/views/usage/components/UsageDetailFieldGrid.vue +++ b/frontend/src/views/usage/components/UsageDetailFieldGrid.vue @@ -3,7 +3,7 @@ import { displayValue, fieldLabelClass, fieldValueClass } from '../utils/detail' // 详情弹窗的两列字段网格:label + 值(可选等宽字体),空值以“—”占位。 defineProps<{ - items: Array<{ label: string, value: unknown, mono?: boolean, wrap?: boolean }> + items: Array<{ label: string, value: unknown, mono?: boolean, wrap?: boolean, fullWidth?: boolean }> }>() function valueClass(item: { mono?: boolean, wrap?: boolean }) { @@ -19,7 +19,12 @@ function valueClass(item: { mono?: boolean, wrap?: boolean }) {