hyperstack_server/
telemetry.rs

1//! Optional telemetry initialization helper.
2//!
3//! Provides a convenient way to initialize tracing with optional OpenTelemetry integration.
4//! This is an optional helper - you can configure tracing yourself if you prefer.
5
6use tracing_subscriber::layer::SubscriberExt;
7use tracing_subscriber::util::SubscriberInitExt;
8use tracing_subscriber::EnvFilter;
9
10#[derive(Debug, Clone)]
11pub struct TelemetryConfig {
12    pub service_name: String,
13    pub json_logs: bool,
14    #[cfg(feature = "otel")]
15    pub otlp_endpoint: Option<String>,
16}
17
18impl Default for TelemetryConfig {
19    fn default() -> Self {
20        Self {
21            service_name: "hyperstack".to_string(),
22            json_logs: false,
23            #[cfg(feature = "otel")]
24            otlp_endpoint: None,
25        }
26    }
27}
28
29impl TelemetryConfig {
30    pub fn new(service_name: impl Into<String>) -> Self {
31        Self {
32            service_name: service_name.into(),
33            ..Default::default()
34        }
35    }
36
37    pub fn with_json_logs(mut self, enabled: bool) -> Self {
38        self.json_logs = enabled;
39        self
40    }
41
42    #[cfg(feature = "otel")]
43    pub fn with_otlp_endpoint(mut self, endpoint: impl Into<String>) -> Self {
44        self.otlp_endpoint = Some(endpoint.into());
45        self
46    }
47}
48
49pub fn init(config: TelemetryConfig) -> anyhow::Result<()> {
50    let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
51
52    let registry = tracing_subscriber::registry().with(env_filter);
53
54    if config.json_logs {
55        let fmt_layer = tracing_subscriber::fmt::layer().json().flatten_event(true);
56        registry.with(fmt_layer).init();
57    } else {
58        let fmt_layer = tracing_subscriber::fmt::layer();
59        registry.with(fmt_layer).init();
60    }
61
62    Ok(())
63}
64
65#[cfg(feature = "otel")]
66pub fn init_with_otel(config: TelemetryConfig) -> anyhow::Result<TelemetryGuard> {
67    use opentelemetry::global;
68    use opentelemetry_otlp::WithExportConfig;
69    use opentelemetry_sdk::propagation::TraceContextPropagator;
70    use opentelemetry_sdk::trace::Tracer;
71    use opentelemetry_sdk::Resource;
72
73    global::set_text_map_propagator(TraceContextPropagator::new());
74
75    let endpoint = config
76        .otlp_endpoint
77        .as_deref()
78        .unwrap_or("http://localhost:4317");
79
80    let tracer: Tracer = opentelemetry_otlp::new_pipeline()
81        .tracing()
82        .with_exporter(
83            opentelemetry_otlp::new_exporter()
84                .tonic()
85                .with_endpoint(endpoint),
86        )
87        .with_trace_config(
88            opentelemetry_sdk::trace::config().with_resource(Resource::new(vec![
89                opentelemetry::KeyValue::new("service.name", config.service_name.clone()),
90            ])),
91        )
92        .install_batch(opentelemetry_sdk::runtime::Tokio)?;
93
94    let otel_layer = tracing_opentelemetry::layer().with_tracer(tracer);
95
96    let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
97
98    let registry = tracing_subscriber::registry()
99        .with(env_filter)
100        .with(otel_layer);
101
102    if config.json_logs {
103        let fmt_layer = tracing_subscriber::fmt::layer().json().flatten_event(true);
104        registry.with(fmt_layer).init();
105    } else {
106        let fmt_layer = tracing_subscriber::fmt::layer();
107        registry.with(fmt_layer).init();
108    }
109
110    Ok(TelemetryGuard)
111}
112
113#[cfg(feature = "otel")]
114pub struct TelemetryGuard;
115
116#[cfg(feature = "otel")]
117impl Drop for TelemetryGuard {
118    fn drop(&mut self) {
119        opentelemetry::global::shutdown_tracer_provider();
120    }
121}