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

@@ -145,6 +145,23 @@ pub(crate) enum LocalFailoverClassification {
RetryUpstreamFailure,
}
impl LocalFailoverClassification {
pub(crate) const fn as_str(self) -> &'static str {
match self {
Self::UseDefault => "use_default",
Self::StopStatusCode => "stop_status_code",
Self::StopErrorPattern => "stop_error_pattern",
Self::StopSemanticClientError => "stop_semantic_client_error",
Self::RetrySuccessPattern => "retry_success_pattern",
Self::RetrySemanticCompatibilityError => "retry_semantic_compatibility_error",
Self::RetrySemanticRateLimit => "retry_semantic_rate_limit",
Self::RetrySemanticThinkingError => "retry_semantic_thinking_error",
Self::RetryStatusCode => "retry_status_code",
Self::RetryUpstreamFailure => "retry_upstream_failure",
}
}
}
pub(crate) fn classify_local_failover(
policy: &LocalFailoverPolicy,
input: LocalFailoverInput<'_>,

View File

@@ -1,4 +1,5 @@
use aether_contracts::ExecutionPlan;
use serde_json::{json, Value};
use crate::AppState;
@@ -78,3 +79,43 @@ pub(crate) async fn resolve_local_failover_decision_for_attempt(
.await
.decision
}
pub(crate) fn build_local_error_flow_metadata(
status_code: u16,
response_text: Option<&str>,
analysis: LocalFailoverAnalysis,
) -> Value {
let safe_to_expose = matches!(
analysis.classification,
LocalFailoverClassification::StopSemanticClientError
| LocalFailoverClassification::StopStatusCode
| LocalFailoverClassification::StopErrorPattern
);
let propagation = match analysis.decision {
LocalFailoverDecision::RetryNextCandidate => "suppressed",
LocalFailoverDecision::StopLocalFailover if safe_to_expose => "converted",
LocalFailoverDecision::StopLocalFailover => "suppressed",
LocalFailoverDecision::UseDefault if status_code >= 400 => "passthrough",
LocalFailoverDecision::UseDefault => "none",
};
json!({
"stage": "candidate",
"source": "upstream_response",
"status_code": status_code,
"classification": analysis.classification.as_str(),
"decision": analysis.decision.as_str(),
"retryable": matches!(analysis.decision, LocalFailoverDecision::RetryNextCandidate),
"safe_to_expose": safe_to_expose,
"propagation": propagation,
"message": local_failover_error_message(response_text),
})
}
pub(crate) fn with_error_flow_report_context(
report_context: Option<&Value>,
error_flow: Value,
) -> Option<Value> {
let mut object = report_context?.as_object()?.clone();
object.insert("error_flow".to_string(), error_flow);
Some(Value::Object(object))
}