Observability
Effectful includes lazy span instrumentation for production tracing without forcing an OpenTelemetry SDK or exporter dependency into core.
Use spans when you want to know when an Effect starts, succeeds, fails, how long it ran, what trace/span identity it had, and how it relates to parent spans.
Manual Spans
Wrap any existing effect with with_span or the method form.
#![allow(unused)]
fn main() {
use effectful::{TracingConfig, install_tracing_layer, run_blocking, succeed};
let _ = run_blocking(install_tracing_layer(TracingConfig::enabled()), ());
let effect = succeed::<u32, (), ()>(42).with_span("manual.demo");
assert_eq!(run_blocking(effect, ()), Ok(42));
}
Use SpanOptions for levels and typed startup attributes.
#![allow(unused)]
fn main() {
use effectful::{SpanLevel, SpanOptions, succeed};
let effect = succeed::<(), (), ()>(()).with_span_options(
SpanOptions::new("manual.options")
.with_level(SpanLevel::Debug)
.with_attribute("cached", true)
.with_attribute("attempt", 2_i32),
);
}
Function Spans
Use #[effectful::span] on functions returning Effect.
#![allow(unused)]
fn main() {
use effectful::{Effect, Never, span, succeed};
#[span(name = "user.load", level = debug)]
fn load_user(id: u32) -> Effect<u32, Never, ()> {
succeed::<u32, Never, ()>(id + 1)
}
}
Calling load_user only constructs an effect. The span starts when that returned effect is executed.
By default, span names are module_path::function_name, and function arguments are captured with Debug formatting as string attributes.
Privacy Controls
Skip sensitive, large, or non-Debug arguments with skip(...).
#![allow(unused)]
fn main() {
use effectful::{Effect, Never, span, succeed};
struct Secret(String);
#[span(name = "auth.login", skip(password))]
fn login(user_id: u64, password: Secret) -> Effect<(), Never, ()> {
let _ = password.0.len();
succeed::<(), Never, ()>(())
}
}
Use skip_all to capture only explicit fields.
#![allow(unused)]
fn main() {
#[effectful::span(skip_all, fields(route = "/users/:id", cache_hit = true))]
fn route() -> effectful::Effect<(), effectful::Never, ()> {
effectful::succeed::<(), effectful::Never, ()>(())
}
}
Field values can be typed, display-formatted with %, or debug-formatted with ?.
#![allow(unused)]
fn main() {
#[effectful::span(fields(count = 3_i32, user = %user_id, payload = ?payload))]
fn work(user_id: u64, payload: Vec<u8>) -> effectful::Effect<(), effectful::Never, ()> {
effectful::succeed::<(), effectful::Never, ()>(())
}
}
Typed fields preserve strings, booleans, signed integers, and floats. % and ? fields are stored as strings.
Disabled Tracing
When tracing is disabled or not installed, span hooks are no-ops. For non-async #[span] functions, the span macro also avoids formatting captured arguments while tracing is disabled or the span is sampled out.
#![allow(unused)]
fn main() {
use effectful::{TracingConfig, install_tracing_layer, run_blocking};
let _ = run_blocking(install_tracing_layer(TracingConfig::default()), ());
}
Trace Context
Every span record has local OpenTelemetry-shaped identity: TraceId, SpanId, TraceFlags, and parent span id.
Use W3C traceparent helpers at integration boundaries.
#![allow(unused)]
fn main() {
use effectful::SpanContext;
let context = SpanContext::from_traceparent(
"00-01010101010101010101010101010101-0202020202020202-01",
)?;
let header = context.to_traceparent();
Ok::<(), effectful::TraceParentParseError>(())
}
Rust tracing Bridge
Enable the Rust tracing bridge when you want existing subscribers, Loki pipelines, or tracing-opentelemetry stacks to consume effectful spans. Use TracingConfig::tracing_bridge_only() for production forwarding without retaining snapshot_tracing() buffers; use enabled_with_tracing_bridge() when you also need in-memory snapshots.
#![allow(unused)]
fn main() {
use effectful::{TracingConfig, install_tracing_layer, run_blocking};
let _ = tracing_subscriber::fmt().try_init();
let _ = run_blocking(
install_tracing_layer(TracingConfig::tracing_bridge_only()),
(),
);
}
Full snapshot+bridge mode emits spans named effectful.span with otel_name, otel_trace_id, otel_span_id, otel_parent_span_id, otel_trace_flags, status, duration, and attribute fields. Span events emitted with emit_current_span_event are emitted as Rust tracing events inside the current span.
Bridge-only mode emits name-only Rust tracing spans with otel_name. It intentionally skips trace id generation, parent lookup, attribute recording, and snapshot storage so it can stay production-oriented.
For high-throughput services, use an async/batched subscriber/exporter. Subscriber formatting, locks, and exporter backpressure usually dominate span overhead.
Sampling
Use sampling on hot paths. default_sample_rate applies globally; SpanOptions::sample_rate and #[effectful::span(sample = ...)] override it per span. Rates are rounded down to a power-of-two cadence, so 0.3 records at most one in four spans.
#![allow(unused)]
fn main() {
use effectful::{TracingConfig, install_tracing_layer, run_blocking};
let mut config = TracingConfig::tracing_bridge_only();
config.default_sample_rate = 0.0625;
let _ = run_blocking(install_tracing_layer(config), ());
}
Snapshot mode is best for tests, local debugging, and diagnostics. Bridge-only plus sampling is the production-oriented path for very small effects.
See examples 106_span_macro.rs and 107_tracing_bridge.rs for executable coverage.