refactor: split telemetry module

This commit is contained in:
2026-06-13 06:38:33 +02:00
parent da910a88c7
commit 0f195de9e5
4 changed files with 122 additions and 119 deletions

View File

@@ -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<String, AppError> {
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\""));
}
}

56
src/telemetry/mod.rs Normal file
View File

@@ -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<String, AppError> {
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)
}
}

11
src/telemetry/tracing.rs Normal file
View File

@@ -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();
}