kamu-logging 2.1.0

Structured tracing setup for native services and Cloudflare Workers.
Documentation
# kamu-logging

[![Crates.io][badge-crates]][link-crates]
[![docs.rs][badge-docs]][link-docs]
[![CI][badge-ci]][link-ci]

[![License][badge-license]][link-license]
[![MSRV][badge-msrv]][link-msrv]

Structured `tracing` setup for native services and Cloudflare Workers. Includes
console and journald output, Actix request spans, W3C correlation parsing, and
optional OTLP export.

Part of the [`kamu-public-crates`](https://github.com/pt-immer/kamu-public-crates) workspace.

## Install

```toml
[dependencies]
kamu-logging = "2"
```

## Features

| Feature          | Default | What it enables                                                       |
|------------------|:-------:|-----------------------------------------------------------------------|
| `correlation`    |   yes   | W3C `traceparent` parsing + correlation spans; installs no subscriber  |
| `systemd`        |   yes   | TTY-aware console + journald sink, `RUST_LOG`, `log``tracing` bridge |
| `with-actix-web` |   yes   | Correlation-enriched Actix Web middleware                              |
| `with-otlp`      |   no    | OpenTelemetry OTLP exporter (HTTP/protobuf); requires `systemd`         |
| `wasm32`         |   no    | Cloudflare Worker / web console + panic hook (mutually exclusive)     |

`systemd`, `wasm32`, and `with-actix-web` each imply `correlation`, and at least
one of the four must be enabled. `systemd` and `wasm32` are mutually exclusive,
as are `wasm32` and the Actix Web and OTLP features.

## Quickstart

```rust
fn main() -> Result<(), Box<dyn std::error::Error>> {
    kamu_logging::init()?;
    kamu_logging::info!("hello");
    Ok(())
}
```

On native targets, `init()` writes to stderr: pretty output when stderr is a
terminal and compact output otherwise. `RUST_LOG` controls filtering. Journald
is available through explicit configuration.

## Configuration

For anything beyond the default path, build an `InitOptions`:

```rust
use kamu_logging::{Format, InitOptions, Sink, init_with};

init_with(
    InitOptions::default()
        .with_service_name("my-service")
        .with_default_filter("info,my_service=debug")
        .with_env_var("MY_SERVICE_LOG")
        .with_format(Format::Json)
        .with_sink(Sink::Stdout),
)?;
```

Builder methods (all consume `self`, all return `Self`):

| Method                   | Purpose                                                            |
|--------------------------|--------------------------------------------------------------------|
| `with_service_name(n)`   | Attach `service.name` to the startup event + OTLP `Resource`       |
| `with_default_filter(f)` | Filter directive used when the env var is unset                    |
| `with_env_var(v)`        | Env var read for the filter (default `RUST_LOG`)                   |
| `with_format(f)`         | `Auto` / `Compact` / `Pretty` / `Json`                             |
| `with_sink(s)`           | `Auto` / `Stdout` / `Stderr` / `Journald`                          |
| `idempotent(true)`       | Accept a repeated installation owned by this crate                 |
| `with_otlp(cfg)`         | (with-otlp) Add an OTLP exporter layer                             |

### Env-var triggers (no code change)

| Variable           | Values                                  | Effect                                  |
|--------------------|-----------------------------------------|-----------------------------------------|
| `RUST_LOG`         | tracing-subscriber directive            | Filter directive (overridable per init) |
| `KAMU_LOG_FORMAT`  | `auto`, `compact`, `pretty`, `json`     | Sets `Format` when the option is `Auto` |
| `KAMU_LOG_SINK`    | `auto`, `stdout`, `stderr`, `journald`  | Sets `Sink` when the option is `Auto`   |

Unknown values are errors. They never silently select `Auto`.

## JSON output for log aggregators

```rust
use kamu_logging::{Format, InitOptions, Sink, init_with};

init_with(
    InitOptions::default()
        .with_format(Format::Json)
        .with_sink(Sink::Stdout),
)?;
```

Or set `KAMU_LOG_FORMAT=json KAMU_LOG_SINK=stdout` without rebuilding. Each
event is one JSON line suitable for a log collector.

## Actix Web

```rust
use actix_web::{App, HttpServer};
use kamu_logging::get_actix_web_logger;

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    kamu_logging::init().expect("init logging");
    HttpServer::new(|| App::new().wrap(get_actix_web_logger()))
        .bind(("127.0.0.1", 8080))?
        .run()
        .await
}
```

`get_actix_web_logger()` uses an `EnrichedRootSpanBuilder` that adds a
`correlation_id` field to the root span by extracting (in order):
`X-Request-ID`, `X-Correlation-ID`, `traceparent`. For a custom builder,
use `get_actix_web_logger_with::<MyBuilder>()`.

Request and correlation IDs must contain 1–128 visible ASCII bytes. A
`traceparent` value is used only after its complete four-field prefix passes
W3C validation.

## Correlation outside HTTP

For queue consumers, scheduled tasks, or any non-HTTP entry point:

```rust
use kamu_logging::correlation::{with_id, extract_from_headers, DEFAULT_HEADER_CHAIN};

with_id("job-42", || {
    kamu_logging::info!("processing job");
});
```

The header-chain extractor is reusable for any framework — pass a closure
that fetches a header by name:

```rust
let id = extract_from_headers(&headers, DEFAULT_HEADER_CHAIN, |h, name| {
    h.get(name).cloned()
});
```

To inspect every validated W3C field:

```rust
use kamu_logging::correlation::TraceParent;

let parent = TraceParent::parse(
    "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
)?;
assert_eq!(parent.trace_id(), "4bf92f3577b34da6a3ce929d0e0e4736");
assert!(parent.is_sampled());
# Ok::<(), kamu_logging::correlation::TraceParentError>(())
```

## Correlation without a subscriber

A library has no business choosing the logging backend for the binary that
embeds it. Take `correlation` on its own and nothing that installs or sinks logs
enters the dependency graph — no `tracing-subscriber`, no journald, no console
writer, no exporter:

```toml
[dependencies]
kamu-logging = { version = "2", default-features = false, features = ["correlation"] }
```

That build exposes `kamu_logging::correlation` and the re-exported `tracing`
vocabulary. `init`, `init_with`, `InitOptions`, `Format`, `Sink`, and `Error`
belong to the subscriber-owning surface and require `systemd` or `wasm32`.

The same holds one layer up: `features = ["with-actix-web"]` alone gives the
correlation-enriched middleware without committing the binary to a subscriber.

## OTLP export

Enable the `with-otlp` feature and attach an `OtlpConfig`:

```rust
use kamu_logging::{InitOptions, init_with, otlp::OtlpConfig};

init_with(
    InitOptions::default()
        .with_service_name("checkout-api")
        .with_otlp(
            OtlpConfig::new("https://otel-collector.example.com:4318")
                .with_header("authorization", "Bearer …")
                .with_resource_attribute("deployment.environment", "production"),
        ),
)?;
```

`OtlpConfig::new` accepts the collector base URL and adds `/v1/traces`.
`with_service_name` on `InitOptions` supplies the resource name unless the OTLP
configuration overrides it. Debug output redacts the endpoint, all header
values, and secret-like resource attributes.

Export uses an in-process `BatchSpanProcessor` by default: spans are buffered
and flushed from a dedicated background OS thread, so export never blocks the
thread that closed the span and **no async runtime is required**. Tune the
buffer with `with_max_queue_size`, `with_scheduled_delay`, and
`with_max_export_batch_size`.

The batch buffer drains on a periodic timer (~5 s by default), so a process
that exits abruptly can lose the last, unflushed batch. Call `shutdown_otlp()`
(or `flush_otlp()`) before exit — e.g. from your `SIGTERM` handler — to drain
it:

```rust
// after the server stops accepting work, before the process exits:
kamu_logging::shutdown_otlp()?;
```

For deterministic synchronous export (handy in tests or short-lived CLIs), opt
back into the inline processor with
`.with_processor(SpanProcessorMode::Simple)`.

## WASM

```toml
[dependencies]
kamu-logging = { version = "2", default-features = false, features = ["wasm32"] }
```

`init()` on wasm32 installs `console_error_panic_hook` and a
`tracing-subscriber` console writer suitable for Cloudflare Workers Logs.
`Format::Auto` resolves to JSON, while `Sink::Auto`, `Sink::Stdout`, and
`Sink::Stderr` all write to the JavaScript console. A repeated strict `init()`
returns `Error::AlreadyInitialized`; use `init_or_skip()` when repetition is
intentional.

## Cloudflare Workers

Use `workers-rs` as usual, disable default features, and enable `wasm32`:

```toml
[dependencies]
kamu-logging = { version = "2", default-features = false, features = ["wasm32"] }
worker = "0.8"
```

The repository includes a standalone Worker app in
`examples/cloudflare-worker/`. For setup, Wrangler config, Workers Logs, and
correlation-id examples, see [the Cloudflare Workers guide](docs/CLOUDFLARE_WORKERS.md).

## Idempotence

- `init()` returns `Err(Error::AlreadyInitialized)` on a second call.
  Surfaces library double-init as a bug.
- `init_or_skip()` returns `Ok(())` on a second call. Use from test
  harnesses and embedded CLI runs.
- `InitOptions::idempotent(true)` does the same thing via the builder.
- A subscriber or `log` facade installed elsewhere returns
  `ForeignGlobalSubscriber` or `ForeignGlobalLogger`, even in idempotent mode.

`ForeignGlobalLogger` is detected after `kamu-logging` commits its tracing
subscriber and any OTLP provider. Those remain active; the pre-existing logger
continues to own the `log` facade, and retries return the same conflict.

## Migrating from 1.x

| 1.x behavior                         | 2.x replacement                                                   |
|--------------------------------------|-------------------------------------------------------------------|
| Unknown format or sink became `Auto` | Handle the `Result` from `from_env_value`, or use `FromStr`        |
| Non-TTY `Auto` selected journald     | Select `Sink::Journald` explicitly                                |
| `TracingGlobal` / `TracingLog`       | `ForeignGlobalSubscriber` / `ForeignGlobalLogger`                  |
| Partial `traceparent` acceptance     | Use `TraceParent::parse`; invalid parent IDs and flags are rejected |

## Re-exported `tracing` items

So you can avoid a separate `tracing` import for the basics:

```rust
use kamu_logging::{debug, info, warn, error, instrument, span, Level, Span};
```

## Troubleshooting

| Symptom                                | Fix                                                                         |
|----------------------------------------|-----------------------------------------------------------------------------|
| Logs should go to stdout               | Set `KAMU_LOG_SINK=stdout` or select `Sink::Stdout`                          |
| `Error::IO` with explicit journald     | Journal socket is unavailable; choose stdout or stderr                      |
| `ForeignGlobalSubscriber` during tests | Install one harness subscriber, or let `kamu-logging` own the global slot   |
| `ForeignGlobalLogger`                  | Remove or coordinate the earlier `log` owner; retrying cannot replace it    |
| OTLP exporter is slow in simple mode   | Keep the default batch processor for high-volume services                   |
| `service.name` missing from fmt output | It is on the startup event and OTLP resource, not every formatted log event |

## Examples

Run with `cargo run --example <name> --features systemd`:

| Example | What it shows |
| --- | --- |
| [`minimal`]examples/minimal.rs | Zero-config `init()`. |
| [`json_stdout`]examples/json_stdout.rs | JSON on stdout for log aggregators (Vector, Promtail, Datadog). |
| [`actix`]examples/actix.rs | Correlation-enriched root spans; also needs `with-actix-web`. |

The standalone Worker app is [`examples/cloudflare-worker/`](examples/cloudflare-worker/),
covered in [Cloudflare Workers](#cloudflare-workers) above.

## SemVer policy

`2.x.y` — breaking changes only on major bumps. Additive changes ship as minor
releases. Bug fixes ship as patches. The `Error` enum is
`#[non_exhaustive]`; new variants are not breaking.

## License

Dual-licensed under either [MIT](LICENSE-MIT) or [Apache-2.0](LICENSE-APACHE) at
your option (`MIT OR Apache-2.0`). Previously MIT-only through 1.1.1.

[badge-crates]: https://img.shields.io/crates/v/kamu-logging?style=flat-square&logo=rust
[badge-docs]: https://img.shields.io/docsrs/kamu-logging?style=flat-square&logo=docs.rs&label=docs.rs
[badge-ci]: https://img.shields.io/github/actions/workflow/status/pt-immer/kamu-public-crates/on-pr-synced.yml?branch=main&style=flat-square&label=CI
[badge-license]: https://img.shields.io/crates/l/kamu-logging?style=flat-square
[badge-msrv]: https://img.shields.io/crates/msrv/kamu-logging?style=flat-square&logo=rust&label=MSRV

[link-crates]: https://crates.io/crates/kamu-logging
[link-docs]: https://docs.rs/kamu-logging
[link-ci]: https://github.com/pt-immer/kamu-public-crates/actions/workflows/on-pr-synced.yml
[link-license]: https://github.com/pt-immer/kamu-public-crates/blob/main/crates/logging
[link-msrv]: https://github.com/pt-immer/kamu-public-crates/blob/main/Cargo.toml