# 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
| `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`):
| `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)
| `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
});
```
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
| 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
| 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`:
| [`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