Skip to main content

sqlx_otel/
attributes.rs

1use opentelemetry::KeyValue;
2use opentelemetry_semantic_conventions::attribute;
3
4/// Controls whether and how `db.query.text` is captured on spans.
5///
6/// # Automatic parameter capture
7///
8/// `SQLx` uses parameterised queries (`$1`, `?`) by default, so `Full` mode is safe for
9/// most use cases. If you build SQL via `format!` with interpolated values, use
10/// `Obfuscated` or `Off` to avoid capturing sensitive data.
11///
12/// Note that `db.query.parameter.<key>` capture is not supported automatically – `SQLx`'s
13/// `Execute` trait does not expose bind parameter values. Users who need per-parameter
14/// attributes can add them manually via the OpenTelemetry span context API.
15#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
16pub enum QueryTextMode {
17    /// Capture the parameterised query text as-is. This is the default because `SQLx`
18    /// queries use bind parameters (`$1`, `?`), so literal values are not present in the
19    /// query string.
20    #[default]
21    Full,
22    /// Replace literal values in the query text with placeholders. Useful when queries are
23    /// built via string interpolation rather than bind parameters.
24    Obfuscated,
25    /// Do not capture `db.query.text` at all.
26    Off,
27}
28
29/// Immutable, connection-level OpenTelemetry attributes shared by every span and metric
30/// recording from a single pool.
31///
32/// Built once by [`PoolBuilder`](crate::PoolBuilder) and wrapped in `Arc` so that every
33/// wrapper type (`Pool`, `PoolConnection`, `Transaction`, `Connection`) can reference the
34/// same allocation.
35#[derive(Debug, Clone)]
36pub(crate) struct ConnectionAttributes {
37    /// `db.system.name` – always present.
38    pub system: &'static str,
39    /// `server.address` – the logical hostname. May be `None` for embedded databases.
40    pub host: Option<String>,
41    /// `server.port`.
42    pub port: Option<u16>,
43    /// `db.namespace` – the database name.
44    pub namespace: Option<String>,
45    /// `network.peer.address` – the resolved IP address, user-provided.
46    pub network_peer_address: Option<String>,
47    /// `network.peer.port` – the resolved port, user-provided.
48    pub network_peer_port: Option<u16>,
49    /// Controls `db.query.text` capture.
50    pub query_text_mode: QueryTextMode,
51}
52
53impl ConnectionAttributes {
54    /// Produce the base `KeyValue` set for span and metric attribute lists. Only includes
55    /// attributes that have a value – optional fields are omitted when `None`.
56    pub fn base_key_values(&self) -> Vec<KeyValue> {
57        let mut attrs = Vec::with_capacity(6);
58        attrs.push(KeyValue::new(attribute::DB_SYSTEM_NAME, self.system));
59        if let Some(ref host) = self.host {
60            attrs.push(KeyValue::new(attribute::SERVER_ADDRESS, host.clone()));
61        }
62        if let Some(port) = self.port {
63            attrs.push(KeyValue::new(attribute::SERVER_PORT, i64::from(port)));
64        }
65        if let Some(ref ns) = self.namespace {
66            attrs.push(KeyValue::new(attribute::DB_NAMESPACE, ns.clone()));
67        }
68        if let Some(ref addr) = self.network_peer_address {
69            attrs.push(KeyValue::new(attribute::NETWORK_PEER_ADDRESS, addr.clone()));
70        }
71        if let Some(port) = self.network_peer_port {
72            attrs.push(KeyValue::new(attribute::NETWORK_PEER_PORT, i64::from(port)));
73        }
74        attrs
75    }
76}
77
78/// Build a span name following the database client semconv hierarchy:
79///
80/// 1. `db.query.summary` when provided (wins unconditionally – this is the spec's
81///    designated slot for callers who cannot guarantee a low-cardinality
82///    `db.operation.name`).
83/// 2. `"{db.operation.name} {db.collection.name}"` when both are provided.
84/// 3. `"{db.operation.name}"` when only the operation is known.
85/// 4. `"{db.system.name}"` as the final fallback.
86pub(crate) fn span_name(
87    system: &str,
88    operation: Option<&str>,
89    collection: Option<&str>,
90    summary: Option<&str>,
91) -> String {
92    if let Some(s) = summary {
93        return s.to_owned();
94    }
95    match (operation, collection) {
96        (Some(op), Some(coll)) => format!("{op} {coll}"),
97        (Some(op), None) => op.to_owned(),
98        _ => system.to_owned(),
99    }
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105
106    #[test]
107    fn span_name_with_operation_and_collection() {
108        assert_eq!(
109            span_name("postgresql", Some("SELECT"), Some("users"), None),
110            "SELECT users"
111        );
112    }
113
114    #[test]
115    fn span_name_with_operation_only() {
116        assert_eq!(
117            span_name("postgresql", Some("SELECT"), None, None),
118            "SELECT"
119        );
120    }
121
122    #[test]
123    fn span_name_fallback_to_system() {
124        assert_eq!(span_name("sqlite", None, None, None), "sqlite");
125    }
126
127    #[test]
128    fn span_name_collection_without_operation_falls_back() {
129        assert_eq!(span_name("mysql", None, Some("orders"), None), "mysql");
130    }
131
132    #[test]
133    fn span_name_summary_wins_over_operation_and_collection() {
134        assert_eq!(
135            span_name(
136                "postgresql",
137                Some("SELECT"),
138                Some("users"),
139                Some("daily report")
140            ),
141            "daily report"
142        );
143    }
144
145    #[test]
146    fn span_name_summary_alone() {
147        assert_eq!(
148            span_name("sqlite", None, None, Some("custom name")),
149            "custom name"
150        );
151    }
152
153    #[test]
154    fn base_key_values_all_fields() {
155        let attrs = ConnectionAttributes {
156            system: "postgresql",
157            host: Some("localhost".into()),
158            port: Some(5432),
159            namespace: Some("mydb".into()),
160            network_peer_address: Some("127.0.0.1".into()),
161            network_peer_port: Some(5432),
162            query_text_mode: QueryTextMode::Full,
163        };
164        let kvs = attrs.base_key_values();
165        assert_eq!(kvs.len(), 6);
166        assert_eq!(kvs[0].key.as_str(), "db.system.name");
167        assert_eq!(kvs[1].key.as_str(), "server.address");
168        assert_eq!(kvs[2].key.as_str(), "server.port");
169        assert_eq!(kvs[3].key.as_str(), "db.namespace");
170        assert_eq!(kvs[4].key.as_str(), "network.peer.address");
171        assert_eq!(kvs[5].key.as_str(), "network.peer.port");
172    }
173
174    #[test]
175    fn base_key_values_minimal() {
176        let attrs = ConnectionAttributes {
177            system: "sqlite",
178            host: None,
179            port: None,
180            namespace: None,
181            network_peer_address: None,
182            network_peer_port: None,
183            query_text_mode: QueryTextMode::Off,
184        };
185        let kvs = attrs.base_key_values();
186        assert_eq!(kvs.len(), 1);
187        assert_eq!(kvs[0].key.as_str(), "db.system.name");
188    }
189}