# Eventline
**Causality-aware execution journal for systems-level programs.**
Eventline records *what happened*, *when it happened*, and *in what causal context* — without assuming logging, tracing, or telemetry semantics. Built for daemons, CLI tools, and eventually Linux distributions.
---
## Features
- **Append-only journal** — never mutates or removes records
- **Scoped execution** — track outcomes and durations
- **Structured data** — attach key-value fields to events for rich context
- **Runtime log levels** — filter events globally (Debug, Info, Warning, Error)
- **Dual output mode** — journal + optional real-time console printing
- **Live logging** — automatic, timestamped append to disk
- **Unified color control** — consistent ANSI colors across console and renderer
- **Flexible filtering** — by outcome, depth, duration, event kind, message content
- **High-throughput batching** — `JournalBuffer` for batch writes
- **Async runtime support** — fire-and-forget logging and async scopes (`scoped_async()`)
- **Deterministic replay** — safe concurrent reads, reliable audit trails
---
## Why Eventline?
Eventline is not "better logging" - It's **structured execution history.**
- Events are **append-only** and never rewritten
- Work is grouped into scopes with outcomes and durations
- **Event != results** (warnings can happen in successful work)
- Journals can be **replayed deterministically**
- Console output is optional - Structure is always preserved
You get:
- human-readable output
- A complete execution record for post-mortem analysis
---
## Quick Start
### Runtime API (Fire-and-Forget Async)
```rust
use eventline::runtime;
use eventline::{event_info, event_scope_async, event_scope_unnamed};
#[tokio::main]
async fn main() {
// Initialize runtime
runtime::init().await;
// Enable console output
runtime::enable_console_output(true).await;
runtime::enable_console_color(true).await;
// Fire-and-forget logging
event_info!("Async logging example");
// Named async scope
event_scope_async!("AsyncTask", {
event_info!("Inside async scope");
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
});
// Unnamed async scope (note: requires 'async' keyword)
event_scope_unnamed!(async {
event_info!("Unnamed async work");
});
}
```
### Core Journal API (Explicit Control)
For libraries or embedded systems:
```rust
use eventline::journal::Journal;
use eventline::journal::outcome::Outcome;
use eventline::journal::writer::JournalWriter;
let mut journal = Journal::new();
journal.warn(Some(scope), "Low memory");
});
// Use JournalWriter to output the journal
let writer = JournalWriter::new();
// writer.write_to(&mut std::fs::File::create("events.log")?, &journal)?;
```
### Structured Event Example
This example demonstrates how to record an event with structured fields using `eventline`.
```rust
use eventline::runtime;
use eventline::core::{Fields, Value};
use eventline::macros::fields;
use eventline::macros::event_info_fields;
#[tokio::main]
async fn main() {
// Initialize the runtime
runtime::init().await;
// Build structured fields using the helper macro
let f = fields!({
"user_id" => 12345,
"action" => "login",
"success" => true
});
// Fire-and-forget info event with structured fields (no await needed)
event_info_fields!("User login attempt", f);
// Wait a tiny bit to let the detached task complete
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
// Shutdown runtime cleanly (optional for tests)
runtime::reset().await;
}
```
---
### Console Output (Simple Format)
Clean, minimal output optimized for watching logs during development:
```text
Starting server
Binding to 0.0.0.0:8080
Server started successfully
warning: cache at 95% capacity
```
### Live Log File (Canonical Format)
Structured output with scope headers, timestamps, and aligned formatting:
```text
[19:04:12.381] Scope startup (id=1) → Success (142ms)
• info Starting server
• info Binding to 0.0.0.0:8080
• info Server started successfully
• warning cache at 95% capacity
```
---
## Architecture
```
┌─────────────┐
│ Macros │ event_info!(), event_scope!()
└──────┬──────┘
↓
┌─────────────┐
│ Runtime │ Global, thread-safe facade (optional)
└──────┬──────┘
↓
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Journal │ │ Console │ │ LiveLogFile │
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ │ │
└─────────────────┴─────────────────┘
↓
┌─────────────────────┐
│ Canonical Format │ Single rendering source
└─────────────────────┘
```
**Core Layer** (always available):
- **Journal** — Pure data structure
- **Scope** — Logical units of work
- **Record** — Individual events
- **Filter** — Composable criteria
**Runtime Layer** (optional):
- **runtime** — Global facade
- **console** — Dual output control
- **live_log** — Automatic, timestamped disk logging
- **Macros** — Zero-overhead convenience
- **Log levels** — Runtime event filtering
---
## Live Logging
Live logging is **enabled per file path** using
`runtime::enable_live_logging(PathBuf)`
```rust
use std::path::PathBuf;
runtime::enable_live_logging(PathBuf::from("/tmp/eventline.log")).await;
event_info!("This event will be written to the live log file automatically");
event_warn!("Timestamped and indented according to scope depth");
```
---
## Dual Output Mode
Eventline supports two output modes:
### Silent Journaling (Default)
Events are recorded but not printed:
```rust
runtime::init().await;
runtime::enable_console_output(false).await; // Default
event_info!("Silent"); // Recorded, not printed
// Later, examine the journal
runtime::with_journal(|journal| {
eventline::render::render_journal_tree(journal, true, None);
}).await;
```
### Dual Output (Traditional Logging Feel)
Events are both journaled AND printed to console:
```rust
runtime::init().await;
runtime::enable_console_output(true).await;
runtime::enable_console_color(true).await;
event_info!("Starting"); // Journaled + printed
event_warn!("Warning"); // Journaled + printed in yellow
event_error!("Error"); // Journaled + printed to stderr in red
```
**Benefits:**
- Get traditional logging behavior when you want it
- Always have structured journal for post-mortem
- Single flag to toggle: `enable_console_output(bool)`
---
## Key Concepts
### Outcomes vs Events
**Event kinds** describe *what happened*:
- `Info` — routine progress
- `Warning` — unexpected but recoverable
- `Error` — something went wrong
- `Debug` — verbose diagnostics
**Scope outcomes** describe *the result*:
- `Success` — completed normally
- `Failure` — completed with errors
- `Aborted` — interrupted by panic
This separation enables:
- Warnings during successful operations
- Errors that don't cause failure
- Clear diagnostics vs results
### Filtering at Render Time
Filtering happens when **reading** the journal, not when writing:
```rust
use eventline::journal::filter::*;
use eventline::journal::outcome::Outcome;
let filter = Filter::scope(ScopeFilter::Outcome(Outcome::Failure));
render_journal_tree(&journal, true, Some(&filter));
```
Benefits:
- Zero overhead when not filtering
- Complete journal always preserved
- Multiple views from same data
---
## Advanced Usage
### Batched Logging
```rust
use eventline::journal::Journal;
use eventline::journal::outcome::Outcome;
let mut journal = Journal::new();
let mut buffer = journal.create_buffer();
let scope = buffer.enter_scope(None, Some("BatchTask"));
for item in items {
buffer.record(Some(scope), format!("Processing {}", item));
}
buffer.exit_scope(scope, Outcome::Success);
journal.flush_buffer(buffer); // Atomic ID rebase
```
### Custom Output
```rust
use eventline::journal::writer::JournalWriter;
use std::io;
let mut file = std::fs::File::create("output.log")?;
// Customize canonical format
JournalWriter::new()
.with_color(false) // Disable colors
.with_timestamps(true) // Include timestamps
.with_bullet("→") // Custom bullet
.write_to_all(
&mut [
&mut io::stdout() as &mut dyn io::Write,
&mut file as &mut dyn io::Write,
],
&journal
)?;
```
```
### Nested Scopes
```rust
event_scope!("Deployment", {
event_info!("Starting deployment");
event_scope!("BuildImage", {
event_info!("Building Docker image");
});
event_scope!("PushRegistry", {
event_info!("Pushing to registry");
});
event_info!("Deployment complete");
});
```
### Environment Variable Support
Respect common conventions:
```rust
use eventline::runtime::log_level::{set_log_level, LogLevel};
#[tokio::main]
async fn main() {
runtime::init().await;
// Respect NO_COLOR
let use_color = std::env::var("NO_COLOR").is_err();
runtime::enable_console_color(use_color).await;
// Optional: RUST_LOG compatibility
let log_level = std::env::var("RUST_LOG")
.map(|s| match s.to_lowercase().as_str() {
"debug" => LogLevel::Debug,
"info" => LogLevel::Info,
"warn" => LogLevel::Warning,
"error" => LogLevel::Error,
_ => LogLevel::Info,
})
.unwrap_or(LogLevel::Info);
set_log_level(log_level).await;
runtime::enable_console_output(true).await;
}
```
---
## CLI Integration
Typical pattern for command-line tools:
```rust
use clap::Parser;
use eventline::runtime::log_level::{set_log_level, LogLevel};
#[derive(Parser)]
struct Args {
/// Enable verbose output
#[arg(short, long)]
verbose: bool,
/// Suppress console output
#[arg(short, long)]
quiet: bool,
/// Disable colored output
#[arg(long)]
no_color: bool,
}
#[tokio::main]
async fn main() {
let args = Args::parse();
runtime::init().await;
if args.quiet {
runtime::enable_console_output(false).await;
set_log_level(LogLevel::Warning).await;
} else {
runtime::enable_console_output(true).await;
runtime::enable_console_color(!args.no_color).await;
if args.verbose {
set_log_level(LogLevel::Debug).await;
} else {
set_log_level(LogLevel::Info).await;
}
}
event_info!("Application started");
// Your application logic here
// At exit, optionally render summary with same color setting
runtime::with_journal(|journal| {
let use_color = !args.no_color;
eventline::render::render_summary(journal, use_color, None);
}).await;
}
```
---
## Design Principles
- **Append-only be default** - safe, auditable, deterministic
- **Seperation of concerns** - data != rendering != runtime
- **Human-first output** - readable without tooling
- **Optional global state** - usable in libraries
- **Async-safe** - fire-and-foget from any task
### Test-Friendly
```rust
#[tokio::test]
async fn test_task() {
runtime::init().await;
runtime::enable_console_output(false).await; // Quiet in tests
event_info!("test");
runtime::with_journal(|journal| {
assert_eq!(journal.records().len(), 1);
}).await;
runtime::reset().await; // Clean up
}
```
---
## Installation
```toml
[dependencies]
eventline = "0.4.1"
tokio = { version = "1", features = ["full"] }
```
Optional features:
```toml
[dependencies]
eventline = { version = "0.4.1", features = ["colour"] }
```
---
## Roadmap
- Custom formatters (JSON, binary)
- Zero-copy query interface
- Tag-based filtering
- systemd journal integration
---
## License
MIT