Skip to main content

es_entity/
sql_commenter.rs

1//! Correlate SQL statements with OpenTelemetry traces.
2//!
3//! [`annotate_sql`] appends a [sqlcommenter](https://google.github.io/sqlcommenter)-style
4//! `traceparent` comment carrying the currently active span's trace context to
5//! a SQL statement, e.g.
6//!
7//! ```text
8//! SELECT * FROM users WHERE id = $1 /*traceparent='00-<32-hex>-<16-hex>-01'*/
9//! ```
10//!
11//! The comment survives Postgres query normalization and appears in
12//! `pg_stat_activity` and in Postgres logs (slow query log, `auto_explain`,
13//! lock waits), so a statement observed server-side can be matched back to a
14//! distributed trace. `pg_stat_statements` retains the comment of the *first*
15//! execution seen per query id (comments are excluded from the query id
16//! jumble), which yields an exemplar trace per statement shape.
17//!
18//! # Sampling and the prepared statement cache
19//!
20//! Annotation only happens when the active span is **sampled**: an un-sampled
21//! span is never exported, so its trace id cannot be looked up in the tracing
22//! backend and annotating would be pure cost. The cost matters because an
23//! annotated statement's text is unique per span — it can never be served
24//! from sqlx's per-connection prepared statement cache and is executed
25//! non-persistently (a server-side parse + plan per execution). Gating on the
26//! sampled flag keeps full prepared-statement reuse for all un-sampled
27//! traffic.
28//!
29//! Annotation requires the `tracing-context` feature, an initialized
30//! OpenTelemetry tracing layer, and must be switched on via
31//! [`set_annotation_enabled`] (it is disabled by default). Otherwise
32//! [`annotate_sql`] is a zero-cost pass-through.
33
34use std::borrow::Cow;
35use std::sync::atomic::{AtomicBool, Ordering};
36
37static ANNOTATION_ENABLED: AtomicBool = AtomicBool::new(false);
38
39/// Globally enables or disables SQL trace-context annotation (disabled by
40/// default).
41///
42/// While disabled, [`annotate_sql`] is a pass-through and executors delegate
43/// statements untouched — prepared-statement reuse for all traffic at the
44/// cost of losing statement-level trace correlation. Intended to be set once
45/// at process startup from application configuration.
46pub fn set_annotation_enabled(enabled: bool) {
47    ANNOTATION_ENABLED.store(enabled, Ordering::Relaxed);
48}
49
50/// Whether SQL trace-context annotation is currently enabled.
51pub fn annotation_enabled() -> bool {
52    ANNOTATION_ENABLED.load(Ordering::Relaxed)
53}
54
55/// The W3C `trace-flags` sampled bit.
56#[cfg(feature = "tracing-context")]
57const SAMPLED: u8 = 0x01;
58
59/// W3C `traceparent` of the currently active span, e.g.
60/// `00-<32-hex-trace-id>-<16-hex-span-id>-01`, if that span is sampled.
61///
62/// Returns `None` when there is no valid span context (outside any span, or
63/// tracing/OTEL not initialized), when the span is not sampled (its trace is
64/// never exported, so the annotation could not be correlated with anything),
65/// or when the `tracing-context` feature is disabled.
66#[cfg(feature = "tracing-context")]
67pub fn current_traceparent() -> Option<String> {
68    crate::context::TracingContext::current()
69        .filter(|ctx| ctx.trace_flags & SAMPLED != 0)
70        .map(|ctx| ctx.traceparent)
71}
72
73/// W3C `traceparent` of the currently active span. Always `None` without the
74/// `tracing-context` feature.
75#[cfg(not(feature = "tracing-context"))]
76pub fn current_traceparent() -> Option<String> {
77    None
78}
79
80/// Appends the current sampled span's `traceparent` as a SQL comment to `sql`.
81///
82/// Returns [`Cow::Borrowed`] (no allocation) when there is no sampled span
83/// context; callers can use this to skip any annotation-dependent work.
84pub fn annotate_sql(sql: &str) -> Cow<'_, str> {
85    if !annotation_enabled() {
86        return Cow::Borrowed(sql);
87    }
88    match current_traceparent() {
89        Some(traceparent) => Cow::Owned(format!("{sql} /*traceparent='{traceparent}'*/")),
90        None => Cow::Borrowed(sql),
91    }
92}
93
94#[cfg(all(test, feature = "tracing-context"))]
95mod tests {
96    use opentelemetry::trace::TracerProvider as _;
97    use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
98
99    use super::*;
100
101    /// Serializes tests that are sensitive to the global annotation toggle.
102    static ANNOTATION_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
103
104    /// Locks the toggle, tolerating poisoning from a previously panicked test.
105    fn annotation_lock() -> std::sync::MutexGuard<'static, ()> {
106        ANNOTATION_LOCK.lock().unwrap_or_else(|e| e.into_inner())
107    }
108
109    /// Enables annotation for the duration of a test, restoring the default
110    /// (disabled) on drop — including when the test panics.
111    struct AnnotationEnabled(std::sync::MutexGuard<'static, ()>);
112
113    impl AnnotationEnabled {
114        fn for_test() -> Self {
115            let guard = annotation_lock();
116            set_annotation_enabled(true);
117            Self(guard)
118        }
119    }
120
121    impl Drop for AnnotationEnabled {
122        fn drop(&mut self) {
123            set_annotation_enabled(false);
124        }
125    }
126
127    fn subscriber_with_sampler(
128        sampler: opentelemetry_sdk::trace::Sampler,
129    ) -> tracing::subscriber::DefaultGuard {
130        let provider = opentelemetry_sdk::trace::SdkTracerProvider::builder()
131            .with_sampler(sampler)
132            .build();
133        let tracer = provider.tracer("sql-commenter-test");
134        tracing_subscriber::registry()
135            .with(tracing_opentelemetry::layer().with_tracer(tracer))
136            .set_default()
137    }
138
139    #[test]
140    fn no_span_context_is_noop() {
141        assert!(current_traceparent().is_none());
142        assert_eq!(annotate_sql("SELECT 1"), "SELECT 1");
143    }
144
145    #[test]
146    fn annotates_within_sampled_span() {
147        let _enabled = AnnotationEnabled::for_test();
148        let _subscriber = subscriber_with_sampler(opentelemetry_sdk::trace::Sampler::AlwaysOn);
149
150        let span = tracing::info_span!("test_span");
151        let _guard = span.enter();
152
153        let tp = current_traceparent().expect("should have traceparent in span");
154        let parts: Vec<&str> = tp.split('-').collect();
155        assert_eq!(parts.len(), 4);
156        assert_eq!(parts[0], "00");
157        assert_eq!(parts[1].len(), 32);
158        assert_eq!(parts[2].len(), 16);
159        assert_eq!(parts[3], "01");
160
161        assert_eq!(
162            annotate_sql("SELECT 1"),
163            format!("SELECT 1 /*traceparent='{tp}'*/")
164        );
165    }
166
167    #[test]
168    fn unsampled_span_is_not_annotated() {
169        // Even with annotation enabled, an un-sampled span must not be
170        // annotated: its trace is never exported, so the comment could not
171        // be correlated with anything.
172        let _enabled = AnnotationEnabled::for_test();
173        let _subscriber = subscriber_with_sampler(opentelemetry_sdk::trace::Sampler::AlwaysOff);
174
175        let span = tracing::info_span!("test_span");
176        let _guard = span.enter();
177
178        assert!(current_traceparent().is_none());
179        assert_eq!(annotate_sql("SELECT 1"), "SELECT 1");
180    }
181
182    #[test]
183    fn annotation_disabled_by_default_and_toggleable() {
184        let _lock = annotation_lock();
185        let _subscriber = subscriber_with_sampler(opentelemetry_sdk::trace::Sampler::AlwaysOn);
186
187        let span = tracing::info_span!("test_span");
188        let _guard = span.enter();
189
190        // Disabled by default: pass-through even within a sampled span.
191        assert_eq!(annotate_sql("SELECT 1"), "SELECT 1");
192
193        set_annotation_enabled(true);
194        assert!(annotate_sql("SELECT 1").contains("/*traceparent='"));
195        set_annotation_enabled(false);
196    }
197}