use opentelemetry::{global, KeyValue};
use opentelemetry_otlp::WithExportConfig;
use opentelemetry_sdk::{
propagation::TraceContextPropagator,
runtime,
trace::{self},
Resource,
};
use opentelemetry_semantic_conventions as semconv;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Registry};
#[derive(Clone, Debug)]
pub struct TracingConfig {
pub service_name: String,
pub service_version: String,
pub environment: String,
pub otlp_endpoint: String,
pub json_logging: bool,
pub log_level: String,
}
impl Default for TracingConfig {
fn default() -> Self {
Self {
service_name: "uvb-api".to_string(),
service_version: env!("CARGO_PKG_VERSION").to_string(),
environment: "development".to_string(),
otlp_endpoint: "http://localhost:4317".to_string(),
json_logging: false,
log_level: "info".to_string(),
}
}
}
pub fn init_tracing(config: TracingConfig) -> Result<(), Box<dyn std::error::Error>> {
global::set_text_map_propagator(TraceContextPropagator::new());
let tracer = opentelemetry_otlp::new_pipeline()
.tracing()
.with_exporter(
opentelemetry_otlp::new_exporter()
.tonic()
.with_endpoint(&config.otlp_endpoint),
)
.with_trace_config(trace::config().with_resource(Resource::new(vec![
KeyValue::new(semconv::resource::SERVICE_NAME, config.service_name.clone()),
KeyValue::new(semconv::resource::SERVICE_VERSION, config.service_version),
KeyValue::new(
semconv::resource::DEPLOYMENT_ENVIRONMENT,
config.environment,
),
])))
.install_batch(runtime::Tokio)?;
let telemetry_layer = tracing_opentelemetry::layer().with_tracer(tracer);
let env_filter =
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(&config.log_level));
let subscriber = Registry::default().with(env_filter).with(telemetry_layer);
if config.json_logging {
let fmt_layer = tracing_subscriber::fmt::layer()
.json()
.with_current_span(true)
.with_span_list(true);
subscriber.with(fmt_layer).try_init()?;
} else {
let fmt_layer = tracing_subscriber::fmt::layer()
.with_target(true)
.with_level(true)
.with_thread_ids(true);
subscriber.with(fmt_layer).try_init()?;
}
Ok(())
}
pub fn shutdown_tracing() {
global::shutdown_tracer_provider();
}
pub mod attributes {
pub const TENANT_ID: &str = "uvb.tenant_id";
pub const USER_ID: &str = "uvb.user_id";
pub const APPLICATION_ID: &str = "uvb.application_id";
pub const TRANSACTION_ID: &str = "uvb.transaction_id";
pub const FACTOR_ID: &str = "uvb.factor_id";
pub const CHALLENGE_ID: &str = "uvb.challenge_id";
pub const ENROLLMENT_ID: &str = "uvb.enrollment_id";
pub const SESSION_ID: &str = "uvb.session_id";
pub const INTENT: &str = "uvb.intent";
pub const VERIFICATION_STATUS: &str = "uvb.verification_status";
pub const ASSURANCE_LEVEL: &str = "uvb.assurance_level";
pub const POLICY_ID: &str = "uvb.policy_id";
}
#[macro_export]
macro_rules! span_with_tenant {
($name:expr, $tenant_id:expr) => {
tracing::info_span!(
$name,
{ $crate::attributes::TENANT_ID } = %$tenant_id
)
};
}
#[macro_export]
macro_rules! span_with_transaction {
($name:expr, $transaction_id:expr, $tenant_id:expr) => {
tracing::info_span!(
$name,
{ $crate::attributes::TRANSACTION_ID } = %$transaction_id,
{ $crate::attributes::TENANT_ID } = %$tenant_id
)
};
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = TracingConfig::default();
assert_eq!(config.service_name, "uvb-api");
assert_eq!(config.environment, "development");
}
#[test]
fn test_config_builder() {
let config = TracingConfig {
service_name: "test-service".to_string(),
environment: "test".to_string(),
..Default::default()
};
assert_eq!(config.service_name, "test-service");
assert_eq!(config.environment, "test");
}
}