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 occurrence(&self) -> DiagnosticOccurrence {
230 DiagnosticOccurrence {
231 diagnostic_id: self.diagnostic_id,
232 primary_diagnostic_id: self.primary_diagnostic_id,
233 }
234 }
235}
236
237#[derive(Clone, Copy, Serialize)]
244pub struct DiagnosticOccurrence {
245 diagnostic_id: u64,
246 primary_diagnostic_id: Option<u64>,
247}
248impl DiagnosticOccurrence {
249 pub(crate) const fn source_id(&self) -> u64 {
250 self.diagnostic_id
251 }
252 pub(crate) fn from_diagnostic(diagnostic: &crate::Diagnostic) -> Self {
253 Self {
254 diagnostic_id: diagnostic.id(),
255 primary_diagnostic_id: diagnostic.primary_id_for_projection(),
256 }
257 }
258}
259
260#[derive(Clone, Copy, Serialize)]
261#[serde(rename_all = "snake_case")]
262pub enum OperationOutcome {
263 Succeeded,
264 Rejected,
265 Failed,
266 Cancelled,
267 TimedOut,
268 Panicked,
269 Unknown,
270}
271#[derive(Clone, Copy, Serialize)]
272#[serde(rename_all = "snake_case")]
273pub enum PhysicalDispositionFact {
274 NotUsed,
275 Returned,
276 Discarded,
277 Unknown,
278}
279#[derive(Clone, Copy, Serialize)]
280#[serde(rename_all = "snake_case")]
281pub enum BusinessOutcome {
282 Success,
283 Failure,
284 Unknown,
285}
286#[derive(Clone, Copy, Serialize)]
287#[serde(rename_all = "snake_case")]
288pub enum ResponseDelivery {
289 NotStarted,
290 Partial,
291 LocalWriteComplete,
292 Failed,
293 Cancelled,
294 TimedOut,
295 Unknown,
296}
297#[derive(Clone, Copy, Serialize)]
298#[serde(rename_all = "snake_case")]
299pub enum CleanupOutcome {
300 NotRun,
301 Succeeded,
302 Failed,
303 Unknown,
304}
305
306#[derive(Serialize)]
309pub struct DiagnosticOutcomeAxes {
310 pub operation: OperationOutcome,
311 pub physical: PhysicalDispositionFact,
312 pub business: BusinessOutcome,
313 pub business_code: Option<RegisteredDiagnosticCode>,
314 pub delivery: ResponseDelivery,
315 pub bytes_written: Option<u64>,
316 pub cleanup: CleanupOutcome,
317}
318impl Default for DiagnosticOutcomeAxes {
319 fn default() -> Self {
320 Self {
321 operation: OperationOutcome::Unknown,
322 physical: PhysicalDispositionFact::Unknown,
323 business: BusinessOutcome::Unknown,
324 business_code: None,
325 delivery: ResponseDelivery::Unknown,
326 bytes_written: None,
327 cleanup: CleanupOutcome::Unknown,
328 }
329 }
330}
331
332#[cfg(test)]
333mod tests {
334 use super::*;
335 #[test]
336 fn bounded_causes_primary_cleanup_metadata_and_business_code() {
337 let cause = BoundedDiagnosticCause::new(
338 DiagnosticStage::RequestDb,
339 DiagnosticCode::new("db.decode").unwrap(),
340 )
341 .with_database_code(1064)
342 .with_sqlstate("42000")
343 .with_types(
344 None,
345 Some(InlineDiagnosticText::metadata("VARCHAR")),
346 Some(DiagnosticFactUnavailable::OpaqueSource),
347 );
348 let mut primary = BoundedDiagnostic::capture(
349 DiagnosticCategory::UnexpectedError,
350 CaptureSite::FirstObserved,
351 cause,
352 );
353 let id = primary.id();
354 for _ in 0..6 {
355 primary = primary.wrap(cause);
356 }
357 let p = serde_json::to_value(&primary).unwrap();
358 assert_eq!(p["diagnostic_id"], id);
359 assert_eq!(p["omitted_causes"], 3);
360 assert_eq!(p["causes"][0]["sqlstate"]["value"], "42000");
361 assert_eq!(p["causes"][0]["target_type_unavailable"], "opaque_source");
362 assert!(p["causes"][0]["actual_type_unavailable"].is_null());
363 let cleanup = BoundedDiagnostic::capture(
364 DiagnosticCategory::UnexpectedError,
365 CaptureSite::FirstObserved,
366 cause,
367 )
368 .during_cleanup_of(&primary);
369 assert_ne!(cleanup.id(), id);
370 assert_eq!(
371 serde_json::to_value(cleanup).unwrap()["primary_diagnostic_id"],
372 id
373 );
374 let secret =
375 serde_json::to_value(InlineDiagnosticText::metadata("mysql://SECRET@host")).unwrap();
376 assert_eq!(secret["redacted"], true);
377 assert!(!secret.to_string().contains("SECRET"));
378 let long = serde_json::to_value(InlineDiagnosticText::metadata(&"界".repeat(100))).unwrap();
379 assert_eq!(long["truncated"], true);
380 assert!(
381 RegisteredDiagnosticCode::from_registered("INVALID_VALUE", &["INVALID_VALUE"])
382 .is_some()
383 );
384 assert!(RegisteredDiagnosticCode::from_registered("FOREIGN", &["INVALID_VALUE"]).is_none());
385 assert!(
386 RegisteredDiagnosticCode::from_registered("SECRET@host", &["SECRET@host"]).is_none()
387 );
388 }
389}