Expand description
§Alien Events System
A Rust-based events system for user-facing events in Alien. This system is designed to handle events that can be consumed by dashboards, CLIs, and other monitoring tools to show users what’s happening during builds, deployments, and other operations.
§Why Not Use Traces?
This events system is not an alternative to traces - traces are for engineering observability and internal debugging, while this events module is about user-facing events that you show in UIs (either GUI or TUI). Key differences:
- Reliable User Flow: Events are blocking (unlike non-blocking traces) because we want to guarantee users see critical progress updates and fail fast if we can’t communicate status
- Rich UI Components: Events have strict schemas (see
AlienEventenum) enabling translation to custom React components, progress bars, and interactive UI elements that show meaningful progress - Flexible Granularity: Unlike traces (spans + events), we only have events, with infinite hierarchy and unique IDs. Users can choose their view: high-level “Building” → “Deploying” or drill down to see “Building image”, “Pushing image”, etc. Each event can be scoped for perfect user control over detail level
- Live Progress Updates: Events can be updated with new information (e.g., “pushing image… layer 1/5, layer 2/5, layer 3/5”) enabling real-time progress indicators, which is impossible with immutable traces
- Clear Success/Failure States: Each scoped event has explicit states (Started, Success, Failed) with detailed error information, giving users immediate feedback on what succeeded, what failed, and exactly why
§Key Features
- Global Event Bus: Events can be emitted from anywhere in the code without passing around event bus instances, making it easy to add to large Rust workspaces with many crates.
- Hierarchical Events: Events can have parent-child relationships for organizing complex operations.
- State Management: Events can track their lifecycle (None, Started, Success, Failed).
- Durable Execution Support: Designed to work with frameworks like Temporal, Inngest, and Restate where processes can restart and state needs to be preserved externally.
- Change-based Architecture: Instead of storing events in memory, the system emits changes (Created, Updated, StateChanged) that handlers can persist or react to.
- Macro Support: The
#[alien_event]macro provides a convenient way to instrument functions with events, similar to tracing’s#[instrument]macro.
§Basic Usage
§Using the #[alien_event] Macro (Recommended)
The easiest way to instrument functions with events is using the #[alien_event] macro:
use alien_core::{AlienEvent, EventBus, alien_event, Result};
#[alien_event(AlienEvent::BuildingStack { stack: "my-stack".to_string() })]
async fn build_stack() -> Result<()> {
// All events emitted within this function will automatically be children
// of the BuildingStack event. The event will be marked as successful
// if the function returns Ok, or failed if it returns an error.
AlienEvent::BuildingImage {
image: "api:latest".to_string(),
}
.emit()
.await?;
Ok(())
}
let bus = EventBus::new();
bus.run(|| async {
build_stack().await?;
Ok(())
}).await?;§Simple Event Emission
For more control, you can emit events manually:
use alien_core::{AlienEvent, EventBus};
let bus = EventBus::new();
bus.run(|| async {
// Emit a simple event
let handle = AlienEvent::BuildingStack {
stack: "my-stack".to_string(),
}
.emit()
.await?;
println!("Emitted event with ID: {}", handle.id);
Ok::<_, Box<dyn std::error::Error>>(())
}).await?;§Event Updates
use alien_core::{AlienEvent, EventBus};
let bus = EventBus::new();
bus.run(|| async {
// Emit an event and get a handle
let handle = AlienEvent::BuildingImage {
image: "api:latest".to_string(),
}
.emit()
.await?;
// Update the event with new information
handle.update(AlienEvent::BuildingImage {
image: "api:latest-v2".to_string(),
}).await;
// Mark as completed
handle.complete().await;
Ok::<_, Box<dyn std::error::Error>>(())
}).await?;§Scoped Events with Automatic State Management
You can also use in_scope directly for more control over the event lifecycle:
use alien_core::{AlienEvent, EventBus, ErrorData, Result};
use alien_error::AlienError;
let bus = EventBus::new();
bus.run(|| async {
// Use in_scope for automatic success/failure tracking
// All events emitted within the scope automatically become children
let result = AlienEvent::BuildingStack {
stack: "my-stack".to_string(),
}
.in_scope(|_handle| async move {
// This event will automatically be a child of BuildingStack
AlienEvent::BuildingImage {
image: "api:latest".to_string(),
}
.emit()
.await?;
// Do some work that might fail
std::fs::create_dir_all("/tmp/build")
.map_err(|e| AlienError::new(ErrorData::GenericError {
message: e.to_string(),
}))?;
// Return success
Ok::<_, AlienError<ErrorData>>(42)
})
.await?;
println!("Operation completed with result: {}", result);
Ok(())
}).await?;§Event Hierarchy
use alien_core::{AlienEvent, EventBus, Result};
let bus = EventBus::new();
bus.run(|| async {
let parent = AlienEvent::BuildingStack {
stack: "my-stack".to_string(),
}
.emit()
.await?;
// Create a parent context for multiple child events
parent.as_parent(|_handle| async {
// All events emitted here will be children of the parent
AlienEvent::BuildingImage {
image: "api:latest".to_string(),
}
.emit()
.await?;
AlienEvent::PushingImage {
image: "api:latest".to_string(),
progress: None,
}
.emit()
.await?;
Ok(())
}).await?;
// Complete the parent when all children are done
parent.complete().await;
Ok(())
}).await?;§Durable Execution Support
The events system is designed to work with durable execution frameworks where processes can restart and lose in-memory state. Instead of storing events in memory, the system emits changes that external handlers can persist.
§Manual State Management for Durable Workflows
use alien_core::{AlienEvent, EventBus, EventState, Result};
let bus = EventBus::new();
bus.run(|| async {
// In a durable execution framework like Temporal:
// Step 1: Start a long-running operation
let parent_handle = AlienEvent::BuildingStack {
stack: "my-stack".to_string(),
}
.emit_with_state(EventState::Started)
.await?;
// Step 2: Perform work across multiple durable steps
parent_handle.as_parent(|_handle| async {
// ctx.run(|| { ... }) - durable step 1
AlienEvent::BuildingImage {
image: "api:latest".to_string(),
}
.emit()
.await?;
// ctx.run(|| { ... }) - durable step 2
AlienEvent::PushingImage {
image: "api:latest".to_string(),
progress: None,
}
.emit()
.await?;
Ok(())
}).await?;
// Step 3: Complete the operation
parent_handle.complete().await;
Ok(())
}).await?;§The #[alien_event] Macro
The #[alien_event] macro provides a convenient way to instrument async functions with events,
similar to how tracing’s #[instrument] macro works for logging. It automatically:
- Creates an event when the function starts (with
EventState::Started) - Establishes a parent context so all events emitted within the function become children
- Marks the event as successful (
EventState::Success) if the function returnsOk - Marks the event as failed (
EventState::Failed) if the function returns anErr
§Basic Usage
use alien_core::{AlienEvent, alien_event, Result};
#[alien_event(AlienEvent::BuildingStack { stack: "my-stack".to_string() })]
async fn build_stack() -> Result<()> {
// Function implementation
Ok(())
}§Dynamic Values
You can use function parameters and expressions in the event definition:
use alien_core::{AlienEvent, alien_event, Result};
#[alien_event(AlienEvent::BuildingStack { stack: format!("stack-{}", stack_id) })]
async fn build_dynamic_stack(stack_id: u32) -> Result<()> {
// Function implementation
Ok(())
}§Comparison with Manual Event Management
The macro transforms this:
#[alien_event(AlienEvent::BuildingStack { stack: "my-stack".to_string() })]
async fn build_stack() -> Result<()> {
// function body
Ok(())
}Into this:
async fn build_stack() -> Result<()> {
AlienEvent::BuildingStack { stack: "my-stack".to_string() }
.in_scope(|_event_handle| async move {
// function body
Ok(())
})
.await
}§Limitations
- The macro only works with
asyncfunctions - For sync functions, use
AlienEvent::emit()manually - The event expression is evaluated when the function is called, not when the macro is expanded
§Event Handlers
Event handlers receive changes and can persist them, update UIs, or trigger other actions:
use alien_core::{EventHandler, EventChange, EventBus, AlienEvent};
use async_trait::async_trait;
struct PostgresEventHandler {
// database connection pool, etc.
}
#[async_trait]
impl EventHandler for PostgresEventHandler {
async fn on_event_change(&self, change: EventChange) -> alien_core::Result<()> {
match change {
EventChange::Created { id, parent_id, created_at, event, state } => {
// Insert new event record into database
println!("Creating event {} with parent {:?}", id, parent_id);
}
EventChange::Updated { id, updated_at, event } => {
// Update event data in database
println!("Updating event {} at {}", id, updated_at);
}
EventChange::StateChanged { id, updated_at, new_state } => {
// Update event state in database
println!("Event {} state changed to {:?} at {}", id, new_state, updated_at);
}
}
Ok(())
}
}
let handler = std::sync::Arc::new(PostgresEventHandler {});
let bus = EventBus::with_handlers(vec![handler]);
bus.run(|| async {
// Events will now be persisted to PostgreSQL
AlienEvent::BuildingStack {
stack: "my-stack".to_string(),
}
.emit()
.await?;
Ok::<_, Box<dyn std::error::Error>>(())
}).await?;§Architecture Notes
§Change-Based Design
Unlike traditional event systems that store complete event state, this system emits incremental changes:
EventChange::Created: A new event was created with initial data and stateEventChange::Updated: An event’s data was updatedEventChange::StateChanged: An event’s state transitioned (None → Started → Success/Failed)
This design is crucial for durable execution where the event bus itself doesn’t persist state across process restarts.
§Task-Local Context
The event bus uses Tokio’s task-local storage to provide a global context without
requiring explicit parameter passing. This makes it easy to add event emission to
existing codebases. The #[alien_event] macro leverages this design to provide
seamless instrumentation without requiring changes to function signatures.
§Error Handling
Event emission is designed to be non-blocking and fault-tolerant. If no event bus
context is available, operations will return EventBusError::NoEventBusContext
but won’t panic, allowing code to continue running even without event tracking.
Structs§
- Event
Bus - The event bus for managing events within a task context
- Event
Handle - Handle to an emitted event that allows updating it
- NoOp
Event Handler - A no-op event handler for testing
- Push
Progress - Progress information for image push operations
Enums§
- Alien
Event - Represents all possible events in the Alien system
- Event
Change - Represents a change to an event
- Event
State - Represents the state of an event
Traits§
- Event
Handler - Trait for handling events