diff --git a/payjoin-mailroom/Cargo.toml b/payjoin-mailroom/Cargo.toml index 8f3f92569..6306a2a27 100644 --- a/payjoin-mailroom/Cargo.toml +++ b/payjoin-mailroom/Cargo.toml @@ -21,7 +21,7 @@ ws-bootstrap = ["dep:tokio-tungstenite", "dep:rustls"] _manual-tls = ["dep:axum-server", "dep:rustls"] acme = ["dep:tokio-rustls-acme", "dep:axum-server", "dep:rustls"] access-control = ["dep:flate2", "dep:ipnet", "dep:maxminddb", "dep:reqwest"] -telemetry = ["dep:opentelemetry-otlp"] +telemetry = ["dep:opentelemetry-otlp", "dep:uuid"] [dependencies] anyhow = "1.0.99" @@ -82,6 +82,7 @@ tower-http = { version = "0.6.11", features = ["trace"] } tracing = "0.1.41" tracing-subscriber = { version = "0.3.19", features = ["env-filter", "json"] } unicode-segmentation = "=1.12.0" +uuid = { version = "1.18.0", features = ["v4"], optional = true } # Pinned transitive dependencies that appear unused to cargo-machete [package.metadata.cargo-machete] diff --git a/payjoin-mailroom/src/main.rs b/payjoin-mailroom/src/main.rs index 1eaf5c531..bdf663f49 100644 --- a/payjoin-mailroom/src/main.rs +++ b/payjoin-mailroom/src/main.rs @@ -36,14 +36,10 @@ fn init_tracing() -> Option { #[cfg(feature = "telemetry")] fn init_tracing_with_telemetry(telemetry: &config::TelemetryConfig) -> SdkMeterProvider { - use opentelemetry::KeyValue; use opentelemetry_otlp::{WithExportConfig, WithHttpConfig}; - use opentelemetry_sdk::Resource; + use payjoin_mailroom::metrics::build_telemetry_resource; - let resource = Resource::builder() - .with_service_name("payjoin-mailroom") - .with_attribute(KeyValue::new("operator.domain", telemetry.operator_domain.clone())) - .build(); + let resource = build_telemetry_resource(&telemetry.operator_domain); let headers: std::collections::HashMap = [("Authorization".to_string(), format!("Basic {}", telemetry.auth_token))].into(); diff --git a/payjoin-mailroom/src/metrics.rs b/payjoin-mailroom/src/metrics.rs index 702678bc9..43468e716 100644 --- a/payjoin-mailroom/src/metrics.rs +++ b/payjoin-mailroom/src/metrics.rs @@ -4,6 +4,16 @@ use std::fmt; use std::sync::{Arc, Mutex}; use std::time::{Duration, SystemTime, UNIX_EPOCH}; +#[cfg(feature = "telemetry")] +pub fn build_telemetry_resource(operator_domain: &str) -> opentelemetry_sdk::Resource { + use opentelemetry::KeyValue; + opentelemetry_sdk::Resource::builder() + .with_service_name("payjoin-mailroom") + .with_attribute(KeyValue::new("operator.domain", operator_domain.to_string())) + .with_attribute(KeyValue::new("service.instance.id", uuid::Uuid::new_v4().to_string())) + .build() +} + use hyperloglogplus::{HyperLogLog, HyperLogLogPlus}; use opentelemetry::metrics::{Counter, MeterProvider, ObservableGauge, UpDownCounter}; use opentelemetry::KeyValue; @@ -337,6 +347,41 @@ impl Drop for InFlightGuard { #[cfg(test)] mod tests { use opentelemetry_sdk::metrics::data::{AggregatedMetrics, MetricData}; + + #[cfg(feature = "telemetry")] + #[test] + fn telemetry_resource_attributes() { + use opentelemetry::Key; + + use super::build_telemetry_resource; + + let r1 = build_telemetry_resource("example.com"); + + assert_eq!( + r1.get(&Key::from("service.name")), + Some(opentelemetry::Value::String("payjoin-mailroom".into())), + "service.name must be payjoin-mailroom" + ); + assert_eq!( + r1.get(&Key::from("operator.domain")), + Some(opentelemetry::Value::String("example.com".into())), + "operator.domain must match configured value" + ); + + let id1 = r1 + .get(&Key::from("service.instance.id")) + .expect("service.instance.id must be present") + .to_string(); + assert!(!id1.is_empty(), "service.instance.id must not be empty"); + uuid::Uuid::parse_str(&id1).expect("service.instance.id must parse as a UUID"); + + let r2 = build_telemetry_resource("example.com"); + let id2 = r2 + .get(&Key::from("service.instance.id")) + .expect("service.instance.id must be present in second resource") + .to_string(); + assert_ne!(id1, id2, "service.instance.id must differ across constructions"); + } use opentelemetry_sdk::metrics::{InMemoryMetricExporter, PeriodicReader, SdkMeterProvider}; use super::*;