1use serde_json::{Value, json};
9
10use crate::{
11 correlation::{CompiledTaskId, PlanNodeId, QueryId},
12 error::{QueryError, QueryErrorCode, triage_code_for_query_error},
13};
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
17#[non_exhaustive]
18pub enum TriageCode {
19 Cancelled,
21 NotSelectedByCost,
23 UnsupportedPlanShape,
25 UnsupportedOperator,
27 UnsupportedExpression,
29 UnsupportedType,
31 UnsupportedSource,
33 InvalidInput,
35 InvalidPlan,
37 CapabilityEnvelopeRejected,
39 MemoryReservationDenied,
41 ResourceExhausted,
43 ServiceOverloaded,
45 SourceFailure,
47 DependencyFailure,
49 InvariantViolation,
51 Internal,
53}
54
55impl TriageCode {
56 pub const fn as_str(self) -> &'static str {
58 match self {
59 Self::Cancelled => "cancelled",
60 Self::NotSelectedByCost => "not_selected_by_cost",
61 Self::UnsupportedPlanShape => "unsupported_plan_shape",
62 Self::UnsupportedOperator => "unsupported_operator",
63 Self::UnsupportedExpression => "unsupported_expression",
64 Self::UnsupportedType => "unsupported_type",
65 Self::UnsupportedSource => "unsupported_source",
66 Self::InvalidInput => "invalid_input",
67 Self::InvalidPlan => "invalid_plan",
68 Self::CapabilityEnvelopeRejected => "bounded_rejected",
70 Self::MemoryReservationDenied => "memory_reservation_denied",
71 Self::ResourceExhausted => "resource_exhausted",
72 Self::ServiceOverloaded => "service_overloaded",
73 Self::SourceFailure => "source_failure",
74 Self::DependencyFailure => "dependency_failure",
75 Self::InvariantViolation => "invariant_violation",
76 Self::Internal => "internal",
77 }
78 }
79}
80
81impl std::fmt::Display for TriageCode {
82 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
83 f.write_str(self.as_str())
84 }
85}
86
87impl From<QueryErrorCode> for TriageCode {
88 fn from(code: QueryErrorCode) -> Self {
89 triage_code_for_query_error(code)
90 }
91}
92
93#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
100pub enum TriageVerdict {
101 Healthy,
103 Degraded,
105 Failed,
107 NotSelected,
109}
110
111impl TriageVerdict {
112 pub(crate) const fn as_str(self) -> &'static str {
114 match self {
115 Self::Healthy => "healthy",
116 Self::Degraded => "degraded",
117 Self::Failed => "failed",
118 Self::NotSelected => "not_selected",
119 }
120 }
121}
122
123impl std::fmt::Display for TriageVerdict {
124 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
125 f.write_str(self.as_str())
126 }
127}
128
129#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
131pub enum TriageSeverity {
132 Info,
134 Warning,
136 Error,
138}
139
140impl TriageSeverity {
141 pub(crate) const fn as_str(self) -> &'static str {
143 match self {
144 Self::Info => "info",
145 Self::Warning => "warning",
146 Self::Error => "error",
147 }
148 }
149}
150
151impl std::fmt::Display for TriageSeverity {
152 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
153 f.write_str(self.as_str())
154 }
155}
156
157#[derive(Debug, Clone, PartialEq, Eq)]
159pub struct TriageEvidenceRef {
160 pub(crate) query_id: Option<QueryId>,
161 pub(crate) plan_node_id: Option<PlanNodeId>,
162 pub(crate) compiled_task_id: Option<CompiledTaskId>,
163 pub(crate) surface: &'static str,
164 pub(crate) row_index: Option<usize>,
165}
166
167impl TriageEvidenceRef {
168 pub fn new(surface: &'static str) -> Self {
170 Self { query_id: None, plan_node_id: None, compiled_task_id: None, surface, row_index: None }
171 }
172
173 pub fn with_query_id(mut self, query_id: impl Into<QueryId>) -> Self {
175 self.query_id = Some(query_id.into());
176 self
177 }
178
179 pub fn with_plan_node_id(mut self, plan_node_id: impl Into<PlanNodeId>) -> Self {
181 self.plan_node_id = Some(plan_node_id.into());
182 self
183 }
184
185 pub(crate) fn with_compiled_task_id(mut self, compiled_task_id: impl Into<CompiledTaskId>) -> Self {
186 self.compiled_task_id = Some(compiled_task_id.into());
187 self
188 }
189
190 pub fn with_row_index(mut self, row_index: usize) -> Self {
192 self.row_index = Some(row_index);
193 self
194 }
195
196 pub(crate) fn to_json(&self) -> Value {
198 json!({
199 "query_id": self.query_id.as_ref().map(QueryId::as_str),
200 "plan_node_id": self.plan_node_id.as_ref().map(PlanNodeId::as_str),
201 "compiled_task_id": self.compiled_task_id.as_ref().map(CompiledTaskId::as_str),
202 "surface": self.surface,
203 "row_index": self.row_index,
204 })
205 }
206}
207
208#[derive(Debug, Clone, PartialEq, Eq)]
210pub struct TriageSignal {
211 pub(crate) severity: TriageSeverity,
212 pub code: TriageCode,
214 pub(crate) message: String,
215 pub(crate) evidence: Option<TriageEvidenceRef>,
216}
217
218impl TriageSignal {
219 pub fn new(severity: TriageSeverity, code: TriageCode, message: impl Into<String>) -> Self {
221 Self { severity, code, message: message.into(), evidence: None }
222 }
223
224 pub fn with_evidence(mut self, evidence: TriageEvidenceRef) -> Self {
226 self.evidence = Some(evidence);
227 self
228 }
229
230 pub(crate) fn to_json(&self) -> Value {
232 json!({
233 "severity": self.severity.as_str(),
234 "code": self.code.as_str(),
235 "message": self.message,
236 "evidence": self.evidence.as_ref().map(TriageEvidenceRef::to_json),
237 })
238 }
239}
240
241#[derive(Debug, Clone, PartialEq, Eq)]
243pub struct TriageSummary {
244 pub verdict: TriageVerdict,
246 pub signals: Vec<TriageSignal>,
248}
249
250impl TriageSummary {
251 pub fn new(verdict: TriageVerdict, mut signals: Vec<TriageSignal>) -> Self {
256 signals.sort_by_key(|signal| std::cmp::Reverse(signal.severity));
257 Self { verdict, signals }
258 }
259
260 pub fn healthy() -> Self {
262 Self::new(TriageVerdict::Healthy, Vec::new())
263 }
264
265 pub fn to_json(&self) -> Value {
267 json!({
268 "verdict": self.verdict.as_str(),
269 "signals": self.signals.iter().map(TriageSignal::to_json).collect::<Vec<_>>(),
270 })
271 }
272}
273
274impl QueryError {
275 pub fn triage_summary(&self) -> TriageSummary {
282 let mut evidence = TriageEvidenceRef::new("query_error");
283 if let Some(query_id) = self.query_id().cloned().or_else(|| self.fact_value("query_id").map(QueryId::new)) {
284 evidence = evidence.with_query_id(query_id);
285 }
286 if let Some(node_id) = self.context().boundary.and_then(|boundary| boundary.node_id) {
287 evidence = evidence.with_plan_node_id(node_id);
288 } else if let Some(node_id) = self.fact_value("node_id") {
289 evidence = evidence.with_plan_node_id(node_id);
290 }
291 if let Some(compiled_task_id) = self.fact_value("compiled_task_id") {
292 evidence = evidence.with_compiled_task_id(compiled_task_id);
293 }
294 let signal = TriageSignal::new(TriageSeverity::Error, self.code().into(), self.message().to_string())
295 .with_evidence(evidence);
296 TriageSummary::new(TriageVerdict::Failed, vec![signal])
297 }
298}