1use std::fmt;
4use std::num::NonZeroU64;
5use std::time::Duration;
6
7use crate::{
8 BatchStatus, ChunkCount, FailureSummary, FaultPhase, JobExecutionId, JobInstanceId, JobName,
9 RetryOrdinal, StepExecutionId, StepName,
10};
11
12#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
14pub struct ExecutionAttempt(NonZeroU64);
15
16impl ExecutionAttempt {
17 #[must_use]
19 pub const fn new(value: NonZeroU64) -> Self {
20 Self(value)
21 }
22
23 #[must_use]
25 pub const fn get(self) -> u64 {
26 self.0.get()
27 }
28}
29
30impl fmt::Display for ExecutionAttempt {
31 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
32 self.get().fmt(formatter)
33 }
34}
35
36#[derive(Clone, Debug, Eq, PartialEq)]
38pub struct ExecutionCorrelation {
39 job_name: JobName,
40 job_instance_id: JobInstanceId,
41 job_execution_id: JobExecutionId,
42 job_attempt: ExecutionAttempt,
43 step_name: StepName,
44 step_execution_id: StepExecutionId,
45 step_attempt: ExecutionAttempt,
46}
47
48impl ExecutionCorrelation {
49 #[must_use]
51 #[allow(clippy::too_many_arguments)]
52 pub const fn new(
53 job_name: JobName,
54 job_instance_id: JobInstanceId,
55 job_execution_id: JobExecutionId,
56 job_attempt: ExecutionAttempt,
57 step_name: StepName,
58 step_execution_id: StepExecutionId,
59 step_attempt: ExecutionAttempt,
60 ) -> Self {
61 Self {
62 job_name,
63 job_instance_id,
64 job_execution_id,
65 job_attempt,
66 step_name,
67 step_execution_id,
68 step_attempt,
69 }
70 }
71
72 #[must_use]
74 pub const fn job_name(&self) -> &JobName {
75 &self.job_name
76 }
77
78 #[must_use]
80 pub const fn job_instance_id(&self) -> JobInstanceId {
81 self.job_instance_id
82 }
83
84 #[must_use]
86 pub const fn job_execution_id(&self) -> JobExecutionId {
87 self.job_execution_id
88 }
89
90 #[must_use]
92 pub const fn job_attempt(&self) -> ExecutionAttempt {
93 self.job_attempt
94 }
95
96 #[must_use]
98 pub const fn step_name(&self) -> &StepName {
99 &self.step_name
100 }
101
102 #[must_use]
104 pub const fn step_execution_id(&self) -> StepExecutionId {
105 self.step_execution_id
106 }
107
108 #[must_use]
110 pub const fn step_attempt(&self) -> ExecutionAttempt {
111 self.step_attempt
112 }
113}
114
115#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
117#[non_exhaustive]
118pub enum EventSeverity {
119 Debug,
121 Info,
123 Warn,
125 Error,
127}
128
129impl EventSeverity {
130 #[must_use]
132 pub const fn as_str(self) -> &'static str {
133 match self {
134 Self::Debug => "debug",
135 Self::Info => "info",
136 Self::Warn => "warn",
137 Self::Error => "error",
138 }
139 }
140}
141
142impl fmt::Display for EventSeverity {
143 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
144 formatter.write_str(self.as_str())
145 }
146}
147
148#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
150#[non_exhaustive]
151pub enum EventComponent {
152 Launcher,
154 Job,
156 Step,
158 Chunk,
160 Listener,
162 Retry,
164 Item,
166 Fault,
168 Flow,
170 Repository,
172 Checkpoint,
174 Operator,
176 Explorer,
178 Shutdown,
180 Recovery,
182 Retention,
184 Split,
186 Partition,
188 Telemetry,
190 Migration,
192}
193
194impl EventComponent {
195 #[must_use]
197 pub const fn as_str(self) -> &'static str {
198 match self {
199 Self::Launcher => "launcher",
200 Self::Job => "job",
201 Self::Step => "step",
202 Self::Chunk => "chunk",
203 Self::Listener => "listener",
204 Self::Retry => "retry",
205 Self::Item => "item",
206 Self::Fault => "fault",
207 Self::Flow => "flow",
208 Self::Repository => "repository",
209 Self::Checkpoint => "checkpoint",
210 Self::Operator => "operator",
211 Self::Explorer => "explorer",
212 Self::Shutdown => "shutdown",
213 Self::Recovery => "recovery",
214 Self::Retention => "retention",
215 Self::Split => "split",
216 Self::Partition => "partition",
217 Self::Telemetry => "telemetry",
218 Self::Migration => "migration",
219 }
220 }
221}
222
223impl fmt::Display for EventComponent {
224 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
225 formatter.write_str(self.as_str())
226 }
227}
228
229#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
231#[non_exhaustive]
232pub enum LifecycleEventKind {
233 LaunchAccepted,
235 JobStarting,
237 StepStarting,
239 JobStarted,
241 StepStarted,
243 JobStopping,
245 StepStopping,
247 JobStopped,
249 StepStopped,
251 JobCompleted,
253 StepCompleted,
255 JobFailed,
257 StepFailed,
259 JobUnknown,
261 StepUnknown,
263 ChunkStarted,
265 ChunkCommitted,
267 ChunkRolledBack,
269 ChunkUnknown,
271 JobBeforeListenerFailed,
273 JobAfterListenerFailed,
275 StepBeforeListenerFailed,
277 StepAfterListenerFailed,
279 RetryReserved,
281 RetryBackoffStarted,
283 RetryBackoffCancelled,
285 RetryExhausted,
287 ItemSkipped,
289 FaultRollbackCommitted,
291 FaultNoRollbackCommitted,
293}
294
295impl LifecycleEventKind {
296 #[must_use]
298 pub const fn telemetry_kind(self) -> crate::TelemetryEventKind {
299 match self {
300 Self::LaunchAccepted => crate::TelemetryEventKind::LaunchAccepted,
301 Self::JobStarting => crate::TelemetryEventKind::JobStarting,
302 Self::StepStarting => crate::TelemetryEventKind::StepStarting,
303 Self::JobStarted => crate::TelemetryEventKind::JobStarted,
304 Self::StepStarted => crate::TelemetryEventKind::StepStarted,
305 Self::JobStopping => crate::TelemetryEventKind::JobStopping,
306 Self::StepStopping => crate::TelemetryEventKind::StepStopping,
307 Self::JobStopped => crate::TelemetryEventKind::JobStopped,
308 Self::StepStopped => crate::TelemetryEventKind::StepStopped,
309 Self::JobCompleted => crate::TelemetryEventKind::JobCompleted,
310 Self::StepCompleted => crate::TelemetryEventKind::StepCompleted,
311 Self::JobFailed => crate::TelemetryEventKind::JobFailed,
312 Self::StepFailed => crate::TelemetryEventKind::StepFailed,
313 Self::JobUnknown => crate::TelemetryEventKind::JobUnknown,
314 Self::StepUnknown => crate::TelemetryEventKind::StepUnknown,
315 Self::ChunkStarted => crate::TelemetryEventKind::ChunkStarted,
316 Self::ChunkCommitted => crate::TelemetryEventKind::ChunkCommitted,
317 Self::ChunkRolledBack => crate::TelemetryEventKind::ChunkRolledBack,
318 Self::ChunkUnknown => crate::TelemetryEventKind::ChunkUnknown,
319 Self::JobBeforeListenerFailed => crate::TelemetryEventKind::JobBeforeListenerFailed,
320 Self::JobAfterListenerFailed => crate::TelemetryEventKind::JobAfterListenerFailed,
321 Self::StepBeforeListenerFailed => crate::TelemetryEventKind::StepBeforeListenerFailed,
322 Self::StepAfterListenerFailed => crate::TelemetryEventKind::StepAfterListenerFailed,
323 Self::RetryReserved => crate::TelemetryEventKind::RetryReserved,
324 Self::RetryBackoffStarted => crate::TelemetryEventKind::RetryBackoffStarted,
325 Self::RetryBackoffCancelled => crate::TelemetryEventKind::RetryBackoffCancelled,
326 Self::RetryExhausted => crate::TelemetryEventKind::RetryExhausted,
327 Self::ItemSkipped => crate::TelemetryEventKind::ItemSkipped,
328 Self::FaultRollbackCommitted => crate::TelemetryEventKind::FaultRollbackCommitted,
329 Self::FaultNoRollbackCommitted => crate::TelemetryEventKind::FaultNoRollbackCommitted,
330 }
331 }
332
333 #[must_use]
335 pub const fn as_str(self) -> &'static str {
336 match self {
337 Self::LaunchAccepted => "launch.accepted",
338 Self::JobStarting => "job.starting",
339 Self::StepStarting => "step.starting",
340 Self::JobStarted => "job.started",
341 Self::StepStarted => "step.started",
342 Self::JobStopping => "job.stopping",
343 Self::StepStopping => "step.stopping",
344 Self::JobStopped => "job.stopped",
345 Self::StepStopped => "step.stopped",
346 Self::JobCompleted => "job.completed",
347 Self::StepCompleted => "step.completed",
348 Self::JobFailed => "job.failed",
349 Self::StepFailed => "step.failed",
350 Self::JobUnknown => "job.unknown",
351 Self::StepUnknown => "step.unknown",
352 Self::ChunkStarted => "chunk.started",
353 Self::ChunkCommitted => "chunk.committed",
354 Self::ChunkRolledBack => "chunk.rolled_back",
355 Self::ChunkUnknown => "chunk.unknown",
356 Self::JobBeforeListenerFailed => "job.before_listener.failed",
357 Self::JobAfterListenerFailed => "job.after_listener.failed",
358 Self::StepBeforeListenerFailed => "step.before_listener.failed",
359 Self::StepAfterListenerFailed => "step.after_listener.failed",
360 Self::RetryReserved => "retry.reserved",
361 Self::RetryBackoffStarted => "retry.backoff_started",
362 Self::RetryBackoffCancelled => "retry.backoff_cancelled",
363 Self::RetryExhausted => "retry.exhausted",
364 Self::ItemSkipped => "item.skipped",
365 Self::FaultRollbackCommitted => "fault.rollback_committed",
366 Self::FaultNoRollbackCommitted => "fault.no_rollback_committed",
367 }
368 }
369
370 #[must_use]
372 pub const fn component(self) -> EventComponent {
373 match self {
374 Self::LaunchAccepted => EventComponent::Launcher,
375 Self::JobStarting
376 | Self::JobStarted
377 | Self::JobStopping
378 | Self::JobStopped
379 | Self::JobCompleted
380 | Self::JobFailed
381 | Self::JobUnknown => EventComponent::Job,
382 Self::StepStarting
383 | Self::StepStarted
384 | Self::StepStopping
385 | Self::StepStopped
386 | Self::StepCompleted
387 | Self::StepFailed
388 | Self::StepUnknown => EventComponent::Step,
389 Self::ChunkStarted
390 | Self::ChunkCommitted
391 | Self::ChunkRolledBack
392 | Self::ChunkUnknown => EventComponent::Chunk,
393 Self::JobBeforeListenerFailed
394 | Self::JobAfterListenerFailed
395 | Self::StepBeforeListenerFailed
396 | Self::StepAfterListenerFailed => EventComponent::Listener,
397 Self::RetryReserved
398 | Self::RetryBackoffStarted
399 | Self::RetryBackoffCancelled
400 | Self::RetryExhausted => EventComponent::Retry,
401 Self::ItemSkipped => EventComponent::Item,
402 Self::FaultRollbackCommitted | Self::FaultNoRollbackCommitted => EventComponent::Fault,
403 }
404 }
405
406 #[must_use]
408 pub const fn status(self) -> Option<BatchStatus> {
409 match self {
410 Self::JobStarting | Self::StepStarting => Some(BatchStatus::Starting),
411 Self::JobStarted | Self::StepStarted => Some(BatchStatus::Started),
412 Self::JobStopping | Self::StepStopping => Some(BatchStatus::Stopping),
413 Self::JobStopped | Self::StepStopped => Some(BatchStatus::Stopped),
414 Self::JobCompleted | Self::StepCompleted => Some(BatchStatus::Completed),
415 Self::JobFailed | Self::StepFailed => Some(BatchStatus::Failed),
416 Self::JobUnknown | Self::StepUnknown => Some(BatchStatus::Unknown),
417 Self::LaunchAccepted
418 | Self::ChunkStarted
419 | Self::ChunkCommitted
420 | Self::ChunkRolledBack
421 | Self::ChunkUnknown
422 | Self::JobBeforeListenerFailed
423 | Self::JobAfterListenerFailed
424 | Self::StepBeforeListenerFailed
425 | Self::StepAfterListenerFailed
426 | Self::RetryReserved
427 | Self::RetryBackoffStarted
428 | Self::RetryBackoffCancelled
429 | Self::RetryExhausted
430 | Self::ItemSkipped
431 | Self::FaultRollbackCommitted
432 | Self::FaultNoRollbackCommitted => None,
433 }
434 }
435
436 #[must_use]
438 pub const fn severity(self) -> EventSeverity {
439 match self {
440 Self::JobFailed
441 | Self::StepFailed
442 | Self::JobUnknown
443 | Self::StepUnknown
444 | Self::ChunkUnknown
445 | Self::JobBeforeListenerFailed
446 | Self::JobAfterListenerFailed
447 | Self::StepBeforeListenerFailed
448 | Self::StepAfterListenerFailed
449 | Self::RetryExhausted => EventSeverity::Error,
450 Self::JobStopping
451 | Self::StepStopping
452 | Self::JobStopped
453 | Self::StepStopped
454 | Self::ChunkRolledBack
455 | Self::RetryReserved
456 | Self::RetryBackoffCancelled
457 | Self::ItemSkipped
458 | Self::FaultRollbackCommitted
459 | Self::FaultNoRollbackCommitted => EventSeverity::Warn,
460 Self::LaunchAccepted
461 | Self::RetryBackoffStarted
462 | Self::JobStarting
463 | Self::StepStarting
464 | Self::JobStarted
465 | Self::StepStarted
466 | Self::JobCompleted
467 | Self::StepCompleted
468 | Self::ChunkStarted
469 | Self::ChunkCommitted => EventSeverity::Info,
470 }
471 }
472}
473
474impl fmt::Display for LifecycleEventKind {
475 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
476 formatter.write_str(self.as_str())
477 }
478}
479
480#[derive(Clone, Debug, Eq, PartialEq)]
482pub struct LifecycleEvent {
483 kind: LifecycleEventKind,
484 correlation: ExecutionCorrelation,
485 failure: Option<FailureSummary>,
486 chunk_sequence: Option<ChunkCount>,
487 fault_phase: Option<FaultPhase>,
488 retry_ordinal: Option<RetryOrdinal>,
489 backoff: Option<Duration>,
490}
491
492impl LifecycleEvent {
493 #[must_use]
495 pub const fn schema_version(&self) -> u16 {
496 crate::TELEMETRY_SCHEMA_VERSION
497 }
498 pub(crate) const fn new(kind: LifecycleEventKind, correlation: ExecutionCorrelation) -> Self {
499 Self {
500 kind,
501 correlation,
502 failure: None,
503 chunk_sequence: None,
504 fault_phase: None,
505 retry_ordinal: None,
506 backoff: None,
507 }
508 }
509
510 pub(crate) const fn failed(
511 kind: LifecycleEventKind,
512 correlation: ExecutionCorrelation,
513 failure: FailureSummary,
514 ) -> Self {
515 Self {
516 kind,
517 correlation,
518 failure: Some(failure),
519 chunk_sequence: None,
520 fault_phase: None,
521 retry_ordinal: None,
522 backoff: None,
523 }
524 }
525
526 pub(crate) const fn chunk(
527 kind: LifecycleEventKind,
528 correlation: ExecutionCorrelation,
529 sequence: ChunkCount,
530 ) -> Self {
531 Self {
532 kind,
533 correlation,
534 failure: None,
535 chunk_sequence: Some(sequence),
536 fault_phase: None,
537 retry_ordinal: None,
538 backoff: None,
539 }
540 }
541
542 pub(crate) const fn fault(
543 kind: LifecycleEventKind,
544 correlation: ExecutionCorrelation,
545 sequence: ChunkCount,
546 phase: FaultPhase,
547 ) -> Self {
548 Self {
549 kind,
550 correlation,
551 failure: None,
552 chunk_sequence: Some(sequence),
553 fault_phase: Some(phase),
554 retry_ordinal: None,
555 backoff: None,
556 }
557 }
558
559 pub(crate) const fn with_failure(mut self, failure: FailureSummary) -> Self {
560 self.failure = Some(failure);
561 self
562 }
563
564 pub(crate) const fn with_retry_ordinal(mut self, ordinal: RetryOrdinal) -> Self {
565 self.retry_ordinal = Some(ordinal);
566 self
567 }
568
569 pub(crate) const fn with_backoff(mut self, backoff: Duration) -> Self {
570 self.backoff = Some(backoff);
571 self
572 }
573
574 #[must_use]
576 pub const fn fault_phase(&self) -> Option<FaultPhase> {
577 self.fault_phase
578 }
579
580 #[must_use]
582 pub const fn retry_ordinal(&self) -> Option<RetryOrdinal> {
583 self.retry_ordinal
584 }
585
586 #[must_use]
588 pub const fn backoff(&self) -> Option<Duration> {
589 self.backoff
590 }
591
592 #[must_use]
594 pub const fn kind(&self) -> LifecycleEventKind {
595 self.kind
596 }
597
598 #[must_use]
600 pub const fn correlation(&self) -> &ExecutionCorrelation {
601 &self.correlation
602 }
603
604 #[must_use]
606 pub const fn failure(&self) -> Option<FailureSummary> {
607 self.failure
608 }
609
610 #[must_use]
612 pub const fn chunk_sequence(&self) -> Option<ChunkCount> {
613 self.chunk_sequence
614 }
615
616 #[must_use]
618 pub fn span_fields(&self) -> Vec<DiagnosticField> {
619 let mut fields = vec![
620 DiagnosticField::new("event.name", self.kind.as_str()),
621 DiagnosticField::new("event.severity", self.kind.severity().as_str()),
622 DiagnosticField::new("component", self.kind.component().as_str()),
623 DiagnosticField::new("job.name", self.correlation.job_name().as_str()),
624 DiagnosticField::new(
625 "job.instance.id",
626 self.correlation.job_instance_id().to_string(),
627 ),
628 DiagnosticField::new(
629 "job.execution.id",
630 self.correlation.job_execution_id().to_string(),
631 ),
632 DiagnosticField::new("job.attempt", self.correlation.job_attempt().to_string()),
633 DiagnosticField::new("step.name", self.correlation.step_name().as_str()),
634 DiagnosticField::new(
635 "step.execution.id",
636 self.correlation.step_execution_id().to_string(),
637 ),
638 DiagnosticField::new("step.attempt", self.correlation.step_attempt().to_string()),
639 ];
640 if let Some(status) = self.kind.status() {
641 fields.push(DiagnosticField::new("batch.status", status.to_string()));
642 }
643 if let Some(sequence) = self.chunk_sequence {
644 fields.push(DiagnosticField::new(
645 "chunk.sequence",
646 sequence.get().to_string(),
647 ));
648 }
649 if let Some(phase) = self.fault_phase {
650 fields.push(DiagnosticField::new("fault.phase", phase.as_str()));
651 }
652 if let Some(ordinal) = self.retry_ordinal {
653 fields.push(DiagnosticField::new(
654 "retry.ordinal",
655 ordinal.get().to_string(),
656 ));
657 }
658 if let Some(backoff) = self.backoff {
659 fields.push(DiagnosticField::new(
660 "retry.backoff_ms",
661 u64::try_from(backoff.as_millis())
662 .unwrap_or(u64::MAX)
663 .to_string(),
664 ));
665 }
666 if let Some(failure) = self.failure {
667 fields.push(DiagnosticField::new(
668 "failure.category",
669 format!("{:?}", failure.category()),
670 ));
671 fields.push(DiagnosticField::new(
672 "failure.id",
673 failure.failure_id().to_string(),
674 ));
675 }
676 fields
677 }
678
679 #[must_use]
684 pub fn metric_labels(&self) -> Vec<MetricLabel> {
685 let mut labels = vec![
686 MetricLabel::new("event", self.kind.as_str()),
687 MetricLabel::new("component", self.kind.component().as_str()),
688 ];
689 if let Some(status) = self.kind.status() {
690 labels.push(MetricLabel::new("status", status.to_string()));
691 }
692 if let Some(phase) = self.fault_phase {
693 labels.push(MetricLabel::new("fault_phase", phase.as_str()));
694 }
695 labels
696 }
697}
698
699impl fmt::Display for LifecycleEvent {
700 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
701 write!(
702 formatter,
703 "event={} severity={} job={} job_instance_id={} job_execution_id={} \
704 job_attempt={} step={} step_execution_id={} step_attempt={}",
705 self.kind,
706 self.kind.severity(),
707 self.correlation.job_name(),
708 self.correlation.job_instance_id(),
709 self.correlation.job_execution_id(),
710 self.correlation.job_attempt(),
711 self.correlation.step_name(),
712 self.correlation.step_execution_id(),
713 self.correlation.step_attempt(),
714 )?;
715 if let Some(status) = self.kind.status() {
716 write!(formatter, " status={status}")?;
717 }
718 if let Some(sequence) = self.chunk_sequence {
719 write!(formatter, " chunk_sequence={}", sequence.get())?;
720 }
721 if let Some(phase) = self.fault_phase {
722 write!(formatter, " fault_phase={phase}")?;
723 }
724 if let Some(ordinal) = self.retry_ordinal {
725 write!(formatter, " retry_ordinal={}", ordinal.get())?;
726 }
727 if let Some(backoff) = self.backoff {
728 write!(formatter, " backoff_ms={}", backoff.as_millis())?;
729 }
730 if let Some(failure) = self.failure {
731 write!(
732 formatter,
733 " failure_category={:?} failure_id={}",
734 failure.category(),
735 failure.failure_id()
736 )?;
737 }
738 Ok(())
739 }
740}
741
742#[derive(Clone, Debug, Eq, PartialEq)]
744pub struct DiagnosticField {
745 key: &'static str,
746 value: String,
747}
748
749impl DiagnosticField {
750 pub(crate) fn new(key: &'static str, value: impl Into<String>) -> Self {
751 Self {
752 key,
753 value: value.into(),
754 }
755 }
756
757 #[must_use]
759 pub const fn key(&self) -> &'static str {
760 self.key
761 }
762
763 #[must_use]
765 pub fn value(&self) -> &str {
766 &self.value
767 }
768}
769
770impl fmt::Display for DiagnosticField {
771 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
772 write!(formatter, "{}={}", self.key, self.value)
773 }
774}
775
776#[derive(Clone, Debug, Eq, PartialEq)]
778pub struct MetricLabel {
779 key: &'static str,
780 value: String,
781}
782
783impl MetricLabel {
784 pub(crate) fn new(key: &'static str, value: impl Into<String>) -> Self {
785 Self {
786 key,
787 value: value.into(),
788 }
789 }
790
791 #[must_use]
793 pub const fn key(&self) -> &'static str {
794 self.key
795 }
796
797 #[must_use]
799 pub fn value(&self) -> &str {
800 &self.value
801 }
802
803 pub(crate) fn replace_value(&mut self, value: &'static str) {
804 value.clone_into(&mut self.value);
805 }
806}
807
808impl fmt::Display for MetricLabel {
809 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
810 write!(formatter, "{}={}", self.key, self.value)
811 }
812}
813
814pub trait LifecycleEventSink: Send + Sync {
819 fn emit(&self, event: &LifecycleEvent);
821}