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}