rolly
Lightweight Rust observability. Hand-rolled OTLP protobuf over HTTP, built on tracing.
7 direct dependencies. No opentelemetry, tonic, or prost.
Crates
| Crate | Description |
|---|---|
rolly |
Runtime-agnostic core. Encoding, metrics, tracing layer. Zero runtime deps. |
rolly-tokio |
Tokio transport. Batching HTTP exporter via reqwest. |
rolly-monoio |
Monoio transport. HTTP exporter via ureq on OS threads. |
All three share a workspace version. Add the runtime crate that matches your async runtime — it re-exports everything from rolly core.
Quick Start (tokio)
[]
= "0.16"
use ;
use Duration;
let _guard = init_global_once;
// All tracing spans and events are now exported as OTLP protobuf
info_span!.in_scope;
Set any endpoint to None to disable that signal. Hold the guard until shutdown — it flushes pending telemetry on drop. For deterministic drain, call guard.shutdown().await instead.
Quick Start (monoio)
[]
= "0.16"
use ;
let _guard = init_global_once;
Composable Setup (no global subscriber)
For frameworks that manage their own tracing subscriber:
use ;
use ;
use Arc;
let exporter = start?;
let sink: = new;
let layer = build_layer;
registry
.with
.with // compose with your own layers
.init;
CLI / No Runtime
use ;
use Arc;
let layer = build_layer;
registry.with.init;
Signals
| Signal | Format | Standard |
|---|---|---|
| Traces | OTLP ExportTraceServiceRequest protobuf |
Yes |
| Logs | OTLP ExportLogsServiceRequest protobuf |
Yes |
| Metrics | OTLP ExportMetricsServiceRequest protobuf |
Yes |
All three follow the OTLP specification and work with any OTLP-compatible backend (Vector, Grafana Alloy, Jaeger, OTEL Collector).
Metrics
Counter, Gauge, and Histogram with client-side aggregation:
use ;
let req = counter;
req.add;
let mem = gauge;
mem.set;
let latency = histogram;
latency.observe;
Attribute order does not matter. When called inside a tracing span, metrics automatically capture an exemplar with trace_id + span_id from the active span.
OTel Semantic Fields
The tracing layer recognizes standard OTel control fields:
let span = info_span!;
// Set status after processing:
span.record;
| Field | OTLP Mapping | Values |
|---|---|---|
otel.kind |
SpanKind |
server, client, producer, consumer, internal |
otel.status_code |
StatusCode |
ok, error, unset |
otel.status_message |
SpanStatus.message |
any string |
These fields are consumed by the layer and do not appear as span attributes in exported data.
Sampling
Trace sampling is deterministic based on trace_id — the same trace always gets the same decision across services.
Some(1.0)orNone— export all traces (default)Some(0.1)— export 10% of tracesSome(0.0)— export no traces
Child spans and log events within a sampled-out trace are suppressed. Metrics are never sampled.
Why not OpenTelemetry SDK?
- Version lock-step across
opentelemetry-*crates - ~120 transitive dependencies, 3+ minute compile times
drop()doesn't flush (shutdown footgun)- gRPC bloat from
tonic/prost
rolly hand-rolls the protobuf wire format. 7 direct dependencies. The wire format has been stable since 2008.
Verification
- 19 Kani proof harnesses for protobuf encoding correctness
- 2 TLA+ specifications (exporter protocol, metrics registry concurrency)
- 3 cargo-fuzz targets for crash/panic freedom
- 9 proptests for roundtrip encoding
- Wire-compatibility tests: rolly encodes, prost/opentelemetry-proto decodes
License
MIT