autometrics-rs icon indicating copy to clipboard operation
autometrics-rs copied to clipboard

axum additional metrics

Open horseinthesky opened this issue 1 year ago • 3 comments

Hello.

I've checked examples but could find a way to instrument the axum app to include additional metrics.

Here is the code: main.rs Instrument handler with #[autometrics] macro work great - I get the defaults

use autometrics::{autometrics, prometheus_exporter};
use axum::{routing::get, Json, Router};
use serde::Serialize;
use serde_json::{json, Value};

mod images;
use images::get_images;

const DEFAULT_PORT: &str = "8000";
const DEFAULT_HOST: &str = "0.0.0.0";

#[derive(Serialize)]
#[serde(rename_all(serialize = "lowercase"))]
enum Status {
    OK,
    ERROR,
}

#[derive(Serialize)]
struct AppResponse<'a> {
    status: Status,
    #[serde(skip_serializing_if = "Option::is_none")]
    message: Option<&'a str>,
    #[serde(skip_serializing_if = "Option::is_none")]
    result: Option<Value>,
}

impl<'a> AppResponse<'a> {
    fn ok() -> Self {
        Self {
            status: Status::OK,
            message: None,
            result: None,
        }
    }

    fn error() -> Self {
        Self {
            status: Status::ERROR,
            message: None,
            result: None,
        }
    }

    fn with_message(self, message: &'a str) -> Self {
        Self {
            status: self.status,
            message: Some(message),
            result: self.result,
        }
    }

    fn with_result(self, result: Option<Value>) -> Self {
        Self {
            status: self.status,
            message: self.message,
            result,
        }
    }
}

#[tokio::main]
async fn main() -> Result<(), axum::BoxError> {
    let app = Router::new()
        .route("/api/images", get(images))
        .route(
            "/metrics",
            get(|| async { prometheus_exporter::encode_http_response() }),
        );

    let port = std::env::var("PORT").unwrap_or(DEFAULT_PORT.to_string());
    let host = std::env::var("PORT").unwrap_or(DEFAULT_HOST.to_string());

    let listener =
        tokio::net::TcpListener::bind(format!("{}:{}", host, port)).await?;

    axum::serve(listener, app).await?;

    Ok(())
}

async fn health() -> Json<AppResponse<'static>> {
    Json(AppResponse::ok().with_message("up"))
}

#[autometrics]
async fn images() -> Json<AppResponse<'static>> {
    get_images().await;

    Json(AppResponse::ok().with_message("saved"))
}

images.rs I'm trying to add additional metrics to measure some parts of the handler

use std::time::{Instant, SystemTime};
use tokio::time::{sleep, Duration};
use uuid::Uuid;

#[allow(dead_code)]
#[derive(Debug)]
struct Image {
    uuid: Uuid,
    modified: SystemTime,
}

impl Image {
    fn new() -> Self {
        Image {
            uuid: Uuid::new_v4(),
            modified: SystemTime::now(),
        }
    }
}

async fn download() {
    sleep(Duration::from_millis(5)).await;
}

async fn save(_image: Image) {
    sleep(Duration::from_millis(2)).await;
}

pub async fn get_images() {
    let start = Instant::now();
    let latency = start.elapsed().as_secs_f64();
    let labels = [("operation", "s3".to_string())];

    download().await;

    metrics::counter!("http_requests_total", &labels).increment(1);
    metrics::histogram!(
        "myapp_request_duration_seconds",
        &labels
    )
    .record(latency);

    let image = Image::new();
    save(image).await;
}

But this does nothing.

Is there a way to include new metrics to the storage? Thank you.

horseinthesky avatar Mar 01 '24 14:03 horseinthesky

Hi @horseinthesky, if you enable the metrics feature for autometrics, it should pick up the custom metrics you're creating with the metrics crate. You can take a look at https://github.com/autometrics-dev/autometrics-rs/tree/main/examples/custom-metrics for an example of this.

emschwartz avatar Mar 01 '24 17:03 emschwartz

Sorry, the feature is called metrics-0_21 (and you need to use that version of the metrics crate): https://docs.rs/autometrics/latest/autometrics/#metrics-backends

emschwartz avatar Mar 01 '24 17:03 emschwartz

@emschwartz This is my `Cargo.toml at the moment:

[package]
name = "rust-app"
version = "0.1.0"
edition = "2021"

[dependencies]
autometrics = { version = "1.0.1", features = ["prometheus-exporter"] }
axum = "0.7.4"
metrics = "0.22.1"
metrics-exporter-prometheus = "0.13.1"
opentelemetry = "0.21.0"
opentelemetry-otlp = { version = "0.14.0", features = ["grpc-sys"] }
opentelemetry-semantic-conventions = "0.13.0"
opentelemetry_sdk = { version = "0.21.2", features = ["rt-tokio"] }
serde = { version = "1.0.196", features = ["derive"] }
serde_json = "1.0.114"
tokio = { version = "1.36.0", features = ["full"] }
tower-http = { version = "0.5.1", features = ["trace"] }
tracing = "0.1.40"
tracing-opentelemetry = "0.22.0"
tracing-subscriber = "0.3.18"
uuid = { version = "1.7.0", features = ["v4", "serde"] }

If I run:

cargo run . --features=metrics-0_22

it changes nothing.

Docs say:

If you are exporting metrics yourself rather than using the prometheus-exporter, you must ensure that you are using the exact same version of the metrics library as autometrics (and it must come from crates.io rather than git or another source). If not, the autometrics metrics will not appear in your exported metrics.

but I do use prometheus-exporter. There is a

        .route(
            "/metrics",
            get(|| async { prometheus_exporter::encode_http_response() }),
        );

in the code to handle prometheus requests.

horseinthesky avatar Mar 02 '24 06:03 horseinthesky