1use crate::{CaptureSite, DiagnosticCategory, DiagnosticCode, DiagnosticStage};
3use serde::{Serialize, Serializer};
4
5#[derive(Clone, Copy)]
7pub struct InlineDiagnosticText {
8 bytes: [u8; 192],
9 len: u8,
10 truncated: bool,
11 redacted: bool,
12}
13impl InlineDiagnosticText {
14 pub fn metadata(value: &str) -> Self {
15 let mut out = Self {
16 bytes: [0; 192],
17 len: 0,
18 truncated: false,
19 redacted: false,
20 };
21 let mut len = value.len().min(192);
22 while !value.is_char_boundary(len) {
23 len -= 1;
24 }
25 out.truncated = len < value.len();
26 if !value[..len]
27 .chars()
28 .all(|c| c.is_alphanumeric() || "_:<>[],(); &*.-".contains(c))
29 {
30 out.redacted = true;
31 return out;
32 }
33 out.bytes[..len].copy_from_slice(&value.as_bytes()[..len]);
34 out.len = len as u8;
35 out.truncated = len < value.len();
36 out
37 }
38}
39impl Serialize for InlineDiagnosticText {
40 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
41 use serde::ser::SerializeStruct;
42 let mut value = serializer.serialize_struct("InlineDiagnosticText", 3)?;
43 value.serialize_field(
44 "value",
45 std::str::from_utf8(&self.bytes[..self.len as usize]).unwrap_or(""),
46 )?;
47 value.serialize_field("truncated", &self.truncated)?;
48 value.serialize_field("redacted", &self.redacted)?;
49 value.end()
50 }
51}
52
53#[derive(Clone, Copy, Serialize)]
54#[serde(rename_all = "snake_case")]
55pub enum DiagnosticFactUnavailable {
56 OpaqueSource,
57 MetadataUnavailable,
58 NotApplicable,
59}
60
61#[derive(Clone, Copy, Serialize)]
63pub struct RegisteredDiagnosticCode(&'static str);
64impl RegisteredDiagnosticCode {
65 pub fn from_registered(code: &'static str, registry: &[&'static str]) -> Option<Self> {
66 (!code.is_empty()
67 && code.len() <= 128
68 && code
69 .bytes()
70 .all(|b| b.is_ascii_alphanumeric() || b"._-".contains(&b))
71 && registry.len() <= 256
72 && registry.contains(&code))
73 .then_some(Self(code))
74 }
75}
76
77#[derive(Clone, Copy, Serialize)]
79pub struct BoundedDiagnosticCause {
80 stage: DiagnosticStage,
81 code: DiagnosticCode,
82 io_kind: Option<DiagnosticCode>,
83 os_code: Option<i32>,
84 database_code: Option<u32>,
85 sqlstate: Option<InlineDiagnosticText>,
86 column_index: Option<u64>,
87 column_count: Option<u64>,
88 target_rust_type: Option<InlineDiagnosticText>,
89 actual_db_type: Option<InlineDiagnosticText>,
90 object: Option<InlineDiagnosticText>,
91 unavailable: Option<DiagnosticFactUnavailable>,
92 target_type_unavailable: Option<DiagnosticFactUnavailable>,
93 actual_type_unavailable: Option<DiagnosticFactUnavailable>,
94}
95impl BoundedDiagnosticCause {
96 pub fn new(stage: DiagnosticStage, code: DiagnosticCode) -> Self {
97 Self {
98 stage,
99 code,
100 io_kind: None,
101 os_code: None,
102 database_code: None,
103 sqlstate: None,
104 column_index: None,
105 column_count: None,
106 target_rust_type: None,
107 actual_db_type: None,
108 object: None,
109 unavailable: None,
110 target_type_unavailable: Some(DiagnosticFactUnavailable::MetadataUnavailable),
111 actual_type_unavailable: Some(DiagnosticFactUnavailable::MetadataUnavailable),
112 }
113 }
114 pub fn with_system(mut self, kind: DiagnosticCode, os: Option<i32>) -> Self {
115 self.io_kind = Some(kind);
116 self.os_code = os;
117 self
118 }
119 pub fn with_database_code(mut self, code: u32) -> Self {
120 self.database_code = Some(code);
121 self
122 }
123 pub fn with_sqlstate(mut self, state: &str) -> Self {
124 self.sqlstate = (state.len() == 5
125 && state
126 .bytes()
127 .all(|b| b.is_ascii_uppercase() || b.is_ascii_digit()))
128 .then(|| InlineDiagnosticText::metadata(state));
129 self
130 }
131 pub fn with_column(mut self, index: Option<u64>, count: Option<u64>) -> Self {
132 self.column_index = index;
133 self.column_count = count;
134 self
135 }
136 pub fn with_types(
137 mut self,
138 target: Option<InlineDiagnosticText>,
139 actual: Option<InlineDiagnosticText>,
140 unavailable: Option<DiagnosticFactUnavailable>,
141 ) -> Self {
142 self.target_rust_type = target;
143 self.actual_db_type = actual;
144 self.unavailable = unavailable;
145 self.target_type_unavailable = if target.is_some() {
146 None
147 } else {
148 Some(unavailable.unwrap_or(DiagnosticFactUnavailable::MetadataUnavailable))
149 };
150 self.actual_type_unavailable = if actual.is_some() {
151 None
152 } else {
153 Some(unavailable.unwrap_or(DiagnosticFactUnavailable::MetadataUnavailable))
154 };
155 self
156 }
157 pub fn with_object(mut self, object: InlineDiagnosticText) -> Self {
158 self.object = Some(object);
159 self
160 }
161}
162
163#[derive(Serialize)]
166pub struct BoundedDiagnostic {
167 diagnostic_id: u64,
168 primary_diagnostic_id: Option<u64>,
169 category: DiagnosticCategory,
170 capture_site: CaptureSite,
171 origin_file: &'static str,
172 origin_line: u32,
173 origin_column: u32,
174 causes: [Option<BoundedDiagnosticCause>; 4],
175 cause_count: usize,
176 omitted_causes: u64,
177 stack_status: &'static str,
178}
179impl BoundedDiagnostic {
180 #[track_caller]
181 pub fn capture(
182 category: DiagnosticCategory,
183 site: CaptureSite,
184 cause: BoundedDiagnosticCause,
185 ) -> Self {
186 let origin = std::panic::Location::caller();
187 let file = origin
188 .file()
189 .rsplit("/crates/")
190 .next()
191 .unwrap_or(origin.file());
192 let file = if file.starts_with('/') || file.contains('\\') {
193 file.rsplit(['/', '\\']).next().unwrap_or("unknown")
194 } else {
195 file
196 };
197 Self {
198 diagnostic_id: crate::diagnostic::NEXT_ID
199 .fetch_add(1, std::sync::atomic::Ordering::Relaxed),
200 primary_diagnostic_id: None,
201 category,
202 capture_site: site,
203 origin_file: file,
204 origin_line: origin.line(),
205 origin_column: origin.column(),
206 causes: [Some(cause), None, None, None],
207 cause_count: 1,
208 omitted_causes: 0,
209 stack_status: "unavailable_bounded_capture",
210 }
211 }
212 pub fn wrap(mut self, cause: BoundedDiagnosticCause) -> Self {
213 if self.cause_count == self.causes.len() {
214 self.omitted_causes = self.omitted_causes.saturating_add(1);
215 } else {
216 self.causes.copy_within(0..self.cause_count, 1);
217 self.causes[0] = Some(cause);
218 self.cause_count += 1;
219 }
220 self
221 }
222 pub fn during_cleanup_of(mut self, primary: &Self) -> Self {
223 self.primary_diagnostic_id = Some(primary.diagnostic_id);
224 self
225 }
226 pub fn id(&self) -> u64 {
227 self.diagnostic_id
228 }
229 pub fn category(&self) -> DiagnosticCategory {
230 self.category
231 }
232 pub fn during_cleanup_occurrence(mut self, primary: DiagnosticOccurrence) -> Self {
234 self.primary_diagnostic_id = Some(primary.source_id());
235 self
236 }
237 pub fn occurrence(&self) -> DiagnosticOccurrence {
238 DiagnosticOccurrence {
239 diagnostic_id: self.diagnostic_id,
240 primary_diagnostic_id: self.primary_diagnostic_id,
241 }
242 }
243 pub(crate) fn primary_id_for_projection(&self) -> Option<u64> {
244 self.primary_diagnostic_id
245 }
246 pub(crate) fn simple_projection(&self) -> Option<(DiagnosticCategory, CaptureSite,
247 &'static str, u32, u32, DiagnosticStage, DiagnosticCode)> {
248 let cause = self.causes[0]?;
249 (self.cause_count == 1 && self.omitted_causes == 0
250 && cause.io_kind.is_none() && cause.os_code.is_none()
251 && cause.database_code.is_none() && cause.sqlstate.is_none()
252 && cause.column_index.is_none() && cause.column_count.is_none()
253 && cause.target_rust_type.is_none() && cause.actual_db_type.is_none()
254 && cause.object.is_none() && cause.unavailable.is_none()
255 && matches!(cause.target_type_unavailable,
256 Some(DiagnosticFactUnavailable::MetadataUnavailable))
257 && matches!(cause.actual_type_unavailable,
258 Some(DiagnosticFactUnavailable::MetadataUnavailable)))
259 .then_some((self.category, self.capture_site, self.origin_file,
260 self.origin_line, self.origin_column, cause.stage, cause.code))
261 }
262}
263
264#[derive(Clone, Copy, Serialize)]
271pub struct DiagnosticOccurrence {
272 diagnostic_id: u64,
273 primary_diagnostic_id: Option<u64>,
274}
275impl DiagnosticOccurrence {
276 pub fn matches_bounded_diagnostic(&self, diagnostic: &BoundedDiagnostic) -> bool {
278 self.diagnostic_id == diagnostic.id()
279 }
280 pub fn matches_diagnostic(&self, diagnostic: &crate::Diagnostic) -> bool {
282 self.diagnostic_id == diagnostic.id()
283 }
284 pub(crate) const fn source_id(&self) -> u64 {
285 self.diagnostic_id
286 }
287 pub(crate) fn from_diagnostic(diagnostic: &crate::Diagnostic) -> Self {
288 Self {
289 diagnostic_id: diagnostic.id(),
290 primary_diagnostic_id: diagnostic.primary_id_for_projection(),
291 }
292 }
293}
294
295#[derive(Clone, Copy, Serialize)]
296#[serde(rename_all = "snake_case")]
297pub enum OperationOutcome {
298 Succeeded,
299 Rejected,
300 Failed,
301 Cancelled,
302 TimedOut,
303 Panicked,
304 Unknown,
305}
306#[derive(Clone, Copy, Serialize)]
307#[serde(rename_all = "snake_case")]
308pub enum PhysicalDispositionFact {
309 NotUsed,
310 Returned,
311 Discarded,
312 Unknown,
313}
314#[derive(Clone, Copy, Serialize)]
315#[serde(rename_all = "snake_case")]
316pub enum BusinessOutcome {
317 Success,
318 Failure,
319 Unknown,
320}
321#[derive(Clone, Copy, Serialize)]
322#[serde(rename_all = "snake_case")]
323pub enum ResponseDelivery {
324 NotStarted,
325 Partial,
326 LocalWriteComplete,
327 Failed,
328 Cancelled,
329 TimedOut,
330 Unknown,
331}
332#[derive(Clone, Copy, Serialize)]
333#[serde(rename_all = "snake_case")]
334pub enum CleanupOutcome {
335 NotRun,
336 Succeeded,
337 Failed,
338 Unknown,
339}
340
341#[derive(Clone, Copy, Serialize)]
344pub struct DiagnosticOutcomeAxes {
345 pub operation: OperationOutcome,
346 pub physical: PhysicalDispositionFact,
347 pub business: BusinessOutcome,
348 pub business_code: Option<RegisteredDiagnosticCode>,
349 pub delivery: ResponseDelivery,
350 pub bytes_written: Option<u64>,
351 pub cleanup: CleanupOutcome,
352}
353impl Default for DiagnosticOutcomeAxes {
354 fn default() -> Self {
355 Self {
356 operation: OperationOutcome::Unknown,
357 physical: PhysicalDispositionFact::Unknown,
358 business: BusinessOutcome::Unknown,
359 business_code: None,
360 delivery: ResponseDelivery::Unknown,
361 bytes_written: None,
362 cleanup: CleanupOutcome::Unknown,
363 }
364 }
365}
366
367#[cfg(test)]
368mod tests {
369 use super::*;
370 #[test]
371 fn bounded_causes_primary_cleanup_metadata_and_business_code() {
372 let cause = BoundedDiagnosticCause::new(
373 DiagnosticStage::RequestDb,
374 DiagnosticCode::new("db.decode").unwrap(),
375 )
376 .with_database_code(1064)
377 .with_sqlstate("42000")
378 .with_types(
379 None,
380 Some(InlineDiagnosticText::metadata("VARCHAR")),
381 Some(DiagnosticFactUnavailable::OpaqueSource),
382 );
383 let mut primary = BoundedDiagnostic::capture(
384 DiagnosticCategory::UnexpectedError,
385 CaptureSite::FirstObserved,
386 cause,
387 );
388 let id = primary.id();
389 for _ in 0..6 {
390 primary = primary.wrap(cause);
391 }
392 let p = serde_json::to_value(&primary).unwrap();
393 assert_eq!(p["diagnostic_id"], id);
394 assert_eq!(p["omitted_causes"], 3);
395 assert_eq!(p["causes"][0]["sqlstate"]["value"], "42000");
396 assert_eq!(p["causes"][0]["target_type_unavailable"], "opaque_source");
397 assert!(p["causes"][0]["actual_type_unavailable"].is_null());
398 let cleanup = BoundedDiagnostic::capture(
399 DiagnosticCategory::UnexpectedError,
400 CaptureSite::FirstObserved,
401 cause,
402 )
403 .during_cleanup_of(&primary);
404 assert_ne!(cleanup.id(), id);
405 assert_eq!(
406 serde_json::to_value(cleanup).unwrap()["primary_diagnostic_id"],
407 id
408 );
409 let secret =
410 serde_json::to_value(InlineDiagnosticText::metadata("mysql://SECRET@host")).unwrap();
411 assert_eq!(secret["redacted"], true);
412 assert!(!secret.to_string().contains("SECRET"));
413 let long = serde_json::to_value(InlineDiagnosticText::metadata(&"界".repeat(100))).unwrap();
414 assert_eq!(long["truncated"], true);
415 assert!(
416 RegisteredDiagnosticCode::from_registered("INVALID_VALUE", &["INVALID_VALUE"])
417 .is_some()
418 );
419 assert!(RegisteredDiagnosticCode::from_registered("FOREIGN", &["INVALID_VALUE"]).is_none());
420 assert!(
421 RegisteredDiagnosticCode::from_registered("SECRET@host", &["SECRET@host"]).is_none()
422 );
423 }
424}