otel-bootstrap 2.11.0

One-call OpenTelemetry bootstrap — traces + metrics with OTLP export
Documentation
# otel-bootstrap

[![CI](https://github.com/brefwiz/otel-bootstrap/actions/workflows/ci.yml/badge.svg)](https://github.com/brefwiz/otel-bootstrap/actions/workflows/ci.yml)
[![crates.io](https://img.shields.io/crates/v/otel-bootstrap.svg)](https://crates.io/crates/otel-bootstrap)
[![docs.rs](https://docs.rs/otel-bootstrap/badge.svg)](https://docs.rs/otel-bootstrap)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
[![Rust 1.85+](https://img.shields.io/badge/rust-1.85%2B-orange.svg)](https://www.rust-lang.org)

One-call OpenTelemetry bootstrap for Rust services — traces, metrics, and logs over OTLP with sensible defaults.

The standard `opentelemetry` + `opentelemetry-otlp` + `tracing-subscriber` wiring is the same in every service. This crate does it once: call `init_telemetry("my-service")`, keep the handle alive, and drop it to flush.

## Features

- **One call to wire it all up** — traces, metrics, and logs over OTLP, `tracing-subscriber` configured, W3C TraceContext + Baggage propagators registered.
- **gRPC or HTTP/protobuf** — select transport via cargo feature.
- **Env-var configuration** — follows the OpenTelemetry spec (`OTEL_EXPORTER_OTLP_ENDPOINT`, `OTEL_SERVICE_NAME`, `OTEL_TRACES_SAMPLER`, …).
- **Builder API** — version, environment, sampler, custom meter setup, extra subscriber layers.
- **Optional axum middleware**`OtelTraceLayer` for inbound HTTP trace propagation.
- **Generic `enduser.*` span enrichment** — implement `EnrichSpan` on your context type and plug it into `SpanEnricherLayer<T>` with no brefwiz dependencies required.
- **Graceful shutdown** — drop `TelemetryHandles` to flush and shut down both providers.

## Quick start

```toml
[dependencies]
otel-bootstrap = "2"
tokio = { version = "1", features = ["full"] }
```

```rust
use otel_bootstrap::init_telemetry;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let _telemetry = init_telemetry("my-service")?;

    tracing::info!("service started");
    // ... run your server ...
    Ok(())
    // _telemetry dropped here → SDK flushes and shuts down
}
```

For more control, use the builder:

```rust
use otel_bootstrap::{LogFormat, Telemetry};

let _telemetry = Telemetry::builder("my-service")
    .with_version(env!("CARGO_PKG_VERSION"))
    .with_environment("production")
    .with_log_filter("info,opentelemetry_sdk=warn")
    .with_log_format(LogFormat::Json)
    .init()?;
```

`with_log_filter` and `with_log_format` take precedence over environment-backed
logging defaults, so typed configuration sources can configure logging without
mutating process-global environment.

## Cargo features

| Feature       | Default | Description |
|---------------|---------|-------------|
| `grpc`        || OTLP/gRPC transport via `tonic` |
| `http`        || OTLP/HTTP-protobuf transport via `reqwest` |
| `axum`        || `OtelTraceLayer` + `SpanEnricherLayer<T>` for axum servers |
| `tonic-tracing` || `grpc_client_layer()` + `grpc_server_layer()` for raw tonic gRPC clients/servers |
| `testing`     || In-memory exporters for unit tests |

At least one of `grpc` or `http` must be enabled (enforced at compile time).

## Configuration

| Variable | Default |
|---|---|
| `OTEL_EXPORTER_OTLP_ENDPOINT` | `http://localhost:4317` (gRPC) / `http://localhost:4318` (HTTP) |
| `OTEL_EXPORTER_OTLP_PROTOCOL` | `grpc` |
| `OTEL_EXPORTER_OTLP_TIMEOUT` | `10000` ms |
| `OTEL_SERVICE_NAME` | overridden by the `service_name` argument |
| `OTEL_TRACES_SAMPLER` | `parentbased_always_on` |
| `OTEL_TRACES_SAMPLER_ARG` | sampler-specific (e.g. ratio for `traceidratio`) |

Full reference: [`OTEL_TRACES_SAMPLER` values](https://opentelemetry.io/docs/concepts/sdk-configuration/general-sdk-configuration/#otel_traces_sampler).

## Axum middleware

```toml
otel-bootstrap = { version = "2", features = ["axum"] }
```

```rust
use otel_bootstrap::axum_layer;

let app = Router::new()
    .route("/", get(handler))
    .layer(axum_layer());
```

This layer reads the `traceparent` / `tracestate` headers, starts a server span, and propagates context to all child spans.

## `enduser.*` span enrichment

```toml
otel-bootstrap = { version = "2", features = ["axum"] }
```

Implement `EnrichSpan` on your context type, then plug it into `SpanEnricherLayer`:

```rust
use otel_bootstrap::span_enrichment::{EnrichSpan, span_enricher_layer};
use tracing::Span;

#[derive(Clone)]
struct MyContext { user_id: String }

impl EnrichSpan for MyContext {
    fn enrich(&self, span: &Span) {
        span.record("enduser.id", &self.user_id.as_str());
    }
}

let app = Router::new()
    .route("/", get(handler))
    .layer(span_enricher_layer::<MyContext>());
```

Routes with no `MyContext` extension are silently skipped.

## Span-field log propagation

When `with_logs(true)` is set, otel-bootstrap replaces the upstream log bridge with `SpanAwareLogBridge`. It propagates span-level attributes into every OTLP log record so that `request.id`, `enduser.*`, and `http.*` fields appear as Loki labels alongside the trace context.

Two propagation paths are supported:

- **Tracing fields** declared at span creation (`info_span!("req", "request.id" = id)`) — captured automatically for names in [`PROPAGATED_SPAN_FIELDS`].
- **Post-creation attributes** set via `record_span_log_attr_on` — use this from middleware or `EnrichSpan::enrich_span` for fields that arrive after span creation.

```rust
use otel_bootstrap::{span_enrichment::emit_request_id, record_span_log_attr_on};
use opentelemetry::{Key, logs::AnyValue};

// In your request handler / middleware:
emit_request_id(&request_id);  // → appears in traces + logs

// In EnrichSpan::enrich_span for enduser fields:
span.set_attribute("enduser.id", user_id.clone());
record_span_log_attr_on(span, Key::new("enduser.id"), AnyValue::String(user_id.into()));
```

Override the captured field set per service:

```rust
const FIELDS: &[&str] = &["request.id", "enduser.id", "tenant.id"];
let _telemetry = Telemetry::builder("my-service")
    .with_logs(true)
    .with_propagated_span_fields(FIELDS)
    .init()?;
```

## Continuous profiling

Profiling is the fourth OTLP signal alongside traces, metrics, and logs
(ADR platform/0201). It ships as an explicit, off-by-default feature:

| Feature | Default | Description |
|---|---|---|
| `profiling` || Enables `Telemetry::builder(...).with_profiling(endpoint)` |
| `profiling-bridge-pyroscope-rs` || Implies `profiling`; wires the pyroscope-rs exporter |

```rust
let _telemetry = otel_bootstrap::Telemetry::builder("my-service")
    .with_profiling("http://localhost:4040")
    .init()?;
```

**Single export plane.** There is no direct-to-backend pyroscope client
path. `pyroscope-rs` hardcodes its own HTTP client with no TLS/identity
hook, so profiles are pushed over plain HTTP to a **localhost-only
SPIFFE-terminating sidecar** that carries the workload identity upstream
(ADR platform/0203). The `with_profiling` endpoint is validated at init
time to be loopback-only (`127.0.0.1`, `::1`, `localhost`, or a unix
socket) — anything else is rejected. This bridge is a temporary,
sunset-bound exception (ADR platform/0202, Tracks #40) pending a native Rust OTLP
profiles exporter; every profile carries `trace_id`/`span_id` tags for
correlation with the trace pillar.

**SVID rotation requires a pod restart.** Like the rest of the telemetry
channel, mTLS material for the profiling sidecar path is read once at
`init()` time and held for the process lifetime. SPIFFE SVID rotation is
not hot-reloaded into an already-running telemetry channel — a rotated
SVID only takes effect on the next process start (pod restart). This is a
standing platform constraint, not a profiling-specific bug.

## Examples

- [`basic_setup`]examples/basic_setup.rs — minimal init
- [`shutdown_handling`]examples/shutdown_handling.rs — explicit graceful flush
- [`custom_config`]examples/custom_config.rs — builder API with version, environment, sampler
- [`axum_span_enricher`]examples/axum_span_enricher.rs — axum + generic `EnrichSpan` enrichment
- [`telemetry_profiling`]examples/telemetry_profiling.rs — continuous profiling via the builder

## License

MIT — see [LICENSE](LICENSE).