kamu-logging 2.0.0

Structured tracing setup for native services and Cloudflare Workers.
Documentation

kamu-logging

Crates.io docs.rs CI

License 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 workspace.

Install

[dependencies]
kamu-logging = "2"

MSRV: Rust 1.94 (edition 2024).

Features

Feature Default What it enables
systemd yes TTY-aware console + journald sink, RUST_LOG, logtracing bridge
with-actix-web yes Correlation-enriched Actix Web middleware
with-otlp no OpenTelemetry OTLP exporter (HTTP/protobuf)
wasm32 no Cloudflare Worker / web console + panic hook (mutually exclusive)

Quickstart

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:

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

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

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:

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:

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

To inspect every validated W3C field:

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>(())

OTLP export

Enable the with-otlp feature and attach an OtlpConfig:

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:

// 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

[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:

[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.

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:

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 Zero-config init().
json_stdout JSON on stdout for log aggregators (Vector, Promtail, Datadog).
actix Correlation-enriched root spans; also needs with-actix-web.

The standalone Worker app is examples/cloudflare-worker/, covered in 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 or Apache-2.0 at your option (MIT OR Apache-2.0). Previously MIT-only through 1.1.1.