From 0f195de9e5b33892ae48726b05466be1576cae28 Mon Sep 17 00:00:00 2001 From: Alberto Moretti Date: Sat, 13 Jun 2026 06:38:33 +0200 Subject: [PATCH] refactor: split telemetry module --- src/{telemetry.rs => telemetry/metrics.rs} | 156 +++++---------------- src/telemetry/mod.rs | 56 ++++++++ src/telemetry/tracing.rs | 11 ++ tests/telemetry.rs | 18 +++ 4 files changed, 122 insertions(+), 119 deletions(-) rename src/{telemetry.rs => telemetry/metrics.rs} (54%) create mode 100644 src/telemetry/mod.rs create mode 100644 src/telemetry/tracing.rs create mode 100644 tests/telemetry.rs diff --git a/src/telemetry.rs b/src/telemetry/metrics.rs similarity index 54% rename from src/telemetry.rs rename to src/telemetry/metrics.rs index 2c09abc..2dc1985 100644 --- a/src/telemetry.rs +++ b/src/telemetry/metrics.rs @@ -1,9 +1,5 @@ -use crate::error::AppError; -use opentelemetry::{KeyValue, metrics::MeterProvider}; -use opentelemetry_prometheus_text_exporter::PrometheusExporter; -use opentelemetry_sdk::{Resource, metrics::SdkMeterProvider}; +use opentelemetry::{KeyValue, metrics::Meter}; use std::time::Duration; -use tracing_subscriber::{EnvFilter, layer::SubscriberExt, util::SubscriberInitExt}; #[derive(Clone, Debug)] pub struct AppMetrics { @@ -16,6 +12,42 @@ pub struct AppMetrics { } impl AppMetrics { + #[must_use] + pub fn new(meter: &Meter) -> Self { + Self { + http_requests: meter + .u64_counter("http.server.requests") + .with_description("Total HTTP requests") + .with_unit("requests") + .build(), + http_errors: meter + .u64_counter("http.server.errors") + .with_description("Total HTTP requests with error status codes") + .with_unit("errors") + .build(), + http_request_duration: meter + .f64_histogram("http.server.request.duration") + .with_description("HTTP request duration") + .with_unit("s") + .build(), + upstream_requests: meter + .u64_counter("upstream.client.requests") + .with_description("Total upstream requests") + .with_unit("requests") + .build(), + upstream_errors: meter + .u64_counter("upstream.client.errors") + .with_description("Total failed upstream requests") + .with_unit("errors") + .build(), + upstream_request_duration: meter + .f64_histogram("upstream.client.request.duration") + .with_description("Upstream request duration") + .with_unit("s") + .build(), + } + } + pub fn record_http_request(&self, method: &str, path: &str, status: u16, duration: Duration) { let is_error = status_is_error(status); let status = status.to_string(); @@ -54,120 +86,6 @@ impl AppMetrics { } } -pub struct Telemetry { - provider: SdkMeterProvider, - exporter: PrometheusExporter, - metrics: AppMetrics, -} - -impl Telemetry { - #[must_use] - pub fn new(service_name: &str) -> Self { - let exporter = PrometheusExporter::builder().build(); - let provider = SdkMeterProvider::builder() - .with_reader(exporter.clone()) - .with_resource( - Resource::builder_empty() - .with_service_name(service_name.to_owned()) - .build(), - ) - .build(); - let meter = provider.meter(env!("CARGO_PKG_NAME")); - let metrics = AppMetrics { - http_requests: meter - .u64_counter("http.server.requests") - .with_description("Total HTTP requests") - .with_unit("requests") - .build(), - http_errors: meter - .u64_counter("http.server.errors") - .with_description("Total HTTP requests with error status codes") - .with_unit("errors") - .build(), - http_request_duration: meter - .f64_histogram("http.server.request.duration") - .with_description("HTTP request duration") - .with_unit("s") - .build(), - upstream_requests: meter - .u64_counter("upstream.client.requests") - .with_description("Total upstream requests") - .with_unit("requests") - .build(), - upstream_errors: meter - .u64_counter("upstream.client.errors") - .with_description("Total failed upstream requests") - .with_unit("errors") - .build(), - upstream_request_duration: meter - .f64_histogram("upstream.client.request.duration") - .with_description("Upstream request duration") - .with_unit("s") - .build(), - }; - - Self { - provider, - exporter, - metrics, - } - } - - #[must_use] - pub fn metrics(&self) -> AppMetrics { - self.metrics.clone() - } - - pub fn render_prometheus(&self) -> Result { - let mut output = Vec::new(); - self.exporter - .export(&mut output) - .map_err(|_| AppError::Internal)?; - String::from_utf8(output).map_err(|_| AppError::Internal) - } - - pub fn shutdown(self) -> Result<(), AppError> { - self.provider.shutdown().map_err(|_| AppError::Internal) - } -} - -pub fn init_tracing() { - let env_filter = EnvFilter::try_from_default_env() - .unwrap_or_else(|_| EnvFilter::new("pokedex_api=info,tower_http=info")); - - tracing_subscriber::registry() - .with(env_filter) - .with(tracing_subscriber::fmt::layer().json()) - .init(); -} - const fn status_is_error(status: u16) -> bool { status >= 400 } - -#[cfg(test)] -mod tests { - use super::Telemetry; - use std::time::Duration; - - #[test] - fn records_http_and_upstream_metrics_in_prometheus_text_format() { - let telemetry = Telemetry::new("test-service"); - let metrics = telemetry.metrics(); - - metrics.record_http_request("GET", "/pokemon/{name}", 200, Duration::from_millis(10)); - metrics.record_upstream_request( - "pokeapi", - "pokemon_species", - true, - Duration::from_millis(5), - ); - let rendered = telemetry - .render_prometheus() - .expect("metrics should render as text"); - - assert!(rendered.contains("http_server_requests")); - assert!(rendered.contains("upstream_client_requests")); - assert!(rendered.contains("service_name=\"test-service\"")); - } -} diff --git a/src/telemetry/mod.rs b/src/telemetry/mod.rs new file mode 100644 index 0000000..0cc123d --- /dev/null +++ b/src/telemetry/mod.rs @@ -0,0 +1,56 @@ +mod metrics; +mod tracing; + +use crate::error::AppError; +use opentelemetry::metrics::MeterProvider; +use opentelemetry_prometheus_text_exporter::PrometheusExporter; +use opentelemetry_sdk::{Resource, metrics::SdkMeterProvider}; + +pub use metrics::AppMetrics; +pub use tracing::init_tracing; + +pub struct Telemetry { + provider: SdkMeterProvider, + exporter: PrometheusExporter, + metrics: AppMetrics, +} + +impl Telemetry { + #[must_use] + pub fn new(service_name: &str) -> Self { + let exporter = PrometheusExporter::builder().build(); + let provider = SdkMeterProvider::builder() + .with_reader(exporter.clone()) + .with_resource( + Resource::builder_empty() + .with_service_name(service_name.to_owned()) + .build(), + ) + .build(); + let meter = provider.meter(env!("CARGO_PKG_NAME")); + let metrics = AppMetrics::new(&meter); + + Self { + provider, + exporter, + metrics, + } + } + + #[must_use] + pub fn metrics(&self) -> AppMetrics { + self.metrics.clone() + } + + pub fn render_prometheus(&self) -> Result { + let mut output = Vec::new(); + self.exporter + .export(&mut output) + .map_err(|_| AppError::Internal)?; + String::from_utf8(output).map_err(|_| AppError::Internal) + } + + pub fn shutdown(self) -> Result<(), AppError> { + self.provider.shutdown().map_err(|_| AppError::Internal) + } +} diff --git a/src/telemetry/tracing.rs b/src/telemetry/tracing.rs new file mode 100644 index 0000000..e2ffe64 --- /dev/null +++ b/src/telemetry/tracing.rs @@ -0,0 +1,11 @@ +use tracing_subscriber::{EnvFilter, layer::SubscriberExt, util::SubscriberInitExt}; + +pub fn init_tracing() { + let env_filter = EnvFilter::try_from_default_env() + .unwrap_or_else(|_| EnvFilter::new("pokedex_api=info,tower_http=info")); + + tracing_subscriber::registry() + .with(env_filter) + .with(tracing_subscriber::fmt::layer().json()) + .init(); +} diff --git a/tests/telemetry.rs b/tests/telemetry.rs new file mode 100644 index 0000000..13362ff --- /dev/null +++ b/tests/telemetry.rs @@ -0,0 +1,18 @@ +use pokedex_api::telemetry::Telemetry; +use std::time::Duration; + +#[test] +fn records_http_and_upstream_metrics_in_prometheus_text_format() { + let telemetry = Telemetry::new("test-service"); + let metrics = telemetry.metrics(); + + metrics.record_http_request("GET", "/pokemon/{name}", 200, Duration::from_millis(10)); + metrics.record_upstream_request("pokeapi", "pokemon_species", true, Duration::from_millis(5)); + let rendered = telemetry + .render_prometheus() + .expect("metrics should render as text"); + + assert!(rendered.contains("http_server_requests")); + assert!(rendered.contains("upstream_client_requests")); + assert!(rendered.contains("service_name=\"test-service\"")); +}