rolly
Lightweight Rust observability. Hand-rolled OTLP protobuf over HTTP, built on tracing.
Core and middleware
rolly has two layers:
Generic core — works with any Rust application, not just HTTP servers:
- Custom
tracing::Layerthat captures all spans and events - Encodes them as OTLP protobuf (
ExportTraceServiceRequest,ExportLogsServiceRequest) - Ships via HTTP POST to any OTLP-compatible collector (Vector, Grafana Alloy, OTEL Collector)
- Dual output: OTLP HTTP primary + JSON stderr fallback (local dev / CloudWatch)
- Background exporter with batching (512 items / 1s window), concurrent workers, and 3-retry exponential backoff — telemetry never blocks your application
- Probabilistic head-based trace sampling — deterministic based on trace_id, so the same trace gets the same decision across services
- Native OTLP metrics with Counter, Gauge, and Histogram instruments, client-side aggregation, and
ExportMetricsServiceRequestexport - Automatic exemplar capture — metric data points are annotated with trace_id + span_id from the active span, enabling drill-down from metric spikes to traces
- Process metrics (CPU, memory) via
/procpolling on Linux
HTTP middleware (optional, tower feature) — framework-specific request instrumentation:
- Tower middleware for Axum
- Extracts request IDs (CloudFront,
x-request-id, or any header), generates deterministic trace IDs via BLAKE3 - Creates request spans with method, path, status, latency
- Emits RED metrics (request duration, count, errors)
- W3C
traceparentpropagation for outbound requests
Any tracing span or event from anywhere in your application — HTTP handlers, background tasks, queue consumers, batch jobs — flows through the same OTLP export pipeline.
Signals
| Signal | Format | Standard |
|---|---|---|
| Traces | OTLP ExportTraceServiceRequest protobuf |
Yes |
| Logs | OTLP ExportLogsServiceRequest protobuf |
Yes |
| Metrics | OTLP ExportMetricsServiceRequest protobuf |
Yes |
All three signals follow the OTLP specification and are encoded as native protobuf. Any OTLP-compatible backend can ingest them directly.
Metrics
rolly provides Counter, Gauge, and Histogram instruments with client-side aggregation. Metrics are accumulated in-process and flushed as ExportMetricsServiceRequest on a configurable interval (default 10s).
use ;
// Counters are monotonic and cumulative
let req_counter = counter;
req_counter.add;
// Gauges record last-value
let mem_gauge = gauge;
mem_gauge.set;
// Histograms with configurable bucket boundaries
let latency = histogram;
latency.observe;
Attribute order does not matter — [("a", "1"), ("b", "2")] and [("b", "2"), ("a", "1")] aggregate to the same data point.
When called inside a tracing span, metric recordings automatically capture an exemplar with the current trace_id and span_id. This lets you click from a latency spike on a dashboard straight to the offending trace — no configuration needed.
Usage
use ;
use Duration;
let _guard = init;
// All tracing spans/events are now exported as OTLP protobuf
info_span!.in_scope;
Endpoints can be configured independently — send traces to Jaeger, logs to Vector, and metrics to a different collector:
let _guard = init;
Set any endpoint to None to disable that signal. Set sampling_rate to None or Some(1.0) to export all traces (default).
Sampling
Trace sampling reduces export volume at high scale. The decision is deterministic based on trace_id — the same trace always gets the same decision, so sampling is consistent across services sharing a trace.
Some(1.0)orNone— export all traces (default)Some(0.1)— export 10% of tracesSome(0.01)— export 1% of tracesSome(0.0)— export no traces
Child spans and log events within a sampled-out trace are also suppressed. Metrics are never sampled — counters and gauges always reflect the full traffic.
HTTP middleware (Axum/Tower)
The tower feature is enabled by default.
let app = new
.route
.layer // inbound: request spans + RED metrics
.layer; // outbound: W3C traceparent injection
To disable Tower middleware (e.g. for non-HTTP applications):
[]
= { = "0.5", = false }
Pipeline
Application (tracing) → rolly (protobuf) → HTTP POST → Vector/Collector (OTLP) → storage
Why not OpenTelemetry SDK?
- Version lock-step across
opentelemetry-*crates - ~120 transitive dependencies, 3+ minute compile times
- Shutdown footgun (
drop()doesn't flush) - gRPC bloat from
tonic/prost
rolly hand-rolls the protobuf wire format (~200 lines). The format has been stable since 2008.
Dependencies
7 direct dependencies. No opentelemetry, tonic, or prost.
Performance
rolly targets <10% CPU overhead at 3000 req/s on ARM64.
Benchmarks simulate realistic e-commerce API traffic. See benches/baseline.toml for raw numbers.
License
MIT OR Apache-2.0