use std::fmt;
use std::num::NonZeroU64;
use std::time::Duration;
use crate::{
BatchStatus, ChunkCount, FailureSummary, FaultPhase, JobExecutionId, JobInstanceId, JobName,
RetryOrdinal, StepExecutionId, StepName,
};
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct ExecutionAttempt(NonZeroU64);
impl ExecutionAttempt {
#[must_use]
pub const fn new(value: NonZeroU64) -> Self {
Self(value)
}
#[must_use]
pub const fn get(self) -> u64 {
self.0.get()
}
}
impl fmt::Display for ExecutionAttempt {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
self.get().fmt(formatter)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ExecutionCorrelation {
job_name: JobName,
job_instance_id: JobInstanceId,
job_execution_id: JobExecutionId,
job_attempt: ExecutionAttempt,
step_name: StepName,
step_execution_id: StepExecutionId,
step_attempt: ExecutionAttempt,
}
impl ExecutionCorrelation {
#[must_use]
#[allow(clippy::too_many_arguments)]
pub const fn new(
job_name: JobName,
job_instance_id: JobInstanceId,
job_execution_id: JobExecutionId,
job_attempt: ExecutionAttempt,
step_name: StepName,
step_execution_id: StepExecutionId,
step_attempt: ExecutionAttempt,
) -> Self {
Self {
job_name,
job_instance_id,
job_execution_id,
job_attempt,
step_name,
step_execution_id,
step_attempt,
}
}
#[must_use]
pub const fn job_name(&self) -> &JobName {
&self.job_name
}
#[must_use]
pub const fn job_instance_id(&self) -> JobInstanceId {
self.job_instance_id
}
#[must_use]
pub const fn job_execution_id(&self) -> JobExecutionId {
self.job_execution_id
}
#[must_use]
pub const fn job_attempt(&self) -> ExecutionAttempt {
self.job_attempt
}
#[must_use]
pub const fn step_name(&self) -> &StepName {
&self.step_name
}
#[must_use]
pub const fn step_execution_id(&self) -> StepExecutionId {
self.step_execution_id
}
#[must_use]
pub const fn step_attempt(&self) -> ExecutionAttempt {
self.step_attempt
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
#[non_exhaustive]
pub enum EventSeverity {
Debug,
Info,
Warn,
Error,
}
impl EventSeverity {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Debug => "debug",
Self::Info => "info",
Self::Warn => "warn",
Self::Error => "error",
}
}
}
impl fmt::Display for EventSeverity {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
#[non_exhaustive]
pub enum EventComponent {
Launcher,
Job,
Step,
Chunk,
Listener,
Retry,
Item,
Fault,
Flow,
Repository,
Checkpoint,
Operator,
Explorer,
Shutdown,
Recovery,
Retention,
Split,
Partition,
Telemetry,
Migration,
}
impl EventComponent {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Launcher => "launcher",
Self::Job => "job",
Self::Step => "step",
Self::Chunk => "chunk",
Self::Listener => "listener",
Self::Retry => "retry",
Self::Item => "item",
Self::Fault => "fault",
Self::Flow => "flow",
Self::Repository => "repository",
Self::Checkpoint => "checkpoint",
Self::Operator => "operator",
Self::Explorer => "explorer",
Self::Shutdown => "shutdown",
Self::Recovery => "recovery",
Self::Retention => "retention",
Self::Split => "split",
Self::Partition => "partition",
Self::Telemetry => "telemetry",
Self::Migration => "migration",
}
}
}
impl fmt::Display for EventComponent {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
#[non_exhaustive]
pub enum LifecycleEventKind {
LaunchAccepted,
JobStarting,
StepStarting,
JobStarted,
StepStarted,
JobStopping,
StepStopping,
JobStopped,
StepStopped,
JobCompleted,
StepCompleted,
JobFailed,
StepFailed,
JobUnknown,
StepUnknown,
ChunkStarted,
ChunkCommitted,
ChunkRolledBack,
ChunkUnknown,
JobBeforeListenerFailed,
JobAfterListenerFailed,
StepBeforeListenerFailed,
StepAfterListenerFailed,
RetryReserved,
RetryBackoffStarted,
RetryBackoffCancelled,
RetryExhausted,
ItemSkipped,
FaultRollbackCommitted,
FaultNoRollbackCommitted,
}
impl LifecycleEventKind {
#[must_use]
pub const fn telemetry_kind(self) -> crate::TelemetryEventKind {
match self {
Self::LaunchAccepted => crate::TelemetryEventKind::LaunchAccepted,
Self::JobStarting => crate::TelemetryEventKind::JobStarting,
Self::StepStarting => crate::TelemetryEventKind::StepStarting,
Self::JobStarted => crate::TelemetryEventKind::JobStarted,
Self::StepStarted => crate::TelemetryEventKind::StepStarted,
Self::JobStopping => crate::TelemetryEventKind::JobStopping,
Self::StepStopping => crate::TelemetryEventKind::StepStopping,
Self::JobStopped => crate::TelemetryEventKind::JobStopped,
Self::StepStopped => crate::TelemetryEventKind::StepStopped,
Self::JobCompleted => crate::TelemetryEventKind::JobCompleted,
Self::StepCompleted => crate::TelemetryEventKind::StepCompleted,
Self::JobFailed => crate::TelemetryEventKind::JobFailed,
Self::StepFailed => crate::TelemetryEventKind::StepFailed,
Self::JobUnknown => crate::TelemetryEventKind::JobUnknown,
Self::StepUnknown => crate::TelemetryEventKind::StepUnknown,
Self::ChunkStarted => crate::TelemetryEventKind::ChunkStarted,
Self::ChunkCommitted => crate::TelemetryEventKind::ChunkCommitted,
Self::ChunkRolledBack => crate::TelemetryEventKind::ChunkRolledBack,
Self::ChunkUnknown => crate::TelemetryEventKind::ChunkUnknown,
Self::JobBeforeListenerFailed => crate::TelemetryEventKind::JobBeforeListenerFailed,
Self::JobAfterListenerFailed => crate::TelemetryEventKind::JobAfterListenerFailed,
Self::StepBeforeListenerFailed => crate::TelemetryEventKind::StepBeforeListenerFailed,
Self::StepAfterListenerFailed => crate::TelemetryEventKind::StepAfterListenerFailed,
Self::RetryReserved => crate::TelemetryEventKind::RetryReserved,
Self::RetryBackoffStarted => crate::TelemetryEventKind::RetryBackoffStarted,
Self::RetryBackoffCancelled => crate::TelemetryEventKind::RetryBackoffCancelled,
Self::RetryExhausted => crate::TelemetryEventKind::RetryExhausted,
Self::ItemSkipped => crate::TelemetryEventKind::ItemSkipped,
Self::FaultRollbackCommitted => crate::TelemetryEventKind::FaultRollbackCommitted,
Self::FaultNoRollbackCommitted => crate::TelemetryEventKind::FaultNoRollbackCommitted,
}
}
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::LaunchAccepted => "launch.accepted",
Self::JobStarting => "job.starting",
Self::StepStarting => "step.starting",
Self::JobStarted => "job.started",
Self::StepStarted => "step.started",
Self::JobStopping => "job.stopping",
Self::StepStopping => "step.stopping",
Self::JobStopped => "job.stopped",
Self::StepStopped => "step.stopped",
Self::JobCompleted => "job.completed",
Self::StepCompleted => "step.completed",
Self::JobFailed => "job.failed",
Self::StepFailed => "step.failed",
Self::JobUnknown => "job.unknown",
Self::StepUnknown => "step.unknown",
Self::ChunkStarted => "chunk.started",
Self::ChunkCommitted => "chunk.committed",
Self::ChunkRolledBack => "chunk.rolled_back",
Self::ChunkUnknown => "chunk.unknown",
Self::JobBeforeListenerFailed => "job.before_listener.failed",
Self::JobAfterListenerFailed => "job.after_listener.failed",
Self::StepBeforeListenerFailed => "step.before_listener.failed",
Self::StepAfterListenerFailed => "step.after_listener.failed",
Self::RetryReserved => "retry.reserved",
Self::RetryBackoffStarted => "retry.backoff_started",
Self::RetryBackoffCancelled => "retry.backoff_cancelled",
Self::RetryExhausted => "retry.exhausted",
Self::ItemSkipped => "item.skipped",
Self::FaultRollbackCommitted => "fault.rollback_committed",
Self::FaultNoRollbackCommitted => "fault.no_rollback_committed",
}
}
#[must_use]
pub const fn component(self) -> EventComponent {
match self {
Self::LaunchAccepted => EventComponent::Launcher,
Self::JobStarting
| Self::JobStarted
| Self::JobStopping
| Self::JobStopped
| Self::JobCompleted
| Self::JobFailed
| Self::JobUnknown => EventComponent::Job,
Self::StepStarting
| Self::StepStarted
| Self::StepStopping
| Self::StepStopped
| Self::StepCompleted
| Self::StepFailed
| Self::StepUnknown => EventComponent::Step,
Self::ChunkStarted
| Self::ChunkCommitted
| Self::ChunkRolledBack
| Self::ChunkUnknown => EventComponent::Chunk,
Self::JobBeforeListenerFailed
| Self::JobAfterListenerFailed
| Self::StepBeforeListenerFailed
| Self::StepAfterListenerFailed => EventComponent::Listener,
Self::RetryReserved
| Self::RetryBackoffStarted
| Self::RetryBackoffCancelled
| Self::RetryExhausted => EventComponent::Retry,
Self::ItemSkipped => EventComponent::Item,
Self::FaultRollbackCommitted | Self::FaultNoRollbackCommitted => EventComponent::Fault,
}
}
#[must_use]
pub const fn status(self) -> Option<BatchStatus> {
match self {
Self::JobStarting | Self::StepStarting => Some(BatchStatus::Starting),
Self::JobStarted | Self::StepStarted => Some(BatchStatus::Started),
Self::JobStopping | Self::StepStopping => Some(BatchStatus::Stopping),
Self::JobStopped | Self::StepStopped => Some(BatchStatus::Stopped),
Self::JobCompleted | Self::StepCompleted => Some(BatchStatus::Completed),
Self::JobFailed | Self::StepFailed => Some(BatchStatus::Failed),
Self::JobUnknown | Self::StepUnknown => Some(BatchStatus::Unknown),
Self::LaunchAccepted
| Self::ChunkStarted
| Self::ChunkCommitted
| Self::ChunkRolledBack
| Self::ChunkUnknown
| Self::JobBeforeListenerFailed
| Self::JobAfterListenerFailed
| Self::StepBeforeListenerFailed
| Self::StepAfterListenerFailed
| Self::RetryReserved
| Self::RetryBackoffStarted
| Self::RetryBackoffCancelled
| Self::RetryExhausted
| Self::ItemSkipped
| Self::FaultRollbackCommitted
| Self::FaultNoRollbackCommitted => None,
}
}
#[must_use]
pub const fn severity(self) -> EventSeverity {
match self {
Self::JobFailed
| Self::StepFailed
| Self::JobUnknown
| Self::StepUnknown
| Self::ChunkUnknown
| Self::JobBeforeListenerFailed
| Self::JobAfterListenerFailed
| Self::StepBeforeListenerFailed
| Self::StepAfterListenerFailed
| Self::RetryExhausted => EventSeverity::Error,
Self::JobStopping
| Self::StepStopping
| Self::JobStopped
| Self::StepStopped
| Self::ChunkRolledBack
| Self::RetryReserved
| Self::RetryBackoffCancelled
| Self::ItemSkipped
| Self::FaultRollbackCommitted
| Self::FaultNoRollbackCommitted => EventSeverity::Warn,
Self::LaunchAccepted
| Self::RetryBackoffStarted
| Self::JobStarting
| Self::StepStarting
| Self::JobStarted
| Self::StepStarted
| Self::JobCompleted
| Self::StepCompleted
| Self::ChunkStarted
| Self::ChunkCommitted => EventSeverity::Info,
}
}
}
impl fmt::Display for LifecycleEventKind {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct LifecycleEvent {
kind: LifecycleEventKind,
correlation: ExecutionCorrelation,
failure: Option<FailureSummary>,
chunk_sequence: Option<ChunkCount>,
fault_phase: Option<FaultPhase>,
retry_ordinal: Option<RetryOrdinal>,
backoff: Option<Duration>,
}
impl LifecycleEvent {
#[must_use]
pub const fn schema_version(&self) -> u16 {
crate::TELEMETRY_SCHEMA_VERSION
}
pub(crate) const fn new(kind: LifecycleEventKind, correlation: ExecutionCorrelation) -> Self {
Self {
kind,
correlation,
failure: None,
chunk_sequence: None,
fault_phase: None,
retry_ordinal: None,
backoff: None,
}
}
pub(crate) const fn failed(
kind: LifecycleEventKind,
correlation: ExecutionCorrelation,
failure: FailureSummary,
) -> Self {
Self {
kind,
correlation,
failure: Some(failure),
chunk_sequence: None,
fault_phase: None,
retry_ordinal: None,
backoff: None,
}
}
pub(crate) const fn chunk(
kind: LifecycleEventKind,
correlation: ExecutionCorrelation,
sequence: ChunkCount,
) -> Self {
Self {
kind,
correlation,
failure: None,
chunk_sequence: Some(sequence),
fault_phase: None,
retry_ordinal: None,
backoff: None,
}
}
pub(crate) const fn fault(
kind: LifecycleEventKind,
correlation: ExecutionCorrelation,
sequence: ChunkCount,
phase: FaultPhase,
) -> Self {
Self {
kind,
correlation,
failure: None,
chunk_sequence: Some(sequence),
fault_phase: Some(phase),
retry_ordinal: None,
backoff: None,
}
}
pub(crate) const fn with_failure(mut self, failure: FailureSummary) -> Self {
self.failure = Some(failure);
self
}
pub(crate) const fn with_retry_ordinal(mut self, ordinal: RetryOrdinal) -> Self {
self.retry_ordinal = Some(ordinal);
self
}
pub(crate) const fn with_backoff(mut self, backoff: Duration) -> Self {
self.backoff = Some(backoff);
self
}
#[must_use]
pub const fn fault_phase(&self) -> Option<FaultPhase> {
self.fault_phase
}
#[must_use]
pub const fn retry_ordinal(&self) -> Option<RetryOrdinal> {
self.retry_ordinal
}
#[must_use]
pub const fn backoff(&self) -> Option<Duration> {
self.backoff
}
#[must_use]
pub const fn kind(&self) -> LifecycleEventKind {
self.kind
}
#[must_use]
pub const fn correlation(&self) -> &ExecutionCorrelation {
&self.correlation
}
#[must_use]
pub const fn failure(&self) -> Option<FailureSummary> {
self.failure
}
#[must_use]
pub const fn chunk_sequence(&self) -> Option<ChunkCount> {
self.chunk_sequence
}
#[must_use]
pub fn span_fields(&self) -> Vec<DiagnosticField> {
let mut fields = vec![
DiagnosticField::new("event.name", self.kind.as_str()),
DiagnosticField::new("event.severity", self.kind.severity().as_str()),
DiagnosticField::new("component", self.kind.component().as_str()),
DiagnosticField::new("job.name", self.correlation.job_name().as_str()),
DiagnosticField::new(
"job.instance.id",
self.correlation.job_instance_id().to_string(),
),
DiagnosticField::new(
"job.execution.id",
self.correlation.job_execution_id().to_string(),
),
DiagnosticField::new("job.attempt", self.correlation.job_attempt().to_string()),
DiagnosticField::new("step.name", self.correlation.step_name().as_str()),
DiagnosticField::new(
"step.execution.id",
self.correlation.step_execution_id().to_string(),
),
DiagnosticField::new("step.attempt", self.correlation.step_attempt().to_string()),
];
if let Some(status) = self.kind.status() {
fields.push(DiagnosticField::new("batch.status", status.to_string()));
}
if let Some(sequence) = self.chunk_sequence {
fields.push(DiagnosticField::new(
"chunk.sequence",
sequence.get().to_string(),
));
}
if let Some(phase) = self.fault_phase {
fields.push(DiagnosticField::new("fault.phase", phase.as_str()));
}
if let Some(ordinal) = self.retry_ordinal {
fields.push(DiagnosticField::new(
"retry.ordinal",
ordinal.get().to_string(),
));
}
if let Some(backoff) = self.backoff {
fields.push(DiagnosticField::new(
"retry.backoff_ms",
u64::try_from(backoff.as_millis())
.unwrap_or(u64::MAX)
.to_string(),
));
}
if let Some(failure) = self.failure {
fields.push(DiagnosticField::new(
"failure.category",
format!("{:?}", failure.category()),
));
fields.push(DiagnosticField::new(
"failure.id",
failure.failure_id().to_string(),
));
}
fields
}
#[must_use]
pub fn metric_labels(&self) -> Vec<MetricLabel> {
let mut labels = vec![
MetricLabel::new("event", self.kind.as_str()),
MetricLabel::new("component", self.kind.component().as_str()),
];
if let Some(status) = self.kind.status() {
labels.push(MetricLabel::new("status", status.to_string()));
}
if let Some(phase) = self.fault_phase {
labels.push(MetricLabel::new("fault_phase", phase.as_str()));
}
labels
}
}
impl fmt::Display for LifecycleEvent {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
formatter,
"event={} severity={} job={} job_instance_id={} job_execution_id={} \
job_attempt={} step={} step_execution_id={} step_attempt={}",
self.kind,
self.kind.severity(),
self.correlation.job_name(),
self.correlation.job_instance_id(),
self.correlation.job_execution_id(),
self.correlation.job_attempt(),
self.correlation.step_name(),
self.correlation.step_execution_id(),
self.correlation.step_attempt(),
)?;
if let Some(status) = self.kind.status() {
write!(formatter, " status={status}")?;
}
if let Some(sequence) = self.chunk_sequence {
write!(formatter, " chunk_sequence={}", sequence.get())?;
}
if let Some(phase) = self.fault_phase {
write!(formatter, " fault_phase={phase}")?;
}
if let Some(ordinal) = self.retry_ordinal {
write!(formatter, " retry_ordinal={}", ordinal.get())?;
}
if let Some(backoff) = self.backoff {
write!(formatter, " backoff_ms={}", backoff.as_millis())?;
}
if let Some(failure) = self.failure {
write!(
formatter,
" failure_category={:?} failure_id={}",
failure.category(),
failure.failure_id()
)?;
}
Ok(())
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DiagnosticField {
key: &'static str,
value: String,
}
impl DiagnosticField {
pub(crate) fn new(key: &'static str, value: impl Into<String>) -> Self {
Self {
key,
value: value.into(),
}
}
#[must_use]
pub const fn key(&self) -> &'static str {
self.key
}
#[must_use]
pub fn value(&self) -> &str {
&self.value
}
}
impl fmt::Display for DiagnosticField {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "{}={}", self.key, self.value)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct MetricLabel {
key: &'static str,
value: String,
}
impl MetricLabel {
pub(crate) fn new(key: &'static str, value: impl Into<String>) -> Self {
Self {
key,
value: value.into(),
}
}
#[must_use]
pub const fn key(&self) -> &'static str {
self.key
}
#[must_use]
pub fn value(&self) -> &str {
&self.value
}
pub(crate) fn replace_value(&mut self, value: &'static str) {
value.clone_into(&mut self.value);
}
}
impl fmt::Display for MetricLabel {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "{}={}", self.key, self.value)
}
}
pub trait LifecycleEventSink: Send + Sync {
fn emit(&self, event: &LifecycleEvent);
}