🐙 tako
Middleware

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.

On this page