mirror of
https://github.com/zyycn/codex-proxy-rs.git
synced 2026-11-06 08:06:19 +00:00
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:
@@ -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
|
||||
)",
|
||||
|
||||
@@ -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 }">
|
||||
|
||||
@@ -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' },
|
||||
|
||||
Reference in New Issue
Block a user