1use std::borrow::Cow;
2use std::pin::Pin;
3use std::task::{Context, Poll};
4use std::time::Instant;
5
6use futures::Stream;
7use futures::stream::BoxStream;
8use opentelemetry::trace::{SpanKind, Status, TraceContextExt, Tracer};
9use opentelemetry::{Context as OtelContext, KeyValue};
10use opentelemetry_semantic_conventions::attribute;
11
12use crate::annotations::QueryAnnotations;
13use crate::attributes::{self, ConnectionAttributes, QueryTextMode};
14use crate::database::Database;
15use crate::metrics::Metrics;
16
17fn append_annotation_attrs(kv: &mut Vec<KeyValue>, annotations: Option<&QueryAnnotations>) {
27 let Some(ann) = annotations else { return };
28 if let Some(ref op) = ann.operation {
29 kv.push(KeyValue::new(attribute::DB_OPERATION_NAME, op.clone()));
30 }
31 if let Some(ref coll) = ann.collection {
32 kv.push(KeyValue::new(attribute::DB_COLLECTION_NAME, coll.clone()));
33 }
34 if let Some(ref summary) = ann.query_summary {
35 kv.push(KeyValue::new(attribute::DB_QUERY_SUMMARY, summary.clone()));
36 }
37 if let Some(ref sp) = ann.stored_procedure {
38 kv.push(KeyValue::new(
39 attribute::DB_STORED_PROCEDURE_NAME,
40 sp.clone(),
41 ));
42 }
43}
44
45fn build_attributes(
51 attrs: &ConnectionAttributes,
52 sql: Option<&str>,
53 annotations: Option<&QueryAnnotations>,
54) -> Vec<KeyValue> {
55 let mut kv = attrs.base_key_values();
56 append_annotation_attrs(&mut kv, annotations);
57 if let Some(sql) = sql {
58 match attrs.query_text_mode {
59 QueryTextMode::Full => {
60 kv.push(KeyValue::new(
61 attribute::DB_QUERY_TEXT,
62 crate::compact::compact_whitespace(sql),
63 ));
64 }
65 QueryTextMode::Obfuscated => {
66 let obfuscated = crate::obfuscate::obfuscate(sql);
67 kv.push(KeyValue::new(
68 attribute::DB_QUERY_TEXT,
69 crate::compact::compact_whitespace(&obfuscated),
70 ));
71 }
72 QueryTextMode::Off => {}
73 }
74 }
75 kv
76}
77
78fn start_span(name: &str, span_attrs: Vec<KeyValue>) -> (OtelContext, Instant) {
80 let tracer = opentelemetry::global::tracer("sqlx-otel");
81 let span = tracer
82 .span_builder(name.to_owned())
83 .with_kind(SpanKind::Client)
84 .with_attributes(span_attrs)
85 .start(&tracer);
86 let cx = OtelContext::current_with_span(span);
87 (cx, Instant::now())
88}
89
90fn begin_query_span(
103 attrs: &ConnectionAttributes,
104 sql: Option<&str>,
105 annotations: Option<&QueryAnnotations>,
106) -> (OtelContext, Instant, Vec<KeyValue>) {
107 let (op, coll, summary) = annotations.map_or((None, None, None), |a| {
108 (
109 a.operation.as_deref(),
110 a.collection.as_deref(),
111 a.query_summary.as_deref(),
112 )
113 });
114 let name = attributes::span_name(attrs.system, op, coll, summary);
115 let span_attrs = build_attributes(attrs, sql, annotations);
116 let mut metric_attrs = attrs.base_key_values();
117 append_annotation_attrs(&mut metric_attrs, annotations);
118 let (cx, start) = start_span(&name, span_attrs);
119 (cx, start, metric_attrs)
120}
121
122fn error_type(err: &sqlx::Error) -> &'static str {
124 match err {
125 sqlx::Error::Configuration(_) => "Configuration",
126 sqlx::Error::Database(_) => "Database",
127 sqlx::Error::Io(_) => "Io",
128 sqlx::Error::Tls(_) => "Tls",
129 sqlx::Error::Protocol(_) => "Protocol",
130 sqlx::Error::RowNotFound => "RowNotFound",
131 sqlx::Error::TypeNotFound { .. } => "TypeNotFound",
132 sqlx::Error::ColumnIndexOutOfBounds { .. } => "ColumnIndexOutOfBounds",
133 sqlx::Error::ColumnNotFound(_) => "ColumnNotFound",
134 sqlx::Error::ColumnDecode { .. } => "ColumnDecode",
135 sqlx::Error::Decode(_) => "Decode",
136 sqlx::Error::AnyDriverError(_) => "AnyDriverError",
137 sqlx::Error::PoolTimedOut => "PoolTimedOut",
138 sqlx::Error::PoolClosed => "PoolClosed",
139 sqlx::Error::WorkerCrashed => "WorkerCrashed",
140 sqlx::Error::Migrate(_) => "Migrate",
141 _ => "Unknown",
142 }
143}
144
145fn record_error(cx: &OtelContext, err: &sqlx::Error, metric_attrs: &mut Vec<KeyValue>) {
151 let span = cx.span();
152 let kind = error_type(err);
153 span.set_status(Status::Error {
154 description: Cow::Owned(err.to_string()),
155 });
156 span.set_attribute(KeyValue::new(attribute::ERROR_TYPE, kind));
157 metric_attrs.push(KeyValue::new(attribute::ERROR_TYPE, kind));
158 if let sqlx::Error::Database(db_err) = err
160 && let Some(code) = db_err.code()
161 {
162 let code = code.into_owned();
163 span.set_attribute(KeyValue::new(
164 attribute::DB_RESPONSE_STATUS_CODE,
165 code.clone(),
166 ));
167 metric_attrs.push(KeyValue::new(attribute::DB_RESPONSE_STATUS_CODE, code));
168 }
169 span.add_event(
170 "exception",
171 vec![
172 KeyValue::new("exception.type", kind),
173 KeyValue::new("exception.message", err.to_string()),
174 ],
175 );
176}
177
178fn record_rows(cx: &OtelContext, rows: u64) {
180 cx.span().set_attribute(KeyValue::new(
181 attribute::DB_RESPONSE_RETURNED_ROWS,
182 i64::try_from(rows).unwrap_or(i64::MAX),
183 ));
184}
185
186fn record_affected_rows(cx: &OtelContext, rows: u64) {
188 cx.span().set_attribute(KeyValue::new(
189 "db.response.affected_rows",
190 i64::try_from(rows).unwrap_or(i64::MAX),
191 ));
192}
193
194fn finish(
198 cx: &OtelContext,
199 start: Instant,
200 returned_rows: Option<u64>,
201 affected_rows: Option<u64>,
202 metrics: &Metrics,
203 attrs: &[KeyValue],
204) {
205 cx.span().end();
206 metrics.record(start.elapsed(), returned_rows, affected_rows, attrs);
207}
208
209async fn execute_instrumented<T>(
212 fut: futures::future::BoxFuture<'_, Result<T, sqlx::Error>>,
213 cx: OtelContext,
214 start: Instant,
215 metrics: std::sync::Arc<Metrics>,
216 mut metric_attrs: Vec<KeyValue>,
217) -> Result<T, sqlx::Error> {
218 let result = fut.await;
219 if let Err(err) = &result {
220 record_error(&cx, err, &mut metric_attrs);
221 }
222 finish(&cx, start, None, None, &metrics, &metric_attrs);
223 result
224}
225
226trait RowCounter<T> {
232 fn count(item: &T) -> u64;
234}
235
236struct CountAll;
238
239impl<T> RowCounter<T> for CountAll {
240 fn count(_item: &T) -> u64 {
241 1
242 }
243}
244
245struct CountRight;
248
249impl<L, R> RowCounter<sqlx::Either<L, R>> for CountRight {
250 fn count(item: &sqlx::Either<L, R>) -> u64 {
251 u64::from(item.is_right())
252 }
253}
254
255struct CountNone;
257
258impl<T> RowCounter<T> for CountNone {
259 fn count(_item: &T) -> u64 {
260 0
261 }
262}
263
264struct InstrumentedStream<S, C> {
267 inner: S,
268 cx: OtelContext,
269 start: Instant,
270 rows: u64,
271 metrics: std::sync::Arc<Metrics>,
272 metric_attrs: Vec<KeyValue>,
273 error_recorded: bool,
274 finished: bool,
275 _counter: std::marker::PhantomData<C>,
276}
277
278impl<S, C> InstrumentedStream<S, C> {
279 fn new(
280 inner: S,
281 cx: OtelContext,
282 start: Instant,
283 metrics: std::sync::Arc<Metrics>,
284 metric_attrs: Vec<KeyValue>,
285 ) -> Self {
286 Self {
287 inner,
288 cx,
289 start,
290 rows: 0,
291 metrics,
292 metric_attrs,
293 error_recorded: false,
294 finished: false,
295 _counter: std::marker::PhantomData,
296 }
297 }
298
299 fn complete(&mut self) {
300 if !self.finished {
301 self.finished = true;
302 record_rows(&self.cx, self.rows);
303 finish(
304 &self.cx,
305 self.start,
306 Some(self.rows),
307 None,
308 &self.metrics,
309 &self.metric_attrs,
310 );
311 }
312 }
313}
314
315impl<S: Unpin, C> Unpin for InstrumentedStream<S, C> {}
318
319impl<S, T, C> Stream for InstrumentedStream<S, C>
320where
321 S: Stream<Item = Result<T, sqlx::Error>> + Unpin,
322 C: RowCounter<T>,
323{
324 type Item = Result<T, sqlx::Error>;
325
326 fn poll_next(mut self: Pin<&mut Self>, task_cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
327 match Pin::new(&mut self.inner).poll_next(task_cx) {
328 Poll::Ready(Some(Ok(item))) => {
329 self.rows += C::count(&item);
330 Poll::Ready(Some(Ok(item)))
331 }
332 Poll::Ready(Some(Err(err))) => {
333 if !self.error_recorded {
334 self.error_recorded = true;
335 let this = &mut *self;
341 record_error(&this.cx, &err, &mut this.metric_attrs);
342 }
343 Poll::Ready(Some(Err(err)))
344 }
345 Poll::Ready(None) => {
346 self.complete();
347 Poll::Ready(None)
348 }
349 Poll::Pending => Poll::Pending,
350 }
351 }
352}
353
354impl<S, C> Drop for InstrumentedStream<S, C> {
355 fn drop(&mut self) {
356 self.complete();
357 }
358}
359
360macro_rules! impl_executor {
373 ($ty:ty, $self_:ident => $inner:expr) => {
374 impl_executor!(@impl $ty, $self_ => $inner, None);
375 };
376 ($ty:ty, $self_:ident => $inner:expr, annotations: $ann:expr) => {
377 impl_executor!(@impl $ty, $self_ => $inner, $ann);
378 };
379 (@impl $ty:ty, $self_:ident => $inner:expr, $ann:expr) => {
380 impl<'c, DB> sqlx::Executor<'c> for $ty
381 where
382 DB: Database,
383 for<'a> &'a mut DB::Connection: sqlx::Executor<'a, Database = DB>,
384 {
385 type Database = DB;
386
387 fn execute<'e, 'q: 'e, E>(
389 $self_,
390 query: E,
391 ) -> futures::future::BoxFuture<
392 'e,
393 Result<<DB as sqlx::Database>::QueryResult, sqlx::Error>,
394 >
395 where
396 E: 'q + sqlx::Execute<'q, DB>,
397 'c: 'e,
398 {
399 let query = crate::rebuilt_query::RebuiltQuery::<DB>::split(query);
400 let state = $self_.state.clone();
401 let (cx, start, mut metric_attrs) =
402 begin_query_span(&state.attrs, Some(query.sql_str()), $ann);
403 let fut = ($inner).execute(query);
404 Box::pin(async move {
405 let result = fut.await;
406 let affected = match &result {
407 Ok(qr) => {
408 let n = DB::rows_affected(qr);
409 record_affected_rows(&cx, n);
410 Some(n)
411 }
412 Err(err) => {
413 record_error(&cx, err, &mut metric_attrs);
414 None
415 }
416 };
417 finish(&cx, start, None, affected, &state.metrics, &metric_attrs);
418 result
419 })
420 }
421
422 fn execute_many<'e, 'q: 'e, E>(
425 $self_,
426 query: E,
427 ) -> BoxStream<'e, Result<<DB as sqlx::Database>::QueryResult, sqlx::Error>>
428 where
429 E: 'q + sqlx::Execute<'q, DB>,
430 'c: 'e,
431 {
432 let query = crate::rebuilt_query::RebuiltQuery::<DB>::split(query);
433 let state = $self_.state.clone();
434 let (cx, start, metric_attrs) =
435 begin_query_span(&state.attrs, Some(query.sql_str()), $ann);
436 let stream = ($inner).execute_many(query);
437 Box::pin(InstrumentedStream::<_, CountNone>::new(
438 stream,
439 cx,
440 start,
441 state.metrics,
442 metric_attrs,
443 ))
444 }
445
446 fn fetch<'e, 'q: 'e, E>(
448 $self_,
449 query: E,
450 ) -> BoxStream<'e, Result<<DB as sqlx::Database>::Row, sqlx::Error>>
451 where
452 E: 'q + sqlx::Execute<'q, DB>,
453 'c: 'e,
454 {
455 let query = crate::rebuilt_query::RebuiltQuery::<DB>::split(query);
456 let state = $self_.state.clone();
457 let (cx, start, metric_attrs) =
458 begin_query_span(&state.attrs, Some(query.sql_str()), $ann);
459 let stream = ($inner).fetch(query);
460 Box::pin(InstrumentedStream::<_, CountAll>::new(
461 stream,
462 cx,
463 start,
464 state.metrics,
465 metric_attrs,
466 ))
467 }
468
469 fn fetch_many<'e, 'q: 'e, E>(
472 $self_,
473 query: E,
474 ) -> BoxStream<
475 'e,
476 Result<
477 sqlx::Either<
478 <DB as sqlx::Database>::QueryResult,
479 <DB as sqlx::Database>::Row,
480 >,
481 sqlx::Error,
482 >,
483 >
484 where
485 E: 'q + sqlx::Execute<'q, DB>,
486 'c: 'e,
487 {
488 let query = crate::rebuilt_query::RebuiltQuery::<DB>::split(query);
489 let state = $self_.state.clone();
490 let (cx, start, metric_attrs) =
491 begin_query_span(&state.attrs, Some(query.sql_str()), $ann);
492 let stream = ($inner).fetch_many(query);
493 Box::pin(InstrumentedStream::<_, CountRight>::new(
494 stream,
495 cx,
496 start,
497 state.metrics,
498 metric_attrs,
499 ))
500 }
501
502 fn fetch_all<'e, 'q: 'e, E>(
505 $self_,
506 query: E,
507 ) -> futures::future::BoxFuture<
508 'e,
509 Result<Vec<<DB as sqlx::Database>::Row>, sqlx::Error>,
510 >
511 where
512 E: 'q + sqlx::Execute<'q, DB>,
513 'c: 'e,
514 {
515 let query = crate::rebuilt_query::RebuiltQuery::<DB>::split(query);
516 let state = $self_.state.clone();
517 let (cx, start, mut metric_attrs) =
518 begin_query_span(&state.attrs, Some(query.sql_str()), $ann);
519 let fut = ($inner).fetch_all(query);
520 Box::pin(async move {
521 let result = fut.await;
522 match &result {
523 Ok(rows) => {
524 let count = rows.len() as u64;
525 record_rows(&cx, count);
526 finish(&cx, start, Some(count), None, &state.metrics, &metric_attrs);
527 }
528 Err(err) => {
529 record_error(&cx, err, &mut metric_attrs);
530 finish(&cx, start, None, None, &state.metrics, &metric_attrs);
531 }
532 }
533 result
534 })
535 }
536
537 fn fetch_one<'e, 'q: 'e, E>(
539 $self_,
540 query: E,
541 ) -> futures::future::BoxFuture<
542 'e,
543 Result<<DB as sqlx::Database>::Row, sqlx::Error>,
544 >
545 where
546 E: 'q + sqlx::Execute<'q, DB>,
547 'c: 'e,
548 {
549 let query = crate::rebuilt_query::RebuiltQuery::<DB>::split(query);
550 let state = $self_.state.clone();
551 let (cx, start, mut metric_attrs) =
552 begin_query_span(&state.attrs, Some(query.sql_str()), $ann);
553 let fut = ($inner).fetch_one(query);
554 Box::pin(async move {
555 let result = fut.await;
556 match &result {
557 Ok(_) => {
558 record_rows(&cx, 1);
559 finish(&cx, start, Some(1), None, &state.metrics, &metric_attrs);
560 }
561 Err(err) => {
562 record_error(&cx, err, &mut metric_attrs);
563 finish(&cx, start, None, None, &state.metrics, &metric_attrs);
564 }
565 }
566 result
567 })
568 }
569
570 fn fetch_optional<'e, 'q: 'e, E>(
572 $self_,
573 query: E,
574 ) -> futures::future::BoxFuture<
575 'e,
576 Result<Option<<DB as sqlx::Database>::Row>, sqlx::Error>,
577 >
578 where
579 E: 'q + sqlx::Execute<'q, DB>,
580 'c: 'e,
581 {
582 let query = crate::rebuilt_query::RebuiltQuery::<DB>::split(query);
583 let state = $self_.state.clone();
584 let (cx, start, mut metric_attrs) =
585 begin_query_span(&state.attrs, Some(query.sql_str()), $ann);
586 let fut = ($inner).fetch_optional(query);
587 Box::pin(async move {
588 let result = fut.await;
589 match &result {
590 Ok(maybe_row) => {
591 let count = u64::from(maybe_row.is_some());
592 record_rows(&cx, count);
593 finish(&cx, start, Some(count), None, &state.metrics, &metric_attrs);
594 }
595 Err(err) => {
596 record_error(&cx, err, &mut metric_attrs);
597 finish(&cx, start, None, None, &state.metrics, &metric_attrs);
598 }
599 }
600 result
601 })
602 }
603
604 fn prepare<'e>(
613 $self_,
614 query: sqlx::SqlStr,
615 ) -> futures::future::BoxFuture<
616 'e,
617 Result<<DB as sqlx::Database>::Statement, sqlx::Error>,
618 >
619 where
620 'c: 'e,
621 {
622 let state = $self_.state.clone();
623 let (cx, start, metric_attrs) =
624 begin_query_span(&state.attrs, Some(query.as_str()), $ann);
625 let fut = ($inner).prepare(query);
626 Box::pin(execute_instrumented(
627 fut, cx, start, state.metrics, metric_attrs,
628 ))
629 }
630
631 fn prepare_with<'e>(
637 $self_,
638 sql: sqlx::SqlStr,
639 parameters: &'e [<DB as sqlx::Database>::TypeInfo],
640 ) -> futures::future::BoxFuture<
641 'e,
642 Result<<DB as sqlx::Database>::Statement, sqlx::Error>,
643 >
644 where
645 'c: 'e,
646 {
647 let state = $self_.state.clone();
648 let (cx, start, metric_attrs) =
649 begin_query_span(&state.attrs, Some(sql.as_str()), $ann);
650 let fut = ($inner).prepare_with(sql, parameters);
651 Box::pin(execute_instrumented(
652 fut, cx, start, state.metrics, metric_attrs,
653 ))
654 }
655
656 #[doc(hidden)]
664 fn describe<'e>(
665 $self_,
666 sql: sqlx::SqlStr,
667 ) -> futures::future::BoxFuture<
668 'e,
669 Result<sqlx::Describe<DB>, sqlx::Error>,
670 >
671 where
672 'c: 'e,
673 {
674 let state = $self_.state.clone();
675 let (cx, start, metric_attrs) =
676 begin_query_span(&state.attrs, Some(sql.as_str()), $ann);
677 let fut = ($inner).describe(sql);
678 Box::pin(execute_instrumented(
679 fut, cx, start, state.metrics, metric_attrs,
680 ))
681 }
682 }
683 };
684}
685
686impl_executor!(&'_ crate::Pool<DB>, self => &self.inner);
691impl_executor!(&'c mut crate::PoolConnection<DB>, self => self.inner.as_mut());
692impl_executor!(&'c mut crate::Transaction<'_, DB>, self => &mut *self.inner);
693
694impl_executor!(
696 crate::annotations::Annotated<'c, crate::Pool<DB>>,
697 self => &self.inner.inner,
698 annotations: Some(&self.annotations)
699);
700impl_executor!(
701 crate::annotations::AnnotatedMut<'c, crate::PoolConnection<DB>>,
702 self => self.inner.inner.as_mut(),
703 annotations: Some(&self.annotations)
704);
705impl_executor!(
706 crate::annotations::AnnotatedMut<'c, crate::Transaction<'_, DB>>,
707 self => &mut *self.inner.inner,
708 annotations: Some(&self.annotations)
709);
710
711#[cfg(test)]
712mod tests {
713 use super::*;
714 use crate::attributes::ConnectionAttributes;
715
716 #[test]
717 fn error_type_classification() {
718 assert_eq!(error_type(&sqlx::Error::RowNotFound), "RowNotFound");
720 assert_eq!(error_type(&sqlx::Error::PoolTimedOut), "PoolTimedOut");
721 assert_eq!(error_type(&sqlx::Error::PoolClosed), "PoolClosed");
722 assert_eq!(error_type(&sqlx::Error::WorkerCrashed), "WorkerCrashed");
723
724 assert_eq!(
726 error_type(&sqlx::Error::Configuration("bad".into())),
727 "Configuration"
728 );
729 assert_eq!(
730 error_type(&sqlx::Error::Io(std::io::Error::other("test"))),
731 "Io"
732 );
733 assert_eq!(error_type(&sqlx::Error::Tls("tls".into())), "Tls");
734 assert_eq!(
735 error_type(&sqlx::Error::Protocol("proto".into())),
736 "Protocol"
737 );
738 assert_eq!(error_type(&sqlx::Error::Decode("dec".into())), "Decode");
739 assert_eq!(
740 error_type(&sqlx::Error::AnyDriverError("any".into())),
741 "AnyDriverError"
742 );
743
744 assert_eq!(
746 error_type(&sqlx::Error::ColumnNotFound("x".into())),
747 "ColumnNotFound"
748 );
749 assert_eq!(
750 error_type(&sqlx::Error::ColumnIndexOutOfBounds { index: 5, len: 3 }),
751 "ColumnIndexOutOfBounds"
752 );
753 assert_eq!(
754 error_type(&sqlx::Error::ColumnDecode {
755 index: "0".into(),
756 source: "bad".into(),
757 }),
758 "ColumnDecode"
759 );
760 assert_eq!(
761 error_type(&sqlx::Error::TypeNotFound {
762 type_name: "Foo".into(),
763 }),
764 "TypeNotFound"
765 );
766
767 assert_eq!(
769 error_type(&sqlx::Error::Migrate(Box::new(
770 sqlx::migrate::MigrateError::Execute(sqlx::Error::Protocol("test".into()))
771 ))),
772 "Migrate"
773 );
774
775 }
779
780 #[test]
787 fn instrumented_stream_records_error_only_once_when_polled_past_err() {
788 use futures::StreamExt as _;
789 use futures::executor::block_on;
790 use futures::stream;
791
792 let metrics = std::sync::Arc::new(crate::metrics::Metrics::new());
793 let metric_attrs = vec![KeyValue::new(attribute::DB_SYSTEM_NAME, "postgresql")];
794 let (cx, start) = start_span("test", Vec::new());
795
796 let inner = stream::iter(vec![
798 Err::<u64, _>(sqlx::Error::ColumnNotFound("x".into())),
799 Err(sqlx::Error::ColumnNotFound("y".into())),
800 ]);
801 let mut s = InstrumentedStream::<_, CountAll>::new(inner, cx, start, metrics, metric_attrs);
802
803 block_on(async {
804 assert!(matches!(s.next().await, Some(Err(_))), "expected first Err");
805 assert!(
806 matches!(s.next().await, Some(Err(_))),
807 "expected second Err"
808 );
809 assert!(s.next().await.is_none(), "expected stream to terminate");
810 });
811
812 let error_type_count = s
813 .metric_attrs
814 .iter()
815 .filter(|kv| kv.key.as_str() == "error.type")
816 .count();
817 assert_eq!(
818 error_type_count, 1,
819 "error.type must appear exactly once even when the stream yields multiple Err items",
820 );
821 assert!(
822 s.error_recorded,
823 "error_recorded should latch true after the first Err",
824 );
825 }
826
827 fn test_attrs() -> ConnectionAttributes {
828 ConnectionAttributes {
829 system: "postgresql",
830 host: Some("localhost".into()),
831 port: Some(5432),
832 namespace: Some("mydb".into()),
833 network_peer_address: None,
834 network_peer_port: None,
835 network_protocol_name: None,
836 network_transport: None,
837 pool_name: None,
838 query_text_mode: QueryTextMode::Full,
839 }
840 }
841
842 #[test]
847 fn build_attributes_with_full_query_text() {
848 let attrs = test_attrs();
849 let kv = build_attributes(&attrs, Some("SELECT 1"), None);
850 let keys: Vec<&str> = kv.iter().map(|k| k.key.as_str()).collect();
851 assert!(keys.contains(&"db.query.text"));
852 }
853
854 #[test]
855 fn build_attributes_with_off_query_text() {
856 let mut attrs = test_attrs();
857 attrs.query_text_mode = QueryTextMode::Off;
858 let kv = build_attributes(&attrs, Some("SELECT 1"), None);
859 let keys: Vec<&str> = kv.iter().map(|k| k.key.as_str()).collect();
860 assert!(!keys.contains(&"db.query.text"));
861 }
862
863 #[test]
864 fn build_attributes_obfuscated_replaces_literals() {
865 let mut attrs = test_attrs();
866 attrs.query_text_mode = QueryTextMode::Obfuscated;
867 let kv = build_attributes(
868 &attrs,
869 Some("INSERT INTO t (id, name) VALUES (1, 'alice')"),
870 None,
871 );
872 let text = kv
873 .iter()
874 .find(|k| k.key.as_str() == "db.query.text")
875 .map(|k| k.value.clone());
876 assert_eq!(
877 text,
878 Some(opentelemetry::Value::String(
879 "INSERT INTO t (id, name) VALUES (?, ?)".into()
880 ))
881 );
882 }
883
884 #[test]
889 fn build_attributes_no_sql_no_annotations() {
890 let attrs = test_attrs();
891 let kv = build_attributes(&attrs, None, None);
892 let keys: Vec<&str> = kv.iter().map(|k| k.key.as_str()).collect();
893 assert!(!keys.contains(&"db.query.text"));
894 assert!(!keys.contains(&"db.operation.name"));
895 assert!(!keys.contains(&"db.collection.name"));
896 assert!(!keys.contains(&"db.query.summary"));
897 assert!(!keys.contains(&"db.stored_procedure.name"));
898 assert!(keys.contains(&"db.system.name"));
899 }
900
901 #[test]
902 fn build_attributes_with_all_annotation_fields() {
903 let attrs = test_attrs();
904 let ann = QueryAnnotations::new()
905 .operation("SELECT")
906 .collection("users")
907 .query_summary("SELECT users")
908 .stored_procedure("sp_get");
909 let kv = build_attributes(&attrs, Some("SELECT * FROM users"), Some(&ann));
910 let find = |key: &str| {
911 kv.iter()
912 .find(|k| k.key.as_str() == key)
913 .map(|k| k.value.clone())
914 };
915 assert_eq!(
916 find("db.operation.name"),
917 Some(opentelemetry::Value::String("SELECT".into()))
918 );
919 assert_eq!(
920 find("db.collection.name"),
921 Some(opentelemetry::Value::String("users".into()))
922 );
923 assert_eq!(
924 find("db.query.summary"),
925 Some(opentelemetry::Value::String("SELECT users".into()))
926 );
927 assert_eq!(
928 find("db.stored_procedure.name"),
929 Some(opentelemetry::Value::String("sp_get".into()))
930 );
931 assert_eq!(
932 find("db.query.text"),
933 Some(opentelemetry::Value::String("SELECT * FROM users".into()))
934 );
935 }
936
937 #[test]
938 fn append_annotation_attrs_pushes_all_four_when_set() {
939 let ann = QueryAnnotations::new()
940 .operation("SELECT")
941 .collection("users")
942 .query_summary("users by id")
943 .stored_procedure("sp_get_users");
944 let mut kv = Vec::new();
945 append_annotation_attrs(&mut kv, Some(&ann));
946 let pairs: Vec<(&str, &opentelemetry::Value)> =
947 kv.iter().map(|k| (k.key.as_str(), &k.value)).collect();
948 assert_eq!(pairs.len(), 4, "expected one push per annotation field");
949 assert!(pairs.contains(&(
950 "db.operation.name",
951 &opentelemetry::Value::String("SELECT".into())
952 )));
953 assert!(pairs.contains(&(
954 "db.collection.name",
955 &opentelemetry::Value::String("users".into())
956 )));
957 assert!(pairs.contains(&(
958 "db.query.summary",
959 &opentelemetry::Value::String("users by id".into())
960 )));
961 assert!(pairs.contains(&(
962 "db.stored_procedure.name",
963 &opentelemetry::Value::String("sp_get_users".into())
964 )));
965 }
966
967 #[test]
968 fn append_annotation_attrs_none_pushes_nothing() {
969 let mut kv = Vec::new();
970 append_annotation_attrs(&mut kv, None);
971 assert!(kv.is_empty(), "no pushes expected when annotations is None");
972 }
973
974 #[test]
975 fn append_annotation_attrs_default_pushes_nothing() {
976 let mut kv = Vec::new();
977 append_annotation_attrs(&mut kv, Some(&QueryAnnotations::new()));
978 assert!(
979 kv.is_empty(),
980 "no pushes expected when every annotation field is None"
981 );
982 }
983
984 #[test]
985 fn build_attributes_annotation_field_permutations() {
986 type Setter = fn(QueryAnnotations) -> QueryAnnotations;
987
988 let attrs = test_attrs();
989 let fields: &[(&str, Setter)] = &[
990 ("db.operation.name", |a| a.operation("SELECT")),
991 ("db.collection.name", |a| a.collection("users")),
992 ("db.query.summary", |a| a.query_summary("SELECT users")),
993 ("db.stored_procedure.name", |a| a.stored_procedure("sp")),
994 ];
995
996 for mask in 0u8..16 {
999 let mut ann = QueryAnnotations::new();
1000 for (i, &(_, setter)) in fields.iter().enumerate() {
1001 if mask & (1 << i) != 0 {
1002 ann = setter(ann);
1003 }
1004 }
1005 let kv = build_attributes(&attrs, None, Some(&ann));
1006 let keys: Vec<&str> = kv.iter().map(|k| k.key.as_str()).collect();
1007 for (i, &(key, _)) in fields.iter().enumerate() {
1008 println!(
1009 "mask: {:08b}, field: {}, key: {}; contains: {}",
1010 mask,
1011 i,
1012 key,
1013 keys.contains(&key)
1014 );
1015 if mask & (1 << i) != 0 {
1016 assert!(
1017 keys.contains(&key),
1018 "{key} should be present for mask {mask:#06b}"
1019 );
1020 } else {
1021 assert!(
1022 !keys.contains(&key),
1023 "{key} should be absent for mask {mask:#06b}"
1024 );
1025 }
1026 }
1027 }
1028 }
1029
1030 use proptest::prelude::*;
1031
1032 fn make_connection_attributes(
1036 host: Option<String>,
1037 port: Option<u16>,
1038 namespace: Option<String>,
1039 network_peer_address: Option<String>,
1040 network_peer_port: Option<u16>,
1041 query_text_mode: QueryTextMode,
1042 ) -> ConnectionAttributes {
1043 ConnectionAttributes {
1044 system: "postgresql",
1045 host,
1046 port,
1047 namespace,
1048 network_peer_address,
1049 network_peer_port,
1050 network_protocol_name: None,
1051 network_transport: None,
1052 pool_name: None,
1053 query_text_mode,
1054 }
1055 }
1056
1057 fn any_query_text_mode() -> impl Strategy<Value = QueryTextMode> {
1059 prop_oneof![
1060 Just(QueryTextMode::Full),
1061 Just(QueryTextMode::Obfuscated),
1062 Just(QueryTextMode::Off),
1063 ]
1064 }
1065
1066 const CHAIN_SENTINEL: &str = "XSECRETX";
1074
1075 fn chain_fragment_any() -> impl Strategy<Value = String> {
1080 let token = prop_oneof![
1081 "[a-z_][a-z0-9_]{0,7}".prop_map(String::from),
1082 "[ \t\n]{0,5}".prop_map(String::from),
1083 "[a-z0-9 _]{0,8}".prop_map(|inner| format!("'{inner}'")),
1084 "[a-z0-9 _]{0,8}".prop_map(|inner| format!("\"{inner}\"")),
1085 (
1086 "[a-z_]{0,3}".prop_map(String::from),
1087 "[a-z0-9 _]{0,8}".prop_map(String::from),
1088 )
1089 .prop_map(|(tag, body)| format!("${tag}${body}${tag}$")),
1090 "[a-z0-9 _]{0,12}".prop_map(|inner| format!("--{inner}\n")),
1091 "[a-z0-9 _]{0,12}".prop_map(|inner| format!("/*{inner}*/")),
1092 "[0-9]{1,5}".prop_map(String::from),
1093 prop::sample::select(vec![",", ";", "=", "(", ")", "+", "*", "?"])
1094 .prop_map(String::from),
1095 ];
1096 prop::collection::vec(token, 0..12).prop_map(|tokens| tokens.concat())
1097 }
1098
1099 fn chain_fragment_marked() -> impl Strategy<Value = String> {
1104 let token = prop_oneof![
1105 "[a-z_][a-z0-9_]{0,7}".prop_map(String::from),
1106 "[ \t\n]{0,5}".prop_map(String::from),
1107 Just(format!("'{CHAIN_SENTINEL}'")),
1108 "[a-z_]{0,3}".prop_map(|tag| format!("${tag}${CHAIN_SENTINEL}${tag}$")),
1109 prop::sample::select(vec![",", ";", "=", "(", ")"]).prop_map(String::from),
1110 ];
1111 prop::collection::vec(token, 0..10).prop_map(|tokens| tokens.concat())
1112 }
1113
1114 fn any_annotations() -> impl Strategy<Value = QueryAnnotations> {
1117 (
1118 proptest::option::of(".{0,32}"),
1119 proptest::option::of(".{0,32}"),
1120 proptest::option::of(".{0,32}"),
1121 proptest::option::of(".{0,32}"),
1122 )
1123 .prop_map(|(op, coll, summary, sp)| {
1124 let mut ann = QueryAnnotations::new();
1125 if let Some(s) = op {
1126 ann = ann.operation(s);
1127 }
1128 if let Some(s) = coll {
1129 ann = ann.collection(s);
1130 }
1131 if let Some(s) = summary {
1132 ann = ann.query_summary(s);
1133 }
1134 if let Some(s) = sp {
1135 ann = ann.stored_procedure(s);
1136 }
1137 ann
1138 })
1139 }
1140
1141 proptest! {
1142 #![proptest_config(ProptestConfig::with_cases(128))]
1143
1144 #[test]
1149 fn build_attributes_membership_invariant(
1150 host in proptest::option::of("[a-z]{1,16}"),
1151 port in proptest::option::of(any::<u16>()),
1152 namespace in proptest::option::of("[a-z]{1,16}"),
1153 network_peer_address in proptest::option::of("[0-9.:]{1,32}"),
1154 network_peer_port in proptest::option::of(any::<u16>()),
1155 mode in any_query_text_mode(),
1156 sql in proptest::option::of(".{0,64}"),
1157 ann in any_annotations(),
1158 ) {
1159 let attrs = make_connection_attributes(
1160 host.clone(), port, namespace.clone(),
1161 network_peer_address.clone(), network_peer_port, mode,
1162 );
1163 let kv = build_attributes(&attrs, sql.as_deref(), Some(&ann));
1164 let keys: Vec<&str> = kv.iter().map(|k| k.key.as_str()).collect();
1165
1166 prop_assert!(keys.contains(&"db.system.name"));
1168
1169 prop_assert_eq!(keys.contains(&"server.address"), host.is_some());
1171 prop_assert_eq!(keys.contains(&"server.port"), port.is_some());
1172 prop_assert_eq!(keys.contains(&"db.namespace"), namespace.is_some());
1173 prop_assert_eq!(keys.contains(&"network.peer.address"), network_peer_address.is_some());
1174 prop_assert_eq!(keys.contains(&"network.peer.port"), network_peer_port.is_some());
1175
1176 prop_assert_eq!(keys.contains(&"db.operation.name"), ann.operation.is_some());
1178 prop_assert_eq!(keys.contains(&"db.collection.name"), ann.collection.is_some());
1179 prop_assert_eq!(keys.contains(&"db.query.summary"), ann.query_summary.is_some());
1180 prop_assert_eq!(keys.contains(&"db.stored_procedure.name"), ann.stored_procedure.is_some());
1181
1182 let expect_query_text = sql.is_some() && mode != QueryTextMode::Off;
1184 prop_assert_eq!(keys.contains(&"db.query.text"), expect_query_text);
1185 }
1186
1187 #[test]
1190 fn build_attributes_has_no_duplicate_keys(
1191 host in proptest::option::of("[a-z]{1,16}"),
1192 port in proptest::option::of(any::<u16>()),
1193 namespace in proptest::option::of("[a-z]{1,16}"),
1194 mode in any_query_text_mode(),
1195 sql in proptest::option::of(".{0,64}"),
1196 ann in any_annotations(),
1197 ) {
1198 let attrs = make_connection_attributes(host, port, namespace, None, None, mode);
1199 let kv = build_attributes(&attrs, sql.as_deref(), Some(&ann));
1200 let mut seen = std::collections::HashSet::new();
1201 for k in &kv {
1202 prop_assert!(
1203 seen.insert(k.key.as_str().to_owned()),
1204 "duplicate key in build_attributes output: {}",
1205 k.key.as_str(),
1206 );
1207 }
1208 }
1209
1210 #[test]
1214 fn build_attributes_no_panic_arbitrary_sql(
1215 sql in proptest::option::of(any::<String>()),
1216 mode in any_query_text_mode(),
1217 ann in any_annotations(),
1218 ) {
1219 let attrs = make_connection_attributes(None, None, None, None, None, mode);
1220 let _ = build_attributes(&attrs, sql.as_deref(), Some(&ann));
1221 }
1222
1223 #[test]
1227 fn build_attributes_no_annotations_emits_no_annotation_keys(
1228 mode in any_query_text_mode(),
1229 sql in proptest::option::of(".{0,64}"),
1230 ) {
1231 let attrs = make_connection_attributes(None, None, None, None, None, mode);
1232 let kv = build_attributes(&attrs, sql.as_deref(), None);
1233 let keys: Vec<&str> = kv.iter().map(|k| k.key.as_str()).collect();
1234 prop_assert!(!keys.contains(&"db.operation.name"));
1235 prop_assert!(!keys.contains(&"db.collection.name"));
1236 prop_assert!(!keys.contains(&"db.query.summary"));
1237 prop_assert!(!keys.contains(&"db.stored_procedure.name"));
1238 }
1239
1240 #[test]
1245 fn chain_compact_obfuscate_idempotent(s in chain_fragment_any()) {
1246 let f = |x: &str| crate::compact::compact_whitespace(&crate::obfuscate::obfuscate(x));
1247 let once = f(&s);
1248 let twice = f(&once);
1249 prop_assert_eq!(once, twice);
1250 }
1251
1252 #[test]
1257 fn chain_compact_obfuscate_no_leak(s in chain_fragment_marked()) {
1258 let f = |x: &str| crate::compact::compact_whitespace(&crate::obfuscate::obfuscate(x));
1259 let out = f(&s);
1260 prop_assert!(
1261 !out.contains("XSECRETX"),
1262 "sentinel leaked through chain: input={s:?} output={out:?}"
1263 );
1264 }
1265
1266 #[test]
1271 fn chain_emitted_query_text_trim_invariant(
1272 sql in any::<String>(),
1273 mode in prop_oneof![
1274 Just(QueryTextMode::Full),
1275 Just(QueryTextMode::Obfuscated),
1276 ],
1277 ) {
1278 let attrs = make_connection_attributes(None, None, None, None, None, mode);
1279 let kv = build_attributes(&attrs, Some(&sql), None);
1280 let value = kv
1281 .iter()
1282 .find(|k| k.key.as_str() == "db.query.text")
1283 .map(|k| k.value.clone());
1284 let value = value.expect("db.query.text must be emitted for Full/Obfuscated");
1289 let opentelemetry::Value::String(s) = value else {
1290 panic!("db.query.text must be a String value, got {value:?}");
1291 };
1292 let s = s.as_str();
1293 prop_assert!(!s.starts_with(' '), "leading space in db.query.text: {s:?}");
1294 prop_assert!(!s.ends_with(' '), "trailing space in db.query.text: {s:?}");
1295 }
1296
1297 #[test]
1304 fn append_annotation_attrs_membership_invariant(ann in any_annotations()) {
1305 let mut kv = Vec::new();
1306 append_annotation_attrs(&mut kv, Some(&ann));
1307 let keys: Vec<&str> = kv.iter().map(|k| k.key.as_str()).collect();
1308
1309 prop_assert_eq!(keys.contains(&"db.operation.name"), ann.operation.is_some());
1310 prop_assert_eq!(keys.contains(&"db.collection.name"), ann.collection.is_some());
1311 prop_assert_eq!(keys.contains(&"db.query.summary"), ann.query_summary.is_some());
1312 prop_assert_eq!(
1313 keys.contains(&"db.stored_procedure.name"),
1314 ann.stored_procedure.is_some(),
1315 );
1316
1317 prop_assert!(!keys.contains(&"db.system.name"));
1320 prop_assert!(!keys.contains(&"db.namespace"));
1321 prop_assert!(!keys.contains(&"db.query.text"));
1322
1323 let expected_count = usize::from(ann.operation.is_some())
1325 + usize::from(ann.collection.is_some())
1326 + usize::from(ann.query_summary.is_some())
1327 + usize::from(ann.stored_procedure.is_some());
1328 prop_assert_eq!(kv.len(), expected_count);
1329 }
1330 }
1331}