Metrics & Observability
Prometheus / OpenTelemetry metrics export, request-ID propagation, and upload-progress callbacks.
Metrics & Observability
tako-rs-plugins provides the observability hooks: a metrics plugin that
exports to Prometheus or OpenTelemetry, request-ID propagation, and
upload-progress tracking. The metrics plugin is wired through Tako's signal
system; see Observability for the broader story.
Metrics export
The metrics plugin listens to Tako's application- and route-level signals and forwards them to a backend. Two backends ship behind feature flags, each with a config type that installs the plugin and wires up the export path in one call.
Prometheus
Requires the metrics-prometheus feature. PrometheusMetricsConfig::install
registers the plugin and mounts a scrape endpoint (default /metrics),
returning the Arc<Registry>.
use tako::plugins::metrics::PrometheusMetricsConfig;
use tako::router::Router;
let mut router = Router::new();
let registry = PrometheusMetricsConfig::default() // endpoint_path = "/metrics"
.install(&mut router);with_buckets(vec) overrides the latency histogram bucket schedule. The scrape
endpoint encodes the registry in the Prometheus text format and returns 500
(not a 200 with an error body) if encoding fails, so scraper alerting fires
correctly.
OpenTelemetry
Requires the metrics-opentelemetry feature. OtelMetricsConfig::install
registers the plugin with an OTLP exporter and returns the SdkMeterProvider,
which you keep alive for the process lifetime and shutdown() during graceful
shutdown.
use tako::plugins::metrics::OtelMetricsConfig;
use tako::router::Router;
let mut router = Router::new();
let meter_provider = OtelMetricsConfig::default()
.with_endpoint("http://localhost:4318/v1/metrics")
.install(&mut router)?;
// ... serve ...
meter_provider.shutdown()?;with_meter_name(name) sets the meter name (default "tako").
//! Metrics with OpenTelemetry OTLP exporter example.
//!
//! This example demonstrates how to use Tako's metrics plugin with
//! OpenTelemetry OTLP exporter to send metrics to collectors like
//! Prometheus, Jaeger, or the OpenTelemetry Collector.
//!
//! Run this example with:
//! ```sh
//! cargo run --example metrics-opentelemetry --features metrics-opentelemetry
//! ```
//!
//! To test with Prometheus:
//! ```sh
//! docker run -p 9090:9090 prom/prometheus --web.enable-otlp-receiver
//! ```
//! Then update the endpoint to "http://localhost:9090/api/v1/otlp/v1/metrics"
use anyhow::Result;
use tako::Method;
use tako::plugins::metrics::OtelMetricsConfig;
use tako::responder::Responder;
use tako::router::Router;
use tokio::net::TcpListener;
async fn hello() -> impl Responder {
"Hello from metrics example".into_response()
}
async fn health() -> impl Responder {
"OK".into_response()
}
#[tokio::main]
async fn main() -> Result<()> {
let listener = TcpListener::bind("127.0.0.1:8080").await?;
let mut router = Router::new();
router.route(Method::GET, "/", hello);
router.route(Method::GET, "/health", health);
// Install the OpenTelemetry metrics plugin with OTLP exporter.
// By default, metrics are exported to http://localhost:4318/v1/metrics
let meter_provider = OtelMetricsConfig::default()
.with_endpoint("http://localhost:4318/v1/metrics")
.install(&mut router)?;
println!("Server running on http://127.0.0.1:8080");
println!("Metrics being exported via OTLP to http://localhost:4318/v1/metrics");
tako::serve(listener, router).await;
// Shutdown the meter provider to flush remaining metrics
meter_provider.shutdown()?;
Ok(())
}
Both backends depend on the signal system, so the metrics features enable
signals transitively. See the feature reference
for the full flag graph.
Request ID
request_id::RequestId generates or propagates a unique request identifier via
the X-Request-ID header. If the incoming request already carries the header
it is preserved (capped at 256 bytes); otherwise a UUID v4 is generated. The ID
is inserted into the request extensions as RequestIdValue and echoed in the
response header.
use tako::middleware::IntoMiddleware;
use tako::middleware::request_id::RequestId;
// Default: X-Request-ID with a UUID v4.
let request_id = RequestId::new().into_middleware();
// Custom header name.
let correlation = RequestId::new()
.header_name("X-Correlation-ID")
.into_middleware();
// Custom generator.
let custom = RequestId::new()
.generator(|| ulid_like_id())
.into_middleware();Register it globally so every request — and every log line and tracing span
that reads RequestIdValue — is correlated:
router.middleware(RequestId::new().into_middleware());Upload progress
upload_progress::UploadProgress wraps the request body to track bytes
received, reporting through a callback and through request extensions. The
ProgressState passed to the callback carries bytes_read, total_bytes
(from Content-Length, when known), and a percent() helper.
use tako::middleware::IntoMiddleware;
use tako::middleware::upload_progress::UploadProgress;
let progress = UploadProgress::new()
.on_progress(|state| {
let pct = state.percent().map(|p| format!("{p}%")).unwrap_or_else(|| "?%".into());
println!("{pct}: {} / {:?} bytes", state.bytes_read, state.total_bytes);
})
.min_notify_interval_bytes(1024) // throttle callbacks to ~once per KiB
.into_middleware();Inside a handler, the live tracker is available as a ProgressTracker
extension, exposing bytes_read(), total_bytes(), and percent():
use tako::middleware::upload_progress::ProgressTracker;
let pct = req
.extensions()
.get::<ProgressTracker>()
.and_then(|t| t.percent())
.unwrap_or(0);use anyhow::Result;
use tako::middleware::upload_progress::{ProgressTracker, UploadProgress};
use tako::middleware::IntoMiddleware;
use tako::responder::Responder;
use tako::router::Router;
use tako::types::Request;
use tako::Method;
async fn upload_handler(req: Request) -> impl Responder {
// Access the progress tracker from request extensions
let info = req
.extensions()
.get::<ProgressTracker>()
.map(|tracker| {
let bytes = tracker.bytes_read();
let total = tracker.total_bytes();
let pct = tracker.percent().unwrap_or(0);
format!("Upload complete: {bytes} bytes received (total: {total:?}, {pct}%)")
})
.unwrap_or_else(|| "No progress tracker found".to_string());
info
}
#[tokio::main]
async fn main() -> Result<()> {
tracing_subscriber::fmt::init();
let progress = UploadProgress::new()
.on_progress(|state| {
let pct = state.percent().map(|p| format!("{p}%")).unwrap_or_else(|| "?%".into());
println!(
"Upload progress: {} / {} bytes ({pct})",
state.bytes_read,
state.total_bytes.map(|t| t.to_string()).unwrap_or_else(|| "unknown".into()),
);
})
.min_notify_interval_bytes(1024); // Notify at most every 1KB
let mw = progress.into_middleware();
let mut router = Router::new();
router
.route(Method::POST, "/upload", upload_handler)
.middleware(mw);
let listener = tokio::net::TcpListener::bind("127.0.0.1:8080").await?;
println!("Upload progress server on http://127.0.0.1:8080");
println!("Test with: curl -X POST -d @somefile http://127.0.0.1:8080/upload");
tako::serve(listener, router).await;
Ok(())
}
See the middleware model for chain ordering and Observability for signals, tracing, and the broader metrics pipeline.