Sideways OTel ๐ฆ
Observability from the side - because crabs walk sideways, and so should your telemetry.
A production-ready Rust telemetry library that provides vendor-neutral OpenTelemetry tracing, metrics, and logs, exported via OTLP to any compatible backend - a local Collector, the .NET Aspire dashboard, or any hosted vendor that speaks OTLP.
Note: This crate was built with substantial AI assistance (Claude Code) - design, implementation, and documentation were all done in collaboration with an AI agent, with human review and testing against real OTLP backends throughout. We're noting this in the interest of transparency.
Features
- ๐ญ Vendor-Neutral OTLP Export - Traces, metrics, and logs exported over gRPC to any OTLP-compatible backend
- ๐ One-Line Initialization - Simple
init_telemetry()call sets up everything - ๐ง Standard
OTEL_*Env Vars - Configure via the environment variables already defined by the OpenTelemetry spec - ๐ช Graceful Degradation - Continues running even if the OTLP endpoint is unavailable
- ๐ Native OpenTelemetry Metrics - No vendor-specific macros; instruments come straight from the OTel metrics API
- ๐งต Span Helpers - Small, generic helpers for attaching attributes and events to the current span
- ๐ Health Check Filtering - Automatically filters out noisy health check spans
Quick Start
Add to your Cargo.toml:
[]
= "0.1"
Initialize in your application:
use ;
use *;
use info;
Configuration
Environment Variables
TelemetryConfig::from_env() reads the standard OpenTelemetry environment variables, plus a few additions for enabling/disabling individual signals:
# Service identity
OTEL_SERVICE_NAME=my-service
OTEL_RESOURCE_ATTRIBUTES=deployment.environment=production,team=platform
# OTLP export
OTEL_EXPORTER_OTLP_PROTOCOL=grpc # "grpc" (default) or "http/protobuf"
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317 # 4317 for grpc, 4318 for http/protobuf
OTEL_EXPORTER_OTLP_HEADERS=x-api-key=some-secret,x-tenant=acme
# Enable/disable individual signals (default: true for all)
OTEL_TRACES_ENABLED=true
OTEL_METRICS_ENABLED=true
OTEL_LOGS_ENABLED=true
# Metrics export interval, in milliseconds (default: 60000)
OTEL_METRIC_EXPORT_INTERVAL=60000
# Logging level - full tracing_subscriber directive syntax is supported,
# including per-target overrides and "off":
RUST_LOG=warn,h2=off,hyper=off,tonic=off,opentelemetry=off,opentelemetry_sdk=off
# Enable JSON-formatted console logging (default: false)
JSON_LOGGING=false
OTEL_EXPORTER_OTLP_HEADERS is the mechanism for passing whatever a given backend needs for authentication (an API key, a tenant header, etc.) - the library itself has no knowledge of any particular vendor. See examples/vendor_backend.rs for a worked, vendor-agnostic example (endpoint and header both come from environment variables, so it works unmodified against whichever OTLP-compatible backend you point it at).
Both http:// and https:// endpoints work for either protocol - https:// automatically gets a TLS-enabled channel trusting Mozilla's bundled root store (via tonic's tls-webpki-roots), so no extra configuration is needed to reach a public vendor endpoint.
OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf is useful where gRPC/HTTP2 gets blocked (some corporate proxies/gateways) or where a vendor only exposes an HTTP ingestion endpoint. When left unset, the exporter falls back to its protocol-specific default endpoint (http://localhost:4317 for gRPC, http://localhost:4318 for HTTP/protobuf) if OTEL_EXPORTER_OTLP_ENDPOINT also isn't set.
Programmatic Configuration
use TelemetryConfig;
let config = builder
.service_name
.otlp_endpoint
.with_otlp_header
.with_resource_attribute
.build;
let telemetry = init_telemetry;
Metrics
Metrics are created and recorded through the native OpenTelemetry metrics API - there are no macros to import. sideways_otel::prelude provides counter/histogram/up_down_counter/gauge shortcuts, each scoped automatically to the service name passed to init_telemetry - no need to repeat it at every call site:
use *;
let requests = counter;
requests.add;
let latency = histogram;
latency.record;
let queue_depth = up_down_counter;
queue_depth.add;
These default to u64 counters, f64 histograms/gauges, and i64 up/down counters - the common case. For a different numeric type, per-instrument description/unit, or a meter scoped to something other than the service name, drop down to meter() (or opentelemetry::global::meter("some-scope")) and its u64_counter()/f64_histogram()/etc. builders directly.
Create each instrument once (e.g. in a OnceLock or at startup) and reuse it - meter() is cheap to call repeatedly, but recreating the instrument itself on every call is wasted work.
Tracing
Once telemetry is initialized, use the standard tracing crate for distributed tracing.
Basic Logging
use ;
info!;
warn!;
error!;
Instrumentation
#[tracing::instrument] wraps a function in a span - by default named after the function, with every argument recorded as a field (via Debug), and child of whatever span was active when it was called:
async
Adding More Data to the Span
The attribute takes several options for going beyond the defaults - useful when the arguments alone don't tell the whole story, contain things you don't want recorded, or when a value is only known partway through the function:
async
A few things worth knowing:
skip(arg1, arg2, ...)(orskip_all) excludes arguments from the span's fields - required for anything that isn'tDebug, and good practice for large payloads or secrets you don't want in your telemetry backend.fields(...)adds fields beyond the function's arguments. Prefix a value with%to format it withDisplay,?forDebug(the default for argument-derived fields), or leave it bare for values that already implementtracing::Value(integers, bools, strings).tracing::field::Emptyreserves a field slot for a value you don't have yet; calltracing::Span::current().record("field_name", &value)later in the function once you do. Fields not declared up front (infields(...)or the argument list) can't be recorded this way.err(orerr(Debug)to useDebuginstead ofDisplay) automatically records anErrreturn value as a field and emits an event for it - handy instead of manually callingrecord_errorat every fallible call site.level = "debug"(ortrace/warn/error) controls the span's level; default isinfo.
Span Attribute Helpers
sideways_otel::span (re-exported from the prelude) provides generic helpers for enriching the current span with OpenTelemetry attributes and events, without reaching for tracing_opentelemetry directly:
use *;
async
Health Check Filtering
The library automatically filters out health check-related spans to reduce noise:
- Spans from
tonic_health - Spans containing "health", "Health", or "Check"
- gRPC health check services
Local Testing
Any OTLP-compatible collector works. Two easy options:
OpenTelemetry Collector (Docker):
.NET Aspire dashboard (standalone, no .NET project required) exposes an OTLP/gRPC endpoint and a browser UI for traces, logs, and metrics:
Then point your service at it:
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:18889 OTEL_SERVICE_NAME=my-service
Open http://localhost:18888 (using the browser auth token printed to the container's console) to see traces, structured logs, and metrics.
Publishing
This crate is published to crates.io.
Before publishing:
Then:
cargo publish requires you to be logged in (cargo login) with an account that has publish rights on the crate, and will refuse to publish if Cargo.toml metadata (description, license, readme, etc.) is incomplete.
License
Dual-licensed under either of
at your option.
Credits
Built by iClassPro team, powered by OpenTelemetry.