feat(observability): 引入错误链路 error_flow 元数据并区分上游/客户端错误

- 网关在本地 failover 时构建 error_flow 元数据(分类/决策/传播策略),写入 report_context
- scheduler-core 解析并透传 error_flow 至候选 extra_data
- admin usage 详情拆分 request/upstream/client/failure_summary 错误域,敏感上游错误标记为 suppressed
- 前端 RequestDetailDrawer 拆出"返回客户端"与"上游响应"双错误卡片
- HorizontalRequestTimeline 节点详情展示真实请求错误及 error_flow 标签
This commit is contained in:
fawney19
2026-04-25 16:58:17 +08:00
parent bc97e383d3
commit 00744c0ce5
9 changed files with 997 additions and 44 deletions

View File

@@ -23,6 +23,7 @@ pub struct SchedulerRequestCandidateReportContext {
pub header_rules: Option<Value>,
pub body_rules: Option<Value>,
pub proxy: Option<Value>,
pub error_flow: Option<Value>,
}
#[derive(Debug, Clone, PartialEq)]
@@ -47,6 +48,19 @@ pub struct SchedulerExecutionRequestCandidateSeed {
pub report_context: Value,
}
#[derive(Debug, Clone, Default)]
struct ReportCandidateExtraDataInput {
client_api_format: Option<String>,
provider_api_format: Option<String>,
upstream_url: Option<String>,
mapped_model: Option<String>,
key_name: Option<String>,
header_rules: Option<Value>,
body_rules: Option<Value>,
proxy: Option<Value>,
error_flow: Option<Value>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SchedulerRequestCandidateStatusUpdate {
pub status: RequestCandidateStatus,
@@ -123,6 +137,10 @@ pub fn parse_request_candidate_report_context(
.get("proxy")
.cloned()
.filter(|value| !value.is_null()),
error_flow: report_context
.get("error_flow")
.cloned()
.filter(|value| !value.is_null()),
})
}
@@ -151,6 +169,7 @@ pub fn resolve_report_request_candidate_slot(
header_rules,
body_rules,
proxy,
error_flow,
} = metadata;
let request_id = request_id?;
let synthesized_extra_data = build_report_candidate_extra_data(ReportCandidateExtraDataInput {
@@ -162,6 +181,7 @@ pub fn resolve_report_request_candidate_slot(
header_rules,
body_rules,
proxy,
error_flow,
});
let created_at_unix_ms = matched_candidate
.as_ref()
@@ -325,6 +345,7 @@ pub fn build_local_request_candidate_status_record(
header_rules: metadata.header_rules.clone(),
body_rules: metadata.body_rules.clone(),
proxy: metadata.proxy.clone(),
error_flow: metadata.error_flow.clone(),
});
let created_at_unix_ms = started_at_unix_ms.or(finished_at_unix_ms);
@@ -526,17 +547,6 @@ fn next_candidate_index(candidates: &[StoredRequestCandidate]) -> u32 {
.unwrap_or_default()
}
struct ReportCandidateExtraDataInput {
client_api_format: Option<String>,
provider_api_format: Option<String>,
upstream_url: Option<String>,
mapped_model: Option<String>,
key_name: Option<String>,
header_rules: Option<Value>,
body_rules: Option<Value>,
proxy: Option<Value>,
}
fn build_report_candidate_extra_data(input: ReportCandidateExtraDataInput) -> Option<Value> {
let ReportCandidateExtraDataInput {
client_api_format,
@@ -547,6 +557,7 @@ fn build_report_candidate_extra_data(input: ReportCandidateExtraDataInput) -> Op
header_rules,
body_rules,
proxy,
error_flow,
} = input;
let mut extra_data = Map::with_capacity(8);
extra_data.insert("gateway_execution_runtime".to_string(), Value::Bool(true));
@@ -581,6 +592,9 @@ fn build_report_candidate_extra_data(input: ReportCandidateExtraDataInput) -> Op
if let Some(proxy) = proxy {
extra_data.insert("proxy".to_string(), proxy);
}
if let Some(error_flow) = error_flow {
extra_data.insert("error_flow".to_string(), error_flow);
}
(!extra_data.is_empty()).then_some(Value::Object(extra_data))
}
@@ -729,6 +743,11 @@ mod tests {
"node_id": "proxy-node-1",
"node_name": "edge-1",
"source": "provider"
},
"error_flow": {
"classification": "retry_upstream_failure",
"decision": "retry_next_candidate",
"propagation": "suppressed"
}
})))
.expect("metadata");
@@ -777,6 +796,13 @@ mod tests {
.map(Vec::len),
Some(1)
);
assert_eq!(
slot.extra_data
.as_ref()
.and_then(|value| value.get("error_flow"))
.and_then(|value| value.get("propagation")),
Some(&json!("suppressed"))
);
}
#[test]