Skip to main content

kaniop_operator/
telemetry.rs

1use std::time::Duration;
2
3use opentelemetry::KeyValue;
4use opentelemetry::trace::{TraceId, TracerProvider as _};
5use opentelemetry_otlp::{ExporterBuildError, WithExportConfig};
6use opentelemetry_sdk::Resource;
7use opentelemetry_sdk::trace::{RandomIdGenerator, Sampler, SdkTracerProvider};
8use serde::Serialize;
9use thiserror::Error;
10use tracing::dispatcher::SetGlobalDefaultError;
11use tracing_opentelemetry::OpenTelemetryLayer;
12use tracing_subscriber::prelude::*;
13use tracing_subscriber::{EnvFilter, Registry};
14
15/// An error type representing various issues that can occur during tracing initialization.
16#[derive(Error, Debug)]
17pub enum Error {
18    /// Error encountered when setting up OpenTelemetry tracing.
19    #[error("ExporterBuildError: {0}")]
20    ExporterBuildError(#[source] ExporterBuildError),
21
22    /// Error encountered when setting the global tracing subscriber.
23    #[error("SetGlobalDefaultError: {0}")]
24    SetGlobalDefaultError(#[source] SetGlobalDefaultError),
25}
26
27/// Fetches the current `opentelemetry::trace::TraceId` as a hexadecimal string.
28///
29/// This function retrieves the `TraceId` by traversing the full tracing stack, from
30/// the current [`tracing::Span`] to its corresponding [`opentelemetry::Context`].
31/// It returns the trace ID associated with the current span.
32///
33/// # Example
34///
35/// ```rust
36/// # use kaniop_operator::telemetry::get_trace_id;
37/// let trace_id = get_trace_id();
38/// println!("Current trace ID: {:?}", trace_id);
39/// ```
40pub fn get_trace_id() -> TraceId {
41    use opentelemetry::trace::TraceContextExt as _; // opentelemetry::Context -> opentelemetry::trace::Span
42    use tracing_opentelemetry::OpenTelemetrySpanExt as _; // tracing::Span to opentelemetry::Context
43
44    tracing::Span::current()
45        .context()
46        .span()
47        .span_context()
48        .trace_id()
49}
50
51/// Specifies the format of log output, either JSON or plain-text.
52///
53/// This enum derives `clap::ValueEnum` for use in command-line argument parsing,
54/// and is serialized in lowercase when used with `serde`.
55#[derive(clap::ValueEnum, Clone, Debug, Serialize)]
56#[serde(rename_all = "lowercase")]
57pub enum LogFormat {
58    /// JSON-formatted log output.
59    Json,
60
61    /// Plain-text log output.
62    Text,
63}
64
65/// Initializes logging and tracing subsystems.
66///
67/// This asynchronous function configures and initializes logging and tracing
68/// according to the provided format and filtering parameters. It supports
69/// both JSON and plain-text log formats, as well as OpenTelemetry tracing
70/// when a tracing URL is specified. If OpenTelemetry is enabled, traces are
71/// sent to the given URL using OTLP over gRPC.
72///
73/// # Example
74///
75/// ```rust
76/// # use kaniop_operator::telemetry::{init, LogFormat};
77///
78/// #[tokio::main]
79/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
80///     // Initialize tracing with a JSON log format and a log filter of "info".
81///     let opentelemetry_endpoint_url = std::env::var("OPENTELEMETRY_ENDPOINT_URL").ok();
82///     init("info", LogFormat::Text, opentelemetry_endpoint_url.as_deref(), 0.1)
83///         .await?;
84///
85///     // Application logic here...
86///
87///     Ok(())
88/// }
89/// ```
90///
91/// In this example, the logging system is initialized with a plain-text format,
92/// an `info` log level filter, and tracing disabled (as `None` is passed for the `some_tracing_url`).
93///
94/// # OpenTelemetry Integration
95///
96/// When a tracing URL is provided, OpenTelemetry tracing is configured using
97/// OTLP (OpenTelemetry Protocol) over gRPC. The function creates a tracing pipeline
98/// with a ratio-based trace sampler and a default random trace ID generator.
99/// Traces will be sampled based on the `trace_ratio` provided.
100///
101/// The function sets a global tracing subscriber using the combination of
102/// the [`tracing_subscriber`] logger and optionally the OpenTelemetry tracer if enabled.
103///
104/// If the tracing subsystem is successfully configured, the function returns
105/// `Ok(())`, otherwise an appropriate error is returned.
106pub async fn init(
107    log_filter: &str,
108    log_format: LogFormat,
109    tracing_url: Option<&str>,
110    trace_ratio: f64,
111) -> Result<(), Error> {
112    let logger = match log_format {
113        LogFormat::Json => tracing_subscriber::fmt::layer().json().compact().boxed(),
114        LogFormat::Text => tracing_subscriber::fmt::layer().compact().boxed(),
115    };
116
117    // Safe unwrap: log_filter is a valid filter directive
118    let filter = EnvFilter::new(log_filter).add_directive("kanidm_client=error".parse().unwrap());
119
120    let collector = Registry::default().with(logger).with(filter);
121
122    if let Some(url) = tracing_url {
123        let exporter = opentelemetry_otlp::SpanExporter::builder()
124            .with_http()
125            .with_endpoint(url)
126            .with_timeout(Duration::from_secs(3))
127            .build()
128            .map_err(Error::ExporterBuildError)?;
129
130        let provider = SdkTracerProvider::builder()
131            .with_sampler(Sampler::TraceIdRatioBased(trace_ratio))
132            .with_id_generator(RandomIdGenerator::default())
133            .with_max_events_per_span(64)
134            .with_max_attributes_per_span(16)
135            .with_max_events_per_span(16)
136            .with_resource(
137                Resource::builder()
138                    .with_service_name("kaniop")
139                    .with_attribute(KeyValue::new("key", "value"))
140                    .build(),
141            )
142            .with_batch_exporter(exporter)
143            .build();
144        let tracer = provider.tracer("opentelemetry-otlp");
145
146        let telemetry = OpenTelemetryLayer::new(tracer);
147        tracing::subscriber::set_global_default(collector.with(telemetry))
148            .map_err(Error::SetGlobalDefaultError)
149    } else {
150        tracing::subscriber::set_global_default(collector).map_err(Error::SetGlobalDefaultError)
151    }
152}
153
154#[cfg(all(test, feature = "integration-test"))]
155mod test {
156    // This test only works when telemetry is initialized fully
157    // and requires OPENTELEMETRY_ENDPOINT_URL pointing to a valid server
158    #[tokio::test]
159    async fn integration_get_trace_id_returns_valid_traces() {
160        use super::*;
161        let opentelemetry_endpoint_url = std::env::var("OPENTELEMETRY_ENDPOINT_URL").ok();
162        super::init(
163            "info",
164            LogFormat::Text,
165            opentelemetry_endpoint_url.as_deref(),
166            0.1,
167        )
168        .await
169        .unwrap();
170        #[tracing::instrument(name = "test_span")] // need to be in an instrumented fn
171        fn test_trace_id() -> TraceId {
172            get_trace_id()
173        }
174        assert_ne!(test_trace_id(), TraceId::INVALID, "valid trace");
175    }
176}