refactor: split telemetry module
This commit is contained in:
@@ -1,9 +1,5 @@
|
|||||||
use crate::error::AppError;
|
use opentelemetry::{KeyValue, metrics::Meter};
|
||||||
use opentelemetry::{KeyValue, metrics::MeterProvider};
|
|
||||||
use opentelemetry_prometheus_text_exporter::PrometheusExporter;
|
|
||||||
use opentelemetry_sdk::{Resource, metrics::SdkMeterProvider};
|
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tracing_subscriber::{EnvFilter, layer::SubscriberExt, util::SubscriberInitExt};
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct AppMetrics {
|
pub struct AppMetrics {
|
||||||
@@ -16,6 +12,42 @@ pub struct AppMetrics {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl 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) {
|
pub fn record_http_request(&self, method: &str, path: &str, status: u16, duration: Duration) {
|
||||||
let is_error = status_is_error(status);
|
let is_error = status_is_error(status);
|
||||||
let status = status.to_string();
|
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 {
|
const fn status_is_error(status: u16) -> bool {
|
||||||
status >= 400
|
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