effectful 0.2.2

Effect<A, E, R> (sync + async), context/layers, pipe — interpreter-style, no bundled executor
Documentation
# 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.

```rust
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.

```rust
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`.

```rust
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(...)`.

```rust
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.

```rust
#[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 `?`.

```rust
#[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.

```rust
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.

```rust
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.

```rust
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.

```rust
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.