Skip to main content

uvb_tracing/
lib.rs

1/*!
2 * UVB Tracing - OpenTelemetry Distributed Tracing
3 *
4 * Provides distributed tracing instrumentation for UVB authentication system
5 * using OpenTelemetry and the tracing ecosystem.
6 */
7
8use opentelemetry::{global, KeyValue};
9use opentelemetry_otlp::WithExportConfig;
10use opentelemetry_sdk::{
11    propagation::TraceContextPropagator,
12    runtime,
13    trace::{self},
14    Resource,
15};
16use opentelemetry_semantic_conventions as semconv;
17use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Registry};
18
19/// Configuration for OpenTelemetry tracing
20#[derive(Clone, Debug)]
21pub struct TracingConfig {
22    /// Service name for tracing
23    pub service_name: String,
24
25    /// Service version
26    pub service_version: String,
27
28    /// Environment (production, staging, development)
29    pub environment: String,
30
31    /// OTLP endpoint (e.g., "http://localhost:4317")
32    pub otlp_endpoint: String,
33
34    /// Enable JSON logging
35    pub json_logging: bool,
36
37    /// Log level filter (e.g., "info,uvb=debug")
38    pub log_level: String,
39}
40
41impl Default for TracingConfig {
42    fn default() -> Self {
43        Self {
44            service_name: "uvb-api".to_string(),
45            service_version: env!("CARGO_PKG_VERSION").to_string(),
46            environment: "development".to_string(),
47            otlp_endpoint: "http://localhost:4317".to_string(),
48            json_logging: false,
49            log_level: "info".to_string(),
50        }
51    }
52}
53
54/// Initialize OpenTelemetry tracing with OTLP exporter
55///
56/// # Example
57///
58/// ```no_run
59/// use uvb_tracing::{TracingConfig, init_tracing};
60///
61/// #[tokio::main]
62/// async fn main() {
63///     let config = TracingConfig {
64///         service_name: "uvb-api".to_string(),
65///         otlp_endpoint: "http://jaeger:4317".to_string(),
66///         ..Default::default()
67///     };
68///
69///     init_tracing(config).expect("Failed to initialize tracing");
70/// }
71/// ```
72pub fn init_tracing(config: TracingConfig) -> Result<(), Box<dyn std::error::Error>> {
73    // Set up trace context propagation
74    global::set_text_map_propagator(TraceContextPropagator::new());
75
76    // Create OTLP trace exporter
77    let tracer = opentelemetry_otlp::new_pipeline()
78        .tracing()
79        .with_exporter(
80            opentelemetry_otlp::new_exporter()
81                .tonic()
82                .with_endpoint(&config.otlp_endpoint),
83        )
84        .with_trace_config(trace::config().with_resource(Resource::new(vec![
85            KeyValue::new(semconv::resource::SERVICE_NAME, config.service_name.clone()),
86            KeyValue::new(semconv::resource::SERVICE_VERSION, config.service_version),
87            KeyValue::new(
88                semconv::resource::DEPLOYMENT_ENVIRONMENT,
89                config.environment,
90            ),
91        ])))
92        .install_batch(runtime::Tokio)?;
93
94    // Create OpenTelemetry tracing layer
95    let telemetry_layer = tracing_opentelemetry::layer().with_tracer(tracer);
96
97    // Create env filter layer
98    let env_filter =
99        EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(&config.log_level));
100
101    // Create subscriber
102    let subscriber = Registry::default().with(env_filter).with(telemetry_layer);
103
104    // Add JSON formatting if enabled
105    if config.json_logging {
106        let fmt_layer = tracing_subscriber::fmt::layer()
107            .json()
108            .with_current_span(true)
109            .with_span_list(true);
110
111        subscriber.with(fmt_layer).try_init()?;
112    } else {
113        let fmt_layer = tracing_subscriber::fmt::layer()
114            .with_target(true)
115            .with_level(true)
116            .with_thread_ids(true);
117
118        subscriber.with(fmt_layer).try_init()?;
119    }
120
121    Ok(())
122}
123
124/// Shutdown OpenTelemetry gracefully
125///
126/// Call this before application exit to ensure all spans are flushed
127pub fn shutdown_tracing() {
128    global::shutdown_tracer_provider();
129}
130
131/// Span attribute constants for UVB-specific tracing
132pub mod attributes {
133    /// Tenant ID attribute
134    pub const TENANT_ID: &str = "uvb.tenant_id";
135
136    /// User ID attribute
137    pub const USER_ID: &str = "uvb.user_id";
138
139    /// Application ID attribute
140    pub const APPLICATION_ID: &str = "uvb.application_id";
141
142    /// Transaction ID attribute
143    pub const TRANSACTION_ID: &str = "uvb.transaction_id";
144
145    /// Factor ID attribute
146    pub const FACTOR_ID: &str = "uvb.factor_id";
147
148    /// Challenge ID attribute
149    pub const CHALLENGE_ID: &str = "uvb.challenge_id";
150
151    /// Enrollment ID attribute
152    pub const ENROLLMENT_ID: &str = "uvb.enrollment_id";
153
154    /// Session ID attribute
155    pub const SESSION_ID: &str = "uvb.session_id";
156
157    /// Intent attribute (login, payment, etc.)
158    pub const INTENT: &str = "uvb.intent";
159
160    /// Verification status attribute
161    pub const VERIFICATION_STATUS: &str = "uvb.verification_status";
162
163    /// Assurance level attribute
164    pub const ASSURANCE_LEVEL: &str = "uvb.assurance_level";
165
166    /// Policy ID attribute
167    pub const POLICY_ID: &str = "uvb.policy_id";
168}
169
170/// Helper macros for common tracing patterns
171#[macro_export]
172macro_rules! span_with_tenant {
173    ($name:expr, $tenant_id:expr) => {
174        tracing::info_span!(
175            $name,
176            { $crate::attributes::TENANT_ID } = %$tenant_id
177        )
178    };
179}
180
181#[macro_export]
182macro_rules! span_with_transaction {
183    ($name:expr, $transaction_id:expr, $tenant_id:expr) => {
184        tracing::info_span!(
185            $name,
186            { $crate::attributes::TRANSACTION_ID } = %$transaction_id,
187            { $crate::attributes::TENANT_ID } = %$tenant_id
188        )
189    };
190}
191
192#[cfg(test)]
193mod tests {
194    use super::*;
195
196    #[test]
197    fn test_default_config() {
198        let config = TracingConfig::default();
199        assert_eq!(config.service_name, "uvb-api");
200        assert_eq!(config.environment, "development");
201    }
202
203    #[test]
204    fn test_config_builder() {
205        let config = TracingConfig {
206            service_name: "test-service".to_string(),
207            environment: "test".to_string(),
208            ..Default::default()
209        };
210
211        assert_eq!(config.service_name, "test-service");
212        assert_eq!(config.environment, "test");
213    }
214}