1use crate::{DiagnosticSubmission, EmergencyDiagnosticHandle, EventContext, Observer};
3use saddle_core::{
4 BoundedDiagnostic, ContextFact, Diagnostic, DiagnosticCategory, DiagnosticCode,
5 DiagnosticOccurrence, DiagnosticOutcomeAxes, RequestExecutionView,
6};
7use serde::Serialize;
8mod original;
9pub use original::{
10 OriginalCaptureState, UnrootedCaptureFacts, UnrootedDiagnosticScope, original_capture_layout,
11 database_background_error,
12};
13
14pub fn request_identity_group(
16 call: &saddle_core::CallContext,
17 event: &EventContext,
18 zone: ContextFact<saddle_core::ContextLabel>,
19) -> Result<saddle_core::RequestIdentityGroup, saddle_core::ContextConflict> {
20 saddle_core::RequestIdentityGroup::from_validated(
21 call,
22 event.diagnostic_request(),
23 event.diagnostic_route(),
24 event.diagnostic_attempt(),
25 zone,
26 )
27}
28pub fn request_child_view(
30 parent: &RequestExecutionView,
31 call: &saddle_core::CallContext,
32 event: &EventContext,
33) -> Result<RequestExecutionView, saddle_core::ContextConflict> {
34 parent.child(
35 call,
36 event.diagnostic_request(),
37 event.diagnostic_route(),
38 event.diagnostic_attempt(),
39 )
40}
41
42#[derive(Clone, Copy, Debug, Serialize)]
44#[serde(rename_all = "snake_case")]
45pub enum RootRequestEvent {
46 Ingress,
47 Admission,
48 Handler,
49 Database,
50 Outbound,
51 Response,
52 Finalization,
53 Supervision,
54}
55
56#[derive(Clone, Copy, Serialize)]
58#[serde(rename_all = "snake_case")]
59pub enum RequestTransactionFact {
60 Committed,
61 Rejected,
62 Unknown,
63}
64#[derive(Clone, Copy, Serialize)]
65pub struct RootOutcomeFacts {
66 pub axes: DiagnosticOutcomeAxes,
67 pub transaction: ContextFact<RequestTransactionFact>,
68}
69impl Default for RootOutcomeFacts {
70 fn default() -> Self {
71 Self {
72 axes: DiagnosticOutcomeAxes::default(),
73 transaction: ContextFact::Unavailable,
74 }
75 }
76}
77
78#[derive(Serialize)]
79#[serde(untagged)]
80enum SourceDetail<'a> {
81 Bounded(&'a BoundedDiagnostic),
82 Existing(&'a Diagnostic),
83}
84
85fn timestamp() -> u128 {
86 std::time::SystemTime::now()
87 .duration_since(std::time::UNIX_EPOCH)
88 .unwrap_or_default()
89 .as_millis()
90}
91
92#[derive(Serialize)]
93struct RootRecord<'a> {
94 schema_version: u8,
95 timestamp_unix_ms: u128,
96 level: &'static str,
97 elapsed_ms: Option<u64>,
98 event: &'static str,
99 stage: RootRequestEvent,
100 context: &'a RequestExecutionView,
101 source_context: Option<&'a RequestExecutionView>,
102 occurrence: Option<DiagnosticOccurrence>,
103 classification: Option<DiagnosticCode>,
104 source_submission: Option<DiagnosticSubmission>,
105 original_capture: Option<OriginalCaptureState>,
106 axes: RootOutcomeFacts,
107 source_outcome: Option<RootOutcomeFacts>,
108 category: Option<DiagnosticCategory>,
109 detail_status: &'static str,
110 diagnostic: Option<SourceDetail<'a>>,
111}
112
113#[must_use = "carry the source receipt to its declared terminal boundary"]
125pub struct RootRequestFailure {
126 source: RequestExecutionView,
127 occurrence: DiagnosticOccurrence,
128 classification: DiagnosticCode,
129 source_submission: DiagnosticSubmission,
130 category: DiagnosticCategory,
131 outcome: RootOutcomeFacts,
132 original_capture: OriginalCaptureState,
133}
134
135#[derive(Clone, Copy, Serialize)]
137pub struct PublicRequestFailure {
138 occurrence: DiagnosticOccurrence,
139 classification: DiagnosticCode,
140 source_submission: DiagnosticSubmission,
141 terminal_submission: DiagnosticSubmission,
142 category: DiagnosticCategory,
143 source_outcome: RootOutcomeFacts,
144 original_capture: OriginalCaptureState,
145}
146
147#[must_use = "the supervisor must consume the actual task result"]
163pub struct RootSupervisionReturn<T> {
164 result: Result<T, RootRequestFailure>,
165}
166impl<T> RootSupervisionReturn<T> {
167 pub fn completed(value: T) -> Self {
168 Self { result: Ok(value) }
169 }
170 pub fn failed(failure: RootRequestFailure) -> Self {
171 Self {
172 result: Err(failure),
173 }
174 }
175 pub fn consume(self) -> Result<T, RootRequestFailure> {
176 self.result
177 }
178}
179
180pub struct RootDiagnosticScope<'a> {
182 view: &'a RequestExecutionView,
183 output: Option<&'a EmergencyDiagnosticHandle>,
184}
185impl<'a> RootDiagnosticScope<'a> {
186 pub fn new(
187 view: &'a RequestExecutionView,
188 output: Option<&'a EmergencyDiagnosticHandle>,
189 ) -> Self {
190 Self { view, output }
191 }
192 pub fn source(
195 &self,
196 diagnostic: BoundedDiagnostic,
197 classification: DiagnosticCode,
198 stage: RootRequestEvent,
199 axes: RootOutcomeFacts,
200 ) -> RootRequestFailure {
201 self.submit_source(
202 diagnostic.occurrence(),
203 diagnostic.category(),
204 SourceDetail::Bounded(&diagnostic),
205 classification,
206 stage,
207 axes,
208 )
209 }
210 pub fn source_existing(
213 &self,
214 diagnostic: Diagnostic,
215 classification: DiagnosticCode,
216 stage: RootRequestEvent,
217 axes: RootOutcomeFacts,
218 ) -> RootRequestFailure {
219 self.submit_source(
220 diagnostic.occurrence(),
221 diagnostic.category(),
222 SourceDetail::Existing(&diagnostic),
223 classification,
224 stage,
225 axes,
226 )
227 }
228 fn submit_source(
229 &self,
230 occurrence: DiagnosticOccurrence,
231 category: DiagnosticCategory,
232 diagnostic: SourceDetail<'_>,
233 classification: DiagnosticCode,
234 stage: RootRequestEvent,
235 axes: RootOutcomeFacts,
236 ) -> RootRequestFailure {
237 let record = RootRecord {
238 schema_version: 2,
239 timestamp_unix_ms: timestamp(),
240 elapsed_ms: None,
241 level: if matches!(category, DiagnosticCategory::ExpectedRejection) {
242 "warn"
243 } else {
244 "error"
245 },
246 event: "request_failure_source",
247 stage,
248 context: self.view,
249 source_context: None,
250 occurrence: Some(occurrence),
251 classification: Some(classification),
252 source_submission: None,
253 original_capture: Some(OriginalCaptureState::LegacyProjectionOnly),
254 axes,
255 diagnostic: Some(diagnostic),
256 source_outcome: None,
257 category: Some(category),
258 detail_status: "bounded",
259 };
260 let fallback = RootRecord {
261 diagnostic: None,
262 detail_status: "omitted_encoding_capacity",
263 ..record
264 };
265 let submission = self
266 .output
267 .map_or(DiagnosticSubmission::OutputUnavailable, |output| {
268 output.submit_fixed_with_fallback(&record, &fallback)
269 });
270 RootRequestFailure {
271 source: self.view.clone(),
272 occurrence,
273 classification,
274 source_submission: submission,
275 category,
276 outcome: axes,
277 original_capture: OriginalCaptureState::LegacyProjectionOnly,
278 }
279 }
280 pub fn ordinary(
283 &self,
284 observer: &Observer,
285 stage: RootRequestEvent,
286 axes: RootOutcomeFacts,
287 ) -> DiagnosticSubmission {
288 self.ordinary_at(observer, stage, axes, None, "request_stage")
289 }
290 fn ordinary_at(
291 &self,
292 observer: &Observer,
293 stage: RootRequestEvent,
294 axes: RootOutcomeFacts,
295 elapsed_ms: Option<u64>,
296 event: &'static str,
297 ) -> DiagnosticSubmission {
298 observer.emit_root_record(&RootRecord {
299 schema_version: 2,
300 timestamp_unix_ms: timestamp(),
301 level: "info",
302 elapsed_ms,
303 event,
304 stage,
305 context: self.view,
306 source_context: None,
307 occurrence: None,
308 classification: None,
309 source_submission: None,
310 original_capture: None,
311 axes,
312 diagnostic: None,
313 source_outcome: None,
314 category: None,
315 detail_status: "not_applicable",
316 })
317 }
318 pub fn start_stage(
321 self,
322 observer: &'a Observer,
323 stage: RootRequestEvent,
324 ) -> RootActiveStage<'a> {
325 self.bounded_event(
326 stage,
327 RootOutcomeFacts::default(),
328 Some(0),
329 "request_stage_started",
330 );
331 RootActiveStage {
332 scope: self,
333 observer,
334 stage,
335 started: std::time::Instant::now(),
336 finished: false,
337 }
338 }
339 fn bounded_event(
340 &self,
341 stage: RootRequestEvent,
342 axes: RootOutcomeFacts,
343 elapsed_ms: Option<u64>,
344 event: &'static str,
345 ) -> DiagnosticSubmission {
346 self.output
347 .map_or(DiagnosticSubmission::OutputUnavailable, |output| {
348 output.submit_fixed_record(&RootRecord {
349 schema_version: 2,
350 timestamp_unix_ms: timestamp(),
351 level: "info",
352 elapsed_ms,
353 event,
354 stage,
355 context: self.view,
356 source_context: None,
357 occurrence: None,
358 classification: None,
359 source_submission: None,
360 original_capture: None,
361 axes,
362 source_outcome: None,
363 category: None,
364 detail_status: "not_applicable",
365 diagnostic: None,
366 })
367 })
368 }
369}
370
371impl RootRequestFailure {
372 fn into_public(self, terminal_submission: DiagnosticSubmission) -> PublicRequestFailure {
373 PublicRequestFailure {
374 occurrence: self.occurrence,
375 classification: self.classification,
376 source_submission: self.source_submission,
377 terminal_submission,
378 category: self.category,
379 source_outcome: self.outcome,
380 original_capture: self.original_capture,
381 }
382 }
383 pub fn original_capture(&self) -> OriginalCaptureState {
384 self.original_capture
385 }
386 pub fn occurrence(&self) -> DiagnosticOccurrence {
387 self.occurrence
388 }
389 pub fn submission(&self) -> DiagnosticSubmission {
390 self.source_submission
391 }
392 pub fn source_view(&self) -> &RequestExecutionView {
393 &self.source
394 }
395 pub fn map_classification(mut self, classification: DiagnosticCode) -> Self {
396 self.classification = classification;
397 self
398 }
399 pub fn boundary(
401 self,
402 current: &RequestExecutionView,
403 output: Option<&EmergencyDiagnosticHandle>,
404 stage: RootRequestEvent,
405 axes: RootOutcomeFacts,
406 ) -> Result<(Self, DiagnosticSubmission), Self> {
407 self.boundary_at(current, output, stage, axes, None, "request_failure_boundary")
408 }
409 fn boundary_at(
410 self,
411 current: &RequestExecutionView,
412 output: Option<&EmergencyDiagnosticHandle>,
413 stage: RootRequestEvent,
414 axes: RootOutcomeFacts,
415 elapsed_ms: Option<u64>,
416 event: &'static str,
417 ) -> Result<(Self, DiagnosticSubmission), Self> {
418 if !self.source.same_request(current) {
419 return Err(self);
420 }
421 let submission = output.map_or(DiagnosticSubmission::OutputUnavailable, |output| {
422 output.submit_fixed_record(&RootRecord {
423 schema_version: 2,
424 timestamp_unix_ms: timestamp(),
425 level: "info",
426 elapsed_ms,
427 event,
428 stage,
429 context: current,
430 source_context: Some(&self.source),
431 occurrence: Some(self.occurrence),
432 classification: Some(self.classification),
433 source_submission: Some(self.source_submission),
434 original_capture: Some(self.original_capture),
435 axes,
436 diagnostic: None,
437 source_outcome: Some(self.outcome),
438 category: Some(self.category),
439 detail_status: "source_reference_only",
440 })
441 });
442 Ok((self, submission))
443 }
444 pub fn finish(
447 self,
448 current: &RequestExecutionView,
449 output: Option<&EmergencyDiagnosticHandle>,
450 axes: RootOutcomeFacts,
451 ) -> Result<PublicRequestFailure, Self> {
452 let (receipt, terminal_submission) =
453 self.boundary(current, output, RootRequestEvent::Finalization, axes)?;
454 Ok(PublicRequestFailure {
455 occurrence: receipt.occurrence,
456 classification: receipt.classification,
457 source_submission: receipt.source_submission,
458 terminal_submission,
459 category: receipt.category,
460 source_outcome: receipt.outcome,
461 original_capture: receipt.original_capture,
462 })
463 }
464}
465
466pub struct RootActiveStage<'a> {
470 scope: RootDiagnosticScope<'a>,
471 observer: &'a Observer,
472 stage: RootRequestEvent,
473 started: std::time::Instant,
474 finished: bool,
475}
476impl RootActiveStage<'_> {
477 fn finish_metrics(&mut self, outcome: saddle_core::OperationOutcome) -> u64 {
478 self.finished = true;
479 let elapsed = u64::try_from(self.started.elapsed().as_millis()).unwrap_or(u64::MAX);
480 use crate::{Stage, StageOutcome};
481 use saddle_core::OperationOutcome as Outcome;
482 let stage = match self.stage {
483 RootRequestEvent::Ingress => Some(Stage::Ingress),
484 RootRequestEvent::Admission => Some(Stage::Admission),
485 RootRequestEvent::Handler => Some(Stage::Handler),
486 RootRequestEvent::Database => Some(Stage::Database),
487 RootRequestEvent::Outbound => Some(Stage::ProfuseContract),
488 RootRequestEvent::Response => Some(Stage::Response),
489 RootRequestEvent::Finalization => Some(Stage::ResourceFinalization),
490 RootRequestEvent::Supervision => None,
491 };
492 let outcome = match outcome {
493 Outcome::Succeeded => Some(StageOutcome::Success),
494 Outcome::Rejected => Some(StageOutcome::Rejected),
495 Outcome::Failed | Outcome::Panicked => Some(StageOutcome::Failure),
496 Outcome::Cancelled | Outcome::TimedOut => Some(StageOutcome::Cancelled),
497 Outcome::Unknown => None,
498 };
499 if let (Some(stage), Some(outcome)) = (stage, outcome) {
500 self.observer
501 .inner
502 .metrics
503 .stage_finished(stage, outcome, elapsed);
504 }
505 elapsed
506 }
507 pub fn finish_nonfailure(
509 mut self,
510 facts: RootOutcomeFacts,
511 ) -> Result<DiagnosticSubmission, Self> {
512 if !matches!(
513 facts.axes.operation,
514 saddle_core::OperationOutcome::Succeeded | saddle_core::OperationOutcome::Rejected
515 ) {
516 return Err(self);
517 }
518 let elapsed = self.finish_metrics(facts.axes.operation);
519 Ok(self
520 .scope
521 .bounded_event(self.stage, facts, Some(elapsed), "request_stage_finished"))
522 }
523 pub fn finish_failure_public(
525 self,
526 failure: RootRequestFailure,
527 facts: RootOutcomeFacts,
528 ) -> Result<PublicRequestFailure, (Self, RootRequestFailure)> {
529 self.finish_failure(failure, facts)
530 .map(|(failure, submission)| failure.into_public(submission))
531 }
532 #[allow(clippy::result_large_err)] pub fn finish_failure(
535 self,
536 failure: RootRequestFailure,
537 facts: RootOutcomeFacts,
538 ) -> Result<(RootRequestFailure, DiagnosticSubmission), (Self, RootRequestFailure)> {
539 self.finish_failure_record(failure, facts, "request_failure_boundary")
540 }
541 #[allow(clippy::result_large_err)] pub fn finish_failure_retained(
545 self,
546 failure: RootRequestFailure,
547 facts: RootOutcomeFacts,
548 ) -> Result<(RootRequestFailure, DiagnosticSubmission), (Self, RootRequestFailure)> {
549 self.finish_failure_record(failure, facts, "request_stage_finished")
550 }
551 #[allow(clippy::result_large_err)] fn finish_failure_record(
553 mut self,
554 failure: RootRequestFailure,
555 facts: RootOutcomeFacts,
556 event: &'static str,
557 ) -> Result<(RootRequestFailure, DiagnosticSubmission), (Self, RootRequestFailure)> {
558 if !failure.source.same_request(self.scope.view) {
559 return Err((self, failure));
560 }
561 let elapsed = self.finish_metrics(facts.axes.operation);
562 match failure.boundary_at(
563 self.scope.view,
564 self.scope.output,
565 self.stage,
566 facts,
567 Some(elapsed),
568 event,
569 ) {
570 Ok(result) => Ok(result),
571 Err(failure) => Err((self, failure)),
572 }
573 }
574}
575impl Drop for RootActiveStage<'_> {
576 fn drop(&mut self) {
577 if !self.finished {
578 let elapsed = self.finish_metrics(saddle_core::OperationOutcome::Cancelled);
579 let facts = RootOutcomeFacts {
580 axes: DiagnosticOutcomeAxes {
581 operation: saddle_core::OperationOutcome::Cancelled,
582 ..Default::default()
583 },
584 ..Default::default()
585 };
586 self.scope
587 .bounded_event(self.stage, facts, Some(elapsed), "request_stage_finished");
588 }
589 }
590}
591
592pub fn root_failure_layout() -> std::alloc::Layout {
593 std::alloc::Layout::new::<RootRequestFailure>()
594}
595
596pub struct RequestLoggingLayouts {
600 pub source_frame: std::alloc::Layout,
601 pub emergency_packet: std::alloc::Layout,
602 pub emergency_slots: usize,
603 pub ordinary_command: std::alloc::Layout,
604 pub ordinary_encoded_bytes_max: usize,
605 pub source_record: std::alloc::Layout,
606}
607pub fn request_logging_layouts() -> RequestLoggingLayouts {
608 let (source_frame, emergency_packet, emergency_slots) = crate::diagnostic::root_frame_layouts();
609 RequestLoggingLayouts {
610 source_frame,
611 emergency_packet,
612 emergency_slots,
613 ordinary_command: crate::logger::root_queue_layout(),
614 ordinary_encoded_bytes_max: 8191,
615 source_record: std::alloc::Layout::new::<RootRecord<'static>>(),
616 }
617}
618
619impl PublicRequestFailure {
620 pub fn occurrence(&self) -> DiagnosticOccurrence {
621 self.occurrence
622 }
623 pub fn source_submission(&self) -> DiagnosticSubmission {
624 self.source_submission
625 }
626 pub fn terminal_submission(&self) -> DiagnosticSubmission {
627 self.terminal_submission
628 }
629}
630impl std::fmt::Debug for PublicRequestFailure {
631 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
632 f.debug_struct("PublicRequestFailure")
633 .field("classification", &self.classification)
634 .field("source_submission", &self.source_submission)
635 .finish_non_exhaustive()
636 }
637}
638impl std::fmt::Display for PublicRequestFailure {
639 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
640 write!(f, "framework request failure: {:?}", self.classification)
641 }
642}
643impl std::error::Error for PublicRequestFailure {}
644
645#[doc(hidden)]
647pub fn database_maintenance_success(
648 output: Option<&EmergencyDiagnosticHandle>, datasource: u64, connection: u64,
649 sweep: u64, recovery_acquisition: bool,
650) -> DiagnosticSubmission {
651 #[derive(Serialize)]
652 struct CompletedProbe {
653 schema_version: u8,
654 event: &'static str,
655 datasource: u64,
656 connection: u64,
657 maintenance_sweep: u64,
658 recovery_acquisition: bool,
659 response: &'static str,
660 request: ContextFact<()>,
661 trace_id: ContextFact<()>,
662 }
663 match output {
664 Some(output) => output.submit_fixed_record(&CompletedProbe {
665 schema_version: 1, event: "database_maintenance_probe", datasource, connection,
666 maintenance_sweep: sweep, recovery_acquisition, response: "complete_valid_select1",
667 request: ContextFact::NotApplicable, trace_id: ContextFact::NotApplicable,
668 }),
669 None => DiagnosticSubmission::OutputUnavailable,
670 }
671}