refactor: split telemetry module
This commit is contained in:
@@ -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
56
src/telemetry/mod.rs
Normal 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
11
src/telemetry/tracing.rs
Normal 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();
|
||||
}
|
||||
18
tests/telemetry.rs
Normal file
18
tests/telemetry.rs
Normal file
@@ -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\""));
|
||||
}
|
||||
Reference in New Issue
Block a user