feat(wfe-core): add LogSink trait and wire lifecycle publisher into executor
LogSink trait for real-time step output streaming. Added to StepExecutionContext as optional field (backward compatible). Threaded through WorkflowExecutor and WorkflowHostBuilder. Wired LifecyclePublisher.publish() into executor at 5 points: StepStarted, StepCompleted, Error, Completed, Terminated. Also added lifecycle events to host start/suspend/resume/terminate.
This commit is contained in:
59
wfe-core/src/traits/log_sink.rs
Normal file
59
wfe-core/src/traits/log_sink.rs
Normal file
@@ -0,0 +1,59 @@
|
||||
use async_trait::async_trait;
|
||||
use chrono::{DateTime, Utc};
|
||||
|
||||
/// A chunk of log output from a step execution.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LogChunk {
|
||||
pub workflow_id: String,
|
||||
pub definition_id: String,
|
||||
pub step_id: usize,
|
||||
pub step_name: String,
|
||||
pub stream: LogStreamType,
|
||||
pub data: Vec<u8>,
|
||||
pub timestamp: DateTime<Utc>,
|
||||
}
|
||||
|
||||
/// Whether a log chunk is from stdout or stderr.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum LogStreamType {
|
||||
Stdout,
|
||||
Stderr,
|
||||
}
|
||||
|
||||
/// Receives log chunks as they're produced during step execution.
|
||||
///
|
||||
/// Implementations can broadcast to live subscribers, persist to a database,
|
||||
/// index for search, or any combination. The trait is designed to be called
|
||||
/// from within step executors (shell, containerd, etc.) as lines are produced.
|
||||
#[async_trait]
|
||||
pub trait LogSink: Send + Sync {
|
||||
async fn write_chunk(&self, chunk: LogChunk);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn log_stream_type_equality() {
|
||||
assert_eq!(LogStreamType::Stdout, LogStreamType::Stdout);
|
||||
assert_ne!(LogStreamType::Stdout, LogStreamType::Stderr);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn log_chunk_clone() {
|
||||
let chunk = LogChunk {
|
||||
workflow_id: "wf-1".to_string(),
|
||||
definition_id: "def-1".to_string(),
|
||||
step_id: 0,
|
||||
step_name: "build".to_string(),
|
||||
stream: LogStreamType::Stdout,
|
||||
data: b"hello\n".to_vec(),
|
||||
timestamp: Utc::now(),
|
||||
};
|
||||
let cloned = chunk.clone();
|
||||
assert_eq!(cloned.workflow_id, "wf-1");
|
||||
assert_eq!(cloned.stream, LogStreamType::Stdout);
|
||||
assert_eq!(cloned.data, b"hello\n");
|
||||
}
|
||||
}
|
||||
@@ -69,6 +69,7 @@ mod tests {
|
||||
workflow: &instance,
|
||||
cancellation_token: tokio_util::sync::CancellationToken::new(),
|
||||
host_context: None,
|
||||
log_sink: None,
|
||||
};
|
||||
mw.pre_step(&ctx).await.unwrap();
|
||||
}
|
||||
@@ -88,6 +89,7 @@ mod tests {
|
||||
workflow: &instance,
|
||||
cancellation_token: tokio_util::sync::CancellationToken::new(),
|
||||
host_context: None,
|
||||
log_sink: None,
|
||||
};
|
||||
let result = ExecutionResult::next();
|
||||
mw.post_step(&ctx, &result).await.unwrap();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
pub mod lifecycle;
|
||||
pub mod lock;
|
||||
pub mod log_sink;
|
||||
pub mod middleware;
|
||||
pub mod persistence;
|
||||
pub mod queue;
|
||||
@@ -9,6 +10,7 @@ pub mod step;
|
||||
|
||||
pub use lifecycle::LifecyclePublisher;
|
||||
pub use lock::DistributedLockProvider;
|
||||
pub use log_sink::{LogChunk, LogSink, LogStreamType};
|
||||
pub use middleware::{StepMiddleware, WorkflowMiddleware};
|
||||
pub use persistence::{
|
||||
EventRepository, PersistenceProvider, ScheduledCommandRepository, SubscriptionRepository,
|
||||
|
||||
@@ -38,6 +38,8 @@ pub struct StepExecutionContext<'a> {
|
||||
pub cancellation_token: tokio_util::sync::CancellationToken,
|
||||
/// Host context for starting child workflows. None if not available.
|
||||
pub host_context: Option<&'a dyn HostContext>,
|
||||
/// Log sink for streaming step output. None if not configured.
|
||||
pub log_sink: Option<&'a dyn super::LogSink>,
|
||||
}
|
||||
|
||||
// Manual Debug impl since dyn HostContext is not Debug.
|
||||
@@ -50,6 +52,7 @@ impl<'a> std::fmt::Debug for StepExecutionContext<'a> {
|
||||
.field("step", &self.step)
|
||||
.field("workflow", &self.workflow)
|
||||
.field("host_context", &self.host_context.is_some())
|
||||
.field("log_sink", &self.log_sink.is_some())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user