From 3ee3278a5ebfef95479b29acd9d4bdfc6f6c73ac Mon Sep 17 00:00:00 2001 From: Abraham Egnor Date: Wed, 1 Oct 2025 12:01:59 +0100 Subject: [PATCH 01/30] tracer and options --- Cargo.lock | 21 ++++++++++++++++++--- Cargo.toml | 4 ++++ src/client/options.rs | 32 ++++++++++++++++++++++++++++++++ src/lib.rs | 2 ++ src/otel.rs | 22 ++++++++++++++++++++++ 5 files changed, 78 insertions(+), 3 deletions(-) create mode 100644 src/otel.rs diff --git a/Cargo.lock b/Cargo.lock index 281c3789b..fbadf5729 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -991,7 +991,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.0", ] [[package]] @@ -1972,6 +1972,7 @@ dependencies = [ "num_cpus", "openssl", "openssl-probe", + "opentelemetry", "pbkdf2 0.11.0", "pem", "percent-encoding", @@ -2148,6 +2149,20 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "opentelemetry" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b84bcd6ae87133e903af7ef497404dda70c60d0ea14895fc8a5e6722754fc2a0" +dependencies = [ + "futures-core", + "futures-sink", + "js-sys", + "pin-project-lite", + "thiserror 2.0.12", + "tracing", +] + [[package]] name = "outref" version = "0.5.2" @@ -2645,7 +2660,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.61.0", ] [[package]] @@ -3117,7 +3132,7 @@ dependencies = [ "getrandom 0.3.2", "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.61.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 5bc4d79c6..d3cf8c104 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -78,6 +78,9 @@ tracing-unstable = ["dep:tracing", "dep:log", "bson3?/serde_json-1"] # compatible with the preview version. text-indexes-unstable = [] +# Enable support for opentelemetry instrumentation +opentelemetry = ["dep:opentelemetry"] + [dependencies] base64 = "0.22" bitflags = "2" @@ -101,6 +104,7 @@ mongodb-internal-macros = { path = "macros", version = "3.3.0" } num_cpus = { version = "1.13.1", optional = true } openssl = { version = "0.10.38", optional = true } openssl-probe = { version = "0.1.5", optional = true } +opentelemetry = { version = "0.31.0", optional = true } pem = { version = "3.0.4", optional = true } percent-encoding = "2.0.0" pkcs8 = { version = "0.10.2", features = ["encryption", "pkcs5"], optional = true } diff --git a/src/client/options.rs b/src/client/options.rs index f299ee5d8..43e7e92e5 100644 --- a/src/client/options.rs +++ b/src/client/options.rs @@ -612,6 +612,10 @@ pub struct ClientOptions { /// Limit on the number of mongos connections that may be created for sharded topologies. pub srv_max_hosts: Option, + /// Configuration for opentelemetry. + #[cfg(feature = "opentelemetry")] + pub tracing: Option, + /// Information from the SRV URI that generated these client options, if applicable. #[builder(setter(skip))] #[serde(skip)] @@ -1346,6 +1350,34 @@ impl ClientOptions { None } } + + #[cfg(feature = "opentelemetry")] + pub(crate) fn otel_enabled(&self) -> bool { + static ENABLED_ENV: LazyLock = LazyLock::new(|| { + match std::env::var("OTEL_RUST_INSTRUMENTATION_MONGODB_ENABLED").as_deref() { + Ok("1" | "true" | "yes") => true, + _ => false, + } + }); + self.tracing + .as_ref() + .and_then(|t| t.enabled) + .unwrap_or_else(|| *ENABLED_ENV) + } + + #[cfg(feature = "opentelemetry")] + pub(crate) fn otel_query_text_max_length(&self) -> usize { + static MAX_LENGTH_ENV: LazyLock = LazyLock::new(|| { + std::env::var("OTEL_RUST_INSTRUMENTATION_MONGODB_QUERY_TEXT_MAX_LENGTH") + .ok() + .and_then(|s| s.parse().ok()) + .unwrap_or(0) + }); + self.tracing + .as_ref() + .and_then(|t| t.query_text_max_length) + .unwrap_or_else(|| *MAX_LENGTH_ENV) + } } /// Splits the string once on the first instance of the given delimiter. If the delimiter is not diff --git a/src/lib.rs b/src/lib.rs index b6e7d0930..75c1ccf05 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -45,6 +45,8 @@ mod hello; pub(crate) mod id_set; mod index; mod operation; +#[cfg(feature = "opentelemetry")] +pub mod otel; pub mod results; pub(crate) mod runtime; mod sdam; diff --git a/src/otel.rs b/src/otel.rs new file mode 100644 index 000000000..f22e4f1f8 --- /dev/null +++ b/src/otel.rs @@ -0,0 +1,22 @@ +use std::sync::LazyLock; + +pub(crate) static TRACER: LazyLock = LazyLock::new(|| { + opentelemetry::global::tracer_with_scope( + opentelemetry::InstrumentationScope::builder("mongodb") + .with_version(env!("CARGO_PKG_VERSION")) + .build(), + ) +}); + +#[derive(Debug, Clone, PartialEq, serde::Deserialize)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct Options { + /// Enables or disables OpenTelemtry for this client instance. If unset, will use the value of + /// the `OTEL_RUST_INSTRUMENTATION_MONGODB_ENABLED` environment variable. + pub enabled: Option, + /// Maximum length of the `db.query.text` attribute of command spans. If unset, will use the + /// value of the `OTEL_RUST_INSTRUMENTATION_MONGODB_QUERY_TEXT_MAX_LENGTH` environment + /// variable. + pub query_text_max_length: Option, +} From dbff5c53182eb602cdaf900b738441027b90cd2a Mon Sep 17 00:00:00 2001 From: Abraham Egnor Date: Wed, 1 Oct 2025 15:25:59 +0100 Subject: [PATCH 02/30] untested operation spans --- src/client/executor.rs | 96 +++++++++++++++++------------ src/client/options.rs | 14 ----- src/client/options/parse.rs | 2 + src/error.rs | 33 +++++++++- src/operation.rs | 18 ++++++ src/operation/find.rs | 8 +++ src/operation/raw_output.rs | 8 +++ src/operation/run_cursor_command.rs | 8 +++ src/otel.rs | 94 +++++++++++++++++++++++++--- 9 files changed, 218 insertions(+), 63 deletions(-) diff --git a/src/client/executor.rs b/src/client/executor.rs index 6b68a78e8..49d2e71a9 100644 --- a/src/client/executor.rs +++ b/src/client/executor.rs @@ -106,55 +106,69 @@ impl Client { op: &mut T, session: impl Into>, ) -> Result> { - // Validate inputs that can be checked before server selection and connection checkout. - if self.inner.shutdown.executed.load(Ordering::SeqCst) { - return Err(ErrorKind::Shutdown.into()); - } - // TODO RUST-9: remove this validation - if !op.is_acknowledged() { - return Err(ErrorKind::InvalidArgument { - message: "Unacknowledged write concerns are not supported".to_string(), - } - .into()); - } - if let Some(write_concern) = op.write_concern() { - write_concern.validate()?; - } + #[cfg(feature = "opentelemetry")] + let mut span = self.start_operation_span(op); - // Validate the session and update its transaction status if needed. - let mut session = session.into(); - if let Some(ref mut session) = session { - if !TrackingArc::ptr_eq(&self.inner, &session.client().inner) { - return Err(Error::invalid_argument( - "the session provided to an operation must be created from the same client as \ - the collection/database on which the operation is being performed", - )); + let result = (async move || { + // Validate inputs that can be checked before server selection and connection checkout. + if self.inner.shutdown.executed.load(Ordering::SeqCst) { + return Err(ErrorKind::Shutdown.into()); } - if op - .selection_criteria() - .and_then(|sc| sc.as_read_pref()) - .is_some_and(|rp| rp != &ReadPreference::Primary) - && session.in_transaction() - { - return Err(ErrorKind::Transaction { - message: "read preference in a transaction must be primary".into(), + // TODO RUST-9: remove this validation + if !op.is_acknowledged() { + return Err(ErrorKind::InvalidArgument { + message: "Unacknowledged write concerns are not supported".to_string(), } .into()); } - // If the current transaction has been committed/aborted and it is not being - // re-committed/re-aborted, reset the transaction's state to None. - if matches!( - session.transaction.state, - TransactionState::Committed { .. } - ) && op.name() != CommitTransaction::NAME - || session.transaction.state == TransactionState::Aborted - && op.name() != AbortTransaction::NAME - { - session.transaction.reset(); + if let Some(write_concern) = op.write_concern() { + write_concern.validate()?; } + + let mut session = session.into(); + // Validate the session and update its transaction status if needed. + if let Some(ref mut session) = session { + if !TrackingArc::ptr_eq(&self.inner, &session.client().inner) { + return Err(Error::invalid_argument( + "the session provided to an operation must be created from the same \ + client as the collection/database on which the operation is being \ + performed", + )); + } + if op + .selection_criteria() + .and_then(|sc| sc.as_read_pref()) + .is_some_and(|rp| rp != &ReadPreference::Primary) + && session.in_transaction() + { + return Err(ErrorKind::Transaction { + message: "read preference in a transaction must be primary".into(), + } + .into()); + } + // If the current transaction has been committed/aborted and it is not being + // re-committed/re-aborted, reset the transaction's state to None. + if matches!( + session.transaction.state, + TransactionState::Committed { .. } + ) && op.name() != CommitTransaction::NAME + || session.transaction.state == TransactionState::Aborted + && op.name() != AbortTransaction::NAME + { + session.transaction.reset(); + } + } + + Box::pin(async { self.execute_operation_with_retry(op, session).await }).await + })() + .await; + + #[cfg(feature = "opentelemetry")] + if let Err(error) = result.as_ref() { + span.record_error(error); } - Box::pin(async { self.execute_operation_with_retry(op, session).await }).await + result } /// Execute the given operation, returning the cursor created by the operation. diff --git a/src/client/options.rs b/src/client/options.rs index 43e7e92e5..ebe36de66 100644 --- a/src/client/options.rs +++ b/src/client/options.rs @@ -1351,20 +1351,6 @@ impl ClientOptions { } } - #[cfg(feature = "opentelemetry")] - pub(crate) fn otel_enabled(&self) -> bool { - static ENABLED_ENV: LazyLock = LazyLock::new(|| { - match std::env::var("OTEL_RUST_INSTRUMENTATION_MONGODB_ENABLED").as_deref() { - Ok("1" | "true" | "yes") => true, - _ => false, - } - }); - self.tracing - .as_ref() - .and_then(|t| t.enabled) - .unwrap_or_else(|| *ENABLED_ENV) - } - #[cfg(feature = "opentelemetry")] pub(crate) fn otel_query_text_max_length(&self) -> usize { static MAX_LENGTH_ENV: LazyLock = LazyLock::new(|| { diff --git a/src/client/options/parse.rs b/src/client/options/parse.rs index 8aed36ae4..1aab30163 100644 --- a/src/client/options/parse.rs +++ b/src/client/options/parse.rs @@ -162,6 +162,8 @@ impl ClientOptions { tracing_max_document_length_bytes: None, srv_max_hosts: conn_str.srv_max_hosts, srv_service_name: conn_str.srv_service_name, + #[cfg(feature = "opentelemetry")] + tracing: None, } } } diff --git a/src/error.rs b/src/error.rs index 6601a397b..e17273cf2 100644 --- a/src/error.rs +++ b/src/error.rs @@ -80,7 +80,7 @@ pub struct Error { pub(crate) server_response: Option>, #[cfg(test)] - bt: Arc, + pub(crate) bt: Arc, } impl Error { @@ -782,6 +782,37 @@ impl ErrorKind { _ => None, } } + + pub(crate) fn name(&self) -> &'static str { + match self { + ErrorKind::InvalidArgument { .. } => "InvalidArgument", + ErrorKind::Authentication { .. } => "Authentication", + ErrorKind::BsonDeserialization(..) => "BsonDeserialization", + ErrorKind::BsonSerialization(..) => "BsonSerialization", + #[cfg(feature = "bson-3")] + ErrorKind::Bson(..) => "Bson", + ErrorKind::InsertMany(..) => "InsertMany", + ErrorKind::BulkWrite(..) => "BulkWrite", + ErrorKind::Command(..) => "Command", + ErrorKind::DnsResolve { .. } => "DnsResolve", + ErrorKind::GridFs(..) => "GridFs", + ErrorKind::Internal { .. } => "Internal", + ErrorKind::Io(..) => "Io", + ErrorKind::ConnectionPoolCleared { .. } => "ConnectionPoolCleared", + ErrorKind::InvalidResponse { .. } => "InvalidResponse", + ErrorKind::ServerSelection { .. } => "ServerSelection", + ErrorKind::SessionsNotSupported => "SessionsNotSupported", + ErrorKind::InvalidTlsConfig { .. } => "InvalidTlsConfig", + ErrorKind::Write(..) => "Write", + ErrorKind::Transaction { .. } => "Transaction", + ErrorKind::IncompatibleServer { .. } => "IncompatibleServer", + ErrorKind::MissingResumeToken => "MissingResumeToken", + #[cfg(feature = "in-use-encryption")] + ErrorKind::Encryption(..) => "Encryption", + ErrorKind::Custom(..) => "Custom", + ErrorKind::Shutdown => "Shutdown", + } + } } /// An error that occurred due to a database command failing. diff --git a/src/operation.rs b/src/operation.rs index 174c71d19..481bfc4ac 100644 --- a/src/operation.rs +++ b/src/operation.rs @@ -176,6 +176,10 @@ pub(crate) trait Operation { /// The name of the server side command associated with this operation. fn name(&self) -> &CStr; + + fn database(&self) -> &str; + + fn collection(&self) -> Option<&str>; } pub(crate) type OverrideCriteriaFn = @@ -277,6 +281,14 @@ pub(crate) trait OperationWithDefaults: Send + Sync { fn name(&self) -> &CStr { Self::NAME } + + fn database(&self) -> &str { + "" + } + + fn collection(&self) -> Option<&str> { + None + } } impl Operation for T @@ -331,6 +343,12 @@ where fn name(&self) -> &CStr { self.name() } + fn database(&self) -> &str { + self.database() + } + fn collection(&self) -> Option<&str> { + self.collection() + } } fn should_redact_body(body: &RawDocumentBuf) -> bool { diff --git a/src/operation/find.rs b/src/operation/find.rs index efd8eb32f..57159d981 100644 --- a/src/operation/find.rs +++ b/src/operation/find.rs @@ -128,4 +128,12 @@ impl OperationWithDefaults for Find { fn retryability(&self) -> Retryability { Retryability::Read } + + fn database(&self) -> &str { + &self.ns.db + } + + fn collection(&self) -> Option<&str> { + Some(&self.ns.coll) + } } diff --git a/src/operation/raw_output.rs b/src/operation/raw_output.rs index 5bffe4f4e..7c16b897b 100644 --- a/src/operation/raw_output.rs +++ b/src/operation/raw_output.rs @@ -80,4 +80,12 @@ impl Operation for RawOutput { fn name(&self) -> &CStr { self.0.name() } + + fn database(&self) -> &str { + self.0.database() + } + + fn collection(&self) -> Option<&str> { + self.0.collection() + } } diff --git a/src/operation/run_cursor_command.rs b/src/operation/run_cursor_command.rs index b467f52ed..11c59f0c5 100644 --- a/src/operation/run_cursor_command.rs +++ b/src/operation/run_cursor_command.rs @@ -92,6 +92,14 @@ impl Operation for RunCursorCommand<'_> { self.run_command.name() } + fn database(&self) -> &str { + self.run_command.database() + } + + fn collection(&self) -> Option<&str> { + self.run_command.collection() + } + fn handle_response<'a>( &'a self, response: &'a RawCommandResponse, diff --git a/src/otel.rs b/src/otel.rs index f22e4f1f8..dd2c56916 100644 --- a/src/otel.rs +++ b/src/otel.rs @@ -1,13 +1,16 @@ +//! Support for OpenTelemetry. + use std::sync::LazyLock; -pub(crate) static TRACER: LazyLock = LazyLock::new(|| { - opentelemetry::global::tracer_with_scope( - opentelemetry::InstrumentationScope::builder("mongodb") - .with_version(env!("CARGO_PKG_VERSION")) - .build(), - ) -}); +use opentelemetry::{ + global::BoxedTracer, + trace::{Span as _, Tracer as _}, + KeyValue, +}; + +use crate::{operation::Operation, Client}; +/// Configuration for OpenTelemetry. #[derive(Debug, Clone, PartialEq, serde::Deserialize)] #[serde(rename_all = "camelCase")] #[non_exhaustive] @@ -20,3 +23,80 @@ pub struct Options { /// variable. pub query_text_max_length: Option, } + +static ENABLED_ENV: LazyLock = + LazyLock::new( + || match std::env::var("OTEL_RUST_INSTRUMENTATION_MONGODB_ENABLED").as_deref() { + Ok("1" | "true" | "yes") => true, + _ => false, + }, + ); + +static MAX_LENGTH_ENV: LazyLock = LazyLock::new(|| { + std::env::var("OTEL_RUST_INSTRUMENTATION_MONGODB_QUERY_TEXT_MAX_LENGTH") + .ok() + .and_then(|s| s.parse().ok()) + .unwrap_or(0) +}); + +static TRACER: LazyLock = LazyLock::new(|| { + opentelemetry::global::tracer_with_scope( + opentelemetry::InstrumentationScope::builder("mongodb") + .with_version(env!("CARGO_PKG_VERSION")) + .build(), + ) +}); + +impl Client { + pub(crate) fn start_operation_span(&self, op: &impl Operation) -> Span { + let otel_enabled = self + .options() + .tracing + .as_ref() + .and_then(|t| t.enabled) + .unwrap_or_else(|| *ENABLED_ENV); + if !otel_enabled { + return Span { inner: None }; + } + let span_name = if let Some(coll) = op.collection() { + format!("{} {}.{}", op.name(), op.database(), coll) + } else { + format!("{} {}", op.name(), op.database()) + }; + let mut attrs = vec![ + KeyValue::new("db.system", "mongodb"), + KeyValue::new("db.namespace", op.database().to_owned()), + KeyValue::new("db.operation.name", op.name().as_str().to_owned()), + KeyValue::new("db.operation.summary", span_name.clone()), + ]; + if let Some(coll) = op.collection() { + attrs.push(KeyValue::new("db.collection.name", coll.to_owned())); + } + Span { + inner: Some( + TRACER + .span_builder(span_name) + .with_kind(opentelemetry::trace::SpanKind::Client) + .with_attributes(attrs) + .start(&*TRACER), + ), + } + } +} + +pub(crate) struct Span { + inner: Option<::Span>, +} + +impl Span { + pub(crate) fn record_error(&mut self, error: &crate::error::Error) { + if let Some(inner) = self.inner.as_mut() { + inner.set_attributes([ + KeyValue::new("exception.message", error.to_string()), + KeyValue::new("exception.type", error.kind.name()), + #[cfg(test)] + KeyValue::new("exception.backtrace", error.bt.to_string()), + ]); + } + } +} From ddedc25ecdb8c03f7241128ca9eae14930cb5315 Mon Sep 17 00:00:00 2001 From: Abraham Egnor Date: Thu, 2 Oct 2025 12:08:19 +0100 Subject: [PATCH 03/30] refactor --- src/client/executor.rs | 14 ++------- src/client/options.rs | 14 --------- src/otel.rs | 64 +++++++++++++++++++++++++----------------- 3 files changed, 42 insertions(+), 50 deletions(-) diff --git a/src/client/executor.rs b/src/client/executor.rs index 49d2e71a9..5c9728be0 100644 --- a/src/client/executor.rs +++ b/src/client/executor.rs @@ -108,8 +108,7 @@ impl Client { ) -> Result> { #[cfg(feature = "opentelemetry")] let mut span = self.start_operation_span(op); - - let result = (async move || { + span.record_error(async move || { // Validate inputs that can be checked before server selection and connection checkout. if self.inner.shutdown.executed.load(Ordering::SeqCst) { return Err(ErrorKind::Shutdown.into()); @@ -160,15 +159,8 @@ impl Client { } Box::pin(async { self.execute_operation_with_retry(op, session).await }).await - })() - .await; - - #[cfg(feature = "opentelemetry")] - if let Err(error) = result.as_ref() { - span.record_error(error); - } - - result + }) + .await } /// Execute the given operation, returning the cursor created by the operation. diff --git a/src/client/options.rs b/src/client/options.rs index ebe36de66..1c3b50904 100644 --- a/src/client/options.rs +++ b/src/client/options.rs @@ -1350,20 +1350,6 @@ impl ClientOptions { None } } - - #[cfg(feature = "opentelemetry")] - pub(crate) fn otel_query_text_max_length(&self) -> usize { - static MAX_LENGTH_ENV: LazyLock = LazyLock::new(|| { - std::env::var("OTEL_RUST_INSTRUMENTATION_MONGODB_QUERY_TEXT_MAX_LENGTH") - .ok() - .and_then(|s| s.parse().ok()) - .unwrap_or(0) - }); - self.tracing - .as_ref() - .and_then(|t| t.query_text_max_length) - .unwrap_or_else(|| *MAX_LENGTH_ENV) - } } /// Splits the string once on the first instance of the given delimiter. If the delimiter is not diff --git a/src/otel.rs b/src/otel.rs index dd2c56916..9dfbc0630 100644 --- a/src/otel.rs +++ b/src/otel.rs @@ -8,7 +8,7 @@ use opentelemetry::{ KeyValue, }; -use crate::{operation::Operation, Client}; +use crate::{error::Result, operation::Operation, options::ClientOptions, Client}; /// Configuration for OpenTelemetry. #[derive(Debug, Clone, PartialEq, serde::Deserialize)] @@ -24,21 +24,6 @@ pub struct Options { pub query_text_max_length: Option, } -static ENABLED_ENV: LazyLock = - LazyLock::new( - || match std::env::var("OTEL_RUST_INSTRUMENTATION_MONGODB_ENABLED").as_deref() { - Ok("1" | "true" | "yes") => true, - _ => false, - }, - ); - -static MAX_LENGTH_ENV: LazyLock = LazyLock::new(|| { - std::env::var("OTEL_RUST_INSTRUMENTATION_MONGODB_QUERY_TEXT_MAX_LENGTH") - .ok() - .and_then(|s| s.parse().ok()) - .unwrap_or(0) -}); - static TRACER: LazyLock = LazyLock::new(|| { opentelemetry::global::tracer_with_scope( opentelemetry::InstrumentationScope::builder("mongodb") @@ -47,15 +32,39 @@ static TRACER: LazyLock = LazyLock::new(|| { ) }); -impl Client { - pub(crate) fn start_operation_span(&self, op: &impl Operation) -> Span { - let otel_enabled = self - .options() - .tracing +impl ClientOptions { + fn otel_enabled(&self) -> bool { + static ENABLED_ENV: LazyLock = LazyLock::new(|| { + match std::env::var("OTEL_RUST_INSTRUMENTATION_MONGODB_ENABLED").as_deref() { + Ok("1" | "true" | "yes") => true, + _ => false, + } + }); + + self.tracing .as_ref() .and_then(|t| t.enabled) - .unwrap_or_else(|| *ENABLED_ENV); - if !otel_enabled { + .unwrap_or_else(|| *ENABLED_ENV) + } + + pub(crate) fn otel_query_text_max_length(&self) -> usize { + static MAX_LENGTH_ENV: LazyLock = LazyLock::new(|| { + std::env::var("OTEL_RUST_INSTRUMENTATION_MONGODB_QUERY_TEXT_MAX_LENGTH") + .ok() + .and_then(|s| s.parse().ok()) + .unwrap_or(0) + }); + + self.tracing + .as_ref() + .and_then(|t| t.query_text_max_length) + .unwrap_or_else(|| *MAX_LENGTH_ENV) + } +} + +impl Client { + pub(crate) fn start_operation_span(&self, op: &impl Operation) -> Span { + if !self.options().otel_enabled() { return Span { inner: None }; } let span_name = if let Some(coll) = op.collection() { @@ -89,8 +98,12 @@ pub(crate) struct Span { } impl Span { - pub(crate) fn record_error(&mut self, error: &crate::error::Error) { - if let Some(inner) = self.inner.as_mut() { + pub(crate) async fn record_error( + &mut self, + code: impl AsyncFnOnce() -> Result, + ) -> Result { + let result = code().await; + if let (Some(inner), Err(error)) = (&mut self.inner, &result) { inner.set_attributes([ KeyValue::new("exception.message", error.to_string()), KeyValue::new("exception.type", error.kind.name()), @@ -98,5 +111,6 @@ impl Span { KeyValue::new("exception.backtrace", error.bt.to_string()), ]); } + result } } From 6668f5c30d694d64310ca9bffdf8a02e39401d0b Mon Sep 17 00:00:00 2001 From: Abraham Egnor Date: Thu, 2 Oct 2025 16:20:06 +0100 Subject: [PATCH 04/30] mostly-populated server command spans --- src/bson_util.rs | 30 +++++++ src/client/executor.rs | 23 +++++- src/cmap/conn/command.rs | 4 +- src/otel.rs | 170 ++++++++++++++++++++++++++++++++------- src/test/spec/trace.rs | 2 +- src/trace.rs | 35 +------- src/trace/command.rs | 7 +- src/trace/topology.rs | 4 +- 8 files changed, 199 insertions(+), 76 deletions(-) diff --git a/src/bson_util.rs b/src/bson_util.rs index d29849352..37e813d34 100644 --- a/src/bson_util.rs +++ b/src/bson_util.rs @@ -341,6 +341,36 @@ pub(crate) mod option_u64_as_i64 { } } +/// Truncates the given string at the closest UTF-8 character boundary >= the provided length. +/// If the new length is >= the current length, does nothing. +pub(crate) fn truncate_on_char_boundary(s: &mut String, new_len: usize) { + let original_len = s.len(); + if original_len > new_len { + // to avoid generating invalid UTF-8, find the first index >= max_length_bytes that is + // the end of a character. + // TODO: RUST-1496 we should use ceil_char_boundary here but it's currently nightly-only. + // see: https://doc.rust-lang.org/std/string/struct.String.html#method.ceil_char_boundary + let mut truncate_index = new_len; + // is_char_boundary returns true when the provided value == the length of the string, so + // if we reach the end of the string this loop will terminate. + while !s.is_char_boundary(truncate_index) { + truncate_index += 1; + } + s.truncate(truncate_index); + // due to the "rounding up" behavior we might not actually end up truncating anything. + // if we did, spec requires we add a trailing "...". + if truncate_index < original_len { + s.push_str("...") + } + } +} + +pub(crate) fn doc_to_json_str(doc: crate::bson::Document, max_length_bytes: usize) -> String { + let mut ext_json = Bson::Document(doc).into_relaxed_extjson().to_string(); + truncate_on_char_boundary(&mut ext_json, max_length_bytes); + ext_json +} + #[cfg(test)] mod test { use crate::bson_util::num_decimal_digits; diff --git a/src/client/executor.rs b/src/client/executor.rs index 5c9728be0..adfb2c617 100644 --- a/src/client/executor.rs +++ b/src/client/executor.rs @@ -108,7 +108,7 @@ impl Client { ) -> Result> { #[cfg(feature = "opentelemetry")] let mut span = self.start_operation_span(op); - span.record_error(async move || { + let result = (async move || { // Validate inputs that can be checked before server selection and connection checkout. if self.inner.shutdown.executed.load(Ordering::SeqCst) { return Err(ErrorKind::Shutdown.into()); @@ -159,8 +159,13 @@ impl Client { } Box::pin(async { self.execute_operation_with_retry(op, session).await }).await - }) - .await + })() + .await; + + #[cfg(feature = "opentelemetry")] + span.record_error(&result); + + result } /// Execute the given operation, returning the cursor created by the operation. @@ -502,9 +507,19 @@ impl Client { let should_redact = cmd.should_redact(); let cmd_name = cmd.name.clone(); let target_db = cmd.target_db.clone(); + #[cfg(feature = "opentelemetry")] + let cmd_attrs = crate::otel::CommandAttributes::new(&cmd); let mut message = Message::try_from(cmd)?; message.request_id = Some(request_id); + + /* + db.mongodb.cursor_id: ??? + */ + + #[cfg(feature = "opentelemetry")] + let mut span = self.start_command_span(op, &connection_info, &message, cmd_attrs); + #[cfg(feature = "in-use-encryption")] { let guard = self.inner.csfle.read().await; @@ -635,6 +650,8 @@ impl Client { } } }; + #[cfg(feature = "opentelemetry")] + span.record_error(&result); if result .as_ref() diff --git a/src/cmap/conn/command.rs b/src/cmap/conn/command.rs index 57fbfd974..c30030653 100644 --- a/src/cmap/conn/command.rs +++ b/src/cmap/conn/command.rs @@ -33,7 +33,7 @@ pub(crate) struct Command { #[serde(rename = "$db")] pub(crate) target_db: String, - lsid: Option, + pub(crate) lsid: Option, #[serde(rename = "$clusterTime")] cluster_time: Option, @@ -44,7 +44,7 @@ pub(crate) struct Command { #[serde(rename = "$readPreference")] read_preference: Option, - txn_number: Option, + pub(crate) txn_number: Option, start_transaction: Option, diff --git a/src/otel.rs b/src/otel.rs index 9dfbc0630..1e4d1b53b 100644 --- a/src/otel.rs +++ b/src/otel.rs @@ -4,11 +4,18 @@ use std::sync::LazyLock; use opentelemetry::{ global::BoxedTracer, - trace::{Span as _, Tracer as _}, + trace::{Span as _, SpanKind, Tracer as _}, KeyValue, }; -use crate::{error::Result, operation::Operation, options::ClientOptions, Client}; +use crate::{ + bson::Bson, + cmap::{conn::wire::Message, Command, ConnectionInfo}, + error::{ErrorKind, Result}, + operation::Operation, + options::{ClientOptions, ServerAddress, DEFAULT_PORT}, + Client, +}; /// Configuration for OpenTelemetry. #[derive(Debug, Clone, PartialEq, serde::Deserialize)] @@ -24,14 +31,6 @@ pub struct Options { pub query_text_max_length: Option, } -static TRACER: LazyLock = LazyLock::new(|| { - opentelemetry::global::tracer_with_scope( - opentelemetry::InstrumentationScope::builder("mongodb") - .with_version(env!("CARGO_PKG_VERSION")) - .build(), - ) -}); - impl ClientOptions { fn otel_enabled(&self) -> bool { static ENABLED_ENV: LazyLock = LazyLock::new(|| { @@ -47,7 +46,7 @@ impl ClientOptions { .unwrap_or_else(|| *ENABLED_ENV) } - pub(crate) fn otel_query_text_max_length(&self) -> usize { + fn otel_query_text_max_length(&self) -> usize { static MAX_LENGTH_ENV: LazyLock = LazyLock::new(|| { std::env::var("OTEL_RUST_INSTRUMENTATION_MONGODB_QUERY_TEXT_MAX_LENGTH") .ok() @@ -62,30 +61,86 @@ impl ClientOptions { } } +static TRACER: LazyLock = LazyLock::new(|| { + opentelemetry::global::tracer_with_scope( + opentelemetry::InstrumentationScope::builder("mongodb") + .with_version(env!("CARGO_PKG_VERSION")) + .build(), + ) +}); + impl Client { pub(crate) fn start_operation_span(&self, op: &impl Operation) -> Span { if !self.options().otel_enabled() { return Span { inner: None }; } - let span_name = if let Some(coll) = op.collection() { - format!("{} {}.{}", op.name(), op.database(), coll) - } else { - format!("{} {}", op.name(), op.database()) - }; - let mut attrs = vec![ - KeyValue::new("db.system", "mongodb"), - KeyValue::new("db.namespace", op.database().to_owned()), + let span_name = format!("{} {}", op.name(), op_target(op)); + let mut attrs = common_attrs(op); + attrs.extend([ KeyValue::new("db.operation.name", op.name().as_str().to_owned()), KeyValue::new("db.operation.summary", span_name.clone()), - ]; - if let Some(coll) = op.collection() { - attrs.push(KeyValue::new("db.collection.name", coll.to_owned())); - } + ]); Span { inner: Some( TRACER .span_builder(span_name) - .with_kind(opentelemetry::trace::SpanKind::Client) + .with_kind(SpanKind::Client) + .with_attributes(attrs) + .start(&*TRACER), + ), + } + } + + pub(crate) fn start_command_span( + &self, + op: &impl Operation, + conn_info: &ConnectionInfo, + message: &Message, + cmd_attrs: CommandAttributes, + ) -> Span { + if !self.options().otel_enabled() || cmd_attrs.should_redact { + return Span { inner: None }; + } + let otel_driver_conn_id: i64 = conn_info.id.into(); + let mut attrs = common_attrs(op); + attrs.extend(cmd_attrs.attrs); + attrs.extend([ + KeyValue::new( + "db.query.summary", + format!("{} {}", &cmd_attrs.name, op_target(op)), + ), + KeyValue::new("db.mongodb.driver_connection_id", otel_driver_conn_id), + ]); + match &conn_info.address { + ServerAddress::Tcp { host, port } => { + let otel_port: i64 = port.unwrap_or(DEFAULT_PORT).into(); + attrs.push(KeyValue::new("server.port", otel_port)); + attrs.push(KeyValue::new("server.address", host.clone())); + attrs.push(KeyValue::new("network.transport", "tcp")); + } + ServerAddress::Unix { path } => { + attrs.push(KeyValue::new( + "server.address", + path.to_string_lossy().into_owned(), + )); + attrs.push(KeyValue::new("network.transport", "unix")); + } + } + if let Some(server_id) = &conn_info.server_id { + attrs.push(KeyValue::new("db.mongodb.server_connection_id", *server_id)); + } + let text_max_len = self.options().otel_query_text_max_length(); + if text_max_len > 0 { + attrs.push(KeyValue::new( + "db.query.text", + crate::bson_util::doc_to_json_str(message.get_command_document(), text_max_len), + )); + } + Span { + inner: Some( + TRACER + .span_builder(cmd_attrs.name) + .with_kind(SpanKind::Client) .with_attributes(attrs) .start(&*TRACER), ), @@ -98,19 +153,72 @@ pub(crate) struct Span { } impl Span { - pub(crate) async fn record_error( - &mut self, - code: impl AsyncFnOnce() -> Result, - ) -> Result { - let result = code().await; + pub(crate) fn record_error(&mut self, result: &Result) { if let (Some(inner), Err(error)) = (&mut self.inner, &result) { inner.set_attributes([ KeyValue::new("exception.message", error.to_string()), KeyValue::new("exception.type", error.kind.name()), #[cfg(test)] - KeyValue::new("exception.backtrace", error.bt.to_string()), + KeyValue::new("exception.stacktrace", error.bt.to_string()), ]); + if let ErrorKind::Command(cmd_err) = &*error.kind { + inner.set_attribute(KeyValue::new( + "db.response.status_code", + cmd_err.code_name.clone(), + )); + } + inner.record_error(error); + inner.set_status(opentelemetry::trace::Status::Error { + description: error.to_string().into(), + }); + } + } +} + +fn op_target(op: &impl Operation) -> String { + if let Some(coll) = op.collection() { + format!("{}.{}", op.database(), coll) + } else { + op.database().to_owned() + } +} + +fn common_attrs(op: &impl Operation) -> Vec { + let mut attrs = vec![ + KeyValue::new("db.system", "mongodb"), + KeyValue::new("db.namespace", op.database().to_owned()), + ]; + if let Some(coll) = op.collection() { + attrs.push(KeyValue::new("db.collection.name", coll.to_owned())); + } + attrs +} + +#[derive(Clone)] +pub(crate) struct CommandAttributes { + should_redact: bool, + name: String, + attrs: Vec, +} + +impl CommandAttributes { + pub(crate) fn new(cmd: &Command) -> Self { + let mut attrs = vec![KeyValue::new("db.command.name", cmd.name.clone())]; + if let Some(lsid) = &cmd.lsid { + attrs.push(KeyValue::new( + "db.mongodb.lsid", + Bson::Document(lsid.clone()) + .into_relaxed_extjson() + .to_string(), + )); + } + if let Some(txn_number) = &cmd.txn_number { + attrs.push(KeyValue::new("db.mongodb.txn_number", *txn_number)); + } + Self { + should_redact: cmd.should_redact(), + name: cmd.name.clone(), + attrs, } - result } } diff --git a/src/test/spec/trace.rs b/src/test/spec/trace.rs index 0b150e7d2..056b4f266 100644 --- a/src/test/spec/trace.rs +++ b/src/test/spec/trace.rs @@ -2,6 +2,7 @@ use std::{collections::HashMap, iter, sync::Arc, time::Duration}; use crate::{ bson::{doc, Document}, + bson_util::truncate_on_char_boundary, client::options::ServerAddress, error::{ CommandError, @@ -29,7 +30,6 @@ use crate::{ SERVER_API, }, trace::{ - truncate_on_char_boundary, TracingRepresentation, COMMAND_TRACING_EVENT_TARGET, DEFAULT_MAX_DOCUMENT_LENGTH_BYTES, diff --git a/src/trace.rs b/src/trace.rs index f3841f1ea..e034d0439 100644 --- a/src/trace.rs +++ b/src/trace.rs @@ -1,9 +1,6 @@ #[cfg(feature = "bson-3")] use crate::bson_compat::RawDocumentBufExt; -use crate::{ - bson::Bson, - client::options::{ServerAddress, DEFAULT_PORT}, -}; +use crate::client::options::{ServerAddress, DEFAULT_PORT}; pub(crate) mod command; pub(crate) mod connection; @@ -68,36 +65,6 @@ impl ServerAddress { } } -/// Truncates the given string at the closest UTF-8 character boundary >= the provided length. -/// If the new length is >= the current length, does nothing. -pub(crate) fn truncate_on_char_boundary(s: &mut String, new_len: usize) { - let original_len = s.len(); - if original_len > new_len { - // to avoid generating invalid UTF-8, find the first index >= max_length_bytes that is - // the end of a character. - // TODO: RUST-1496 we should use ceil_char_boundary here but it's currently nightly-only. - // see: https://doc.rust-lang.org/std/string/struct.String.html#method.ceil_char_boundary - let mut truncate_index = new_len; - // is_char_boundary returns true when the provided value == the length of the string, so - // if we reach the end of the string this loop will terminate. - while !s.is_char_boundary(truncate_index) { - truncate_index += 1; - } - s.truncate(truncate_index); - // due to the "rounding up" behavior we might not actually end up truncating anything. - // if we did, spec requires we add a trailing "...". - if truncate_index < original_len { - s.push_str("...") - } - } -} - -fn serialize_command_or_reply(doc: crate::bson::Document, max_length_bytes: usize) -> String { - let mut ext_json = Bson::Document(doc).into_relaxed_extjson().to_string(); - truncate_on_char_boundary(&mut ext_json, max_length_bytes); - ext_json -} - /// We don't currently use all of these levels but they are included for completeness. #[allow(dead_code)] pub(crate) enum TracingOrLogLevel { diff --git a/src/trace/command.rs b/src/trace/command.rs index 9e6bcd973..5eb044cd1 100644 --- a/src/trace/command.rs +++ b/src/trace/command.rs @@ -1,8 +1,9 @@ use crate::bson::oid::ObjectId; use crate::{ + bson_util::doc_to_json_str, event::command::CommandEvent, - trace::{serialize_command_or_reply, TracingRepresentation, COMMAND_TRACING_EVENT_TARGET}, + trace::{TracingRepresentation, COMMAND_TRACING_EVENT_TARGET}, }; use super::DEFAULT_MAX_DOCUMENT_LENGTH_BYTES; @@ -32,7 +33,7 @@ impl CommandTracingEventEmitter { tracing::debug!( target: COMMAND_TRACING_EVENT_TARGET, topologyId = self.topology_id.tracing_representation(), - command = serialize_command_or_reply(event.command, self.max_document_length_bytes), + command = doc_to_json_str(event.command, self.max_document_length_bytes), databaseName = event.db, commandName = event.command_name, requestId = event.request_id, @@ -48,7 +49,7 @@ impl CommandTracingEventEmitter { tracing::debug!( target: COMMAND_TRACING_EVENT_TARGET, topologyId = self.topology_id.tracing_representation(), - reply = serialize_command_or_reply(event.reply, self.max_document_length_bytes), + reply = doc_to_json_str(event.reply, self.max_document_length_bytes), commandName = event.command_name, requestId = event.request_id, driverConnectionId = event.connection.id, diff --git a/src/trace/topology.rs b/src/trace/topology.rs index 2ee67e111..56b3245b2 100644 --- a/src/trace/topology.rs +++ b/src/trace/topology.rs @@ -1,6 +1,7 @@ use crate::bson::oid::ObjectId; use crate::{ + bson_util::doc_to_json_str, event::sdam::{ SdamEvent, ServerClosedEvent, @@ -14,7 +15,6 @@ use crate::{ TopologyDescriptionChangedEvent, TopologyOpeningEvent, }, - trace::serialize_command_or_reply, }; use super::{ @@ -175,7 +175,7 @@ impl TopologyTracingEventEmitter { driverConnectionId = event.driver_connection_id, serverConnectionId = event.server_connection_id, awaited = event.awaited, - reply = serialize_command_or_reply(event.reply, self.max_document_length_bytes), + reply = doc_to_json_str(event.reply, self.max_document_length_bytes), durationMS = event.duration.as_millis(), "Server heartbeat succeeded" ) From d43214cdcda48bdf0e68d49c13427ded245c4c8e Mon Sep 17 00:00:00 2001 From: Abraham Egnor Date: Thu, 2 Oct 2025 16:26:42 +0100 Subject: [PATCH 05/30] tidy --- src/otel.rs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/otel.rs b/src/otel.rs index 1e4d1b53b..f51625d82 100644 --- a/src/otel.rs +++ b/src/otel.rs @@ -114,16 +114,17 @@ impl Client { match &conn_info.address { ServerAddress::Tcp { host, port } => { let otel_port: i64 = port.unwrap_or(DEFAULT_PORT).into(); - attrs.push(KeyValue::new("server.port", otel_port)); - attrs.push(KeyValue::new("server.address", host.clone())); - attrs.push(KeyValue::new("network.transport", "tcp")); + attrs.extend([ + KeyValue::new("server.port", otel_port), + KeyValue::new("server.address", host.clone()), + KeyValue::new("network.transport", "tcp"), + ]); } ServerAddress::Unix { path } => { - attrs.push(KeyValue::new( - "server.address", - path.to_string_lossy().into_owned(), - )); - attrs.push(KeyValue::new("network.transport", "unix")); + attrs.extend([ + KeyValue::new("server.address", path.to_string_lossy().into_owned()), + KeyValue::new("network.transport", "unix"), + ]); } } if let Some(server_id) = &conn_info.server_id { From 80b3363634e9e4e7b2aca6d8e4a03d6e01821c8b Mon Sep 17 00:00:00 2001 From: Abraham Egnor Date: Thu, 2 Oct 2025 16:40:27 +0100 Subject: [PATCH 06/30] record cursor ids --- src/client/executor.rs | 2 +- src/operation.rs | 18 ++++++++++++++++++ src/operation/get_more.rs | 12 ++++++++++++ src/otel.rs | 14 +++++++++++++- 4 files changed, 44 insertions(+), 2 deletions(-) diff --git a/src/client/executor.rs b/src/client/executor.rs index adfb2c617..18fa2a157 100644 --- a/src/client/executor.rs +++ b/src/client/executor.rs @@ -651,7 +651,7 @@ impl Client { } }; #[cfg(feature = "opentelemetry")] - span.record_error(&result); + span.record_command_result::(&result); if result .as_ref() diff --git a/src/operation.rs b/src/operation.rs index 481bfc4ac..a99ed1854 100644 --- a/src/operation.rs +++ b/src/operation.rs @@ -180,6 +180,10 @@ pub(crate) trait Operation { fn database(&self) -> &str; fn collection(&self) -> Option<&str>; + + fn cursor_id(&self) -> Option; + + fn output_cursor_id(output: &Self::O) -> Option; } pub(crate) type OverrideCriteriaFn = @@ -289,6 +293,14 @@ pub(crate) trait OperationWithDefaults: Send + Sync { fn collection(&self) -> Option<&str> { None } + + fn cursor_id(&self) -> Option { + None + } + + fn output_cursor_id(_output: &Self::O) -> Option { + None + } } impl Operation for T @@ -349,6 +361,12 @@ where fn collection(&self) -> Option<&str> { self.collection() } + fn cursor_id(&self) -> Option { + self.cursor_id() + } + fn output_cursor_id(output: &Self::O) -> Option { + Self::output_cursor_id(output) + } } fn should_redact_body(body: &RawDocumentBuf) -> bool { diff --git a/src/operation/get_more.rs b/src/operation/get_more.rs index 49aa7ac0d..a1f03b458 100644 --- a/src/operation/get_more.rs +++ b/src/operation/get_more.rs @@ -105,6 +105,18 @@ impl OperationWithDefaults for GetMore<'_> { fn pinned_connection(&self) -> Option<&PinnedConnectionHandle> { self.pinned_connection } + + fn database(&self) -> &str { + &self.ns.db + } + + fn collection(&self) -> Option<&str> { + Some(&self.ns.coll) + } + + fn cursor_id(&self) -> Option { + Some(self.cursor_id) + } } #[derive(Debug, Deserialize)] diff --git a/src/otel.rs b/src/otel.rs index f51625d82..084eeb230 100644 --- a/src/otel.rs +++ b/src/otel.rs @@ -137,6 +137,9 @@ impl Client { crate::bson_util::doc_to_json_str(message.get_command_document(), text_max_len), )); } + if let Some(cursor_id) = op.cursor_id() { + attrs.push(KeyValue::new("db.mongodb.cursor_id", cursor_id)); + } Span { inner: Some( TRACER @@ -155,7 +158,7 @@ pub(crate) struct Span { impl Span { pub(crate) fn record_error(&mut self, result: &Result) { - if let (Some(inner), Err(error)) = (&mut self.inner, &result) { + if let (Some(inner), Err(error)) = (&mut self.inner, result) { inner.set_attributes([ KeyValue::new("exception.message", error.to_string()), KeyValue::new("exception.type", error.kind.name()), @@ -174,6 +177,15 @@ impl Span { }); } } + + pub(crate) fn record_command_result(&mut self, result: &Result) { + if let (Some(inner), Ok(out)) = (&mut self.inner, result) { + if let Some(cursor_id) = Op::output_cursor_id(out) { + inner.set_attribute(KeyValue::new("db.mongodb.cursor_id", cursor_id)); + } + } + self.record_error(result); + } } fn op_target(op: &impl Operation) -> String { From d04dcffd6740d61af5584a3238dd33479600c8b0 Mon Sep 17 00:00:00 2001 From: Abraham Egnor Date: Thu, 2 Oct 2025 16:46:25 +0100 Subject: [PATCH 07/30] annotate cursor producers --- src/operation/aggregate.rs | 4 ++++ src/operation/find.rs | 4 ++++ src/operation/list_collections.rs | 4 ++++ src/operation/list_indexes.rs | 4 ++++ src/operation/raw_output.rs | 8 ++++++++ src/operation/run_cursor_command.rs | 8 ++++++++ 6 files changed, 32 insertions(+) diff --git a/src/operation/aggregate.rs b/src/operation/aggregate.rs index 13a8932a1..4fbde520b 100644 --- a/src/operation/aggregate.rs +++ b/src/operation/aggregate.rs @@ -153,6 +153,10 @@ impl OperationWithDefaults for Aggregate { None } } + + fn output_cursor_id(output: &Self::O) -> Option { + Some(output.id()) + } } impl Aggregate { diff --git a/src/operation/find.rs b/src/operation/find.rs index 57159d981..6429b06a7 100644 --- a/src/operation/find.rs +++ b/src/operation/find.rs @@ -136,4 +136,8 @@ impl OperationWithDefaults for Find { fn collection(&self) -> Option<&str> { Some(&self.ns.coll) } + + fn output_cursor_id(output: &Self::O) -> Option { + Some(output.id()) + } } diff --git a/src/operation/list_collections.rs b/src/operation/list_collections.rs index c4b76a3b1..56e371185 100644 --- a/src/operation/list_collections.rs +++ b/src/operation/list_collections.rs @@ -81,4 +81,8 @@ impl OperationWithDefaults for ListCollections { fn retryability(&self) -> Retryability { Retryability::Read } + + fn output_cursor_id(output: &Self::O) -> Option { + Some(output.id()) + } } diff --git a/src/operation/list_indexes.rs b/src/operation/list_indexes.rs index 2b7c28867..1c078760b 100644 --- a/src/operation/list_indexes.rs +++ b/src/operation/list_indexes.rs @@ -69,4 +69,8 @@ impl OperationWithDefaults for ListIndexes { fn retryability(&self) -> Retryability { Retryability::Read } + + fn output_cursor_id(output: &Self::O) -> Option { + Some(output.id()) + } } diff --git a/src/operation/raw_output.rs b/src/operation/raw_output.rs index 7c16b897b..af93c3fdd 100644 --- a/src/operation/raw_output.rs +++ b/src/operation/raw_output.rs @@ -88,4 +88,12 @@ impl Operation for RawOutput { fn collection(&self) -> Option<&str> { self.0.collection() } + + fn cursor_id(&self) -> Option { + self.0.cursor_id() + } + + fn output_cursor_id(_output: &Self::O) -> Option { + None + } } diff --git a/src/operation/run_cursor_command.rs b/src/operation/run_cursor_command.rs index 11c59f0c5..9a1cde96e 100644 --- a/src/operation/run_cursor_command.rs +++ b/src/operation/run_cursor_command.rs @@ -127,4 +127,12 @@ impl Operation for RunCursorCommand<'_> { } .boxed() } + + fn cursor_id(&self) -> Option { + self.run_command.cursor_id() + } + + fn output_cursor_id(output: &Self::O) -> Option { + Some(output.id()) + } } From a208ef3328c3b376757949a083818827a3513125 Mon Sep 17 00:00:00 2001 From: Abraham Egnor Date: Thu, 2 Oct 2025 17:41:01 +0100 Subject: [PATCH 08/30] op targeting --- src/operation.rs | 49 ++++++++++++++++-------- src/operation/abort_transaction.rs | 4 ++ src/operation/aggregate.rs | 13 +++++++ src/operation/aggregate/change_stream.rs | 4 ++ src/operation/bulk_write.rs | 4 ++ src/operation/commit_transaction.rs | 4 ++ src/operation/count.rs | 4 ++ src/operation/count_documents.rs | 4 ++ src/operation/create.rs | 4 ++ src/operation/create_indexes.rs | 4 ++ src/operation/delete.rs | 4 ++ src/operation/distinct.rs | 4 ++ src/operation/drop_collection.rs | 4 ++ src/operation/drop_database.rs | 4 ++ src/operation/drop_indexes.rs | 4 ++ src/operation/find.rs | 8 +--- src/operation/find_and_modify.rs | 4 ++ src/operation/get_more.rs | 8 +--- src/operation/insert.rs | 4 ++ src/operation/list_collections.rs | 4 ++ src/operation/list_databases.rs | 4 ++ src/operation/list_indexes.rs | 4 ++ src/operation/raw_output.rs | 8 +--- src/operation/run_command.rs | 4 ++ src/operation/run_cursor_command.rs | 8 +--- src/operation/search_index.rs | 12 ++++++ src/operation/update.rs | 4 ++ src/otel.rs | 12 +++--- 28 files changed, 154 insertions(+), 44 deletions(-) diff --git a/src/operation.rs b/src/operation.rs index a99ed1854..22c5b5c7d 100644 --- a/src/operation.rs +++ b/src/operation.rs @@ -177,15 +177,43 @@ pub(crate) trait Operation { /// The name of the server side command associated with this operation. fn name(&self) -> &CStr; - fn database(&self) -> &str; - - fn collection(&self) -> Option<&str>; + fn target(&self) -> OperationTarget<'_>; fn cursor_id(&self) -> Option; fn output_cursor_id(output: &Self::O) -> Option; } +pub(crate) struct OperationTarget<'a> { + pub(crate) database: &'a str, + pub(crate) collection: Option<&'a str>, +} + +impl OperationTarget<'static> { + pub(crate) const ADMIN: Self = OperationTarget { + database: "admin", + collection: None, + }; +} + +impl<'a> From<&'a str> for OperationTarget<'a> { + fn from(value: &'a str) -> Self { + OperationTarget { + database: value, + collection: None, + } + } +} + +impl<'a> From<&'a Namespace> for OperationTarget<'a> { + fn from(value: &'a Namespace) -> Self { + OperationTarget { + database: &value.db, + collection: Some(&value.coll), + } + } +} + pub(crate) type OverrideCriteriaFn = fn(&SelectionCriteria, &crate::sdam::TopologyDescription) -> Option; @@ -286,13 +314,7 @@ pub(crate) trait OperationWithDefaults: Send + Sync { Self::NAME } - fn database(&self) -> &str { - "" - } - - fn collection(&self) -> Option<&str> { - None - } + fn target(&self) -> OperationTarget<'_>; fn cursor_id(&self) -> Option { None @@ -355,11 +377,8 @@ where fn name(&self) -> &CStr { self.name() } - fn database(&self) -> &str { - self.database() - } - fn collection(&self) -> Option<&str> { - self.collection() + fn target(&self) -> OperationTarget<'_> { + self.target() } fn cursor_id(&self) -> Option { self.cursor_id() diff --git a/src/operation/abort_transaction.rs b/src/operation/abort_transaction.rs index 326a7a5e4..6bd31764d 100644 --- a/src/operation/abort_transaction.rs +++ b/src/operation/abort_transaction.rs @@ -80,4 +80,8 @@ impl OperationWithDefaults for AbortTransaction { // The session must be "unpinned" before server selection for a retry. self.pinned = None; } + + fn target(&self) -> super::OperationTarget { + "admin".into() + } } diff --git a/src/operation/aggregate.rs b/src/operation/aggregate.rs index 4fbde520b..328b30aaa 100644 --- a/src/operation/aggregate.rs +++ b/src/operation/aggregate.rs @@ -157,6 +157,10 @@ impl OperationWithDefaults for Aggregate { fn output_cursor_id(output: &Self::O) -> Option { Some(output.id()) } + + fn target(&self) -> super::OperationTarget<'_> { + (&self.target).into() + } } impl Aggregate { @@ -205,3 +209,12 @@ impl From for AggregateTarget { AggregateTarget::Database(db_name) } } + +impl<'a> From<&'a AggregateTarget> for super::OperationTarget<'a> { + fn from(value: &'a AggregateTarget) -> Self { + match value { + AggregateTarget::Database(db) => db.as_str().into(), + AggregateTarget::Collection(ns) => ns.into(), + } + } +} diff --git a/src/operation/aggregate/change_stream.rs b/src/operation/aggregate/change_stream.rs index d2b384269..99cfbd43e 100644 --- a/src/operation/aggregate/change_stream.rs +++ b/src/operation/aggregate/change_stream.rs @@ -135,4 +135,8 @@ impl OperationWithDefaults for ChangeStreamAggregate { fn retryability(&self) -> Retryability { self.inner.retryability() } + + fn target(&self) -> crate::operation::OperationTarget<'_> { + self.inner.target() + } } diff --git a/src/operation/bulk_write.rs b/src/operation/bulk_write.rs index a1829f1f0..0832fe5c9 100644 --- a/src/operation/bulk_write.rs +++ b/src/operation/bulk_write.rs @@ -485,4 +485,8 @@ where Retryability::Write } } + + fn target(&self) -> super::OperationTarget<'_> { + super::OperationTarget::ADMIN + } } diff --git a/src/operation/commit_transaction.rs b/src/operation/commit_transaction.rs index a920ea68c..db3664369 100644 --- a/src/operation/commit_transaction.rs +++ b/src/operation/commit_transaction.rs @@ -80,4 +80,8 @@ impl OperationWithDefaults for CommitTransaction { } } } + + fn target(&self) -> super::OperationTarget<'_> { + super::OperationTarget::ADMIN + } } diff --git a/src/operation/count.rs b/src/operation/count.rs index ab03656ee..5c968d190 100644 --- a/src/operation/count.rs +++ b/src/operation/count.rs @@ -75,6 +75,10 @@ impl OperationWithDefaults for Count { fn retryability(&self) -> Retryability { Retryability::Read } + + fn target(&self) -> super::OperationTarget<'_> { + (&self.ns).into() + } } #[derive(Debug, Deserialize)] diff --git a/src/operation/count_documents.rs b/src/operation/count_documents.rs index 0fe048fb9..760251017 100644 --- a/src/operation/count_documents.rs +++ b/src/operation/count_documents.rs @@ -110,6 +110,10 @@ impl OperationWithDefaults for CountDocuments { fn supports_read_concern(&self, description: &StreamDescription) -> bool { self.aggregate.supports_read_concern(description) } + + fn target(&self) -> super::OperationTarget<'_> { + self.aggregate.target() + } } #[derive(Debug, Deserialize)] diff --git a/src/operation/create.rs b/src/operation/create.rs index 40aacaf65..c85866bbd 100644 --- a/src/operation/create.rs +++ b/src/operation/create.rs @@ -52,4 +52,8 @@ impl OperationWithDefaults for Create { .as_ref() .and_then(|opts| opts.write_concern.as_ref()) } + + fn target(&self) -> super::OperationTarget<'_> { + (&self.ns).into() + } } diff --git a/src/operation/create_indexes.rs b/src/operation/create_indexes.rs index 1c46a7c05..8240d732f 100644 --- a/src/operation/create_indexes.rs +++ b/src/operation/create_indexes.rs @@ -83,4 +83,8 @@ impl OperationWithDefaults for CreateIndexes { .as_ref() .and_then(|opts| opts.write_concern.as_ref()) } + + fn target(&self) -> super::OperationTarget<'_> { + (&self.ns).into() + } } diff --git a/src/operation/delete.rs b/src/operation/delete.rs index f15bb184f..8847d05e0 100644 --- a/src/operation/delete.rs +++ b/src/operation/delete.rs @@ -99,4 +99,8 @@ impl OperationWithDefaults for Delete { Retryability::None } } + + fn target(&self) -> super::OperationTarget<'_> { + (&self.ns).into() + } } diff --git a/src/operation/distinct.rs b/src/operation/distinct.rs index 8a0f45307..bae8af091 100644 --- a/src/operation/distinct.rs +++ b/src/operation/distinct.rs @@ -89,6 +89,10 @@ impl OperationWithDefaults for Distinct { fn supports_read_concern(&self, _description: &StreamDescription) -> bool { true } + + fn target(&self) -> super::OperationTarget<'_> { + (&self.ns).into() + } } #[derive(Debug, Deserialize)] diff --git a/src/operation/drop_collection.rs b/src/operation/drop_collection.rs index 562b086fd..29f7b2227 100644 --- a/src/operation/drop_collection.rs +++ b/src/operation/drop_collection.rs @@ -60,4 +60,8 @@ impl OperationWithDefaults for DropCollection { .as_ref() .and_then(|opts| opts.write_concern.as_ref()) } + + fn target(&self) -> super::OperationTarget<'_> { + (&self.ns).into() + } } diff --git a/src/operation/drop_database.rs b/src/operation/drop_database.rs index 1273e615f..aedbd2a42 100644 --- a/src/operation/drop_database.rs +++ b/src/operation/drop_database.rs @@ -52,4 +52,8 @@ impl OperationWithDefaults for DropDatabase { .as_ref() .and_then(|opts| opts.write_concern.as_ref()) } + + fn target(&self) -> super::OperationTarget<'_> { + self.target_db.as_str().into() + } } diff --git a/src/operation/drop_indexes.rs b/src/operation/drop_indexes.rs index de9a06d3f..d88972b03 100644 --- a/src/operation/drop_indexes.rs +++ b/src/operation/drop_indexes.rs @@ -51,4 +51,8 @@ impl OperationWithDefaults for DropIndexes { .as_ref() .and_then(|opts| opts.write_concern.as_ref()) } + + fn target(&self) -> super::OperationTarget<'_> { + (&self.ns).into() + } } diff --git a/src/operation/find.rs b/src/operation/find.rs index 6429b06a7..90aa989c7 100644 --- a/src/operation/find.rs +++ b/src/operation/find.rs @@ -129,12 +129,8 @@ impl OperationWithDefaults for Find { Retryability::Read } - fn database(&self) -> &str { - &self.ns.db - } - - fn collection(&self) -> Option<&str> { - Some(&self.ns.coll) + fn target(&self) -> super::OperationTarget<'_> { + (&self.ns).into() } fn output_cursor_id(output: &Self::O) -> Option { diff --git a/src/operation/find_and_modify.rs b/src/operation/find_and_modify.rs index 208795da9..3d700f451 100644 --- a/src/operation/find_and_modify.rs +++ b/src/operation/find_and_modify.rs @@ -118,4 +118,8 @@ impl OperationWithDefaults for FindAndModify { fn retryability(&self) -> Retryability { Retryability::Write } + + fn target(&self) -> super::OperationTarget<'_> { + (&self.ns).into() + } } diff --git a/src/operation/get_more.rs b/src/operation/get_more.rs index a1f03b458..35cd10d07 100644 --- a/src/operation/get_more.rs +++ b/src/operation/get_more.rs @@ -106,12 +106,8 @@ impl OperationWithDefaults for GetMore<'_> { self.pinned_connection } - fn database(&self) -> &str { - &self.ns.db - } - - fn collection(&self) -> Option<&str> { - Some(&self.ns.coll) + fn target(&self) -> super::OperationTarget<'_> { + (&self.ns).into() } fn cursor_id(&self) -> Option { diff --git a/src/operation/insert.rs b/src/operation/insert.rs index b79a1c42b..eb9e25424 100644 --- a/src/operation/insert.rs +++ b/src/operation/insert.rs @@ -177,4 +177,8 @@ impl OperationWithDefaults for Insert<'_> { fn retryability(&self) -> Retryability { Retryability::Write } + + fn target(&self) -> super::OperationTarget<'_> { + (&self.ns).into() + } } diff --git a/src/operation/list_collections.rs b/src/operation/list_collections.rs index 56e371185..8f1ae8c94 100644 --- a/src/operation/list_collections.rs +++ b/src/operation/list_collections.rs @@ -85,4 +85,8 @@ impl OperationWithDefaults for ListCollections { fn output_cursor_id(output: &Self::O) -> Option { Some(output.id()) } + + fn target(&self) -> super::OperationTarget<'_> { + self.db.as_str().into() + } } diff --git a/src/operation/list_databases.rs b/src/operation/list_databases.rs index 9897d4047..e1e420f84 100644 --- a/src/operation/list_databases.rs +++ b/src/operation/list_databases.rs @@ -57,6 +57,10 @@ impl OperationWithDefaults for ListDatabases { fn retryability(&self) -> Retryability { Retryability::Read } + + fn target(&self) -> super::OperationTarget<'_> { + super::OperationTarget::ADMIN + } } #[derive(Debug, Deserialize)] diff --git a/src/operation/list_indexes.rs b/src/operation/list_indexes.rs index 1c078760b..d481d46c3 100644 --- a/src/operation/list_indexes.rs +++ b/src/operation/list_indexes.rs @@ -73,4 +73,8 @@ impl OperationWithDefaults for ListIndexes { fn output_cursor_id(output: &Self::O) -> Option { Some(output.id()) } + + fn target(&self) -> super::OperationTarget<'_> { + (&self.ns).into() + } } diff --git a/src/operation/raw_output.rs b/src/operation/raw_output.rs index af93c3fdd..4bddab422 100644 --- a/src/operation/raw_output.rs +++ b/src/operation/raw_output.rs @@ -81,12 +81,8 @@ impl Operation for RawOutput { self.0.name() } - fn database(&self) -> &str { - self.0.database() - } - - fn collection(&self) -> Option<&str> { - self.0.collection() + fn target(&self) -> super::OperationTarget<'_> { + self.0.target() } fn cursor_id(&self) -> Option { diff --git a/src/operation/run_command.rs b/src/operation/run_command.rs index 06ecac047..3b5f5b50b 100644 --- a/src/operation/run_command.rs +++ b/src/operation/run_command.rs @@ -94,4 +94,8 @@ impl OperationWithDefaults for RunCommand<'_> { fn pinned_connection(&self) -> Option<&PinnedConnectionHandle> { self.pinned_connection } + + fn target(&self) -> super::OperationTarget<'_> { + self.db.as_str().into() + } } diff --git a/src/operation/run_cursor_command.rs b/src/operation/run_cursor_command.rs index 9a1cde96e..12f9d3ada 100644 --- a/src/operation/run_cursor_command.rs +++ b/src/operation/run_cursor_command.rs @@ -92,12 +92,8 @@ impl Operation for RunCursorCommand<'_> { self.run_command.name() } - fn database(&self) -> &str { - self.run_command.database() - } - - fn collection(&self) -> Option<&str> { - self.run_command.collection() + fn target(&self) -> super::OperationTarget<'_> { + self.run_command.target() } fn handle_response<'a>( diff --git a/src/operation/search_index.rs b/src/operation/search_index.rs index 46cc5a977..5c7f7715d 100644 --- a/src/operation/search_index.rs +++ b/src/operation/search_index.rs @@ -73,6 +73,10 @@ impl OperationWithDefaults for CreateSearchIndexes { fn supports_read_concern(&self, _description: &crate::cmap::StreamDescription) -> bool { false } + + fn target(&self) -> super::OperationTarget<'_> { + (&self.ns).into() + } } #[derive(Debug)] @@ -127,6 +131,10 @@ impl OperationWithDefaults for UpdateSearchIndex { fn supports_read_concern(&self, _description: &crate::cmap::StreamDescription) -> bool { false } + + fn target(&self) -> super::OperationTarget<'_> { + (&self.ns).into() + } } #[derive(Debug)] @@ -179,4 +187,8 @@ impl OperationWithDefaults for DropSearchIndex { fn supports_read_concern(&self, _description: &crate::cmap::StreamDescription) -> bool { false } + + fn target(&self) -> super::OperationTarget<'_> { + (&self.ns).into() + } } diff --git a/src/operation/update.rs b/src/operation/update.rs index 01341e87e..5c1989506 100644 --- a/src/operation/update.rs +++ b/src/operation/update.rs @@ -212,6 +212,10 @@ impl OperationWithDefaults for Update { Retryability::None } } + + fn target(&self) -> super::OperationTarget<'_> { + (&self.ns).into() + } } #[derive(Deserialize)] diff --git a/src/otel.rs b/src/otel.rs index 084eeb230..8f6223381 100644 --- a/src/otel.rs +++ b/src/otel.rs @@ -189,19 +189,21 @@ impl Span { } fn op_target(op: &impl Operation) -> String { - if let Some(coll) = op.collection() { - format!("{}.{}", op.database(), coll) + let target = op.target(); + if let Some(coll) = target.collection { + format!("{}.{}", target.database, coll) } else { - op.database().to_owned() + target.database.to_owned() } } fn common_attrs(op: &impl Operation) -> Vec { + let target = op.target(); let mut attrs = vec![ KeyValue::new("db.system", "mongodb"), - KeyValue::new("db.namespace", op.database().to_owned()), + KeyValue::new("db.namespace", target.database.to_owned()), ]; - if let Some(coll) = op.collection() { + if let Some(coll) = target.collection { attrs.push(KeyValue::new("db.collection.name", coll.to_owned())); } attrs From cc36313592760997e4649b93b70968038d58ec7d Mon Sep 17 00:00:00 2001 From: Abraham Egnor Date: Fri, 3 Oct 2025 12:43:21 +0100 Subject: [PATCH 09/30] feature cleanup --- src/bson_util.rs | 2 ++ src/error.rs | 1 + src/operation.rs | 4 ++++ src/otel.rs | 8 ++++++-- 4 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/bson_util.rs b/src/bson_util.rs index 37e813d34..e410595be 100644 --- a/src/bson_util.rs +++ b/src/bson_util.rs @@ -343,6 +343,7 @@ pub(crate) mod option_u64_as_i64 { /// Truncates the given string at the closest UTF-8 character boundary >= the provided length. /// If the new length is >= the current length, does nothing. +#[cfg(any(feature = "tracing-unstable", feature = "opentelemetry"))] pub(crate) fn truncate_on_char_boundary(s: &mut String, new_len: usize) { let original_len = s.len(); if original_len > new_len { @@ -365,6 +366,7 @@ pub(crate) fn truncate_on_char_boundary(s: &mut String, new_len: usize) { } } +#[cfg(any(feature = "tracing-unstable", feature = "opentelemetry"))] pub(crate) fn doc_to_json_str(doc: crate::bson::Document, max_length_bytes: usize) -> String { let mut ext_json = Bson::Document(doc).into_relaxed_extjson().to_string(); truncate_on_char_boundary(&mut ext_json, max_length_bytes); diff --git a/src/error.rs b/src/error.rs index e17273cf2..f00006320 100644 --- a/src/error.rs +++ b/src/error.rs @@ -783,6 +783,7 @@ impl ErrorKind { } } + #[cfg(feature = "opentelemetry")] pub(crate) fn name(&self) -> &'static str { match self { ErrorKind::InvalidArgument { .. } => "InvalidArgument", diff --git a/src/operation.rs b/src/operation.rs index 22c5b5c7d..bd2e0bad2 100644 --- a/src/operation.rs +++ b/src/operation.rs @@ -177,13 +177,17 @@ pub(crate) trait Operation { /// The name of the server side command associated with this operation. fn name(&self) -> &CStr; + #[allow(dead_code)] fn target(&self) -> OperationTarget<'_>; + #[allow(dead_code)] fn cursor_id(&self) -> Option; + #[allow(dead_code)] fn output_cursor_id(output: &Self::O) -> Option; } +#[allow(dead_code)] pub(crate) struct OperationTarget<'a> { pub(crate) database: &'a str, pub(crate) collection: Option<&'a str>, diff --git a/src/otel.rs b/src/otel.rs index 8f6223381..84da63c44 100644 --- a/src/otel.rs +++ b/src/otel.rs @@ -18,7 +18,8 @@ use crate::{ }; /// Configuration for OpenTelemetry. -#[derive(Debug, Clone, PartialEq, serde::Deserialize)] +#[derive(Debug, Clone, PartialEq, serde::Deserialize, typed_builder::TypedBuilder)] +#[builder(field_defaults(default, setter(into)))] #[serde(rename_all = "camelCase")] #[non_exhaustive] pub struct Options { @@ -77,7 +78,10 @@ impl Client { let span_name = format!("{} {}", op.name(), op_target(op)); let mut attrs = common_attrs(op); attrs.extend([ - KeyValue::new("db.operation.name", op.name().as_str().to_owned()), + KeyValue::new( + "db.operation.name", + crate::bson_compat::cstr_to_str(op.name()).to_owned(), + ), KeyValue::new("db.operation.summary", span_name.clone()), ]); Span { From 8f0572cda587910fccc46dd2dd875c9cdc494d47 Mon Sep 17 00:00:00 2001 From: Abraham Egnor Date: Fri, 3 Oct 2025 13:52:07 +0100 Subject: [PATCH 10/30] span parenting --- src/client/executor.rs | 37 +++++++++++++------- src/lib.rs | 5 +++ src/otel.rs | 79 +++++++++++++++++++++--------------------- 3 files changed, 70 insertions(+), 51 deletions(-) diff --git a/src/client/executor.rs b/src/client/executor.rs index 18fa2a157..1ac50a8d1 100644 --- a/src/client/executor.rs +++ b/src/client/executor.rs @@ -3,6 +3,8 @@ use crate::bson::RawDocumentBuf; use crate::bson::{doc, RawBsonRef, RawDocument, Timestamp}; #[cfg(feature = "in-use-encryption")] use futures_core::future::BoxFuture; +#[cfg(feature = "opentelemetry")] +use opentelemetry::context::FutureExt; use serde::de::DeserializeOwned; use std::sync::LazyLock; @@ -106,8 +108,7 @@ impl Client { op: &mut T, session: impl Into>, ) -> Result> { - #[cfg(feature = "opentelemetry")] - let mut span = self.start_operation_span(op); + let ctx = self.start_operation_span(op); let result = (async move || { // Validate inputs that can be checked before server selection and connection checkout. if self.inner.shutdown.executed.load(Ordering::SeqCst) { @@ -158,12 +159,18 @@ impl Client { } } - Box::pin(async { self.execute_operation_with_retry(op, session).await }).await + Box::pin(async { + self.execute_operation_with_retry(op, session) + .with_current_context() + .await + }) + .with_current_context() + .await })() + .with_context(ctx.clone()) .await; - #[cfg(feature = "opentelemetry")] - span.record_error(&result); + self.record_error(&ctx, &result); result } @@ -419,6 +426,7 @@ impl Client { retryability, effective_criteria, ) + .with_current_context() .await { Ok(output) => ExecutionDetails { @@ -513,12 +521,8 @@ impl Client { let mut message = Message::try_from(cmd)?; message.request_id = Some(request_id); - /* - db.mongodb.cursor_id: ??? - */ - #[cfg(feature = "opentelemetry")] - let mut span = self.start_command_span(op, &connection_info, &message, cmd_attrs); + let ctx = self.start_command_span(op, &connection_info, &message, cmd_attrs); #[cfg(feature = "in-use-encryption")] { @@ -650,8 +654,7 @@ impl Client { } } }; - #[cfg(feature = "opentelemetry")] - span.record_command_result::(&result); + self.record_command_result::(&ctx, &result); if result .as_ref() @@ -1111,3 +1114,13 @@ impl RetryHelper for Option { } } } + +#[cfg(not(feature = "opentelemetry"))] +trait OtelFutureStub: Sized { + fn with_current_context(self) -> Self { + self + } +} + +#[cfg(not(feature = "opentelemetry"))] +impl OtelFutureStub for T {} diff --git a/src/lib.rs b/src/lib.rs index 75c1ccf05..6e6c628aa 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -47,6 +47,8 @@ mod index; mod operation; #[cfg(feature = "opentelemetry")] pub mod otel; +#[cfg(not(feature = "opentelemetry"))] +mod otel_stub; pub mod results; pub(crate) mod runtime; mod sdam; @@ -71,6 +73,9 @@ pub use bson2 as bson; #[cfg(feature = "bson-3")] pub use bson3 as bson; +#[cfg(not(feature = "opentelemetry"))] +pub(crate) use otel_stub as otel; + #[cfg(feature = "in-use-encryption")] pub use crate::client::csfle::client_encryption; pub use crate::{ diff --git a/src/otel.rs b/src/otel.rs index 84da63c44..a38e71d69 100644 --- a/src/otel.rs +++ b/src/otel.rs @@ -4,7 +4,8 @@ use std::sync::LazyLock; use opentelemetry::{ global::BoxedTracer, - trace::{Span as _, SpanKind, Tracer as _}, + trace::{SpanKind, TraceContextExt, Tracer as _}, + Context, KeyValue, }; @@ -71,9 +72,9 @@ static TRACER: LazyLock = LazyLock::new(|| { }); impl Client { - pub(crate) fn start_operation_span(&self, op: &impl Operation) -> Span { + pub(crate) fn start_operation_span(&self, op: &impl Operation) -> Context { if !self.options().otel_enabled() { - return Span { inner: None }; + return Context::current(); } let span_name = format!("{} {}", op.name(), op_target(op)); let mut attrs = common_attrs(op); @@ -84,15 +85,12 @@ impl Client { ), KeyValue::new("db.operation.summary", span_name.clone()), ]); - Span { - inner: Some( - TRACER - .span_builder(span_name) - .with_kind(SpanKind::Client) - .with_attributes(attrs) - .start(&*TRACER), - ), - } + let span = TRACER + .span_builder(span_name) + .with_kind(SpanKind::Client) + .with_attributes(attrs) + .start(&*TRACER); + Context::current_with_span(span) } pub(crate) fn start_command_span( @@ -101,9 +99,9 @@ impl Client { conn_info: &ConnectionInfo, message: &Message, cmd_attrs: CommandAttributes, - ) -> Span { + ) -> Context { if !self.options().otel_enabled() || cmd_attrs.should_redact { - return Span { inner: None }; + return Context::current(); } let otel_driver_conn_id: i64 = conn_info.id.into(); let mut attrs = common_attrs(op); @@ -144,51 +142,54 @@ impl Client { if let Some(cursor_id) = op.cursor_id() { attrs.push(KeyValue::new("db.mongodb.cursor_id", cursor_id)); } - Span { - inner: Some( - TRACER - .span_builder(cmd_attrs.name) - .with_kind(SpanKind::Client) - .with_attributes(attrs) - .start(&*TRACER), - ), - } + let span = TRACER + .span_builder(cmd_attrs.name) + .with_kind(SpanKind::Client) + .with_attributes(attrs) + .start(&*TRACER); + Context::current_with_span(span) } -} - -pub(crate) struct Span { - inner: Option<::Span>, -} -impl Span { - pub(crate) fn record_error(&mut self, result: &Result) { - if let (Some(inner), Err(error)) = (&mut self.inner, result) { - inner.set_attributes([ + pub(crate) fn record_error(&self, context: &Context, result: &Result) { + if !self.options().otel_enabled() { + return; + } + if let Err(error) = result { + let span = context.span(); + span.set_attributes([ KeyValue::new("exception.message", error.to_string()), KeyValue::new("exception.type", error.kind.name()), #[cfg(test)] KeyValue::new("exception.stacktrace", error.bt.to_string()), ]); if let ErrorKind::Command(cmd_err) = &*error.kind { - inner.set_attribute(KeyValue::new( + span.set_attribute(KeyValue::new( "db.response.status_code", cmd_err.code_name.clone(), )); } - inner.record_error(error); - inner.set_status(opentelemetry::trace::Status::Error { + span.record_error(error); + span.set_status(opentelemetry::trace::Status::Error { description: error.to_string().into(), }); } } - pub(crate) fn record_command_result(&mut self, result: &Result) { - if let (Some(inner), Ok(out)) = (&mut self.inner, result) { + pub(crate) fn record_command_result( + &self, + context: &Context, + result: &Result, + ) { + if !self.options().otel_enabled() { + return; + } + if let Ok(out) = result { if let Some(cursor_id) = Op::output_cursor_id(out) { - inner.set_attribute(KeyValue::new("db.mongodb.cursor_id", cursor_id)); + let span = context.span(); + span.set_attribute(KeyValue::new("db.mongodb.cursor_id", cursor_id)); } } - self.record_error(result); + self.record_error(context, result); } } From 718c591a7cf3257f62f80310d3632a8d4b5bf2db Mon Sep 17 00:00:00 2001 From: Abraham Egnor Date: Fri, 3 Oct 2025 14:06:04 +0100 Subject: [PATCH 11/30] stub when disabled --- src/client/executor.rs | 15 ++++----------- src/otel_stub.rs | 21 +++++++++++++++++++++ 2 files changed, 25 insertions(+), 11 deletions(-) create mode 100644 src/otel_stub.rs diff --git a/src/client/executor.rs b/src/client/executor.rs index 1ac50a8d1..07fb07576 100644 --- a/src/client/executor.rs +++ b/src/client/executor.rs @@ -16,6 +16,8 @@ use std::{ }; use super::{options::ServerAddress, session::TransactionState, Client, ClientSession}; +#[cfg(not(feature = "opentelemetry"))] +use crate::otel::OtelFutureStub as _; use crate::{ bson::Document, change_stream::{ @@ -169,7 +171,7 @@ impl Client { })() .with_context(ctx.clone()) .await; - + #[cfg(feature = "opentelemetry")] self.record_error(&ctx, &result); result @@ -654,6 +656,7 @@ impl Client { } } }; + #[cfg(feature = "opentelemetry")] self.record_command_result::(&ctx, &result); if result @@ -1114,13 +1117,3 @@ impl RetryHelper for Option { } } } - -#[cfg(not(feature = "opentelemetry"))] -trait OtelFutureStub: Sized { - fn with_current_context(self) -> Self { - self - } -} - -#[cfg(not(feature = "opentelemetry"))] -impl OtelFutureStub for T {} diff --git a/src/otel_stub.rs b/src/otel_stub.rs new file mode 100644 index 000000000..1404be240 --- /dev/null +++ b/src/otel_stub.rs @@ -0,0 +1,21 @@ +use crate::{operation::Operation, Client}; + +type Context = (); + +impl Client { + pub(crate) fn start_operation_span(&self, _op: &impl Operation) -> Context { + () + } +} + +pub(crate) trait OtelFutureStub: Sized { + fn with_context(self, _ctx: Context) -> Self { + self + } + + fn with_current_context(self) -> Self { + self + } +} + +impl OtelFutureStub for T {} From 71c27aaa66f3ed3dc036a729c604cfb1d9b7d1d9 Mon Sep 17 00:00:00 2001 From: Abraham Egnor Date: Mon, 6 Oct 2025 12:06:00 +0100 Subject: [PATCH 12/30] allow per-client providers --- Cargo.lock | 8 +++---- Cargo.toml | 2 +- src/client.rs | 12 ++++++++++ src/otel.rs | 63 ++++++++++++++++++++++++++++++++++++++------------- 4 files changed, 64 insertions(+), 21 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fbadf5729..7e88a78f2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3433,18 +3433,18 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "typed-builder" -version = "0.20.1" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd9d30e3a08026c78f246b173243cf07b3696d274debd26680773b6773c2afc7" +checksum = "398a3a3c918c96de527dc11e6e846cd549d4508030b8a33e1da12789c856b81a" dependencies = [ "typed-builder-macro", ] [[package]] name = "typed-builder-macro" -version = "0.20.1" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c36781cc0e46a83726d9879608e4cf6c2505237e263a8eb8c24502989cfdb28" +checksum = "0e48cea23f68d1f78eb7bc092881b6bb88d3d6b5b7e6234f6f9c911da1ffb221" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index d3cf8c104..d2053c6cd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -122,7 +122,7 @@ take_mut = "0.2.2" thiserror = "1.0.24" tokio-openssl = { version = "0.6.3", optional = true } tracing = { version = "0.1.36", optional = true } -typed-builder = "0.20.0" +typed-builder = "0.22.0" webpki-roots = "1" zstd = { version = "0.11.2", optional = true } macro_magic = "0.5.1" diff --git a/src/client.rs b/src/client.rs index ff4591981..c0ef225d6 100644 --- a/src/client.rs +++ b/src/client.rs @@ -143,6 +143,8 @@ struct ClientInner { end_sessions_token: std::sync::Mutex, #[cfg(feature = "in-use-encryption")] csfle: tokio::sync::RwLock>, + #[cfg(feature = "opentelemetry")] + tracer: opentelemetry::global::BoxedTracer, #[cfg(test)] disable_command_events: AtomicBool, } @@ -181,6 +183,9 @@ impl Client { tx: Some(cleanup_tx), }); + #[cfg(feature = "opentelemetry")] + let tracer = options.tracer(); + let inner = TrackingArc::new(ClientInner { topology: Topology::new(options.clone())?, session_pool: ServerSessionPool::new(), @@ -193,6 +198,8 @@ impl Client { end_sessions_token, #[cfg(feature = "in-use-encryption")] csfle: Default::default(), + #[cfg(feature = "opentelemetry")] + tracer, #[cfg(test)] disable_command_events: AtomicBool::new(false), }); @@ -668,6 +675,11 @@ impl Client { .await; } } + + #[cfg(feature = "opentelemetry")] + pub(crate) fn tracer(&self) -> &opentelemetry::global::BoxedTracer { + &self.inner.tracer + } } #[derive(Clone, Debug)] diff --git a/src/otel.rs b/src/otel.rs index a38e71d69..cf4599e3b 100644 --- a/src/otel.rs +++ b/src/otel.rs @@ -1,10 +1,12 @@ //! Support for OpenTelemetry. -use std::sync::LazyLock; +use std::sync::{Arc, LazyLock}; + +use derive_where::derive_where; use opentelemetry::{ - global::BoxedTracer, - trace::{SpanKind, TraceContextExt, Tracer as _}, + global::{BoxedTracer, ObjectSafeTracerProvider}, + trace::{Span, SpanKind, TraceContextExt, Tracer, TracerProvider}, Context, KeyValue, }; @@ -19,7 +21,8 @@ use crate::{ }; /// Configuration for OpenTelemetry. -#[derive(Debug, Clone, PartialEq, serde::Deserialize, typed_builder::TypedBuilder)] +#[derive(Clone, serde::Deserialize, typed_builder::TypedBuilder)] +#[derive_where(Debug, PartialEq)] #[builder(field_defaults(default, setter(into)))] #[serde(rename_all = "camelCase")] #[non_exhaustive] @@ -31,9 +34,43 @@ pub struct Options { /// value of the `OTEL_RUST_INSTRUMENTATION_MONGODB_QUERY_TEXT_MAX_LENGTH` environment /// variable. pub query_text_max_length: Option, + /// Tracer provider to use. If unset, will use the global instance. + #[serde(skip)] + #[derive_where(skip)] + #[builder( + setter( + fn transform(provider: P) -> Option> + where + S: Span + Send + Sync + 'static, + T: Tracer + Send + Sync + 'static, + P: TracerProvider + Send + Sync + 'static, + { + Some(Arc::new(provider)) + }, + ) + )] + pub tracer_provider: Option>, } impl ClientOptions { + pub(crate) fn tracer(&self) -> BoxedTracer { + let provider: &dyn ObjectSafeTracerProvider = match self + .tracing + .as_ref() + .and_then(|t| t.tracer_provider.as_ref()) + { + Some(provider) => &**provider, + None => &opentelemetry::global::tracer_provider(), + }; + BoxedTracer::new( + provider.boxed_tracer( + opentelemetry::InstrumentationScope::builder("mongodb") + .with_version(env!("CARGO_PKG_VERSION")) + .build(), + ), + ) + } + fn otel_enabled(&self) -> bool { static ENABLED_ENV: LazyLock = LazyLock::new(|| { match std::env::var("OTEL_RUST_INSTRUMENTATION_MONGODB_ENABLED").as_deref() { @@ -63,14 +100,6 @@ impl ClientOptions { } } -static TRACER: LazyLock = LazyLock::new(|| { - opentelemetry::global::tracer_with_scope( - opentelemetry::InstrumentationScope::builder("mongodb") - .with_version(env!("CARGO_PKG_VERSION")) - .build(), - ) -}); - impl Client { pub(crate) fn start_operation_span(&self, op: &impl Operation) -> Context { if !self.options().otel_enabled() { @@ -85,11 +114,12 @@ impl Client { ), KeyValue::new("db.operation.summary", span_name.clone()), ]); - let span = TRACER + let span = self + .tracer() .span_builder(span_name) .with_kind(SpanKind::Client) .with_attributes(attrs) - .start(&*TRACER); + .start(self.tracer()); Context::current_with_span(span) } @@ -142,11 +172,12 @@ impl Client { if let Some(cursor_id) = op.cursor_id() { attrs.push(KeyValue::new("db.mongodb.cursor_id", cursor_id)); } - let span = TRACER + let span = self + .tracer() .span_builder(cmd_attrs.name) .with_kind(SpanKind::Client) .with_attributes(attrs) - .start(&*TRACER); + .start(self.tracer()); Context::current_with_span(span) } From 983f003e68b894614949e6d8766177f4c98a7035 Mon Sep 17 00:00:00 2001 From: Abraham Egnor Date: Tue, 7 Oct 2025 12:12:53 +0100 Subject: [PATCH 13/30] untested span matching --- Cargo.lock | 18 ++ Cargo.toml | 1 + src/otel.rs | 3 + src/otel/testing.rs | 188 ++++++++++++++++++++ src/test/spec/unified_runner/entity.rs | 16 ++ src/test/spec/unified_runner/test_file.rs | 4 + src/test/spec/unified_runner/test_runner.rs | 9 + 7 files changed, 239 insertions(+) create mode 100644 src/otel/testing.rs diff --git a/Cargo.lock b/Cargo.lock index 7e88a78f2..0a59a3ee5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1973,6 +1973,7 @@ dependencies = [ "openssl", "openssl-probe", "opentelemetry", + "opentelemetry_sdk", "pbkdf2 0.11.0", "pem", "percent-encoding", @@ -2163,6 +2164,23 @@ dependencies = [ "tracing", ] +[[package]] +name = "opentelemetry_sdk" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14ae4f5991976fd48df6d843de219ca6d31b01daaab2dad5af2badeded372bd" +dependencies = [ + "futures-channel", + "futures-executor", + "futures-util", + "opentelemetry", + "percent-encoding", + "rand 0.9.1", + "thiserror 2.0.12", + "tokio", + "tokio-stream", +] + [[package]] name = "outref" version = "0.5.2" diff --git a/Cargo.toml b/Cargo.toml index d2053c6cd..3d584263d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -220,6 +220,7 @@ futures = "0.3" hex = "0.4" home = "0.5" lambda_runtime = "0.6.0" +opentelemetry_sdk = { version = "0.31.0", features = ["testing"] } pkcs8 = { version = "0.10.2", features = ["3des", "des-insecure", "sha1-insecure"] } pretty_assertions = "1.3.0" serde = { version = ">= 0.0.0", features = ["rc"] } diff --git a/src/otel.rs b/src/otel.rs index cf4599e3b..435c3c5a3 100644 --- a/src/otel.rs +++ b/src/otel.rs @@ -20,6 +20,9 @@ use crate::{ Client, }; +#[cfg(test)] +pub(crate) mod testing; + /// Configuration for OpenTelemetry. #[derive(Clone, serde::Deserialize, typed_builder::TypedBuilder)] #[derive_where(Debug, PartialEq)] diff --git a/src/otel/testing.rs b/src/otel/testing.rs new file mode 100644 index 000000000..b08b9c185 --- /dev/null +++ b/src/otel/testing.rs @@ -0,0 +1,188 @@ +use std::collections::HashMap; + +use opentelemetry::SpanId; +use opentelemetry_sdk::trace::{ + BatchSpanProcessor, + InMemorySpanExporter, + InMemorySpanExporterBuilder, + SdkTracerProvider, + SpanData, +}; +use serde::Deserialize; + +use crate::{ + bson::{doc, Bson, Document}, + test::spec::unified_runner::{results_match, EntityMap, TestRunner}, +}; + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub(crate) struct ObserveTracingMessages { + enable_command_payload: Option, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub(crate) struct ExpectedTracingMessages { + client: String, + ignore_extra_spans: Option, + spans: Vec, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +struct ExpectedSpan { + name: String, + attributes: Document, + nested: Vec, +} + +#[derive(Clone, Debug)] +pub(crate) struct ClientTracing { + exporter: InMemorySpanExporter, + provider: SdkTracerProvider, +} + +impl ClientTracing { + pub(crate) fn new(observe: &ObserveTracingMessages) -> (Self, super::Options) { + let exporter = InMemorySpanExporterBuilder::new().build(); + let provider = SdkTracerProvider::builder() + .with_span_processor(BatchSpanProcessor::builder(exporter.clone()).build()) + .build(); + let mut options = super::Options::builder() + .enabled(true) + .tracer_provider(provider.clone()) + .build(); + if observe.enable_command_payload.unwrap_or(false) { + options.query_text_max_length = Some(1000); + } + (Self { exporter, provider }, options) + } +} + +impl TestRunner { + pub(crate) async fn match_spans( + &self, + expected: &ExpectedTracingMessages, + ) -> Result<(), String> { + let client_tracing = self.get_client(&expected.client).await.tracing.unwrap(); + client_tracing.provider.force_flush().unwrap(); + let mut root_spans = vec![]; + let mut nested_spans = HashMap::>::new(); + for span in client_tracing.exporter.get_finished_spans().unwrap() { + if span.parent_span_id == SpanId::INVALID { + root_spans.push(span); + } else { + nested_spans + .entry(span.parent_span_id) + .or_default() + .push(span); + } + } + let (root_spans, nested_spans) = (root_spans, nested_spans); + + let entities = self.entities.read().await; + match_span_slice( + &root_spans, + &nested_spans, + &expected.spans, + expected.ignore_extra_spans.unwrap_or(false), + &entities, + )?; + + Ok(()) + } +} + +fn match_span_slice( + actual: &[SpanData], + actual_nested: &HashMap>, + expected: &[ExpectedSpan], + ignore_extra: bool, + entities: &EntityMap, +) -> Result<(), String> { + if ignore_extra { + if actual.len() < expected.len() { + return Err(format!( + "expected at least {} spans, got {}\nactual:\n{:#?}\nexpected:\n{:#?}", + expected.len(), + actual.len(), + actual, + expected, + )); + } + } else { + if actual.len() != expected.len() { + return Err(format!( + "expected exactly {} spans, got {}\nactual:\n{:#?}\nexpected:\n{:#?}", + expected.len(), + actual.len(), + actual, + expected, + )); + } + } + + for (act_span, exp_span) in actual.iter().zip(expected) { + match_span(act_span, actual_nested, exp_span, ignore_extra, &entities)?; + } + + Ok(()) +} + +fn match_span( + actual: &SpanData, + actual_nested: &HashMap>, + expected: &ExpectedSpan, + ignore_extra: bool, + entities: &EntityMap, +) -> Result<(), String> { + let err_suffix = || format!("actual:\n{:#?}\nexpected:\n{:#?}", actual, expected); + if expected.name != actual.name { + return Err(format!( + "expected name {:?}, got {:?}\n{}", + expected.name, + actual.name, + err_suffix(), + )); + } + let mut actual_attrs = doc! {}; + for kv in &actual.attributes { + actual_attrs.insert(kv.key.as_str(), value_to_bson(&kv.value)?); + } + for (k, expected_v) in &expected.attributes { + results_match(actual_attrs.get(k), expected_v, false, Some(entities))?; + } + + let actual_children = actual_nested + .get(&actual.span_context.span_id()) + .map(|v| v.as_slice()) + .unwrap_or(&[]); + match_span_slice( + actual_children, + actual_nested, + &expected.nested, + ignore_extra, + entities, + )?; + + Ok(()) +} + +fn value_to_bson(val: &opentelemetry::Value) -> Result { + use opentelemetry::{Array, Value}; + Ok(match val { + Value::Bool(b) => Bson::Boolean(*b), + Value::I64(i) => Bson::Int64(*i), + Value::F64(f) => Bson::Double(*f), + Value::String(sv) => Bson::String(sv.as_str().to_owned()), + Value::Array(array) => match array { + Array::Bool(items) => items.into(), + Array::I64(items) => items.into(), + Array::F64(items) => items.into(), + Array::String(items) => items.iter().map(|i| i.as_str()).collect::>().into(), + _ => return Err(format!("unhandled opentelemetry array {:?}", array)), + }, + _ => return Err(format!("unhandled opentelemetry value {:?}", val)), + }) +} diff --git a/src/test/spec/unified_runner/entity.rs b/src/test/spec/unified_runner/entity.rs index 27f7255e7..a3b7cf55b 100644 --- a/src/test/spec/unified_runner/entity.rs +++ b/src/test/spec/unified_runner/entity.rs @@ -70,6 +70,8 @@ pub(crate) struct ClientEntity { observe_events: Option>, ignore_command_names: Option>, observe_sensitive_commands: bool, + #[cfg(feature = "opentelemetry")] + pub(crate) tracing: Option, } #[derive(Debug)] @@ -123,9 +125,21 @@ impl ClientEntity { observe_events: Option>, ignore_command_names: Option>, observe_sensitive_commands: bool, + #[cfg(feature = "opentelemetry")] observe_tracing_messages: Option< + &crate::otel::testing::ObserveTracingMessages, + >, ) -> Self { let events = EventBuffer::new(); events.register(&mut client_options); + #[cfg(feature = "opentelemetry")] + let tracing = match observe_tracing_messages { + Some(observe) => { + let (tracing, options) = crate::otel::testing::ClientTracing::new(observe); + client_options.tracing = Some(options); + Some(tracing) + } + None => None, + }; let client = Client::with_options(client_options).unwrap(); let topology_id = client.topology().id; Self { @@ -135,6 +149,8 @@ impl ClientEntity { observe_events, ignore_command_names, observe_sensitive_commands, + #[cfg(feature = "opentelemetry")] + tracing, } } diff --git a/src/test/spec/unified_runner/test_file.rs b/src/test/spec/unified_runner/test_file.rs index 4a0c1400a..13cfbe54f 100644 --- a/src/test/spec/unified_runner/test_file.rs +++ b/src/test/spec/unified_runner/test_file.rs @@ -232,6 +232,8 @@ pub(crate) struct Client { pub(crate) observe_log_messages: Option>, #[cfg(feature = "in-use-encryption")] pub(crate) auto_encrypt_opts: Option, + #[cfg(feature = "opentelemetry")] + pub(crate) observe_tracing_messages: Option, } impl Client { @@ -468,6 +470,8 @@ pub(crate) struct TestCase { pub(crate) expect_events: Option>, #[cfg(feature = "tracing-unstable")] pub(crate) expect_log_messages: Option>, + #[cfg(feature = "opentelemetry")] + pub(crate) expect_tracing_messages: Option, #[serde(default, deserialize_with = "serde_util::deserialize_nonempty_vec")] pub(crate) outcome: Option>, } diff --git a/src/test/spec/unified_runner/test_runner.rs b/src/test/spec/unified_runner/test_runner.rs index b3f089a29..2df8d6ca6 100644 --- a/src/test/spec/unified_runner/test_runner.rs +++ b/src/test/spec/unified_runner/test_runner.rs @@ -362,6 +362,13 @@ impl TestRunner { } } + #[cfg(feature = "opentelemetry")] + if let Some(expected) = &test_case.expect_tracing_messages { + if let Err(e) = self.match_spans(expected).await { + panic!("[{}] {}", test_case.description, e); + } + } + self.fail_point_guards.write().await.clear(); if let Some(ref outcome) = test_case.outcome { @@ -527,6 +534,8 @@ impl TestRunner { observe_events, ignore_command_names, observe_sensitive_commands, + #[cfg(feature = "opentelemetry")] + client.observe_tracing_messages.as_ref(), ); #[cfg(feature = "in-use-encryption")] From 8c7db7abf051ea3d01e23ef2d81bb116d855a80f Mon Sep 17 00:00:00 2001 From: Abraham Egnor Date: Tue, 7 Oct 2025 12:15:33 +0100 Subject: [PATCH 14/30] test import --- src/test/spec/json/open-telemetry/README.md | 60 +++ .../open-telemetry/operation/aggregate.json | 130 ++++++ .../open-telemetry/operation/aggregate.yml | 62 +++ .../operation/atlas_search.json | 239 +++++++++++ .../open-telemetry/operation/atlas_search.yml | 139 +++++++ .../open-telemetry/operation/bulk_write.json | 336 ++++++++++++++++ .../open-telemetry/operation/bulk_write.yml | 199 +++++++++ .../json/open-telemetry/operation/count.json | 123 ++++++ .../json/open-telemetry/operation/count.yml | 63 +++ .../operation/create_collection.json | 110 +++++ .../operation/create_collection.yml | 55 +++ .../operation/create_indexes.json | 127 ++++++ .../operation/create_indexes.yml | 60 +++ .../json/open-telemetry/operation/delete.json | 103 +++++ .../json/open-telemetry/operation/delete.yml | 51 +++ .../open-telemetry/operation/distinct.json | 127 ++++++ .../open-telemetry/operation/distinct.yml | 66 +++ .../operation/drop_collection.json | 117 ++++++ .../operation/drop_collection.yml | 60 +++ .../operation/drop_indexes.json | 145 +++++++ .../open-telemetry/operation/drop_indexes.yml | 78 ++++ .../json/open-telemetry/operation/find.json | 306 ++++++++++++++ .../json/open-telemetry/operation/find.yml | 148 +++++++ .../operation/find_and_modify.json | 138 +++++++ .../operation/find_and_modify.yml | 67 +++ .../operation/find_without_query_text.json | 113 ++++++ .../operation/find_without_query_text.yml | 56 +++ .../json/open-telemetry/operation/insert.json | 117 ++++++ .../json/open-telemetry/operation/insert.yml | 61 +++ .../operation/list_collections.json | 106 +++++ .../operation/list_collections.yml | 52 +++ .../operation/list_databases.json | 101 +++++ .../operation/list_databases.yml | 48 +++ .../operation/list_indexes.json | 121 ++++++ .../open-telemetry/operation/list_indexes.yml | 61 +++ .../open-telemetry/operation/map_reduce.json | 180 +++++++++ .../open-telemetry/operation/map_reduce.yml | 99 +++++ .../open-telemetry/operation/retries.json | 212 ++++++++++ .../json/open-telemetry/operation/retries.yml | 105 +++++ .../json/open-telemetry/operation/update.json | 108 +++++ .../json/open-telemetry/operation/update.yml | 53 +++ .../transaction/convenient.json | 237 +++++++++++ .../open-telemetry/transaction/convenient.yml | 132 ++++++ .../open-telemetry/transaction/core_api.json | 380 ++++++++++++++++++ .../open-telemetry/transaction/core_api.yml | 208 ++++++++++ 45 files changed, 5659 insertions(+) create mode 100644 src/test/spec/json/open-telemetry/README.md create mode 100644 src/test/spec/json/open-telemetry/operation/aggregate.json create mode 100644 src/test/spec/json/open-telemetry/operation/aggregate.yml create mode 100644 src/test/spec/json/open-telemetry/operation/atlas_search.json create mode 100644 src/test/spec/json/open-telemetry/operation/atlas_search.yml create mode 100644 src/test/spec/json/open-telemetry/operation/bulk_write.json create mode 100644 src/test/spec/json/open-telemetry/operation/bulk_write.yml create mode 100644 src/test/spec/json/open-telemetry/operation/count.json create mode 100644 src/test/spec/json/open-telemetry/operation/count.yml create mode 100644 src/test/spec/json/open-telemetry/operation/create_collection.json create mode 100644 src/test/spec/json/open-telemetry/operation/create_collection.yml create mode 100644 src/test/spec/json/open-telemetry/operation/create_indexes.json create mode 100644 src/test/spec/json/open-telemetry/operation/create_indexes.yml create mode 100644 src/test/spec/json/open-telemetry/operation/delete.json create mode 100644 src/test/spec/json/open-telemetry/operation/delete.yml create mode 100644 src/test/spec/json/open-telemetry/operation/distinct.json create mode 100644 src/test/spec/json/open-telemetry/operation/distinct.yml create mode 100644 src/test/spec/json/open-telemetry/operation/drop_collection.json create mode 100644 src/test/spec/json/open-telemetry/operation/drop_collection.yml create mode 100644 src/test/spec/json/open-telemetry/operation/drop_indexes.json create mode 100644 src/test/spec/json/open-telemetry/operation/drop_indexes.yml create mode 100644 src/test/spec/json/open-telemetry/operation/find.json create mode 100644 src/test/spec/json/open-telemetry/operation/find.yml create mode 100644 src/test/spec/json/open-telemetry/operation/find_and_modify.json create mode 100644 src/test/spec/json/open-telemetry/operation/find_and_modify.yml create mode 100644 src/test/spec/json/open-telemetry/operation/find_without_query_text.json create mode 100644 src/test/spec/json/open-telemetry/operation/find_without_query_text.yml create mode 100644 src/test/spec/json/open-telemetry/operation/insert.json create mode 100644 src/test/spec/json/open-telemetry/operation/insert.yml create mode 100644 src/test/spec/json/open-telemetry/operation/list_collections.json create mode 100644 src/test/spec/json/open-telemetry/operation/list_collections.yml create mode 100644 src/test/spec/json/open-telemetry/operation/list_databases.json create mode 100644 src/test/spec/json/open-telemetry/operation/list_databases.yml create mode 100644 src/test/spec/json/open-telemetry/operation/list_indexes.json create mode 100644 src/test/spec/json/open-telemetry/operation/list_indexes.yml create mode 100644 src/test/spec/json/open-telemetry/operation/map_reduce.json create mode 100644 src/test/spec/json/open-telemetry/operation/map_reduce.yml create mode 100644 src/test/spec/json/open-telemetry/operation/retries.json create mode 100644 src/test/spec/json/open-telemetry/operation/retries.yml create mode 100644 src/test/spec/json/open-telemetry/operation/update.json create mode 100644 src/test/spec/json/open-telemetry/operation/update.yml create mode 100644 src/test/spec/json/open-telemetry/transaction/convenient.json create mode 100644 src/test/spec/json/open-telemetry/transaction/convenient.yml create mode 100644 src/test/spec/json/open-telemetry/transaction/core_api.json create mode 100644 src/test/spec/json/open-telemetry/transaction/core_api.yml diff --git a/src/test/spec/json/open-telemetry/README.md b/src/test/spec/json/open-telemetry/README.md new file mode 100644 index 000000000..99bcc8d29 --- /dev/null +++ b/src/test/spec/json/open-telemetry/README.md @@ -0,0 +1,60 @@ +# OpenTelemetry Tests + +______________________________________________________________________ + +## Testing + +### Automated Tests + +The YAML and JSON files in this directory are platform-independent tests meant to exercise a driver's implementation of +the OpenTelemetry specification. These tests utilize the +[Unified Test Format](../../unified-test-format/unified-test-format.md). + +For each test, create a MongoClient, configure it to enable tracing. + +```yaml +createEntities: + - client: + id: client0 + observeTracingMessages: + enableCommandPayload: true +``` + +These tests require the ability to collect tracing [spans](../open-telemetry.md) data in a structured form as described +in the +[Unified Test Format specification.expectTracingMessages](../../unified-test-format/unified-test-format.md#expectTracingMessages). +For example the Java driver uses [Micrometer](https://jira.mongodb.org/browse/JAVA-5732) to collect tracing spans. + +```yaml +expectTracingMessages: + client: client0 + ignoreExtraSpans: false + spans: + ... +``` + +### Prose Tests + +*Test 1: Tracing Enable/Disable via Environment Variable* + +1. Set the environment variable `OTEL_#{LANG}_INSTRUMENTATION_MONGODB_ENABLED` to `false`. +2. Create a `MongoClient` without explicitly enabling tracing. +3. Perform a database operation (e.g., `find()` on a test collection). +4. Assert that no OpenTelemetry tracing spans are emitted for the operation. +5. Set the environment variable `OTEL_#{LANG}_INSTRUMENTATION_MONGODB_ENABLED` to `true`. +6. Create a new `MongoClient` without explicitly enabling tracing. +7. Perform the same database operation. +8. Assert that OpenTelemetry tracing spans are emitted for the operation. + +*Test 2: Command Payload Emission via Environment Variable* + +1. Set the environment variable `OTEL_#{LANG}_INSTRUMENTATION_MONGODB_ENABLED` to `true`. +2. Set the environment variable `OTEL_#{LANG}_INSTRUMENTATION_MONGODB_QUERY_TEXT_MAX_LENGTH` to a positive integer + (e.g., 1024). +3. Create a `MongoClient` without explicitly enabling command payload emission. +4. Perform a database operation (e.g., `find()`). +5. Assert that the emitted tracing span includes the `db.query.text` attribute. +6. Unset the environment variable `OTEL_#{LANG}_INSTRUMENTATION_MONGODB_QUERY_TEXT_MAX_LENGTH`. +7. Create a new `MongoClient`. +8. Perform the same database operation. +9. Assert that the emitted tracing span does not include the `db.query.text` attribute. diff --git a/src/test/spec/json/open-telemetry/operation/aggregate.json b/src/test/spec/json/open-telemetry/operation/aggregate.json new file mode 100644 index 000000000..9c30c36f8 --- /dev/null +++ b/src/test/spec/json/open-telemetry/operation/aggregate.json @@ -0,0 +1,130 @@ +{ + "description": "operation aggregate", + "schemaVersion": "1.27", + "createEntities": [ + { + "client": { + "id": "client0", + "useMultipleMongoses": false, + "observeTracingMessages": { + "enableCommandPayload": true + } + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "operation-aggregate" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "test" + } + } + ], + "tests": [ + { + "description": "aggregation", + "operations": [ + { + "name": "aggregate", + "object": "collection0", + "arguments": { + "pipeline": [ + { + "$match": { + "_id": 1 + } + } + ] + } + } + ], + "expectTracingMessages": [ + { + "client": "client0", + "ignoreExtraSpans": false, + "spans": [ + { + "name": "aggregate operation-aggregate.test", + "attributes": { + "db.system": "mongodb", + "db.namespace": "operation-aggregate", + "db.collection.name": "test", + "db.operation.name": "aggregate", + "db.operation.summary": "aggregate operation-aggregate.test" + }, + "nested": [ + { + "name": "aggregate", + "attributes": { + "db.system": "mongodb", + "db.namespace": "operation-aggregate", + "db.collection.name": "test", + "db.command.name": "aggregate", + "network.transport": "tcp", + "db.mongodb.cursor_id": { + "$$exists": false + }, + "db.response.status_code": { + "$$exists": false + }, + "exception.message": { + "$$exists": false + }, + "exception.type": { + "$$exists": false + }, + "exception.stacktrace": { + "$$exists": false + }, + "server.address": { + "$$type": "string" + }, + "server.port": { + "$$type": [ + "int", + "long" + ] + }, + "db.query.summary": "aggregate operation-aggregate.test", + "db.query.text": { + "$$matchAsDocument": { + "$$matchAsRoot": { + "aggregate": "test", + "pipeline": [ + { + "$match": { + "_id": 1 + } + } + ] + } + } + }, + "db.mongodb.server_connection_id": { + "$$type": [ + "int", + "long" + ] + }, + "db.mongodb.driver_connection_id": { + "$$type": [ + "int", + "long" + ] + } + } + } + ] + } + ] + } + ] + } + ] +} diff --git a/src/test/spec/json/open-telemetry/operation/aggregate.yml b/src/test/spec/json/open-telemetry/operation/aggregate.yml new file mode 100644 index 000000000..5f78a7ed5 --- /dev/null +++ b/src/test/spec/json/open-telemetry/operation/aggregate.yml @@ -0,0 +1,62 @@ +description: operation aggregate +schemaVersion: '1.27' +createEntities: + - client: + id: &client0 client0 + useMultipleMongoses: false + observeTracingMessages: + enableCommandPayload: true + - database: + id: &database0 database0 + client: *client0 + databaseName: operation-aggregate + - collection: + id: &collection0 collection0 + database: *database0 + collectionName: &collectionName0 test + +tests: + - description: aggregation + operations: + - name: aggregate + object: *collection0 + arguments: + pipeline: &pipeline0 + - $match: { _id: 1 } + + expectTracingMessages: + - client: *client0 + ignoreExtraSpans: false + spans: + - name: aggregate operation-aggregate.test + attributes: + db.system: mongodb + db.namespace: operation-aggregate + db.collection.name: test + db.operation.name: aggregate + db.operation.summary: aggregate operation-aggregate.test + nested: + - name: aggregate + attributes: + db.system: mongodb + db.namespace: operation-aggregate + db.collection.name: *collectionName0 + db.command.name: aggregate + network.transport: tcp + db.mongodb.cursor_id: { $$exists: false } + db.response.status_code: { $$exists: false } + exception.message: { $$exists: false } + exception.type: { $$exists: false } + exception.stacktrace: { $$exists: false } + server.address: { $$type: string } + server.port: { $$type: [int, long] } + db.query.summary: aggregate operation-aggregate.test + db.query.text: + $$matchAsDocument: + $$matchAsRoot: + aggregate: test + pipeline: *pipeline0 + db.mongodb.server_connection_id: + $$type: [ int, long ] + db.mongodb.driver_connection_id: + $$type: [ int, long ] diff --git a/src/test/spec/json/open-telemetry/operation/atlas_search.json b/src/test/spec/json/open-telemetry/operation/atlas_search.json new file mode 100644 index 000000000..820f2f507 --- /dev/null +++ b/src/test/spec/json/open-telemetry/operation/atlas_search.json @@ -0,0 +1,239 @@ +{ + "description": "operation atlas_search", + "schemaVersion": "1.27", + "createEntities": [ + { + "client": { + "id": "client0", + "useMultipleMongoses": false, + "observeTracingMessages": { + "enableCommandPayload": true + } + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "operation-atlas-search" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "test" + } + } + ], + "runOnRequirements": [ + { + "minServerVersion": "7.0.5", + "maxServerVersion": "7.0.99", + "topologies": [ + "replicaset", + "load-balanced", + "sharded" + ], + "serverless": "forbid" + }, + { + "minServerVersion": "7.2.0", + "topologies": [ + "replicaset", + "load-balanced", + "sharded" + ], + "serverless": "forbid" + } + ], + "tests": [ + { + "description": "atlas search indexes", + "operations": [ + { + "name": "createSearchIndex", + "object": "collection0", + "arguments": { + "model": { + "definition": { + "mappings": { + "dynamic": true + } + }, + "type": "search" + } + }, + "expectError": { + "isError": true, + "errorContains": "Atlas" + } + }, + { + "name": "updateSearchIndex", + "object": "collection0", + "arguments": { + "name": "test index", + "definition": {} + }, + "expectError": { + "isError": true, + "errorContains": "Atlas" + } + }, + { + "name": "dropSearchIndex", + "object": "collection0", + "arguments": { + "name": "test index" + }, + "expectError": { + "isError": true, + "errorContains": "Atlas" + } + } + ], + "expectTracingMessages": [ + { + "client": "client0", + "ignoreExtraSpans": false, + "spans": [ + { + "name": "createSearchIndexes operation-atlas-search.test", + "attributes": { + "db.system": "mongodb", + "db.namespace": "operation-atlas-search", + "db.collection.name": "test", + "db.operation.name": "createSearchIndexes", + "db.operation.summary": "createSearchIndexes operation-atlas-search.test" + }, + "nested": [ + { + "name": "createSearchIndexes", + "attributes": { + "db.system": "mongodb", + "db.namespace": "operation-atlas-search", + "db.collection.name": "test", + "server.address": { + "$$type": "string" + }, + "server.port": { + "$$type": [ + "long", + "string" + ] + }, + "server.type": { + "$$type": "string" + }, + "db.query.summary": "createSearchIndexes operation-atlas-search.test", + "db.query.text": { + "$$matchAsDocument": { + "$$matchAsRoot": { + "createSearchIndexes": "test", + "indexes": [ + { + "type": "search", + "definition": { + "mappings": { + "dynamic": true + } + } + } + ] + } + } + } + } + } + ] + }, + { + "name": "updateSearchIndex operation-atlas-search.test", + "attributes": { + "db.system": "mongodb", + "db.namespace": "operation-atlas-search", + "db.collection.name": "test", + "db.operation.name": "updateSearchIndex", + "db.operation.summary": "updateSearchIndex operation-atlas-search.test" + }, + "nested": [ + { + "name": "updateSearchIndex", + "attributes": { + "db.system": "mongodb", + "db.namespace": "operation-atlas-search", + "db.collection.name": "test", + "server.address": { + "$$type": "string" + }, + "server.port": { + "$$type": [ + "long", + "string" + ] + }, + "server.type": { + "$$type": "string" + }, + "db.query.summary": "updateSearchIndex operation-atlas-search.test", + "db.query.text": { + "$$matchAsDocument": { + "$$matchAsRoot": { + "updateSearchIndex": "test", + "name": "test index", + "definition": {} + } + } + } + } + } + ] + }, + { + "name": "dropSearchIndex operation-atlas-search.test", + "attributes": { + "db.system": "mongodb", + "db.namespace": "operation-atlas-search", + "db.collection.name": "test", + "db.operation.name": "dropSearchIndex", + "db.operation.summary": "dropSearchIndex operation-atlas-search.test" + }, + "nested": [ + { + "name": "dropSearchIndex", + "attributes": { + "db.system": "mongodb", + "db.namespace": "operation-atlas-search", + "db.collection.name": "test", + "server.address": { + "$$type": "string" + }, + "server.port": { + "$$type": [ + "long", + "string" + ] + }, + "server.type": { + "$$type": "string" + }, + "db.query.summary": "dropSearchIndex operation-atlas-search.test", + "db.query.text": { + "$$matchAsDocument": { + "$$matchAsRoot": { + "dropSearchIndex": "test", + "name": "test index" + } + } + } + } + } + ] + } + ] + } + ] + } + ] +} diff --git a/src/test/spec/json/open-telemetry/operation/atlas_search.yml b/src/test/spec/json/open-telemetry/operation/atlas_search.yml new file mode 100644 index 000000000..9800d9be3 --- /dev/null +++ b/src/test/spec/json/open-telemetry/operation/atlas_search.yml @@ -0,0 +1,139 @@ +description: operation atlas_search +schemaVersion: '1.27' +createEntities: + - client: + id: &client0 client0 + useMultipleMongoses: false + observeTracingMessages: + enableCommandPayload: true + - database: + id: &database0 database0 + client: *client0 + databaseName: operation-atlas-search + - collection: + id: &collection0 collection0 + database: *database0 + collectionName: test + +runOnRequirements: + # Skip server versions without fix of SERVER-83107 to avoid error message "BSON field 'createSearchIndexes.indexes.type' is an unknown field." + # SERVER-83107 was not backported to 7.1. + - minServerVersion: "7.0.5" + maxServerVersion: "7.0.99" + topologies: [ replicaset, load-balanced, sharded ] + serverless: forbid + - minServerVersion: "7.2.0" + topologies: [ replicaset, load-balanced, sharded ] + serverless: forbid + + +tests: + - description: atlas search indexes + operations: + - name: createSearchIndex + object: *collection0 + arguments: + model: { definition: { mappings: { dynamic: true } } , type: 'search' } + expectError: + # This test always errors in a non-Atlas environment. The test functions as a unit test by asserting + # that the driver constructs and sends the correct command. + # The expected error message was changed in SERVER-83003. Check for the substring "Atlas" shared by both error messages. + isError: true + errorContains: Atlas + + - name: updateSearchIndex + object: *collection0 + arguments: + name: 'test index' + definition: {} + expectError: + # This test always errors in a non-Atlas environment. The test functions as a unit test by asserting + # that the driver constructs and sends the correct command. + # The expected error message was changed in SERVER-83003. Check for the substring "Atlas" shared by both error messages. + isError: true + errorContains: Atlas + + - name: dropSearchIndex + object: *collection0 + arguments: + name: 'test index' + expectError: + # This test always errors in a non-Atlas environment. The test functions as a unit test by asserting + # that the driver constructs and sends the correct command. + # The expected error message was changed in SERVER-83003. Check for the substring "Atlas" shared by both error messages. + isError: true + errorContains: Atlas + + expectTracingMessages: + - client: *client0 + ignoreExtraSpans: false + spans: + - name: createSearchIndexes operation-atlas-search.test + attributes: + db.system: mongodb + db.namespace: operation-atlas-search + db.collection.name: test + db.operation.name: createSearchIndexes + db.operation.summary: createSearchIndexes operation-atlas-search.test + nested: + - name: createSearchIndexes + attributes: + db.system: mongodb + db.namespace: operation-atlas-search + db.collection.name: test + server.address: { $$type: string } + server.port: { $$type: [ long, string ] } + server.type: { $$type: string } + db.query.summary: createSearchIndexes operation-atlas-search.test + db.query.text: + $$matchAsDocument: + $$matchAsRoot: + createSearchIndexes: test + indexes: [ { "type": "search", "definition": { "mappings": { "dynamic": true } } } ] + + - name: updateSearchIndex operation-atlas-search.test + attributes: + db.system: mongodb + db.namespace: operation-atlas-search + db.collection.name: test + db.operation.name: updateSearchIndex + db.operation.summary: updateSearchIndex operation-atlas-search.test + nested: + - name: updateSearchIndex + attributes: + db.system: mongodb + db.namespace: operation-atlas-search + db.collection.name: test + server.address: { $$type: string } + server.port: { $$type: [ long, string ] } + server.type: { $$type: string } + db.query.summary: updateSearchIndex operation-atlas-search.test + db.query.text: + $$matchAsDocument: + $$matchAsRoot: + updateSearchIndex: test + name: test index + definition: {} + + - name: dropSearchIndex operation-atlas-search.test + attributes: + db.system: mongodb + db.namespace: operation-atlas-search + db.collection.name: test + db.operation.name: dropSearchIndex + db.operation.summary: dropSearchIndex operation-atlas-search.test + nested: + - name: dropSearchIndex + attributes: + db.system: mongodb + db.namespace: operation-atlas-search + db.collection.name: test + server.address: { $$type: string } + server.port: { $$type: [ long, string ] } + server.type: { $$type: string } + db.query.summary: dropSearchIndex operation-atlas-search.test + db.query.text: + $$matchAsDocument: + $$matchAsRoot: + dropSearchIndex: test + name: test index diff --git a/src/test/spec/json/open-telemetry/operation/bulk_write.json b/src/test/spec/json/open-telemetry/operation/bulk_write.json new file mode 100644 index 000000000..bf9d4d9b6 --- /dev/null +++ b/src/test/spec/json/open-telemetry/operation/bulk_write.json @@ -0,0 +1,336 @@ +{ + "description": "operation bulk_write", + "schemaVersion": "1.27", + "runOnRequirements": [ + { + "minServerVersion": "8.0", + "serverless": "forbid" + } + ], + "createEntities": [ + { + "client": { + "id": "client0", + "useMultipleMongoses": false, + "observeTracingMessages": { + "enableCommandPayload": true + } + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "operation-bulk-write-0" + } + }, + { + "database": { + "id": "database1", + "client": "client0", + "databaseName": "operation-bulk-write-1" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "test0" + } + }, + { + "collection": { + "id": "collection1", + "database": "database1", + "collectionName": "test1" + } + } + ], + "initialData": [ + { + "collectionName": "test0", + "databaseName": "operation-bulk-write-0", + "documents": [] + }, + { + "collectionName": "test1", + "databaseName": "operation-bulk-write-1", + "documents": [] + } + ], + "_yamlAnchors": { + "namespace0": "operation-bulk-write-0.test0", + "namespace1": "operation-bulk-write-1.test1" + }, + "tests": [ + { + "description": "bulkWrite", + "operations": [ + { + "object": "client0", + "name": "clientBulkWrite", + "arguments": { + "models": [ + { + "insertOne": { + "namespace": "operation-bulk-write-0.test0", + "document": { + "_id": 8, + "x": 88 + } + } + }, + { + "updateOne": { + "namespace": "operation-bulk-write-0.test0", + "filter": { + "_id": 1 + }, + "update": { + "$inc": { + "x": 1 + } + } + } + }, + { + "updateMany": { + "namespace": "operation-bulk-write-1.test1", + "filter": { + "$and": [ + { + "_id": { + "$gt": 1 + } + }, + { + "_id": { + "$lte": 3 + } + } + ] + }, + "update": { + "$inc": { + "x": 2 + } + } + } + }, + { + "replaceOne": { + "namespace": "operation-bulk-write-1.test1", + "filter": { + "_id": 4 + }, + "replacement": { + "x": 44 + }, + "upsert": true + } + }, + { + "deleteOne": { + "namespace": "operation-bulk-write-0.test0", + "filter": { + "_id": 5 + } + } + }, + { + "deleteMany": { + "namespace": "operation-bulk-write-1.test1", + "filter": { + "$and": [ + { + "_id": { + "$gt": 5 + } + }, + { + "_id": { + "$lte": 7 + } + } + ] + } + } + } + ] + } + } + ], + "expectTracingMessages": [ + { + "client": "client0", + "ignoreExtraSpans": false, + "spans": [ + { + "name": "bulkWrite admin", + "attributes": { + "db.system": "mongodb", + "db.namespace": "admin", + "db.collection.name": { + "$$exists": false + }, + "db.operation.name": "bulkWrite", + "db.operation.summary": "bulkWrite admin" + }, + "nested": [ + { + "name": "bulkWrite", + "attributes": { + "db.system": "mongodb", + "db.namespace": "admin", + "db.collection.name": { + "$$exists": false + }, + "db.command.name": "bulkWrite", + "network.transport": "tcp", + "db.mongodb.cursor_id": { + "$$exists": false + }, + "db.response.status_code": { + "$$exists": false + }, + "exception.message": { + "$$exists": false + }, + "exception.type": { + "$$exists": false + }, + "exception.stacktrace": { + "$$exists": false + }, + "server.address": { + "$$type": "string" + }, + "server.port": { + "$$type": [ + "int", + "long" + ] + }, + "server.type": { + "$$type": "string" + }, + "db.query.summary": "bulkWrite admin", + "db.query.text": { + "$$matchAsDocument": { + "$$matchAsRoot": { + "bulkWrite": 1, + "errorsOnly": true, + "ordered": true, + "ops": [ + { + "insert": 0, + "document": { + "_id": 8, + "x": 88 + } + }, + { + "update": 0, + "multi": false, + "filter": { + "_id": 1 + }, + "updateMods": { + "$inc": { + "x": 1 + } + } + }, + { + "update": 1, + "multi": true, + "filter": { + "$and": [ + { + "_id": { + "$gt": 1 + } + }, + { + "_id": { + "$lte": 3 + } + } + ] + }, + "updateMods": { + "$inc": { + "x": 2 + } + } + }, + { + "update": 1, + "multi": false, + "filter": { + "_id": 4 + }, + "updateMods": { + "x": 44 + }, + "upsert": true + }, + { + "delete": 0, + "multi": false, + "filter": { + "_id": 5 + } + }, + { + "delete": 1, + "multi": true, + "filter": { + "$and": [ + { + "_id": { + "$gt": 5 + } + }, + { + "_id": { + "$lte": 7 + } + } + ] + } + } + ], + "nsInfo": [ + { + "ns": "operation-bulk-write-0.test0" + }, + { + "ns": "operation-bulk-write-1.test1" + } + ] + } + } + }, + "db.mongodb.server_connection_id": { + "$$type": [ + "int", + "long" + ] + }, + "db.mongodb.driver_connection_id": { + "$$type": [ + "int", + "long" + ] + } + } + } + ] + } + ] + } + ] + } + ] +} diff --git a/src/test/spec/json/open-telemetry/operation/bulk_write.yml b/src/test/spec/json/open-telemetry/operation/bulk_write.yml new file mode 100644 index 000000000..54f3ba2b7 --- /dev/null +++ b/src/test/spec/json/open-telemetry/operation/bulk_write.yml @@ -0,0 +1,199 @@ +description: operation bulk_write +schemaVersion: '1.27' +runOnRequirements: + - minServerVersion: "8.0" + serverless: forbid + +createEntities: + - client: + id: &client0 client0 + useMultipleMongoses: false + observeTracingMessages: + enableCommandPayload: true + - database: + id: &database0 database0 + client: *client0 + databaseName: &databaseName0 operation-bulk-write-0 + - database: + id: &database1 database1 + client: *client0 + databaseName: &databaseName1 operation-bulk-write-1 + - collection: + id: collection0 + database: *database0 + collectionName: &collectionName0 test0 + - collection: + id: collection1 + database: *database1 + collectionName: &collectionName1 test1 + +initialData: + - collectionName: *collectionName0 + databaseName: *databaseName0 + documents: [ ] + - collectionName: *collectionName1 + databaseName: *databaseName1 + documents: [ ] + +_yamlAnchors: + namespace0: &namespace0 "operation-bulk-write-0.test0" + namespace1: &namespace1 "operation-bulk-write-1.test1" + +tests: + - description: bulkWrite + operations: + - object: *client0 + name: clientBulkWrite + arguments: + models: + - insertOne: + namespace: *namespace0 + document: { _id: 8, x: 88 } + - updateOne: + namespace: *namespace0 + filter: { _id: 1 } + update: { $inc: { x: 1 } } + - updateMany: + namespace: *namespace1 + filter: + $and: [ { _id: { $gt: 1 } }, { _id: { $lte: 3 } } ] + update: { $inc: { x: 2 } } + - replaceOne: + namespace: *namespace1 + filter: { _id: 4 } + replacement: { x: 44 } + upsert: true + - deleteOne: + namespace: *namespace0 + filter: { _id: 5 } + - deleteMany: + namespace: *namespace1 + filter: + $and: [ { _id: { $gt: 5 } }, { _id: { $lte: 7 } } ] + + expectTracingMessages: + - client: *client0 + ignoreExtraSpans: false + spans: + - name: bulkWrite admin + attributes: + db.system: mongodb + db.namespace: admin + db.collection.name: { $$exists: false } + db.operation.name: bulkWrite + db.operation.summary: bulkWrite admin + nested: + - name: bulkWrite + attributes: + db.system: mongodb + db.namespace: admin + db.collection.name: { $$exists: false } + db.command.name: bulkWrite + network.transport: tcp + db.mongodb.cursor_id: { $$exists: false } + db.response.status_code: { $$exists: false } + exception.message: { $$exists: false } + exception.type: { $$exists: false } + exception.stacktrace: { $$exists: false } + server.address: { $$type: string } + server.port: { $$type: [ int, long ] } + server.type: { $$type: string } + db.query.summary: bulkWrite admin + db.query.text: + $$matchAsDocument: + $$matchAsRoot: + bulkWrite: 1 + errorsOnly: true + ordered: true + ops: [ + { + "insert": 0, + "document": { + "_id": 8, + "x": 88 + } + }, + { + "update": 0, + "multi": false, + "filter": { + "_id": 1 + }, + "updateMods": { + "$inc": { + "x": 1 + } + } + }, + { + "update": 1, + "multi": true, + "filter": { + "$and": [ + { + "_id": { + "$gt": 1 + } + }, + { + "_id": { + "$lte": 3 + } + } + ] + }, + "updateMods": { + "$inc": { + "x": 2 + } + } + }, + { + "update": 1, + "multi": false, + "filter": { + "_id": 4 + }, + "updateMods": { + "x": 44 + }, + "upsert": true + }, + { + "delete": 0, + "multi": false, + "filter": { + "_id": 5 + } + }, + { + "delete": 1, + "multi": true, + "filter": { + "$and": [ + { + "_id": { + "$gt": 5 + } + }, + { + "_id": { + "$lte": 7 + } + } + ] + } + } + ] + nsInfo: [ + { + "ns": *namespace0 + }, + { + "ns": *namespace1 + } + ] + db.mongodb.server_connection_id: + $$type: [ int, long ] + db.mongodb.driver_connection_id: + $$type: [ int, long ] diff --git a/src/test/spec/json/open-telemetry/operation/count.json b/src/test/spec/json/open-telemetry/operation/count.json new file mode 100644 index 000000000..6b4304dfd --- /dev/null +++ b/src/test/spec/json/open-telemetry/operation/count.json @@ -0,0 +1,123 @@ +{ + "description": "operation count", + "schemaVersion": "1.27", + "createEntities": [ + { + "client": { + "id": "client0", + "useMultipleMongoses": false, + "observeTracingMessages": { + "enableCommandPayload": true + } + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "operation-count" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "test" + } + } + ], + "initialData": [ + { + "collectionName": "test", + "databaseName": "operation-count", + "documents": [] + } + ], + "tests": [ + { + "description": "estimated document count", + "operations": [ + { + "object": "collection0", + "name": "estimatedDocumentCount", + "arguments": {}, + "expectResult": 0 + } + ], + "expectTracingMessages": [ + { + "client": "client0", + "ignoreExtraSpans": false, + "spans": [ + { + "name": "count operation-count.test", + "attributes": { + "db.system": "mongodb", + "db.namespace": "operation-count", + "db.collection.name": "test", + "db.operation.name": "count", + "db.operation.summary": "count operation-count.test" + }, + "nested": [ + { + "name": "count", + "attributes": { + "db.system": "mongodb", + "db.namespace": "operation-count", + "db.collection.name": "test", + "db.command.name": "count", + "network.transport": "tcp", + "db.mongodb.cursor_id": { + "$$exists": false + }, + "db.response.status_code": { + "$$exists": false + }, + "exception.message": { + "$$exists": false + }, + "exception.type": { + "$$exists": false + }, + "exception.stacktrace": { + "$$exists": false + }, + "server.address": { + "$$type": "string" + }, + "server.port": { + "$$type": [ + "int", + "long" + ] + }, + "db.query.summary": "count operation-count.test", + "db.query.text": { + "$$matchAsDocument": { + "$$matchAsRoot": { + "count": "test" + } + } + }, + "db.mongodb.server_connection_id": { + "$$type": [ + "int", + "long" + ] + }, + "db.mongodb.driver_connection_id": { + "$$type": [ + "int", + "long" + ] + } + } + } + ] + } + ] + } + ] + } + ] +} diff --git a/src/test/spec/json/open-telemetry/operation/count.yml b/src/test/spec/json/open-telemetry/operation/count.yml new file mode 100644 index 000000000..3d2abcb04 --- /dev/null +++ b/src/test/spec/json/open-telemetry/operation/count.yml @@ -0,0 +1,63 @@ +description: operation count +schemaVersion: '1.27' +createEntities: + - client: + id: &client0 client0 + useMultipleMongoses: false + observeTracingMessages: + enableCommandPayload: true + - database: + id: database0 + client: *client0 + databaseName: &database0Name operation-count + - collection: + id: &collection0 collection0 + database: database0 + collectionName: &collection0Name test +initialData: + - collectionName: *collection0Name + databaseName: *database0Name + documents: [] +tests: + - description: estimated document count + operations: + - object: *collection0 + name: estimatedDocumentCount + arguments: { } + expectResult: 0 + + expectTracingMessages: + - client: *client0 + ignoreExtraSpans: false + spans: + - name: count operation-count.test + attributes: + db.system: mongodb + db.namespace: *database0Name + db.collection.name: *collection0Name + db.operation.name: count + db.operation.summary: count operation-count.test + nested: + - name: count + attributes: + db.system: mongodb + db.namespace: *database0Name + db.collection.name: *collection0Name + db.command.name: count + network.transport: tcp + db.mongodb.cursor_id: { $$exists: false } + db.response.status_code: { $$exists: false } + exception.message: { $$exists: false } + exception.type: { $$exists: false } + exception.stacktrace: { $$exists: false } + server.address: { $$type: string } + server.port: { $$type: [int, long] } + db.query.summary: count operation-count.test + db.query.text: + $$matchAsDocument: + $$matchAsRoot: + count: test + db.mongodb.server_connection_id: + $$type: [ int, long ] + db.mongodb.driver_connection_id: + $$type: [ int, long ] diff --git a/src/test/spec/json/open-telemetry/operation/create_collection.json b/src/test/spec/json/open-telemetry/operation/create_collection.json new file mode 100644 index 000000000..fc296b4fd --- /dev/null +++ b/src/test/spec/json/open-telemetry/operation/create_collection.json @@ -0,0 +1,110 @@ +{ + "description": "operation create collection", + "schemaVersion": "1.27", + "createEntities": [ + { + "client": { + "id": "client0", + "useMultipleMongoses": false, + "observeTracingMessages": { + "enableCommandPayload": true + } + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "operation-create-collection" + } + } + ], + "tests": [ + { + "description": "create collection", + "operations": [ + { + "object": "database0", + "name": "createCollection", + "arguments": { + "collection": "newlyCreatedCollection" + } + } + ], + "expectTracingMessages": [ + { + "client": "client0", + "ignoreExtraSpans": false, + "spans": [ + { + "name": "createCollection operation-create-collection.newlyCreatedCollection", + "attributes": { + "db.system": "mongodb", + "db.namespace": "operation-create-collection", + "db.collection.name": "newlyCreatedCollection", + "db.operation.name": "createCollection", + "db.operation.summary": "createCollection operation-create-collection.newlyCreatedCollection" + }, + "nested": [ + { + "name": "create", + "attributes": { + "db.system": "mongodb", + "db.namespace": "operation-create-collection", + "db.collection.name": "newlyCreatedCollection", + "db.command.name": "create", + "network.transport": "tcp", + "db.mongodb.cursor_id": { + "$$exists": false + }, + "db.response.status_code": { + "$$exists": false + }, + "exception.message": { + "$$exists": false + }, + "exception.type": { + "$$exists": false + }, + "exception.stacktrace": { + "$$exists": false + }, + "server.address": { + "$$type": "string" + }, + "server.port": { + "$$type": [ + "int", + "long" + ] + }, + "db.query.summary": "create operation-create-collection.newlyCreatedCollection", + "db.query.text": { + "$$matchAsDocument": { + "$$matchAsRoot": { + "create": "newlyCreatedCollection" + } + } + }, + "db.mongodb.server_connection_id": { + "$$type": [ + "int", + "long" + ] + }, + "db.mongodb.driver_connection_id": { + "$$type": [ + "int", + "long" + ] + } + } + } + ] + } + ] + } + ] + } + ] +} diff --git a/src/test/spec/json/open-telemetry/operation/create_collection.yml b/src/test/spec/json/open-telemetry/operation/create_collection.yml new file mode 100644 index 000000000..d0dee3605 --- /dev/null +++ b/src/test/spec/json/open-telemetry/operation/create_collection.yml @@ -0,0 +1,55 @@ +description: operation create collection +schemaVersion: '1.27' +createEntities: + - client: + id: &client0 client0 + useMultipleMongoses: false + observeTracingMessages: + enableCommandPayload: true + - database: + id: &database0 database0 + client: *client0 + databaseName: &database0Name operation-create-collection +tests: + - description: create collection + operations: + - object: *database0 + name: createCollection + arguments: + collection: &collectionName newlyCreatedCollection + + expectTracingMessages: + - client: *client0 + ignoreExtraSpans: false + spans: + - name: createCollection operation-create-collection.newlyCreatedCollection + attributes: + db.system: mongodb + db.namespace: *database0Name + db.collection.name: *collectionName + db.operation.name: createCollection + db.operation.summary: createCollection operation-create-collection.newlyCreatedCollection + nested: + - name: create + attributes: + db.system: mongodb + db.namespace: *database0Name + db.collection.name: *collectionName + db.command.name: create + network.transport: tcp + db.mongodb.cursor_id: { $$exists: false } + db.response.status_code: { $$exists: false } + exception.message: { $$exists: false } + exception.type: { $$exists: false } + exception.stacktrace: { $$exists: false } + server.address: { $$type: string } + server.port: { $$type: [int, long] } + db.query.summary: create operation-create-collection.newlyCreatedCollection + db.query.text: + $$matchAsDocument: + $$matchAsRoot: + create: newlyCreatedCollection + db.mongodb.server_connection_id: + $$type: [ int, long ] + db.mongodb.driver_connection_id: + $$type: [ int, long ] diff --git a/src/test/spec/json/open-telemetry/operation/create_indexes.json b/src/test/spec/json/open-telemetry/operation/create_indexes.json new file mode 100644 index 000000000..40afac514 --- /dev/null +++ b/src/test/spec/json/open-telemetry/operation/create_indexes.json @@ -0,0 +1,127 @@ +{ + "description": "operation create_indexes", + "schemaVersion": "1.27", + "createEntities": [ + { + "client": { + "id": "client0", + "useMultipleMongoses": false, + "observeTracingMessages": { + "enableCommandPayload": true + } + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "operation-create-indexes" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "test" + } + } + ], + "tests": [ + { + "description": "create indexes", + "operations": [ + { + "object": "collection0", + "name": "createIndex", + "arguments": { + "keys": { + "x": 1 + } + } + } + ], + "expectTracingMessages": [ + { + "client": "client0", + "ignoreExtraSpans": false, + "spans": [ + { + "name": "createIndexes operation-create-indexes.test", + "attributes": { + "db.system": "mongodb", + "db.namespace": "operation-create-indexes", + "db.collection.name": "test", + "db.operation.name": "createIndexes", + "db.operation.summary": "createIndexes operation-create-indexes.test" + }, + "nested": [ + { + "name": "createIndexes", + "attributes": { + "db.system": "mongodb", + "db.namespace": "operation-create-indexes", + "db.collection.name": "test", + "db.command.name": "createIndexes", + "network.transport": "tcp", + "db.mongodb.cursor_id": { + "$$exists": false + }, + "db.response.status_code": { + "$$exists": false + }, + "exception.message": { + "$$exists": false + }, + "exception.type": { + "$$exists": false + }, + "exception.stacktrace": { + "$$exists": false + }, + "server.address": { + "$$type": "string" + }, + "server.port": { + "$$type": [ + "int", + "long" + ] + }, + "db.query.summary": "createIndexes operation-create-indexes.test", + "db.query.text": { + "$$matchAsDocument": { + "$$matchAsRoot": { + "createIndexes": "test", + "indexes": [ + { + "key": { + "x": 1 + }, + "name": "x_1" + } + ] + } + } + }, + "db.mongodb.server_connection_id": { + "$$type": [ + "int", + "long" + ] + }, + "db.mongodb.driver_connection_id": { + "$$type": [ + "int", + "long" + ] + } + } + } + ] + } + ] + } + ] + } + ] +} diff --git a/src/test/spec/json/open-telemetry/operation/create_indexes.yml b/src/test/spec/json/open-telemetry/operation/create_indexes.yml new file mode 100644 index 000000000..01e23e242 --- /dev/null +++ b/src/test/spec/json/open-telemetry/operation/create_indexes.yml @@ -0,0 +1,60 @@ +description: operation create_indexes +schemaVersion: '1.27' +createEntities: + - client: + id: &client0 client0 + useMultipleMongoses: false + observeTracingMessages: + enableCommandPayload: true + - database: + id: &database0 database0 + client: *client0 + databaseName: &database0Name operation-create-indexes + - collection: + id: &collection0 collection0 + database: *database0 + collectionName: &collection0Name test +tests: + - description: create indexes + operations: + - object: *collection0 + name: createIndex + arguments: + keys: { x: 1 } + + expectTracingMessages: + - client: *client0 + ignoreExtraSpans: false + spans: + - name: createIndexes operation-create-indexes.test + attributes: + db.system: mongodb + db.namespace: *database0Name + db.collection.name: *collection0Name + db.operation.name: createIndexes + db.operation.summary: createIndexes operation-create-indexes.test + nested: + - name: createIndexes + attributes: + db.system: mongodb + db.namespace: *database0Name + db.collection.name: *collection0Name + db.command.name: createIndexes + network.transport: tcp + db.mongodb.cursor_id: { $$exists: false } + db.response.status_code: { $$exists: false } + exception.message: { $$exists: false } + exception.type: { $$exists: false } + exception.stacktrace: { $$exists: false } + server.address: { $$type: string } + server.port: { $$type: [int, long] } + db.query.summary: createIndexes operation-create-indexes.test + db.query.text: + $$matchAsDocument: + $$matchAsRoot: + createIndexes: test + indexes: [ { key: { x: 1 }, name: "x_1" } ] + db.mongodb.server_connection_id: + $$type: [ int, long ] + db.mongodb.driver_connection_id: + $$type: [ int, long ] diff --git a/src/test/spec/json/open-telemetry/operation/delete.json b/src/test/spec/json/open-telemetry/operation/delete.json new file mode 100644 index 000000000..3dec5c32a --- /dev/null +++ b/src/test/spec/json/open-telemetry/operation/delete.json @@ -0,0 +1,103 @@ +{ + "description": "operation delete", + "schemaVersion": "1.27", + "createEntities": [ + { + "client": { + "id": "client0", + "useMultipleMongoses": false, + "observeTracingMessages": { + "enableCommandPayload": true + } + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "operation-delete" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "test" + } + } + ], + "tests": [ + { + "description": "delete elements", + "operations": [ + { + "object": "collection0", + "name": "deleteMany", + "arguments": { + "filter": { + "_id": { + "$gt": 1 + } + } + } + } + ], + "expectTracingMessages": [ + { + "client": "client0", + "ignoreExtraSpans": false, + "spans": [ + { + "name": "delete operation-delete.test", + "attributes": { + "db.system": "mongodb", + "db.namespace": "operation-delete", + "db.collection.name": "test", + "db.operation.name": "delete", + "db.operation.summary": "delete operation-delete.test" + }, + "nested": [ + { + "name": "delete", + "attributes": { + "db.system": "mongodb", + "db.namespace": "operation-delete", + "db.collection.name": "test", + "server.address": { + "$$type": "string" + }, + "server.port": { + "$$type": [ + "long", + "string" + ] + }, + "db.query.summary": "delete operation-delete.test", + "db.query.text": { + "$$matchAsDocument": { + "$$matchAsRoot": { + "delete": "test", + "ordered": true, + "deletes": [ + { + "q": { + "_id": { + "$gt": 1 + } + }, + "limit": 0 + } + ] + } + } + } + } + } + ] + } + ] + } + ] + } + ] +} diff --git a/src/test/spec/json/open-telemetry/operation/delete.yml b/src/test/spec/json/open-telemetry/operation/delete.yml new file mode 100644 index 000000000..eb9b86668 --- /dev/null +++ b/src/test/spec/json/open-telemetry/operation/delete.yml @@ -0,0 +1,51 @@ +description: operation delete +schemaVersion: '1.27' +createEntities: + - client: + id: &client0 client0 + useMultipleMongoses: false + observeTracingMessages: + enableCommandPayload: true + - database: + id: &database0 database0 + client: *client0 + databaseName: &databaseName0 operation-delete + - collection: + id: &collection0 collection0 + database: *database0 + collectionName: &collectionName0 test + +tests: + - description: delete elements + operations: + - object: *collection0 + name: deleteMany + arguments: + filter: { _id: { $gt: 1 } } + + expectTracingMessages: + - client: *client0 + ignoreExtraSpans: false + spans: + - name: delete operation-delete.test + attributes: + db.system: mongodb + db.namespace: *databaseName0 + db.collection.name: *collectionName0 + db.operation.name: delete + db.operation.summary: delete operation-delete.test + nested: + - name: delete + attributes: + db.system: mongodb + db.namespace: operation-delete + db.collection.name: test + server.address: { $$type: string } + server.port: { $$type: [ long, string ] } + db.query.summary: delete operation-delete.test + db.query.text: + $$matchAsDocument: + $$matchAsRoot: + delete: test + ordered: true + deletes: [ { q: { _id: { $gt: 1 } }, limit: 0 } ] diff --git a/src/test/spec/json/open-telemetry/operation/distinct.json b/src/test/spec/json/open-telemetry/operation/distinct.json new file mode 100644 index 000000000..8478d569f --- /dev/null +++ b/src/test/spec/json/open-telemetry/operation/distinct.json @@ -0,0 +1,127 @@ +{ + "description": "operation distinct", + "schemaVersion": "1.27", + "createEntities": [ + { + "client": { + "id": "client0", + "useMultipleMongoses": false, + "observeTracingMessages": { + "enableCommandPayload": true + } + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "operation-distinct" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "test" + } + } + ], + "initialData": [ + { + "collectionName": "test", + "databaseName": "operation-distinct", + "documents": [] + } + ], + "tests": [ + { + "description": "distinct on a field", + "operations": [ + { + "object": "collection0", + "name": "distinct", + "arguments": { + "fieldName": "x", + "filter": {} + } + } + ], + "expectTracingMessages": [ + { + "client": "client0", + "ignoreExtraSpans": false, + "spans": [ + { + "name": "distinct operation-distinct.test", + "attributes": { + "db.system": "mongodb", + "db.namespace": "operation-distinct", + "db.collection.name": "test", + "db.operation.name": "distinct", + "db.operation.summary": "distinct operation-distinct.test" + }, + "nested": [ + { + "name": "distinct", + "attributes": { + "db.system": "mongodb", + "db.namespace": "operation-distinct", + "db.collection.name": "test", + "db.command.name": "distinct", + "network.transport": "tcp", + "db.mongodb.cursor_id": { + "$$exists": false + }, + "db.response.status_code": { + "$$exists": false + }, + "exception.message": { + "$$exists": false + }, + "exception.type": { + "$$exists": false + }, + "exception.stacktrace": { + "$$exists": false + }, + "server.address": { + "$$type": "string" + }, + "server.port": { + "$$type": [ + "int", + "long" + ] + }, + "db.query.summary": "distinct operation-distinct.test", + "db.query.text": { + "$$matchAsDocument": { + "$$matchAsRoot": { + "distinct": "test", + "key": "x", + "query": {} + } + } + }, + "db.mongodb.server_connection_id": { + "$$type": [ + "int", + "long" + ] + }, + "db.mongodb.driver_connection_id": { + "$$type": [ + "int", + "long" + ] + } + } + } + ] + } + ] + } + ] + } + ] +} diff --git a/src/test/spec/json/open-telemetry/operation/distinct.yml b/src/test/spec/json/open-telemetry/operation/distinct.yml new file mode 100644 index 000000000..0b78cecca --- /dev/null +++ b/src/test/spec/json/open-telemetry/operation/distinct.yml @@ -0,0 +1,66 @@ +description: operation distinct +schemaVersion: '1.27' +createEntities: + - client: + id: &client0 client0 + useMultipleMongoses: false + observeTracingMessages: + enableCommandPayload: true + - database: + id: database0 + client: *client0 + databaseName: operation-distinct + - collection: + id: &collection0 collection0 + database: database0 + collectionName: test +initialData: + - collectionName: test + databaseName: operation-distinct + documents: [] +tests: + - description: distinct on a field + operations: + - object: *collection0 + name: distinct + arguments: + fieldName: x + filter: {} + + expectTracingMessages: + - client: *client0 + ignoreExtraSpans: false + spans: + - name: distinct operation-distinct.test + attributes: + db.system: mongodb + db.namespace: operation-distinct + db.collection.name: test + db.operation.name: distinct + db.operation.summary: distinct operation-distinct.test + nested: + - name: distinct + attributes: + db.system: mongodb + db.namespace: operation-distinct + db.collection.name: test + db.command.name: distinct + network.transport: tcp + db.mongodb.cursor_id: { $$exists: false } + db.response.status_code: { $$exists: false } + exception.message: { $$exists: false } + exception.type: { $$exists: false } + exception.stacktrace: { $$exists: false } + server.address: { $$type: string } + server.port: { $$type: [int, long] } + db.query.summary: distinct operation-distinct.test + db.query.text: + $$matchAsDocument: + $$matchAsRoot: + distinct: test + key: x + query: { } + db.mongodb.server_connection_id: + $$type: [ int, long ] + db.mongodb.driver_connection_id: + $$type: [ int, long ] diff --git a/src/test/spec/json/open-telemetry/operation/drop_collection.json b/src/test/spec/json/open-telemetry/operation/drop_collection.json new file mode 100644 index 000000000..ce4ffe686 --- /dev/null +++ b/src/test/spec/json/open-telemetry/operation/drop_collection.json @@ -0,0 +1,117 @@ +{ + "description": "operation drop collection", + "schemaVersion": "1.27", + "createEntities": [ + { + "client": { + "id": "client0", + "useMultipleMongoses": false, + "observeTracingMessages": { + "enableCommandPayload": true + } + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "operation-drop-collection" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "test" + } + } + ], + "tests": [ + { + "description": "drop collection", + "operations": [ + { + "object": "database0", + "name": "dropCollection", + "arguments": { + "collection": "test" + } + } + ], + "expectTracingMessages": [ + { + "client": "client0", + "ignoreExtraSpans": false, + "spans": [ + { + "name": "dropCollection operation-drop-collection.test", + "attributes": { + "db.system": "mongodb", + "db.namespace": "operation-drop-collection", + "db.collection.name": "test", + "db.operation.name": "dropCollection", + "db.operation.summary": "dropCollection operation-drop-collection.test" + }, + "nested": [ + { + "name": "drop", + "attributes": { + "db.system": "mongodb", + "db.namespace": "operation-drop-collection", + "db.collection.name": "test", + "db.command.name": "drop", + "network.transport": "tcp", + "db.mongodb.cursor_id": { + "$$exists": false + }, + "db.response.status_code": { + "$$exists": false + }, + "exception.message": { + "$$exists": false + }, + "exception.type": { + "$$exists": false + }, + "exception.stacktrace": { + "$$exists": false + }, + "server.address": { + "$$type": "string" + }, + "server.port": { + "$$type": [ + "int", + "long" + ] + }, + "db.query.summary": "drop operation-drop-collection.test", + "db.query.text": { + "$$matchAsDocument": { + "$$matchAsRoot": { + "drop": "test" + } + } + }, + "db.mongodb.server_connection_id": { + "$$type": [ + "int", + "long" + ] + }, + "db.mongodb.driver_connection_id": { + "$$type": [ + "int", + "long" + ] + } + } + } + ] + } + ] + } + ] + } + ] +} diff --git a/src/test/spec/json/open-telemetry/operation/drop_collection.yml b/src/test/spec/json/open-telemetry/operation/drop_collection.yml new file mode 100644 index 000000000..26c33715d --- /dev/null +++ b/src/test/spec/json/open-telemetry/operation/drop_collection.yml @@ -0,0 +1,60 @@ +description: operation drop collection +schemaVersion: '1.27' +createEntities: + - client: + id: &client0 client0 + useMultipleMongoses: false + observeTracingMessages: + enableCommandPayload: true + - database: + id: &database0 database0 + client: *client0 + databaseName: operation-drop-collection + + - collection: + id: collection0 + database: *database0 + collectionName: &collection_name test +tests: + - description: drop collection + operations: + - object: *database0 + name: dropCollection + arguments: + collection: *collection_name + + expectTracingMessages: + - client: *client0 + ignoreExtraSpans: false + spans: + - name: dropCollection operation-drop-collection.test + attributes: + db.system: mongodb + db.namespace: operation-drop-collection + db.collection.name: test + db.operation.name: dropCollection + db.operation.summary: dropCollection operation-drop-collection.test + nested: + - name: drop + attributes: + db.system: mongodb + db.namespace: operation-drop-collection + db.collection.name: test + db.command.name: drop + network.transport: tcp + db.mongodb.cursor_id: { $$exists: false } + db.response.status_code: { $$exists: false } + exception.message: { $$exists: false } + exception.type: { $$exists: false } + exception.stacktrace: { $$exists: false } + server.address: { $$type: string } + server.port: { $$type: [int, long] } + db.query.summary: drop operation-drop-collection.test + db.query.text: + $$matchAsDocument: + $$matchAsRoot: + drop: test + db.mongodb.server_connection_id: + $$type: [ int, long ] + db.mongodb.driver_connection_id: + $$type: [ int, long ] diff --git a/src/test/spec/json/open-telemetry/operation/drop_indexes.json b/src/test/spec/json/open-telemetry/operation/drop_indexes.json new file mode 100644 index 000000000..efc865b32 --- /dev/null +++ b/src/test/spec/json/open-telemetry/operation/drop_indexes.json @@ -0,0 +1,145 @@ +{ + "description": "operation drop indexes", + "schemaVersion": "1.27", + "createEntities": [ + { + "client": { + "id": "client0", + "useMultipleMongoses": false, + "observeTracingMessages": { + "enableCommandPayload": true + } + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "operation-drop-indexes" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "test" + } + }, + { + "client": { + "id": "clientWithoutTracing", + "useMultipleMongoses": false + } + }, + { + "database": { + "id": "databaseWithoutTracing", + "client": "clientWithoutTracing", + "databaseName": "operation-drop-indexes" + } + }, + { + "collection": { + "id": "collectionWithoutTracing", + "database": "databaseWithoutTracing", + "collectionName": "test" + } + } + ], + "tests": [ + { + "description": "drop indexes", + "operations": [ + { + "name": "createIndex", + "object": "collectionWithoutTracing", + "arguments": { + "keys": { + "x": 1 + }, + "name": "x_1" + } + }, + { + "name": "dropIndexes", + "object": "collection0" + } + ], + "expectTracingMessages": [ + { + "client": "client0", + "ignoreExtraSpans": true, + "spans": [ + { + "name": "dropIndexes operation-drop-indexes.test", + "attributes": { + "db.system": "mongodb", + "db.namespace": "operation-drop-indexes", + "db.collection.name": "test", + "db.operation.name": "dropIndexes", + "db.operation.summary": "dropIndexes operation-drop-indexes.test" + }, + "nested": [ + { + "name": "dropIndexes", + "attributes": { + "db.system": "mongodb", + "db.namespace": "operation-drop-indexes", + "db.collection.name": "test", + "db.command.name": "dropIndexes", + "network.transport": "tcp", + "db.mongodb.cursor_id": { + "$$exists": false + }, + "db.response.status_code": { + "$$exists": false + }, + "exception.message": { + "$$exists": false + }, + "exception.type": { + "$$exists": false + }, + "exception.stacktrace": { + "$$exists": false + }, + "server.address": { + "$$type": "string" + }, + "server.port": { + "$$type": [ + "int", + "long" + ] + }, + "db.query.summary": "dropIndexes operation-drop-indexes.test", + "db.query.text": { + "$$matchAsDocument": { + "$$matchAsRoot": { + "dropIndexes": "test", + "index": "*" + } + } + }, + "db.mongodb.server_connection_id": { + "$$type": [ + "int", + "long" + ] + }, + "db.mongodb.driver_connection_id": { + "$$type": [ + "int", + "long" + ] + } + } + } + ] + } + ] + } + ] + } + ] +} diff --git a/src/test/spec/json/open-telemetry/operation/drop_indexes.yml b/src/test/spec/json/open-telemetry/operation/drop_indexes.yml new file mode 100644 index 000000000..519995b74 --- /dev/null +++ b/src/test/spec/json/open-telemetry/operation/drop_indexes.yml @@ -0,0 +1,78 @@ +description: operation drop indexes +schemaVersion: '1.27' +createEntities: + - client: + id: &client0 client0 + useMultipleMongoses: false + observeTracingMessages: + enableCommandPayload: true + - database: + id: &database0 database0 + client: *client0 + databaseName: operation-drop-indexes + - collection: + id: &collection0 collection0 + database: *database0 + collectionName: test + - client: + id: &clientWithoutTracing clientWithoutTracing + useMultipleMongoses: false + - database: + id: &databaseWithoutTracing databaseWithoutTracing + client: *clientWithoutTracing + databaseName: operation-drop-indexes + - collection: + id: &collectionWithoutTracing collectionWithoutTracing + database: *databaseWithoutTracing + collectionName: test + +tests: + - description: drop indexes + operations: + - name: createIndex + object: *collectionWithoutTracing + arguments: + keys: + x: 1 + name: x_1 + + - name: dropIndexes + object: *collection0 + + + expectTracingMessages: + - client: *client0 + ignoreExtraSpans: true + spans: + - name: dropIndexes operation-drop-indexes.test + attributes: + db.system: mongodb + db.namespace: operation-drop-indexes + db.collection.name: test + db.operation.name: dropIndexes + db.operation.summary: dropIndexes operation-drop-indexes.test + nested: + - name: dropIndexes + attributes: + db.system: mongodb + db.namespace: operation-drop-indexes + db.collection.name: test + db.command.name: dropIndexes + network.transport: tcp + db.mongodb.cursor_id: { $$exists: false } + db.response.status_code: { $$exists: false } + exception.message: { $$exists: false } + exception.type: { $$exists: false } + exception.stacktrace: { $$exists: false } + server.address: { $$type: string } + server.port: { $$type: [int, long] } + db.query.summary: dropIndexes operation-drop-indexes.test + db.query.text: + $$matchAsDocument: + $$matchAsRoot: + dropIndexes: test + index: '*' + db.mongodb.server_connection_id: + $$type: [ int, long ] + db.mongodb.driver_connection_id: + $$type: [ int, long ] diff --git a/src/test/spec/json/open-telemetry/operation/find.json b/src/test/spec/json/open-telemetry/operation/find.json new file mode 100644 index 000000000..53dfcb1d2 --- /dev/null +++ b/src/test/spec/json/open-telemetry/operation/find.json @@ -0,0 +1,306 @@ +{ + "description": "operation find", + "schemaVersion": "1.27", + "createEntities": [ + { + "client": { + "id": "client0", + "useMultipleMongoses": false, + "observeTracingMessages": { + "enableCommandPayload": true + } + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "operation-find" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "test" + } + } + ], + "initialData": [ + { + "collectionName": "test", + "databaseName": "operation-find", + "documents": [] + } + ], + "tests": [ + { + "description": "find an element", + "operations": [ + { + "name": "find", + "object": "collection0", + "arguments": { + "filter": { + "x": 1 + } + } + } + ], + "expectTracingMessages": [ + { + "client": "client0", + "ignoreExtraSpans": false, + "spans": [ + { + "name": "find operation-find.test", + "attributes": { + "db.system": "mongodb", + "db.namespace": "operation-find", + "db.collection.name": "test", + "db.operation.name": "find", + "db.operation.summary": "find operation-find.test" + }, + "nested": [ + { + "name": "find", + "attributes": { + "db.system": "mongodb", + "db.namespace": "operation-find", + "db.collection.name": "test", + "db.command.name": "find", + "network.transport": "tcp", + "db.mongodb.cursor_id": { + "$$exists": false + }, + "db.response.status_code": { + "$$exists": false + }, + "exception.message": { + "$$exists": false + }, + "exception.type": { + "$$exists": false + }, + "exception.stacktrace": { + "$$exists": false + }, + "server.address": { + "$$type": "string" + }, + "server.port": { + "$$type": [ + "int", + "long" + ] + }, + "server.type": { + "$$type": "string" + }, + "db.query.summary": "find operation-find.test", + "db.query.text": { + "$$matchAsDocument": { + "$$matchAsRoot": { + "find": "test", + "filter": { + "x": 1 + } + } + } + }, + "db.mongodb.server_connection_id": { + "$$type": [ + "int", + "long" + ] + }, + "db.mongodb.driver_connection_id": { + "$$type": [ + "int", + "long" + ] + } + } + } + ] + } + ] + } + ] + }, + { + "description": "find an element retrying failed command", + "operations": [ + { + "name": "failPoint", + "object": "testRunner", + "arguments": { + "client": "client0", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "find" + ], + "errorCode": 89, + "errorLabels": [ + "RetryableWriteError" + ] + } + } + } + }, + { + "name": "find", + "object": "collection0", + "arguments": { + "filter": { + "x": 1 + } + } + } + ], + "expectTracingMessages": [ + { + "client": "client0", + "ignoreExtraSpans": true, + "spans": [ + { + "name": "find operation-find.test", + "attributes": { + "db.system": "mongodb", + "db.namespace": "operation-find", + "db.collection.name": "test", + "db.operation.name": "find", + "db.operation.summary": "find operation-find.test" + }, + "nested": [ + { + "name": "find", + "attributes": { + "db.system": "mongodb", + "db.namespace": "operation-find", + "db.collection.name": "test", + "db.command.name": "find", + "network.transport": "tcp", + "db.mongodb.cursor_id": { + "$$exists": false + }, + "db.response.status_code": "89", + "exception.message": { + "$$type": "string" + }, + "exception.type": { + "$$type": "string" + }, + "exception.stacktrace": { + "$$type": "string" + }, + "server.address": { + "$$type": "string" + }, + "server.port": { + "$$type": [ + "long", + "string" + ] + }, + "server.type": { + "$$type": "string" + }, + "db.query.summary": "find operation-find.test", + "db.query.text": { + "$$matchAsDocument": { + "$$matchAsRoot": { + "find": "test", + "filter": { + "x": 1 + } + } + } + }, + "db.mongodb.server_connection_id": { + "$$type": [ + "int", + "long" + ] + }, + "db.mongodb.driver_connection_id": { + "$$type": [ + "int", + "long" + ] + } + } + }, + { + "name": "find", + "attributes": { + "db.system": "mongodb", + "db.namespace": "operation-find", + "db.collection.name": "test", + "db.command.name": "find", + "network.transport": "tcp", + "db.mongodb.cursor_id": { + "$$exists": false + }, + "db.response.status_code": { + "$$exists": false + }, + "exception.message": { + "$$exists": false + }, + "exception.type": { + "$$exists": false + }, + "exception.stacktrace": { + "$$exists": false + }, + "server.address": { + "$$type": "string" + }, + "server.port": { + "$$type": [ + "int", + "long" + ] + }, + "server.type": { + "$$type": "string" + }, + "db.query.summary": "find operation-find.test", + "db.query.text": { + "$$matchAsDocument": { + "$$matchAsRoot": { + "find": "test", + "filter": { + "x": 1 + } + } + } + }, + "db.mongodb.server_connection_id": { + "$$type": [ + "int", + "long" + ] + }, + "db.mongodb.driver_connection_id": { + "$$type": [ + "int", + "long" + ] + } + } + } + ] + } + ] + } + ] + } + ] +} diff --git a/src/test/spec/json/open-telemetry/operation/find.yml b/src/test/spec/json/open-telemetry/operation/find.yml new file mode 100644 index 000000000..82f42b90c --- /dev/null +++ b/src/test/spec/json/open-telemetry/operation/find.yml @@ -0,0 +1,148 @@ +description: operation find +schemaVersion: '1.27' +createEntities: + - client: + id: &client0 client0 + useMultipleMongoses: false + observeTracingMessages: + enableCommandPayload: true + - database: + id: &database0 database0 + client: *client0 + databaseName: operation-find + - collection: + id: &collection0 collection0 + database: database0 + collectionName: &collection0Name test +initialData: + - collectionName: test + databaseName: operation-find + documents: [] +tests: + - description: find an element + operations: + - name: find + object: *collection0 + arguments: { filter: { x: 1 } } + + expectTracingMessages: + - client: *client0 + ignoreExtraSpans: false + spans: + - name: find operation-find.test + attributes: + db.system: mongodb + db.namespace: operation-find + db.collection.name: test + db.operation.name: find + db.operation.summary: find operation-find.test + nested: + - name: find + attributes: + db.system: mongodb + db.namespace: operation-find + db.collection.name: test + db.command.name: find + network.transport: tcp + db.mongodb.cursor_id: { $$exists: false } + db.response.status_code: { $$exists: false } + exception.message: { $$exists: false } + exception.type: { $$exists: false } + exception.stacktrace: { $$exists: false } + server.address: { $$type: string } + server.port: { $$type: [int, long] } + server.type: { $$type: string } + db.query.summary: find operation-find.test + db.query.text: + $$matchAsDocument: + $$matchAsRoot: + find: test + filter: + x: 1 + db.mongodb.server_connection_id: + $$type: [ int, long ] + db.mongodb.driver_connection_id: + $$type: [ int, long ] + + - description: find an element retrying failed command + operations: + - name: failPoint + object: testRunner + arguments: + client: *client0 + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: [ find ] + errorCode: 89 + errorLabels: [ RetryableWriteError ] + + - name: find + object: *collection0 + arguments: + filter: { x: 1 } + expectTracingMessages: + - client: *client0 + ignoreExtraSpans: true + spans: + - name: find operation-find.test + attributes: + db.system: mongodb + db.namespace: operation-find + db.collection.name: test + db.operation.name: find + db.operation.summary: find operation-find.test + nested: + - name: find + attributes: + db.system: mongodb + db.namespace: operation-find + db.collection.name: test + db.command.name: find + network.transport: tcp + db.mongodb.cursor_id: { $$exists: false } + db.response.status_code: '89' + exception.message: { $$type: string } + exception.type: { $$type: string } + exception.stacktrace: { $$type: string } + server.address: { $$type: string } + server.port: { $$type: [ long, string ] } + server.type: { $$type: string } + db.query.summary: find operation-find.test + db.query.text: + $$matchAsDocument: + $$matchAsRoot: + find: test + filter: + x: 1 + db.mongodb.server_connection_id: + $$type: [ int, long ] + db.mongodb.driver_connection_id: + $$type: [ int, long ] + - name: find + attributes: + db.system: mongodb + db.namespace: operation-find + db.collection.name: test + db.command.name: find + network.transport: tcp + db.mongodb.cursor_id: { $$exists: false } + db.response.status_code: { $$exists: false } + exception.message: { $$exists: false } + exception.type: { $$exists: false } + exception.stacktrace: { $$exists: false } + server.address: { $$type: string } + server.port: { $$type: [ int, long ] } + server.type: { $$type: string } + db.query.summary: find operation-find.test + db.query.text: + $$matchAsDocument: + $$matchAsRoot: + find: test + filter: + x: 1 + db.mongodb.server_connection_id: + $$type: [ int, long ] + db.mongodb.driver_connection_id: + $$type: [ int, long ] diff --git a/src/test/spec/json/open-telemetry/operation/find_and_modify.json b/src/test/spec/json/open-telemetry/operation/find_and_modify.json new file mode 100644 index 000000000..20d595849 --- /dev/null +++ b/src/test/spec/json/open-telemetry/operation/find_and_modify.json @@ -0,0 +1,138 @@ +{ + "description": "operation find_one_and_update", + "schemaVersion": "1.27", + "createEntities": [ + { + "client": { + "id": "client0", + "useMultipleMongoses": false, + "observeTracingMessages": { + "enableCommandPayload": true + } + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "operation-aggregate" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "test" + } + } + ], + "tests": [ + { + "description": "findOneAndUpdate", + "operations": [ + { + "name": "findOneAndUpdate", + "object": "collection0", + "arguments": { + "filter": { + "_id": 1 + }, + "update": [ + { + "$set": { + "x": 5 + } + } + ], + "comment": "comment" + } + } + ], + "expectTracingMessages": [ + { + "client": "client0", + "ignoreExtraSpans": false, + "spans": [ + { + "name": "findAndModify operation-aggregate.test", + "attributes": { + "db.system": "mongodb", + "db.namespace": "operation-aggregate", + "db.collection.name": "test", + "db.operation.name": "findAndModify", + "db.operation.summary": "findAndModify operation-aggregate.test" + }, + "nested": [ + { + "name": "findAndModify", + "attributes": { + "db.system": "mongodb", + "db.namespace": "operation-aggregate", + "db.collection.name": "test", + "db.command.name": "findAndModify", + "network.transport": "tcp", + "db.mongodb.cursor_id": { + "$$exists": false + }, + "db.response.status_code": { + "$$exists": false + }, + "exception.message": { + "$$exists": false + }, + "exception.type": { + "$$exists": false + }, + "exception.stacktrace": { + "$$exists": false + }, + "server.address": { + "$$type": "string" + }, + "server.port": { + "$$type": [ + "int", + "long" + ] + }, + "db.query.summary": "findAndModify operation-aggregate.test", + "db.query.text": { + "$$matchAsDocument": { + "$$matchAsRoot": { + "findAndModify": "test", + "query": { + "_id": 1 + }, + "update": [ + { + "$set": { + "x": 5 + } + } + ], + "comment": "comment" + } + } + }, + "db.mongodb.server_connection_id": { + "$$type": [ + "int", + "long" + ] + }, + "db.mongodb.driver_connection_id": { + "$$type": [ + "int", + "long" + ] + } + } + } + ] + } + ] + } + ] + } + ] +} diff --git a/src/test/spec/json/open-telemetry/operation/find_and_modify.yml b/src/test/spec/json/open-telemetry/operation/find_and_modify.yml new file mode 100644 index 000000000..d36cd053d --- /dev/null +++ b/src/test/spec/json/open-telemetry/operation/find_and_modify.yml @@ -0,0 +1,67 @@ +description: operation find_one_and_update +schemaVersion: '1.27' +createEntities: + - client: + id: &client0 client0 + useMultipleMongoses: false + observeTracingMessages: + enableCommandPayload: true + - database: + id: &database0 database0 + client: *client0 + databaseName: operation-aggregate + - collection: + id: &collection0 collection0 + database: *database0 + collectionName: test + +tests: + - description: findOneAndUpdate + operations: + - name: findOneAndUpdate + object: *collection0 + arguments: + filter: &filter + _id: 1 + update: &update + - $set: { x: 5 } + comment: "comment" + + expectTracingMessages: + - client: *client0 + ignoreExtraSpans: false + spans: + - name: findAndModify operation-aggregate.test + attributes: + db.system: mongodb + db.namespace: operation-aggregate + db.collection.name: test + db.operation.name: findAndModify + db.operation.summary: findAndModify operation-aggregate.test + nested: + - name: findAndModify + attributes: + db.system: mongodb + db.namespace: operation-aggregate + db.collection.name: test + db.command.name: findAndModify + network.transport: tcp + db.mongodb.cursor_id: { $$exists: false } + db.response.status_code: { $$exists: false } + exception.message: { $$exists: false } + exception.type: { $$exists: false } + exception.stacktrace: { $$exists: false } + server.address: { $$type: string } + server.port: { $$type: [ int, long ] } + db.query.summary: findAndModify operation-aggregate.test + db.query.text: + $$matchAsDocument: + $$matchAsRoot: + findAndModify: test + query: *filter + update: *update + comment: "comment" + db.mongodb.server_connection_id: + $$type: [ int, long ] + db.mongodb.driver_connection_id: + $$type: [ int, long ] diff --git a/src/test/spec/json/open-telemetry/operation/find_without_query_text.json b/src/test/spec/json/open-telemetry/operation/find_without_query_text.json new file mode 100644 index 000000000..df50865ce --- /dev/null +++ b/src/test/spec/json/open-telemetry/operation/find_without_query_text.json @@ -0,0 +1,113 @@ +{ + "description": "operation find without db.query.text", + "schemaVersion": "1.27", + "createEntities": [ + { + "client": { + "id": "client0", + "useMultipleMongoses": false, + "observeTracingMessages": { + "enableCommandPayload": false + } + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "operation-find" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "test" + } + } + ], + "initialData": [ + { + "collectionName": "test", + "databaseName": "operation-find", + "documents": [] + } + ], + "tests": [ + { + "description": "find an element", + "operations": [ + { + "name": "find", + "object": "collection0", + "arguments": { + "filter": { + "x": 1 + } + } + } + ], + "expectTracingMessages": [ + { + "client": "client0", + "ignoreExtraSpans": false, + "spans": [ + { + "name": "find operation-find.test", + "attributes": { + "db.system": "mongodb", + "db.namespace": "operation-find", + "db.collection.name": "test", + "db.operation.name": "find", + "db.operation.summary": "find operation-find.test" + }, + "nested": [ + { + "name": "find", + "attributes": { + "db.system": "mongodb", + "db.namespace": "operation-find", + "db.collection.name": "test", + "db.command.name": "find", + "network.transport": "tcp", + "db.mongodb.cursor_id": { + "$$exists": false + }, + "db.response.status_code": { + "$$exists": false + }, + "exception.message": { + "$$exists": false + }, + "exception.type": { + "$$exists": false + }, + "exception.stacktrace": { + "$$exists": false + }, + "server.address": { + "$$type": "string" + }, + "server.port": { + "$$type": [ + "int", + "long" + ] + }, + "server.type": { + "$$type": "string" + }, + "db.query.summary": "find operation-find.test", + "db.query.text": { + "$$exists": false + } + } + } + ] + } + ] + } + ] + } + ] +} diff --git a/src/test/spec/json/open-telemetry/operation/find_without_query_text.yml b/src/test/spec/json/open-telemetry/operation/find_without_query_text.yml new file mode 100644 index 000000000..26b53b979 --- /dev/null +++ b/src/test/spec/json/open-telemetry/operation/find_without_query_text.yml @@ -0,0 +1,56 @@ +description: operation find without db.query.text +schemaVersion: '1.27' +createEntities: + - client: + id: &client0 client0 + useMultipleMongoses: false + observeTracingMessages: + enableCommandPayload: false + - database: + id: &database0 database0 + client: *client0 + databaseName: operation-find + - collection: + id: &collection0 collection0 + database: database0 + collectionName: &collection0Name test +initialData: + - collectionName: test + databaseName: operation-find + documents: [] +tests: + - description: find an element + operations: + - name: find + object: *collection0 + arguments: { filter: { x: 1 } } + + expectTracingMessages: + - client: *client0 + ignoreExtraSpans: false + spans: + - name: find operation-find.test + attributes: + db.system: mongodb + db.namespace: operation-find + db.collection.name: test + db.operation.name: find + db.operation.summary: find operation-find.test + nested: + - name: find + attributes: + db.system: mongodb + db.namespace: operation-find + db.collection.name: test + db.command.name: find + network.transport: tcp + db.mongodb.cursor_id: { $$exists: false } + db.response.status_code: { $$exists: false } + exception.message: { $$exists: false } + exception.type: { $$exists: false } + exception.stacktrace: { $$exists: false } + server.address: { $$type: string } + server.port: { $$type: [int, long] } + server.type: { $$type: string } + db.query.summary: find operation-find.test + db.query.text: { $$exists: false } diff --git a/src/test/spec/json/open-telemetry/operation/insert.json b/src/test/spec/json/open-telemetry/operation/insert.json new file mode 100644 index 000000000..52e659e1d --- /dev/null +++ b/src/test/spec/json/open-telemetry/operation/insert.json @@ -0,0 +1,117 @@ +{ + "description": "operation insert", + "schemaVersion": "1.27", + "createEntities": [ + { + "client": { + "id": "client0", + "useMultipleMongoses": false, + "observeTracingMessages": { + "enableCommandPayload": true + } + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "operation-insert" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "test" + } + } + ], + "initialData": [ + { + "collectionName": "test", + "databaseName": "operation-insert", + "documents": [] + } + ], + "tests": [ + { + "description": "insert one element", + "operations": [ + { + "object": "collection0", + "name": "insertOne", + "arguments": { + "document": { + "_id": 1 + } + } + } + ], + "expectTracingMessages": [ + { + "client": "client0", + "ignoreExtraSpans": false, + "spans": [ + { + "name": "insert operation-insert.test", + "attributes": { + "db.system": "mongodb", + "db.namespace": "operation-insert", + "db.collection.name": "test", + "db.operation.name": "insert", + "db.operation.summary": "insert operation-insert.test" + }, + "nested": [ + { + "name": "insert", + "attributes": { + "db.system": "mongodb", + "db.namespace": "operation-insert", + "server.address": { + "$$type": "string" + }, + "server.port": { + "$$type": [ + "long", + "string" + ] + }, + "server.type": { + "$$type": "string" + }, + "db.query.summary": "insert operation-insert.test", + "db.query.text": { + "$$matchAsDocument": { + "$$matchAsRoot": { + "insert": "test", + "ordered": true, + "txnNumber": 1, + "documents": [ + { + "_id": 1 + } + ] + } + } + } + } + } + ] + } + ] + } + ], + "outcome": [ + { + "collectionName": "test", + "databaseName": "operation-insert", + "documents": [ + { + "_id": 1 + } + ] + } + ] + } + ] +} diff --git a/src/test/spec/json/open-telemetry/operation/insert.yml b/src/test/spec/json/open-telemetry/operation/insert.yml new file mode 100644 index 000000000..15019edaf --- /dev/null +++ b/src/test/spec/json/open-telemetry/operation/insert.yml @@ -0,0 +1,61 @@ +description: operation insert +schemaVersion: '1.27' +createEntities: + - client: + id: &client0 client0 + useMultipleMongoses: false + observeTracingMessages: + enableCommandPayload: true + - database: + id: &database0 database0 + client: *client0 + databaseName: operation-insert + - collection: + id: &collection0 collection0 + database: *database0 + collectionName: test +initialData: + - collectionName: test + databaseName: operation-insert + documents: [ ] +tests: + - description: insert one element + operations: + - object: *collection0 + name: insertOne + arguments: { document: { _id: 1 } } + + expectTracingMessages: + - client: *client0 + ignoreExtraSpans: false + spans: + - name: insert operation-insert.test + attributes: + db.system: mongodb + db.namespace: operation-insert + db.collection.name: test + db.operation.name: insert + db.operation.summary: insert operation-insert.test + nested: + - name: insert + attributes: + db.system: mongodb + db.namespace: operation-insert + server.address: { $$type: string } + server.port: { $$type: [ long, string ] } + server.type: { $$type: string } + db.query.summary: insert operation-insert.test + db.query.text: + $$matchAsDocument: + $$matchAsRoot: + insert: test + ordered: true + txnNumber: 1 + documents: + - _id: 1 + + outcome: + - collectionName: test + databaseName: operation-insert + documents: + - _id: 1 diff --git a/src/test/spec/json/open-telemetry/operation/list_collections.json b/src/test/spec/json/open-telemetry/operation/list_collections.json new file mode 100644 index 000000000..3d064c3df --- /dev/null +++ b/src/test/spec/json/open-telemetry/operation/list_collections.json @@ -0,0 +1,106 @@ +{ + "description": "operation list_collections", + "schemaVersion": "1.27", + "createEntities": [ + { + "client": { + "id": "client0", + "useMultipleMongoses": false, + "observeTracingMessages": { + "enableCommandPayload": true + } + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "operation-list-collections" + } + } + ], + "tests": [ + { + "description": "List collections", + "operations": [ + { + "object": "database0", + "name": "listCollections" + } + ], + "expectTracingMessages": [ + { + "client": "client0", + "ignoreExtraSpans": false, + "spans": [ + { + "name": "listCollections operation-list-collections", + "attributes": { + "db.system": "mongodb", + "db.namespace": "operation-list-collections", + "db.operation.name": "listCollections", + "db.operation.summary": "listCollections operation-list-collections" + }, + "nested": [ + { + "name": "listCollections", + "attributes": { + "db.system": "mongodb", + "db.namespace": "operation-list-collections", + "db.command.name": "listCollections", + "network.transport": "tcp", + "db.mongodb.cursor_id": { + "$$exists": false + }, + "db.response.status_code": { + "$$exists": false + }, + "exception.message": { + "$$exists": false + }, + "exception.type": { + "$$exists": false + }, + "exception.stacktrace": { + "$$exists": false + }, + "server.address": { + "$$type": "string" + }, + "server.port": { + "$$type": [ + "int", + "long" + ] + }, + "db.query.summary": "listCollections operation-list-collections", + "db.query.text": { + "$$matchAsDocument": { + "$$matchAsRoot": { + "listCollections": 1, + "cursor": {} + } + } + }, + "db.mongodb.server_connection_id": { + "$$type": [ + "int", + "long" + ] + }, + "db.mongodb.driver_connection_id": { + "$$type": [ + "int", + "long" + ] + } + } + } + ] + } + ] + } + ] + } + ] +} diff --git a/src/test/spec/json/open-telemetry/operation/list_collections.yml b/src/test/spec/json/open-telemetry/operation/list_collections.yml new file mode 100644 index 000000000..20c9afa5e --- /dev/null +++ b/src/test/spec/json/open-telemetry/operation/list_collections.yml @@ -0,0 +1,52 @@ +description: operation list_collections +schemaVersion: '1.27' +createEntities: + - client: + id: &client0 client0 + useMultipleMongoses: false + observeTracingMessages: + enableCommandPayload: true + - database: + id: &database0 database0 + client: *client0 + databaseName: operation-list-collections +tests: + - description: List collections + operations: + - object: *database0 + name: listCollections + + expectTracingMessages: + - client: *client0 + ignoreExtraSpans: false + spans: + - name: listCollections operation-list-collections + attributes: + db.system: mongodb + db.namespace: operation-list-collections + db.operation.name: listCollections + db.operation.summary: listCollections operation-list-collections + nested: + - name: listCollections + attributes: + db.system: mongodb + db.namespace: operation-list-collections + db.command.name: listCollections + network.transport: tcp + db.mongodb.cursor_id: { $$exists: false } + db.response.status_code: { $$exists: false } + exception.message: { $$exists: false } + exception.type: { $$exists: false } + exception.stacktrace: { $$exists: false } + server.address: { $$type: string } + server.port: { $$type: [ int, long ] } + db.query.summary: listCollections operation-list-collections + db.query.text: + $$matchAsDocument: + $$matchAsRoot: + listCollections: 1 + cursor: {} + db.mongodb.server_connection_id: + $$type: [ int, long ] + db.mongodb.driver_connection_id: + $$type: [ int, long ] diff --git a/src/test/spec/json/open-telemetry/operation/list_databases.json b/src/test/spec/json/open-telemetry/operation/list_databases.json new file mode 100644 index 000000000..f06ea2cf3 --- /dev/null +++ b/src/test/spec/json/open-telemetry/operation/list_databases.json @@ -0,0 +1,101 @@ +{ + "description": "operation list_databases", + "schemaVersion": "1.27", + "createEntities": [ + { + "client": { + "id": "client0", + "useMultipleMongoses": false, + "observeTracingMessages": { + "enableCommandPayload": true + } + } + } + ], + "tests": [ + { + "description": "list databases", + "operations": [ + { + "object": "client0", + "name": "listDatabases" + } + ], + "expectTracingMessages": [ + { + "client": "client0", + "ignoreExtraSpans": false, + "spans": [ + { + "name": "listDatabases admin", + "attributes": { + "db.system": "mongodb", + "db.namespace": "admin", + "db.operation.name": "listDatabases", + "db.operation.summary": "listDatabases admin" + }, + "nested": [ + { + "name": "listDatabases", + "attributes": { + "db.system": "mongodb", + "db.namespace": "admin", + "db.collection.name": { + "$$exists": false + }, + "db.command.name": "listDatabases", + "network.transport": "tcp", + "db.mongodb.cursor_id": { + "$$exists": false + }, + "db.response.status_code": { + "$$exists": false + }, + "exception.message": { + "$$exists": false + }, + "exception.type": { + "$$exists": false + }, + "exception.stacktrace": { + "$$exists": false + }, + "server.address": { + "$$type": "string" + }, + "server.port": { + "$$type": [ + "int", + "long" + ] + }, + "db.query.summary": "listDatabases admin", + "db.query.text": { + "$$matchAsDocument": { + "$$matchAsRoot": { + "listDatabases": 1 + } + } + }, + "db.mongodb.server_connection_id": { + "$$type": [ + "int", + "long" + ] + }, + "db.mongodb.driver_connection_id": { + "$$type": [ + "int", + "long" + ] + } + } + } + ] + } + ] + } + ] + } + ] +} diff --git a/src/test/spec/json/open-telemetry/operation/list_databases.yml b/src/test/spec/json/open-telemetry/operation/list_databases.yml new file mode 100644 index 000000000..77f0a5650 --- /dev/null +++ b/src/test/spec/json/open-telemetry/operation/list_databases.yml @@ -0,0 +1,48 @@ +description: operation list_databases +schemaVersion: '1.27' +createEntities: + - client: + id: &client0 client0 + useMultipleMongoses: false + observeTracingMessages: + enableCommandPayload: true +tests: + - description: list databases + operations: + - object: *client0 + name: listDatabases + + expectTracingMessages: + - client: *client0 + ignoreExtraSpans: false + spans: + - name: listDatabases admin + attributes: + db.system: mongodb + db.namespace: admin + db.operation.name: listDatabases + db.operation.summary: listDatabases admin + nested: + - name: listDatabases + attributes: + db.system: mongodb + db.namespace: admin + db.collection.name: { $$exists: false } + db.command.name: listDatabases + network.transport: tcp + db.mongodb.cursor_id: { $$exists: false } + db.response.status_code: { $$exists: false } + exception.message: { $$exists: false } + exception.type: { $$exists: false } + exception.stacktrace: { $$exists: false } + server.address: { $$type: string } + server.port: { $$type: [ int, long ] } + db.query.summary: listDatabases admin + db.query.text: + $$matchAsDocument: + $$matchAsRoot: + listDatabases: 1 + db.mongodb.server_connection_id: + $$type: [ int, long ] + db.mongodb.driver_connection_id: + $$type: [ int, long ] diff --git a/src/test/spec/json/open-telemetry/operation/list_indexes.json b/src/test/spec/json/open-telemetry/operation/list_indexes.json new file mode 100644 index 000000000..3e364b789 --- /dev/null +++ b/src/test/spec/json/open-telemetry/operation/list_indexes.json @@ -0,0 +1,121 @@ +{ + "description": "operation list_indexes", + "schemaVersion": "1.27", + "createEntities": [ + { + "client": { + "id": "client0", + "useMultipleMongoses": false, + "observeTracingMessages": { + "enableCommandPayload": true + } + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "operation-list-indexes" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "test" + } + } + ], + "initialData": [ + { + "collectionName": "test", + "databaseName": "operation-list-indexes", + "documents": [] + } + ], + "tests": [ + { + "description": "List indexes", + "operations": [ + { + "object": "collection0", + "name": "listIndexes" + } + ], + "expectTracingMessages": [ + { + "client": "client0", + "ignoreExtraSpans": false, + "spans": [ + { + "name": "listIndexes operation-list-indexes.test", + "attributes": { + "db.system": "mongodb", + "db.namespace": "operation-list-indexes", + "db.collection.name": "test", + "db.operation.name": "listIndexes", + "db.operation.summary": "listIndexes operation-list-indexes.test" + }, + "nested": [ + { + "name": "listIndexes", + "attributes": { + "db.system": "mongodb", + "db.namespace": "operation-list-indexes", + "db.collection.name": "test", + "db.command.name": "listIndexes", + "network.transport": "tcp", + "db.mongodb.cursor_id": { + "$$exists": false + }, + "db.response.status_code": { + "$$exists": false + }, + "exception.message": { + "$$exists": false + }, + "exception.type": { + "$$exists": false + }, + "exception.stacktrace": { + "$$exists": false + }, + "server.address": { + "$$type": "string" + }, + "server.port": { + "$$type": [ + "int", + "long" + ] + }, + "db.query.summary": "listIndexes operation-list-indexes.test", + "db.query.text": { + "$$matchAsDocument": { + "$$matchAsRoot": { + "listIndexes": "test" + } + } + }, + "db.mongodb.server_connection_id": { + "$$type": [ + "int", + "long" + ] + }, + "db.mongodb.driver_connection_id": { + "$$type": [ + "int", + "long" + ] + } + } + } + ] + } + ] + } + ] + } + ] +} diff --git a/src/test/spec/json/open-telemetry/operation/list_indexes.yml b/src/test/spec/json/open-telemetry/operation/list_indexes.yml new file mode 100644 index 000000000..60f8ac23d --- /dev/null +++ b/src/test/spec/json/open-telemetry/operation/list_indexes.yml @@ -0,0 +1,61 @@ +description: operation list_indexes +schemaVersion: '1.27' +createEntities: + - client: + id: &client0 client0 + useMultipleMongoses: false + observeTracingMessages: + enableCommandPayload: true + - database: + id: &database0 database0 + client: *client0 + databaseName: operation-list-indexes + - collection: + id: &collection0 collection0 + database: database0 + collectionName: test +initialData: + - collectionName: test + databaseName: operation-list-indexes + documents: [ ] +tests: + - description: List indexes + operations: + - object: *collection0 + name: listIndexes + + expectTracingMessages: + - client: *client0 + ignoreExtraSpans: false + spans: + - name: listIndexes operation-list-indexes.test + attributes: + db.system: mongodb + db.namespace: operation-list-indexes + db.collection.name: test + db.operation.name: listIndexes + db.operation.summary: listIndexes operation-list-indexes.test + nested: + - name: listIndexes + attributes: + db.system: mongodb + db.namespace: operation-list-indexes + db.collection.name: test + db.command.name: listIndexes + network.transport: tcp + db.mongodb.cursor_id: { $$exists: false } + db.response.status_code: { $$exists: false } + exception.message: { $$exists: false } + exception.type: { $$exists: false } + exception.stacktrace: { $$exists: false } + server.address: { $$type: string } + server.port: { $$type: [ int, long ] } + db.query.summary: listIndexes operation-list-indexes.test + db.query.text: + $$matchAsDocument: + $$matchAsRoot: + listIndexes: test + db.mongodb.server_connection_id: + $$type: [ int, long ] + db.mongodb.driver_connection_id: + $$type: [ int, long ] diff --git a/src/test/spec/json/open-telemetry/operation/map_reduce.json b/src/test/spec/json/open-telemetry/operation/map_reduce.json new file mode 100644 index 000000000..d5ae4f677 --- /dev/null +++ b/src/test/spec/json/open-telemetry/operation/map_reduce.json @@ -0,0 +1,180 @@ +{ + "description": "operation map_reduce", + "schemaVersion": "1.27", + "runOnRequirements": [ + { + "minServerVersion": "4.0", + "topologies": [ + "single", + "replicaset" + ] + }, + { + "minServerVersion": "4.1.7", + "serverless": "forbid", + "topologies": [ + "sharded", + "load-balanced" + ] + } + ], + "createEntities": [ + { + "client": { + "id": "client0", + "useMultipleMongoses": false, + "observeTracingMessages": { + "enableCommandPayload": true + } + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "operation-map-reduce" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "test" + } + } + ], + "initialData": [ + { + "collectionName": "test", + "databaseName": "operation-map-reduce", + "documents": [ + { + "_id": 1, + "x": 0 + }, + { + "_id": 2, + "x": 1 + }, + { + "_id": 3, + "x": 2 + } + ] + } + ], + "tests": [ + { + "description": "mapReduce", + "operations": [ + { + "object": "collection0", + "name": "mapReduce", + "arguments": { + "map": { + "$code": "function inc() { return emit(0, this.x + 1) }" + }, + "reduce": { + "$code": "function sum(key, values) { return values.reduce((acc, x) => acc + x); }" + }, + "out": { + "inline": 1 + } + }, + "expectResult": [ + { + "_id": 0, + "value": 6 + } + ] + } + ], + "expectTracingMessages": [ + { + "client": "client0", + "ignoreExtraSpans": false, + "spans": [ + { + "name": "mapReduce operation-map-reduce.test", + "attributes": { + "db.system": "mongodb", + "db.namespace": "operation-map-reduce", + "db.collection.name": "test", + "db.operation.name": "mapReduce", + "db.operation.summary": "mapReduce operation-map-reduce.test" + }, + "nested": [ + { + "name": "mapReduce", + "attributes": { + "db.system": "mongodb", + "db.namespace": "operation-map-reduce", + "db.collection.name": "test", + "db.command.name": "mapReduce", + "network.transport": "tcp", + "db.mongodb.cursor_id": { + "$$exists": false + }, + "db.response.status_code": { + "$$exists": false + }, + "exception.message": { + "$$exists": false + }, + "exception.type": { + "$$exists": false + }, + "exception.stacktrace": { + "$$exists": false + }, + "server.address": { + "$$type": "string" + }, + "server.port": { + "$$type": [ + "int", + "long" + ] + }, + "server.type": { + "$$type": "string" + }, + "db.query.summary": "mapReduce operation-map-reduce.test", + "db.query.text": { + "$$matchAsDocument": { + "$$matchAsRoot": { + "mapReduce": "test", + "map": { + "$code": "function inc() { return emit(0, this.x + 1) }" + }, + "reduce": { + "$code": "function sum(key, values) { return values.reduce((acc, x) => acc + x); }" + }, + "out": { + "inline": 1 + } + } + } + }, + "db.mongodb.server_connection_id": { + "$$type": [ + "int", + "long" + ] + }, + "db.mongodb.driver_connection_id": { + "$$type": [ + "int", + "long" + ] + } + } + } + ] + } + ] + } + ] + } + ] +} diff --git a/src/test/spec/json/open-telemetry/operation/map_reduce.yml b/src/test/spec/json/open-telemetry/operation/map_reduce.yml new file mode 100644 index 000000000..d96756f9f --- /dev/null +++ b/src/test/spec/json/open-telemetry/operation/map_reduce.yml @@ -0,0 +1,99 @@ +description: operation map_reduce +schemaVersion: '1.27' +runOnRequirements: + - + minServerVersion: '4.0' + topologies: + - single + - replicaset + - + minServerVersion: 4.1.7 + # serverless proxy does not support mapReduce operation + serverless: forbid + topologies: + - sharded + - load-balanced + +createEntities: + - client: + id: &client0 client0 + useMultipleMongoses: false + observeTracingMessages: + enableCommandPayload: true + - database: + id: &database0 database0 + client: *client0 + databaseName: operation-map-reduce + - collection: + id: &collection0 collection0 + database: *database0 + collectionName: test +initialData: + - + collectionName: test + databaseName: operation-map-reduce + documents: + - + _id: 1 + x: 0 + - + _id: 2 + x: 1 + - + _id: 3 + x: 2 +tests: + - description: mapReduce + operations: + - object: *collection0 + name: mapReduce + arguments: + map: + $code: 'function inc() { return emit(0, this.x + 1) }' + reduce: + $code: 'function sum(key, values) { return values.reduce((acc, x) => acc + x); }' + out: + inline: 1 + expectResult: + - + _id: 0 + value: 6 + expectTracingMessages: + - client: *client0 + ignoreExtraSpans: false + spans: + - name: mapReduce operation-map-reduce.test + attributes: + db.system: mongodb + db.namespace: operation-map-reduce + db.collection.name: test + db.operation.name: mapReduce + db.operation.summary: mapReduce operation-map-reduce.test + nested: + - name: mapReduce + attributes: + db.system: mongodb + db.namespace: operation-map-reduce + db.collection.name: test + db.command.name: mapReduce + network.transport: tcp + db.mongodb.cursor_id: { $$exists: false } + db.response.status_code: { $$exists: false } + exception.message: { $$exists: false } + exception.type: { $$exists: false } + exception.stacktrace: { $$exists: false } + server.address: { $$type: string } + server.port: { $$type: [ int, long ] } + server.type: { $$type: string } + db.query.summary: mapReduce operation-map-reduce.test + db.query.text: + $$matchAsDocument: + $$matchAsRoot: + mapReduce: test + map: { $code: 'function inc() { return emit(0, this.x + 1) }' } + reduce: { $code: 'function sum(key, values) { return values.reduce((acc, x) => acc + x); }' } + out: { inline: 1 } + db.mongodb.server_connection_id: + $$type: [ int, long ] + db.mongodb.driver_connection_id: + $$type: [ int, long ] diff --git a/src/test/spec/json/open-telemetry/operation/retries.json b/src/test/spec/json/open-telemetry/operation/retries.json new file mode 100644 index 000000000..97b0174fb --- /dev/null +++ b/src/test/spec/json/open-telemetry/operation/retries.json @@ -0,0 +1,212 @@ +{ + "description": "retries", + "schemaVersion": "1.27", + "createEntities": [ + { + "client": { + "id": "client0", + "useMultipleMongoses": false, + "observeTracingMessages": { + "enableCommandPayload": true + } + } + }, + { + "client": { + "id": "failPointClient", + "useMultipleMongoses": false + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "operation-find-retries" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "test" + } + } + ], + "initialData": [ + { + "collectionName": "test", + "databaseName": "operation-find-retries", + "documents": [] + } + ], + "tests": [ + { + "description": "find an element with retries", + "operations": [ + { + "name": "failPoint", + "object": "testRunner", + "arguments": { + "client": "failPointClient", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "find" + ], + "errorCode": 89, + "errorLabels": [ + "RetryableWriteError" + ] + } + } + } + }, + { + "name": "find", + "object": "collection0", + "arguments": { + "filter": { + "x": 1 + } + } + } + ], + "expectTracingMessages": [ + { + "client": "client0", + "ignoreExtraSpans": true, + "spans": [ + { + "name": "find operation-find-retries.test", + "attributes": { + "db.system": "mongodb", + "db.namespace": "operation-find-retries", + "db.collection.name": "test", + "db.operation.name": "find", + "db.operation.summary": "find operation-find-retries.test" + }, + "nested": [ + { + "name": "find", + "attributes": { + "db.system": "mongodb", + "db.namespace": "operation-find-retries", + "db.collection.name": "test", + "db.command.name": "find", + "network.transport": "tcp", + "db.mongodb.cursor_id": { + "$$exists": false + }, + "db.response.status_code": "89", + "exception.message": { + "$$exists": true + }, + "exception.type": { + "$$exists": true + }, + "exception.stacktrace": { + "$$exists": true + }, + "server.address": { + "$$type": "string" + }, + "server.port": { + "$$type": [ + "long", + "string" + ] + }, + "db.query.summary": "find operation-find-retries.test", + "db.query.text": { + "$$matchAsDocument": { + "$$matchAsRoot": { + "find": "test", + "filter": { + "x": 1 + } + } + } + }, + "db.mongodb.server_connection_id": { + "$$type": [ + "int", + "long" + ] + }, + "db.mongodb.driver_connection_id": { + "$$type": [ + "int", + "long" + ] + } + } + }, + { + "name": "find", + "attributes": { + "db.system": "mongodb", + "db.namespace": "operation-find-retries", + "db.collection.name": "test", + "db.command.name": "find", + "network.transport": "tcp", + "db.mongodb.cursor_id": { + "$$exists": false + }, + "db.response.status_code": { + "$$exists": false + }, + "exception.message": { + "$$exists": false + }, + "exception.type": { + "$$exists": false + }, + "exception.stacktrace": { + "$$exists": false + }, + "server.address": { + "$$type": "string" + }, + "server.port": { + "$$type": [ + "int", + "long" + ] + }, + "db.query.summary": "find operation-find-retries.test", + "db.query.text": { + "$$matchAsDocument": { + "$$matchAsRoot": { + "find": "test", + "filter": { + "x": 1 + } + } + } + }, + "db.mongodb.server_connection_id": { + "$$type": [ + "int", + "long" + ] + }, + "db.mongodb.driver_connection_id": { + "$$type": [ + "int", + "long" + ] + } + } + } + ] + } + ] + } + ] + } + ] +} diff --git a/src/test/spec/json/open-telemetry/operation/retries.yml b/src/test/spec/json/open-telemetry/operation/retries.yml new file mode 100644 index 000000000..8d391c1b0 --- /dev/null +++ b/src/test/spec/json/open-telemetry/operation/retries.yml @@ -0,0 +1,105 @@ +description: retries +schemaVersion: '1.27' +createEntities: + - client: + id: &client0 client0 + useMultipleMongoses: false + observeTracingMessages: + enableCommandPayload: true + - client: + id: &failPointClient failPointClient + useMultipleMongoses: false + - database: + id: &database0 database0 + client: *client0 + databaseName: &database0Name operation-find-retries + - collection: + id: &collection0 collection0 + database: *database0 + collectionName: &collection0Name test +initialData: + - collectionName: test + databaseName: operation-find-retries + documents: [ ] +tests: + - description: find an element with retries + operations: + - name: failPoint + object: testRunner + arguments: + client: *failPointClient + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: [ find ] + errorCode: 89 + errorLabels: [ RetryableWriteError ] + + - name: find + object: *collection0 + arguments: + filter: { x: 1 } + + expectTracingMessages: + - client: *client0 + ignoreExtraSpans: true + spans: + - name: find operation-find-retries.test + attributes: + db.system: mongodb + db.namespace: *database0Name + db.collection.name: *collection0Name + db.operation.name: find + db.operation.summary: find operation-find-retries.test + nested: + - name: find + attributes: + db.system: mongodb + db.namespace: *database0Name + db.collection.name: *collection0Name + db.command.name: find + network.transport: tcp + db.mongodb.cursor_id: { $$exists: false } + db.response.status_code: '89' + exception.message: { $$exists: true } + exception.type: { $$exists: true } + exception.stacktrace: { $$exists: true } + server.address: { $$type: string } + server.port: { $$type: [ long, string ] } + db.query.summary: find operation-find-retries.test + db.query.text: + $$matchAsDocument: + $$matchAsRoot: + find: test + filter: + x: 1 + db.mongodb.server_connection_id: + $$type: [ int, long ] + db.mongodb.driver_connection_id: + $$type: [ int, long ] + - name: find + attributes: + db.system: mongodb + db.namespace: *database0Name + db.collection.name: *collection0Name + db.command.name: find + network.transport: tcp + db.mongodb.cursor_id: { $$exists: false } + db.response.status_code: { $$exists: false } + exception.message: { $$exists: false } + exception.type: { $$exists: false } + exception.stacktrace: { $$exists: false } + server.address: { $$type: string } + server.port: { $$type: [ int, long ] } + db.query.summary: find operation-find-retries.test + db.query.text: + $$matchAsDocument: + $$matchAsRoot: + find: test + filter: + x: 1 + db.mongodb.server_connection_id: + $$type: [ int, long ] + db.mongodb.driver_connection_id: + $$type: [ int, long ] diff --git a/src/test/spec/json/open-telemetry/operation/update.json b/src/test/spec/json/open-telemetry/operation/update.json new file mode 100644 index 000000000..869812fb6 --- /dev/null +++ b/src/test/spec/json/open-telemetry/operation/update.json @@ -0,0 +1,108 @@ +{ + "description": "operation update", + "schemaVersion": "1.27", + "createEntities": [ + { + "client": { + "id": "client0", + "useMultipleMongoses": false, + "observeTracingMessages": { + "enableCommandPayload": true + } + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "operation-update" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "test" + } + } + ], + "tests": [ + { + "description": "update one element", + "operations": [ + { + "object": "collection0", + "name": "updateOne", + "arguments": { + "filter": { + "_id": 1 + }, + "update": { + "$inc": { + "x": 1 + } + } + } + } + ], + "expectTracingMessages": [ + { + "client": "client0", + "ignoreExtraSpans": false, + "spans": [ + { + "name": "update operation-update.test", + "attributes": { + "db.system": "mongodb", + "db.namespace": "operation-update", + "db.collection.name": "test", + "db.operation.name": "update", + "db.operation.summary": "update operation-update.test" + }, + "nested": [ + { + "name": "update", + "attributes": { + "db.system": "mongodb", + "db.namespace": "operation-update", + "server.address": { + "$$type": "string" + }, + "server.port": { + "$$type": [ + "long", + "string" + ] + }, + "db.query.summary": "update operation-update.test", + "db.query.text": { + "$$matchAsDocument": { + "$$matchAsRoot": { + "update": "test", + "ordered": true, + "txnNumber": 1, + "updates": [ + { + "q": { + "_id": 1 + }, + "u": { + "$inc": { + "x": 1 + } + } + } + ] + } + } + } + } + } + ] + } + ] + } + ] + } + ] +} diff --git a/src/test/spec/json/open-telemetry/operation/update.yml b/src/test/spec/json/open-telemetry/operation/update.yml new file mode 100644 index 000000000..1d8e38e3a --- /dev/null +++ b/src/test/spec/json/open-telemetry/operation/update.yml @@ -0,0 +1,53 @@ +description: operation update +schemaVersion: '1.27' +createEntities: + - client: + id: &client0 client0 + useMultipleMongoses: false + observeTracingMessages: + enableCommandPayload: true + - database: + id: &database0 database0 + client: *client0 + databaseName: &databaseName0 operation-update + - collection: + id: &collection0 collection0 + database: *database0 + collectionName: &collectionName0 test + +tests: + - description: update one element + operations: + - + object: *collection0 + name: updateOne + arguments: + filter: { _id: 1 } + update: { $inc: { x: 1 } } + + expectTracingMessages: + - client: *client0 + ignoreExtraSpans: false + spans: + - name: update operation-update.test + attributes: + db.system: mongodb + db.namespace: *databaseName0 + db.collection.name: *collectionName0 + db.operation.name: update + db.operation.summary: update operation-update.test + nested: + - name: update + attributes: + db.system: mongodb + db.namespace: operation-update + server.address: { $$type: string } + server.port: { $$type: [ long, string ] } + db.query.summary: update operation-update.test + db.query.text: + $$matchAsDocument: + $$matchAsRoot: + update: test + ordered: true + txnNumber: 1 + updates: [ { "q": { "_id": 1 }, "u": { "$inc": { "x": 1 } } } ] diff --git a/src/test/spec/json/open-telemetry/transaction/convenient.json b/src/test/spec/json/open-telemetry/transaction/convenient.json new file mode 100644 index 000000000..f3d8994d7 --- /dev/null +++ b/src/test/spec/json/open-telemetry/transaction/convenient.json @@ -0,0 +1,237 @@ +{ + "description": "convenient transactions", + "schemaVersion": "1.27", + "runOnRequirements": [ + { + "minServerVersion": "4.4", + "topologies": [ + "replicaset", + "sharded" + ] + } + ], + "createEntities": [ + { + "client": { + "id": "client", + "useMultipleMongoses": false, + "observeTracingMessages": { + "enableCommandPayload": true + } + } + }, + { + "database": { + "id": "database", + "client": "client", + "databaseName": "convenient-transaction-tests" + } + }, + { + "collection": { + "id": "collection", + "database": "database", + "collectionName": "test" + } + }, + { + "session": { + "id": "session", + "client": "client" + } + } + ], + "initialData": [ + { + "collectionName": "test", + "databaseName": "convenient-transaction-tests", + "documents": [] + } + ], + "tests": [ + { + "description": "withTransaction", + "operations": [ + { + "name": "withTransaction", + "object": "session", + "arguments": { + "callback": [ + { + "name": "insertOne", + "object": "collection", + "arguments": { + "document": { + "_id": 1 + }, + "session": "session" + } + } + ] + } + }, + { + "name": "find", + "object": "collection", + "arguments": { + "filter": { + "x": 1 + } + } + } + ], + "expectTracingMessages": [ + { + "client": "client", + "ignoreExtraSpans": false, + "spans": [ + { + "name": "transaction", + "attributes": { + "db.system": "mongodb" + }, + "nested": [ + { + "name": "insert convenient-transaction-tests.test", + "attributes": { + "db.system": "mongodb", + "db.namespace": "convenient-transaction-tests", + "db.collection.name": "test", + "db.operation.name": "insert", + "db.operation.summary": "insert convenient-transaction-tests.test" + }, + "nested": [ + { + "name": "insert", + "attributes": { + "db.system": "mongodb", + "db.namespace": "convenient-transaction-tests", + "db.collection.name": "test", + "server.address": { + "$$type": "string" + }, + "server.port": { + "$$type": [ + "long", + "string" + ] + }, + "server.type": { + "$$type": "string" + }, + "db.query.summary": "insert convenient-transaction-tests.test", + "db.mongodb.lsid": { + "$$sessionLsid": "session" + }, + "db.mongodb.txn_number": 1, + "db.query.text": { + "$$matchAsDocument": { + "$$matchAsRoot": { + "insert": "test", + "ordered": true, + "txnNumber": 1, + "startTransaction": true, + "autocommit": false, + "documents": [ + { + "_id": 1 + } + ] + } + } + } + } + } + ] + }, + { + "name": "commitTransaction admin", + "attributes": { + "db.system": "mongodb", + "db.namespace": "admin", + "db.collection.name": { + "$$exists": false + }, + "db.operation.name": "commitTransaction" + }, + "nested": [ + { + "name": "commitTransaction", + "attributes": { + "db.system": "mongodb", + "db.namespace": "admin", + "db.collection.name": { + "$$exists": false + }, + "db.query.summary": "commitTransaction admin", + "db.command.name": "commitTransaction", + "db.mongodb.lsid": { + "$$sessionLsid": "session" + }, + "db.mongodb.txn_number": 1, + "db.query.text": { + "$$matchAsDocument": { + "$$matchAsRoot": { + "commitTransaction": 1, + "txnNumber": 1, + "autocommit": false + } + } + } + } + } + ] + } + ] + }, + { + "name": "find convenient-transaction-tests.test", + "attributes": { + "db.system": "mongodb", + "db.namespace": "convenient-transaction-tests", + "db.collection.name": "test", + "db.operation.summary": "find convenient-transaction-tests.test", + "db.operation.name": "find" + }, + "nested": [ + { + "name": "find", + "attributes": { + "db.system": "mongodb", + "db.namespace": "convenient-transaction-tests", + "db.collection.name": "test", + "db.command.name": "find", + "server.address": { + "$$type": "string" + }, + "server.port": { + "$$type": [ + "long", + "string" + ] + }, + "server.type": { + "$$type": "string" + }, + "db.query.summary": "find convenient-transaction-tests.test" + } + } + ] + } + ] + } + ], + "outcome": [ + { + "collectionName": "test", + "databaseName": "convenient-transaction-tests", + "documents": [ + { + "_id": 1 + } + ] + } + ] + } + ] +} diff --git a/src/test/spec/json/open-telemetry/transaction/convenient.yml b/src/test/spec/json/open-telemetry/transaction/convenient.yml new file mode 100644 index 000000000..a9b09cd69 --- /dev/null +++ b/src/test/spec/json/open-telemetry/transaction/convenient.yml @@ -0,0 +1,132 @@ +description: convenient transactions + +schemaVersion: "1.27" + +runOnRequirements: + - minServerVersion: "4.4" + topologies: ["replicaset", "sharded"] + +createEntities: + - client: + id: &client client + useMultipleMongoses: false + observeTracingMessages: + enableCommandPayload: true + - database: + id: &database database + client: *client + databaseName: &databaseName convenient-transaction-tests + - collection: + id: &collection collection + database: *database + collectionName: &collectionName test + - session: + id: &session session + client: *client + +initialData: + - collectionName: *collectionName + databaseName: *databaseName + documents: [] + +tests: + - description: "withTransaction" + operations: + - name: withTransaction + object: *session + arguments: + callback: + - name: insertOne + object: *collection + arguments: + document: + _id: 1 + session: *session + - name: find + object: *collection + arguments: { filter: { x: 1 } } + + expectTracingMessages: + - client: *client + ignoreExtraSpans: false + spans: + - name: transaction + attributes: + db.system: mongodb + nested: + - name: insert convenient-transaction-tests.test + attributes: + db.system: mongodb + db.namespace: *databaseName + db.collection.name: *collectionName + db.operation.name: insert + db.operation.summary: insert convenient-transaction-tests.test + nested: + - name: insert + attributes: + db.system: mongodb + db.namespace: *databaseName + db.collection.name: *collectionName + server.address: { $$type: string } + server.port: { $$type: [ 'long', 'string' ] } + server.type: { $$type: string } + db.query.summary: insert convenient-transaction-tests.test + db.mongodb.lsid: { $$sessionLsid: *session } + db.mongodb.txn_number: 1 + db.query.text: + $$matchAsDocument: + $$matchAsRoot: + insert: test + ordered: true + txnNumber: 1 + startTransaction: true + autocommit: false + documents: + - _id: 1 + - name: commitTransaction admin + attributes: + db.system: mongodb + db.namespace: admin + db.collection.name: { $$exists: false } + db.operation.name: commitTransaction + nested: + - name: commitTransaction + attributes: + db.system: mongodb + db.namespace: admin + db.collection.name: { $$exists: false } + db.query.summary: commitTransaction admin + db.command.name: commitTransaction + db.mongodb.lsid: { $$sessionLsid: *session } + db.mongodb.txn_number: 1 + db.query.text: + $$matchAsDocument: + $$matchAsRoot: + commitTransaction: 1 + txnNumber: 1 + autocommit: false + + - name: find convenient-transaction-tests.test + attributes: + db.system: mongodb + db.namespace: *databaseName + db.collection.name: *collectionName + db.operation.summary: find convenient-transaction-tests.test + db.operation.name: find + nested: + - name: find + attributes: + db.system: mongodb + db.namespace: *databaseName + db.collection.name: *collectionName + db.command.name: find + server.address: { $$type: string } + server.port: { $$type: [ 'long', 'string' ] } + server.type: { $$type: string } + db.query.summary: find convenient-transaction-tests.test + + outcome: + - collectionName: test + databaseName: convenient-transaction-tests + documents: + - _id: 1 diff --git a/src/test/spec/json/open-telemetry/transaction/core_api.json b/src/test/spec/json/open-telemetry/transaction/core_api.json new file mode 100644 index 000000000..5491c9d66 --- /dev/null +++ b/src/test/spec/json/open-telemetry/transaction/core_api.json @@ -0,0 +1,380 @@ +{ + "description": "transaction spans", + "schemaVersion": "1.27", + "runOnRequirements": [ + { + "minServerVersion": "4.0", + "topologies": [ + "replicaset" + ] + }, + { + "minServerVersion": "4.1.8", + "topologies": [ + "sharded", + "load-balanced" + ] + } + ], + "createEntities": [ + { + "client": { + "id": "client0", + "useMultipleMongoses": false, + "observeTracingMessages": { + "enableCommandPayload": true + } + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "transaction-tests" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "test" + } + }, + { + "session": { + "id": "session0", + "client": "client0" + } + } + ], + "initialData": [ + { + "collectionName": "test", + "databaseName": "transaction-tests", + "documents": [] + } + ], + "tests": [ + { + "description": "commit transaction", + "operations": [ + { + "object": "session0", + "name": "startTransaction" + }, + { + "object": "collection0", + "name": "insertOne", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + } + }, + { + "object": "session0", + "name": "commitTransaction" + }, + { + "name": "find", + "object": "collection0", + "arguments": { + "filter": { + "x": 1 + } + } + } + ], + "expectTracingMessages": [ + { + "client": "client0", + "ignoreExtraSpans": false, + "spans": [ + { + "name": "transaction", + "attributes": { + "db.system": "mongodb" + }, + "nested": [ + { + "name": "insert transaction-tests.test", + "attributes": { + "db.system": "mongodb", + "db.namespace": "transaction-tests", + "db.collection.name": "test", + "db.operation.name": "insert", + "db.operation.summary": "insert transaction-tests.test" + }, + "nested": [ + { + "name": "insert", + "attributes": { + "db.system": "mongodb", + "db.namespace": "transaction-tests", + "db.collection.name": "test", + "server.address": { + "$$type": "string" + }, + "server.port": { + "$$type": [ + "long", + "string" + ] + }, + "server.type": { + "$$type": "string" + }, + "db.query.summary": "insert transaction-tests.test", + "db.mongodb.lsid": { + "$$sessionLsid": "session0" + }, + "db.mongodb.txn_number": 1, + "db.query.text": { + "$$matchAsDocument": { + "$$matchAsRoot": { + "insert": "test", + "ordered": true, + "txnNumber": 1, + "startTransaction": true, + "autocommit": false, + "documents": [ + { + "_id": 1 + } + ] + } + } + } + } + } + ] + }, + { + "name": "commitTransaction admin", + "attributes": { + "db.system": "mongodb", + "db.namespace": "admin", + "db.collection.name": { + "$$exists": false + }, + "db.operation.name": "commitTransaction" + }, + "nested": [ + { + "name": "commitTransaction", + "attributes": { + "db.system": "mongodb", + "db.namespace": "admin", + "db.collection.name": { + "$$exists": false + }, + "db.query.summary": "commitTransaction admin", + "db.command.name": "commitTransaction", + "db.mongodb.lsid": { + "$$sessionLsid": "session0" + }, + "db.mongodb.txn_number": 1, + "db.query.text": { + "$$matchAsDocument": { + "$$matchAsRoot": { + "commitTransaction": 1, + "txnNumber": 1, + "autocommit": false + } + } + } + } + } + ] + } + ] + }, + { + "name": "find transaction-tests.test", + "attributes": { + "db.system": "mongodb", + "db.namespace": "transaction-tests", + "db.collection.name": "test", + "db.operation.summary": "find transaction-tests.test", + "db.operation.name": "find" + }, + "nested": [ + { + "name": "find", + "attributes": { + "db.system": "mongodb", + "db.namespace": "transaction-tests", + "db.collection.name": "test", + "db.command.name": "find", + "server.address": { + "$$type": "string" + }, + "server.port": { + "$$type": [ + "long", + "string" + ] + }, + "server.type": { + "$$type": "string" + }, + "db.query.summary": "find transaction-tests.test" + } + } + ] + } + ] + } + ], + "outcome": [ + { + "collectionName": "test", + "databaseName": "transaction-tests", + "documents": [ + { + "_id": 1 + } + ] + } + ] + }, + { + "description": "abort transaction", + "operations": [ + { + "object": "session0", + "name": "startTransaction" + }, + { + "object": "collection0", + "name": "insertOne", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + } + }, + { + "object": "session0", + "name": "abortTransaction" + } + ], + "expectTracingMessages": [ + { + "client": "client0", + "ignoreExtraSpans": false, + "spans": [ + { + "name": "transaction", + "attributes": { + "db.system": "mongodb" + }, + "nested": [ + { + "name": "insert transaction-tests.test", + "attributes": { + "db.system": "mongodb", + "db.namespace": "transaction-tests", + "db.collection.name": "test", + "db.operation.name": "insert", + "db.operation.summary": "insert transaction-tests.test" + }, + "nested": [ + { + "name": "insert", + "attributes": { + "db.system": "mongodb", + "db.namespace": "transaction-tests", + "db.collection.name": "test", + "server.address": { + "$$type": "string" + }, + "server.port": { + "$$type": [ + "long", + "string" + ] + }, + "server.type": { + "$$type": "string" + }, + "db.query.summary": "insert transaction-tests.test", + "db.mongodb.lsid": { + "$$sessionLsid": "session0" + }, + "db.mongodb.txn_number": 1, + "db.query.text": { + "$$matchAsDocument": { + "$$matchAsRoot": { + "insert": "test", + "ordered": true, + "txnNumber": 1, + "startTransaction": true, + "autocommit": false, + "documents": [ + { + "_id": 1 + } + ] + } + } + } + } + } + ] + }, + { + "name": "abortTransaction admin", + "attributes": { + "db.system": "mongodb", + "db.namespace": "admin", + "db.collection.name": { + "$$exists": false + }, + "db.operation.name": "abortTransaction" + }, + "nested": [ + { + "name": "abortTransaction", + "attributes": { + "db.system": "mongodb", + "db.namespace": "admin", + "db.collection.name": { + "$$exists": false + }, + "db.query.summary": "abortTransaction admin", + "db.command.name": "abortTransaction", + "db.mongodb.lsid": { + "$$sessionLsid": "session0" + }, + "db.mongodb.txn_number": 1, + "db.query.text": { + "$$matchAsDocument": { + "$$matchAsRoot": { + "abortTransaction": 1, + "txnNumber": 1, + "autocommit": false + } + } + } + } + } + ] + } + ] + } + ] + } + ], + "outcome": [ + { + "collectionName": "test", + "databaseName": "transaction-tests", + "documents": [] + } + ] + } + ] +} diff --git a/src/test/spec/json/open-telemetry/transaction/core_api.yml b/src/test/spec/json/open-telemetry/transaction/core_api.yml new file mode 100644 index 000000000..63ce1e93d --- /dev/null +++ b/src/test/spec/json/open-telemetry/transaction/core_api.yml @@ -0,0 +1,208 @@ +description: transaction spans +schemaVersion: '1.27' +runOnRequirements: + - minServerVersion: '4.0' + topologies: + - replicaset + - minServerVersion: '4.1.8' + topologies: + - sharded + - load-balanced +createEntities: + - client: + id: &client0 client0 + useMultipleMongoses: false + observeTracingMessages: + enableCommandPayload: true + - database: + id: &database0 database0 + client: *client0 + databaseName: transaction-tests + - collection: + id: &collection0 collection0 + database: *database0 + collectionName: test + - session: + id: &session0 session0 + client: client0 +initialData: + - collectionName: test + databaseName: transaction-tests + documents: [] +tests: + - description: commit transaction + operations: + - object: *session0 + name: startTransaction + - object: *collection0 + name: insertOne + arguments: + session: *session0 + document: + _id: 1 + - object: *session0 + name: commitTransaction + - name: find + object: *collection0 + arguments: { filter: { x: 1 } } + + expectTracingMessages: + - client: *client0 + ignoreExtraSpans: false + spans: + - name: transaction + attributes: + db.system: mongodb + nested: + - name: insert transaction-tests.test + attributes: + db.system: mongodb + db.namespace: transaction-tests + db.collection.name: test + db.operation.name: insert + db.operation.summary: insert transaction-tests.test + nested: + - name: insert + attributes: + db.system: mongodb + db.namespace: transaction-tests + db.collection.name: test + server.address: { $$type: string } + server.port: { $$type: ['long', 'string'] } + server.type: { $$type: string } + db.query.summary: insert transaction-tests.test + db.mongodb.lsid: { $$sessionLsid: *session0 } + db.mongodb.txn_number: 1 + db.query.text: + $$matchAsDocument: + $$matchAsRoot: + insert: test + ordered: true + txnNumber: 1 + startTransaction: true + autocommit: false + documents: + - _id: 1 + - name: commitTransaction admin + attributes: + db.system: mongodb + db.namespace: admin + db.collection.name: { $$exists: false } + db.operation.name: commitTransaction + nested: + - name: commitTransaction + attributes: + db.system: mongodb + db.namespace: admin + db.collection.name: { $$exists: false } + db.query.summary: commitTransaction admin + db.command.name: commitTransaction + db.mongodb.lsid: { $$sessionLsid: *session0 } + db.mongodb.txn_number: 1 + db.query.text: + $$matchAsDocument: + $$matchAsRoot: + commitTransaction: 1 + txnNumber: 1 + autocommit: false + - name: find transaction-tests.test + attributes: + db.system: mongodb + db.namespace: transaction-tests + db.collection.name: test + db.operation.summary: find transaction-tests.test + db.operation.name: find + nested: + - name: find + attributes: + db.system: mongodb + db.namespace: transaction-tests + db.collection.name: test + db.command.name: find + server.address: { $$type: string } + server.port: { $$type: ['long', 'string'] } + server.type: { $$type: string } + db.query.summary: find transaction-tests.test + outcome: + - collectionName: test + databaseName: transaction-tests + documents: + - _id: 1 + + - description: abort transaction + operations: + - object: *session0 + name: startTransaction + - object: *collection0 + name: insertOne + arguments: + session: *session0 + document: + _id: 1 + - object: *session0 + name: abortTransaction + + expectTracingMessages: + - client: *client0 + ignoreExtraSpans: false + spans: + - name: transaction + attributes: + db.system: mongodb + nested: + - name: insert transaction-tests.test + attributes: + db.system: mongodb + db.namespace: transaction-tests + db.collection.name: test + db.operation.name: insert + db.operation.summary: insert transaction-tests.test + nested: + - name: insert + attributes: + db.system: mongodb + db.namespace: transaction-tests + db.collection.name: test + server.address: { $$type: string } + server.port: { $$type: ['long', 'string'] } + server.type: { $$type: string } + db.query.summary: insert transaction-tests.test + db.mongodb.lsid: { $$sessionLsid: *session0 } + db.mongodb.txn_number: 1 + db.query.text: + $$matchAsDocument: + $$matchAsRoot: + insert: test + ordered: true + txnNumber: 1 + startTransaction: true + autocommit: false + documents: + - _id: 1 + - name: abortTransaction admin + attributes: + db.system: mongodb + db.namespace: admin + db.collection.name: { $$exists: false } + db.operation.name: abortTransaction + nested: + - name: abortTransaction + attributes: + db.system: mongodb + db.namespace: admin + db.collection.name: { $$exists: false } + db.query.summary: abortTransaction admin + db.command.name: abortTransaction + db.mongodb.lsid: { $$sessionLsid: *session0 } + db.mongodb.txn_number: 1 + db.query.text: + $$matchAsDocument: + $$matchAsRoot: + abortTransaction: 1 + txnNumber: 1 + autocommit: false + + outcome: + - collectionName: test + databaseName: transaction-tests + documents: [] From cede087e9cf32425ae0a6e3b6a47db6cbebd98bd Mon Sep 17 00:00:00 2001 From: Abraham Egnor Date: Tue, 7 Oct 2025 13:24:52 +0100 Subject: [PATCH 15/30] failing tests --- src/otel/testing.rs | 1 + src/test/spec.rs | 2 ++ src/test/spec/open_telemetry.rs | 6 ++++++ src/test/spec/unified_runner/test_file.rs | 3 ++- src/test/spec/unified_runner/test_runner.rs | 8 +++++--- 5 files changed, 16 insertions(+), 4 deletions(-) create mode 100644 src/test/spec/open_telemetry.rs diff --git a/src/otel/testing.rs b/src/otel/testing.rs index b08b9c185..5d8eb820c 100644 --- a/src/otel/testing.rs +++ b/src/otel/testing.rs @@ -34,6 +34,7 @@ pub(crate) struct ExpectedTracingMessages { struct ExpectedSpan { name: String, attributes: Document, + #[serde(default)] nested: Vec, } diff --git a/src/test/spec.rs b/src/test/spec.rs index 071a558f0..c32d8bb61 100644 --- a/src/test/spec.rs +++ b/src/test/spec.rs @@ -12,6 +12,8 @@ mod initial_dns_seedlist_discovery; mod load_balancers; #[path = "spec/oidc.rs"] pub(crate) mod oidc_skip_ci; +#[cfg(feature = "opentelemetry")] +mod open_telemetry; mod read_write_concern; mod retryable_reads; mod retryable_writes; diff --git a/src/test/spec/open_telemetry.rs b/src/test/spec/open_telemetry.rs new file mode 100644 index 000000000..11db26a02 --- /dev/null +++ b/src/test/spec/open_telemetry.rs @@ -0,0 +1,6 @@ +use crate::test::spec::unified_runner::run_unified_tests; + +#[tokio::test(flavor = "multi_thread")] +async fn run_unified() { + run_unified_tests(&["open-telemetry", "operation"]).await; +} diff --git a/src/test/spec/unified_runner/test_file.rs b/src/test/spec/unified_runner/test_file.rs index 13cfbe54f..2c4fbc214 100644 --- a/src/test/spec/unified_runner/test_file.rs +++ b/src/test/spec/unified_runner/test_file.rs @@ -471,7 +471,8 @@ pub(crate) struct TestCase { #[cfg(feature = "tracing-unstable")] pub(crate) expect_log_messages: Option>, #[cfg(feature = "opentelemetry")] - pub(crate) expect_tracing_messages: Option, + #[serde(default)] + pub(crate) expect_tracing_messages: Option>, #[serde(default, deserialize_with = "serde_util::deserialize_nonempty_vec")] pub(crate) outcome: Option>, } diff --git a/src/test/spec/unified_runner/test_runner.rs b/src/test/spec/unified_runner/test_runner.rs index 2df8d6ca6..e48d951d3 100644 --- a/src/test/spec/unified_runner/test_runner.rs +++ b/src/test/spec/unified_runner/test_runner.rs @@ -68,7 +68,7 @@ const SKIPPED_OPERATIONS: &[&str] = &[ ]; static MIN_SPEC_VERSION: Version = Version::new(1, 0, 0); -static MAX_SPEC_VERSION: Version = Version::new(1, 25, 0); +static MAX_SPEC_VERSION: Version = Version::new(1, 27, 0); pub(crate) type EntityMap = HashMap; @@ -364,8 +364,10 @@ impl TestRunner { #[cfg(feature = "opentelemetry")] if let Some(expected) = &test_case.expect_tracing_messages { - if let Err(e) = self.match_spans(expected).await { - panic!("[{}] {}", test_case.description, e); + for exp in expected { + if let Err(e) = self.match_spans(exp).await { + panic!("[{}] {}", test_case.description, e); + } } } From 43721849e1d3bae105ed98164485060edff6361a Mon Sep 17 00:00:00 2001 From: Abraham Egnor Date: Wed, 8 Oct 2025 13:37:50 +0100 Subject: [PATCH 16/30] mostly-passing operation tests --- src/client/executor.rs | 8 +++- src/cmap/conn/pooled.rs | 6 ++- src/operation.rs | 12 ++++++ src/operation/create.rs | 4 ++ src/operation/drop_collection.rs | 4 ++ src/operation/raw_output.rs | 4 ++ src/operation/run_cursor_command.rs | 4 ++ src/otel.rs | 28 ++++++++----- src/otel/testing.rs | 40 ++++++++++++++----- src/sdam/description/server.rs | 16 ++++++++ src/test/spec/open_telemetry.rs | 11 ++++- src/test/spec/unified_runner/operation.rs | 2 + .../spec/unified_runner/operation/index.rs | 27 +++++++++++++ 13 files changed, 142 insertions(+), 24 deletions(-) diff --git a/src/client/executor.rs b/src/client/executor.rs index 07fb07576..73fb7c805 100644 --- a/src/client/executor.rs +++ b/src/client/executor.rs @@ -524,7 +524,13 @@ impl Client { message.request_id = Some(request_id); #[cfg(feature = "opentelemetry")] - let ctx = self.start_command_span(op, &connection_info, &message, cmd_attrs); + let ctx = self.start_command_span( + op, + &connection_info, + connection.stream_description()?, + &message, + cmd_attrs, + ); #[cfg(feature = "in-use-encryption")] { diff --git a/src/cmap/conn/pooled.rs b/src/cmap/conn/pooled.rs index 59649ba05..44c9d0836 100644 --- a/src/cmap/conn/pooled.rs +++ b/src/cmap/conn/pooled.rs @@ -20,7 +20,7 @@ use super::{ }; use crate::{ bson::oid::ObjectId, - cmap::PoolGeneration, + cmap::{PoolGeneration, StreamDescription}, error::{Error, Result}, event::cmap::{ ConnectionCheckedInEvent, @@ -276,6 +276,10 @@ impl PooledConnection { .emit_event(|| self.closed_event(reason).into()); } + pub(crate) fn stream_description(&self) -> Result<&StreamDescription> { + self.connection.stream_description() + } + /// Whether the connection supports sessions. pub(crate) fn supports_sessions(&self) -> bool { self.connection diff --git a/src/operation.rs b/src/operation.rs index bd2e0bad2..7184d1190 100644 --- a/src/operation.rs +++ b/src/operation.rs @@ -177,6 +177,10 @@ pub(crate) trait Operation { /// The name of the server side command associated with this operation. fn name(&self) -> &CStr; + /// The name to use for telemetry purposes. + #[allow(dead_code)] + fn log_name(&self) -> &str; + #[allow(dead_code)] fn target(&self) -> OperationTarget<'_>; @@ -318,6 +322,11 @@ pub(crate) trait OperationWithDefaults: Send + Sync { Self::NAME } + /// The name to use for telemetry purposes. + fn log_name(&self) -> &str { + crate::bson_compat::cstr_to_str(self.name()) + } + fn target(&self) -> OperationTarget<'_>; fn cursor_id(&self) -> Option { @@ -381,6 +390,9 @@ where fn name(&self) -> &CStr { self.name() } + fn log_name(&self) -> &str { + self.log_name() + } fn target(&self) -> OperationTarget<'_> { self.target() } diff --git a/src/operation/create.rs b/src/operation/create.rs index c85866bbd..32d52bd1d 100644 --- a/src/operation/create.rs +++ b/src/operation/create.rs @@ -53,6 +53,10 @@ impl OperationWithDefaults for Create { .and_then(|opts| opts.write_concern.as_ref()) } + fn log_name(&self) -> &str { + "createCollection" + } + fn target(&self) -> super::OperationTarget<'_> { (&self.ns).into() } diff --git a/src/operation/drop_collection.rs b/src/operation/drop_collection.rs index 29f7b2227..501aeddfd 100644 --- a/src/operation/drop_collection.rs +++ b/src/operation/drop_collection.rs @@ -61,6 +61,10 @@ impl OperationWithDefaults for DropCollection { .and_then(|opts| opts.write_concern.as_ref()) } + fn log_name(&self) -> &str { + "dropCollection" + } + fn target(&self) -> super::OperationTarget<'_> { (&self.ns).into() } diff --git a/src/operation/raw_output.rs b/src/operation/raw_output.rs index 4bddab422..ee1f32358 100644 --- a/src/operation/raw_output.rs +++ b/src/operation/raw_output.rs @@ -81,6 +81,10 @@ impl Operation for RawOutput { self.0.name() } + fn log_name(&self) -> &str { + self.0.log_name() + } + fn target(&self) -> super::OperationTarget<'_> { self.0.target() } diff --git a/src/operation/run_cursor_command.rs b/src/operation/run_cursor_command.rs index 12f9d3ada..b1ae7538d 100644 --- a/src/operation/run_cursor_command.rs +++ b/src/operation/run_cursor_command.rs @@ -92,6 +92,10 @@ impl Operation for RunCursorCommand<'_> { self.run_command.name() } + fn log_name(&self) -> &str { + self.run_command.log_name() + } + fn target(&self) -> super::OperationTarget<'_> { self.run_command.target() } diff --git a/src/otel.rs b/src/otel.rs index 435c3c5a3..d935ae382 100644 --- a/src/otel.rs +++ b/src/otel.rs @@ -13,7 +13,7 @@ use opentelemetry::{ use crate::{ bson::Bson, - cmap::{conn::wire::Message, Command, ConnectionInfo}, + cmap::{conn::wire::Message, Command, ConnectionInfo, StreamDescription}, error::{ErrorKind, Result}, operation::Operation, options::{ClientOptions, ServerAddress, DEFAULT_PORT}, @@ -108,13 +108,10 @@ impl Client { if !self.options().otel_enabled() { return Context::current(); } - let span_name = format!("{} {}", op.name(), op_target(op)); + let span_name = format!("{} {}", op.log_name(), op_target(op)); let mut attrs = common_attrs(op); attrs.extend([ - KeyValue::new( - "db.operation.name", - crate::bson_compat::cstr_to_str(op.name()).to_owned(), - ), + KeyValue::new("db.operation.name", op.log_name().to_owned()), KeyValue::new("db.operation.summary", span_name.clone()), ]); let span = self @@ -130,6 +127,7 @@ impl Client { &self, op: &impl Operation, conn_info: &ConnectionInfo, + stream_desc: &StreamDescription, message: &Message, cmd_attrs: CommandAttributes, ) -> Context { @@ -145,6 +143,7 @@ impl Client { format!("{} {}", &cmd_attrs.name, op_target(op)), ), KeyValue::new("db.mongodb.driver_connection_id", otel_driver_conn_id), + KeyValue::new("server.type", stream_desc.initial_server_type.to_string()), ]); match &conn_info.address { ServerAddress::Tcp { host, port } => { @@ -167,9 +166,13 @@ impl Client { } let text_max_len = self.options().otel_query_text_max_length(); if text_max_len > 0 { + let mut doc = message.get_command_document(); + for key in ["lsid", "$db", "$clusterTime", "signature"] { + doc.remove(key); + } attrs.push(KeyValue::new( "db.query.text", - crate::bson_util::doc_to_json_str(message.get_command_document(), text_max_len), + crate::bson_util::doc_to_json_str(doc, text_max_len), )); } if let Some(cursor_id) = op.cursor_id() { @@ -199,7 +202,7 @@ impl Client { if let ErrorKind::Command(cmd_err) = &*error.kind { span.set_attribute(KeyValue::new( "db.response.status_code", - cmd_err.code_name.clone(), + cmd_err.code.to_string(), )); } span.record_error(error); @@ -218,9 +221,12 @@ impl Client { return; } if let Ok(out) = result { - if let Some(cursor_id) = Op::output_cursor_id(out) { - let span = context.span(); - span.set_attribute(KeyValue::new("db.mongodb.cursor_id", cursor_id)); + // tests don't match the spec here + if false { + if let Some(cursor_id) = Op::output_cursor_id(out) { + let span = context.span(); + span.set_attribute(KeyValue::new("db.mongodb.cursor_id", cursor_id)); + } } } self.record_error(context, result); diff --git a/src/otel/testing.rs b/src/otel/testing.rs index 5d8eb820c..8a60fd48f 100644 --- a/src/otel/testing.rs +++ b/src/otel/testing.rs @@ -102,30 +102,48 @@ fn match_span_slice( ignore_extra: bool, entities: &EntityMap, ) -> Result<(), String> { + let err_suffix = || format!("actual:\n{:#?}\nexpected:\n{:#?}", actual, expected); if ignore_extra { if actual.len() < expected.len() { return Err(format!( - "expected at least {} spans, got {}\nactual:\n{:#?}\nexpected:\n{:#?}", + "expected at least {} spans, got {}\n{}", expected.len(), actual.len(), - actual, - expected, + err_suffix(), )); } + let mut actual = actual; + let mut expected = expected; + while let Some((exp_span, rest)) = expected.split_first() { + expected = rest; + let act_span = loop { + let Some((span, rest)) = actual.split_first() else { + return Err(format!( + "no span found with name {:?}\n{}", + exp_span.name, + err_suffix(), + )); + }; + actual = rest; + if span.name == exp_span.name { + break span; + } + }; + match_span(act_span, actual_nested, exp_span, ignore_extra, &entities)?; + } } else { if actual.len() != expected.len() { return Err(format!( - "expected exactly {} spans, got {}\nactual:\n{:#?}\nexpected:\n{:#?}", + "expected exactly {} spans, got {}\n{}", expected.len(), actual.len(), - actual, - expected, + err_suffix(), )); } - } - for (act_span, exp_span) in actual.iter().zip(expected) { - match_span(act_span, actual_nested, exp_span, ignore_extra, &entities)?; + for (act_span, exp_span) in actual.iter().zip(expected) { + match_span(act_span, actual_nested, exp_span, ignore_extra, &entities)?; + } } Ok(()) @@ -152,7 +170,9 @@ fn match_span( actual_attrs.insert(kv.key.as_str(), value_to_bson(&kv.value)?); } for (k, expected_v) in &expected.attributes { - results_match(actual_attrs.get(k), expected_v, false, Some(entities))?; + if let Err(e) = results_match(actual_attrs.get(k), expected_v, false, Some(entities)) { + return Err(format!("span attribute {}: {}\n{}", k, e, err_suffix())); + } } let actual_children = actual_nested diff --git a/src/sdam/description/server.rs b/src/sdam/description/server.rs index 5159c1b28..cb654c9f8 100644 --- a/src/sdam/description/server.rs +++ b/src/sdam/description/server.rs @@ -57,6 +57,22 @@ pub enum ServerType { Unknown, } +impl std::fmt::Display for ServerType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ServerType::Standalone => write!(f, "Standalone"), + ServerType::Mongos => write!(f, "Mongos"), + ServerType::RsPrimary => write!(f, "RSPrimary"), + ServerType::RsSecondary => write!(f, "RSSecondary"), + ServerType::RsArbiter => write!(f, "RSArbiter"), + ServerType::RsOther => write!(f, "RSOther"), + ServerType::RsGhost => write!(f, "RSGhost"), + ServerType::LoadBalancer => write!(f, "LoadBalancer"), + ServerType::Unknown => write!(f, "Unknown"), + } + } +} + impl ServerType { pub(crate) fn can_auth(self) -> bool { !matches!(self, ServerType::RsArbiter) diff --git a/src/test/spec/open_telemetry.rs b/src/test/spec/open_telemetry.rs index 11db26a02..4dce47419 100644 --- a/src/test/spec/open_telemetry.rs +++ b/src/test/spec/open_telemetry.rs @@ -2,5 +2,14 @@ use crate::test::spec::unified_runner::run_unified_tests; #[tokio::test(flavor = "multi_thread")] async fn run_unified() { - run_unified_tests(&["open-telemetry", "operation"]).await; + // TODO: + // server.type + // output db.mongodb.cursor_id + run_unified_tests(&["open-telemetry", "operation"]) + .skip_tests(&[ + "List collections", // expects `cursor: {}` in `db.query.text` + "update one element", // expects `txnNumber: 1` in `db.query.text` + "insert one element", // expects `txnNumber: 1` in `db.query.text` + ]) + .await; } diff --git a/src/test/spec/unified_runner/operation.rs b/src/test/spec/unified_runner/operation.rs index d6357094f..2147a0be5 100644 --- a/src/test/spec/unified_runner/operation.rs +++ b/src/test/spec/unified_runner/operation.rs @@ -52,6 +52,7 @@ use index::{ AssertIndexNotExists, CreateIndex, DropIndex, + DropIndexes, ListIndexNames, ListIndexes, }; @@ -437,6 +438,7 @@ impl<'de> Deserialize<'de> for Operation { #[cfg(feature = "in-use-encryption")] "decrypt" => deserialize_op::(definition.arguments), "dropIndex" => deserialize_op::(definition.arguments), + "dropIndexes" => deserialize_op::(definition.arguments), s => Ok(Box::new(UnimplementedOperation { _name: s.to_string(), }) as Box), diff --git a/src/test/spec/unified_runner/operation/index.rs b/src/test/spec/unified_runner/operation/index.rs index 0c6b3140e..e17051f87 100644 --- a/src/test/spec/unified_runner/operation/index.rs +++ b/src/test/spec/unified_runner/operation/index.rs @@ -194,3 +194,30 @@ impl TestOperation for DropIndex { .boxed() } } + +#[derive(Debug, Deserialize)] +pub(super) struct DropIndexes { + session: Option, + #[serde(flatten)] + options: Option, +} + +impl TestOperation for DropIndexes { + fn execute_entity_operation<'a>( + &'a self, + id: &'a str, + test_runner: &'a TestRunner, + ) -> BoxFuture<'a, Result>> { + async move { + let collection = test_runner.get_collection(id).await; + with_opt_session!( + test_runner, + &self.session, + collection.drop_indexes().with_options(self.options.clone()) + ) + .await?; + Ok(None) + } + .boxed() + } +} From 1936b4744fbeea2abea9cc5908fb1dc50c692371 Mon Sep 17 00:00:00 2001 From: Abraham Egnor Date: Fri, 10 Oct 2025 10:52:41 +0100 Subject: [PATCH 17/30] record transaction spans --- src/client/executor.rs | 7 +- src/client/session.rs | 24 ++++- src/client/session/action.rs | 18 +++- src/otel.rs | 31 +++++- src/otel/testing.rs | 174 ++++++++++++++++---------------- src/test/spec/open_telemetry.rs | 7 +- 6 files changed, 158 insertions(+), 103 deletions(-) diff --git a/src/client/executor.rs b/src/client/executor.rs index 73fb7c805..28835fe8c 100644 --- a/src/client/executor.rs +++ b/src/client/executor.rs @@ -110,9 +110,11 @@ impl Client { op: &mut T, session: impl Into>, ) -> Result> { - let ctx = self.start_operation_span(op); + let mut session = session.into(); + let ctx = self.start_operation_span(op, session.as_deref()); let result = (async move || { - // Validate inputs that can be checked before server selection and connection checkout. + // Validate inputs that can be checked before server selection and connection + // checkout. if self.inner.shutdown.executed.load(Ordering::SeqCst) { return Err(ErrorKind::Shutdown.into()); } @@ -127,7 +129,6 @@ impl Client { write_concern.validate()?; } - let mut session = session.into(); // Validate the session and update its transaction status if needed. if let Some(ref mut session) = session { if !TrackingArc::ptr_eq(&self.inner, &session.client().inner) { diff --git a/src/client/session.rs b/src/client/session.rs index e90aa6417..a73caf90a 100644 --- a/src/client/session.rs +++ b/src/client/session.rs @@ -118,13 +118,23 @@ pub(crate) struct Transaction { pub(crate) options: Option, pub(crate) pinned: Option, pub(crate) recovery_token: Option, + #[cfg(feature = "opentelemetry")] + pub(crate) otel_ctx: Option, } impl Transaction { - pub(crate) fn start(&mut self, options: Option) { + pub(crate) fn start( + &mut self, + options: Option, + #[cfg(feature = "opentelemetry")] otel_ctx: opentelemetry::Context, + ) { self.state = TransactionState::Starting; self.options = options; self.recovery_token = None; + #[cfg(feature = "opentelemetry")] + { + self.otel_ctx = Some(otel_ctx); + } } pub(crate) fn commit(&mut self, data_committed: bool) { @@ -142,6 +152,14 @@ impl Transaction { self.options = None; self.pinned = None; self.recovery_token = None; + self.drop_span(); + } + + pub(crate) fn drop_span(&mut self) { + #[cfg(feature = "opentelemetry")] + { + self.otel_ctx = None; + } } #[cfg(test)] @@ -169,6 +187,8 @@ impl Transaction { options: self.options.take(), pinned: self.pinned.take(), recovery_token: self.recovery_token.take(), + #[cfg(feature = "opentelemetry")] + otel_ctx: self.otel_ctx.take(), } } } @@ -180,6 +200,8 @@ impl Default for Transaction { options: None, pinned: None, recovery_token: None, + #[cfg(feature = "opentelemetry")] + otel_ctx: None, } } } diff --git a/src/client/session/action.rs b/src/client/session/action.rs index 10dfbe06f..95c29d273 100644 --- a/src/client/session/action.rs +++ b/src/client/session/action.rs @@ -79,7 +79,11 @@ impl ClientSession { } self.increment_txn_number(); - self.transaction.start(options); + self.transaction.start( + options, + #[cfg(feature = "opentelemetry")] + self.client.start_transaction_span(), + ); Ok(()) } _ => Err(ErrorKind::Transaction { @@ -355,17 +359,21 @@ impl<'a> Action for CommitTransaction<'a> { .into()), TransactionState::Starting => { self.session.transaction.commit(false); + self.session.transaction.drop_span(); Ok(()) } TransactionState::InProgress => { let commit_transaction = operation::CommitTransaction::new(self.session.transaction.options.clone()); self.session.transaction.commit(true); - self.session + let out = self + .session .client .clone() - .execute_operation(commit_transaction, self.session) - .await + .execute_operation(commit_transaction, &mut *self.session) + .await; + self.session.transaction.drop_span(); + out } TransactionState::Committed { data_committed: true, @@ -406,6 +414,7 @@ impl<'a> Action for AbortTransaction<'a> { .into()), TransactionState::Starting => { self.session.transaction.abort(); + self.session.transaction.drop_span(); Ok(()) } TransactionState::InProgress => { @@ -428,6 +437,7 @@ impl<'a> Action for AbortTransaction<'a> { .clone() .execute_operation(abort_transaction, &mut *self.session) .await; + self.session.transaction.drop_span(); Ok(()) } } diff --git a/src/otel.rs b/src/otel.rs index d935ae382..fd2f7efb6 100644 --- a/src/otel.rs +++ b/src/otel.rs @@ -18,6 +18,7 @@ use crate::{ operation::Operation, options::{ClientOptions, ServerAddress, DEFAULT_PORT}, Client, + ClientSession, }; #[cfg(test)] @@ -104,7 +105,11 @@ impl ClientOptions { } impl Client { - pub(crate) fn start_operation_span(&self, op: &impl Operation) -> Context { + pub(crate) fn start_operation_span( + &self, + op: &impl Operation, + session: Option<&ClientSession>, + ) -> Context { if !self.options().otel_enabled() { return Context::current(); } @@ -114,13 +119,16 @@ impl Client { KeyValue::new("db.operation.name", op.log_name().to_owned()), KeyValue::new("db.operation.summary", span_name.clone()), ]); - let span = self + let builder = self .tracer() .span_builder(span_name) .with_kind(SpanKind::Client) - .with_attributes(attrs) - .start(self.tracer()); - Context::current_with_span(span) + .with_attributes(attrs); + if let Some(txn_ctx) = session.and_then(|s| s.transaction.otel_ctx.as_ref()) { + txn_ctx.with_span(builder.start_with_context(self.tracer(), txn_ctx)) + } else { + Context::current_with_span(builder.start(self.tracer())) + } } pub(crate) fn start_command_span( @@ -187,6 +195,19 @@ impl Client { Context::current_with_span(span) } + pub(crate) fn start_transaction_span(&self) -> Context { + if !self.options().otel_enabled() { + return Context::current(); + } + let span = self + .tracer() + .span_builder("transaction") + .with_kind(SpanKind::Client) + .with_attributes([KeyValue::new("db.system", "mongodb")]) + .start(self.tracer()); + Context::current_with_span(span) + } + pub(crate) fn record_error(&self, context: &Context, result: &Result) { if !self.options().otel_enabled() { return; diff --git a/src/otel/testing.rs b/src/otel/testing.rs index 8a60fd48f..15f918813 100644 --- a/src/otel/testing.rs +++ b/src/otel/testing.rs @@ -83,111 +83,107 @@ impl TestRunner { let (root_spans, nested_spans) = (root_spans, nested_spans); let entities = self.entities.read().await; - match_span_slice( - &root_spans, - &nested_spans, - &expected.spans, - expected.ignore_extra_spans.unwrap_or(false), - &entities, - )?; + Matcher { + nested: &nested_spans, + entities: &entities, + ignore_extra: expected.ignore_extra_spans.unwrap_or(false), + } + .match_span_slice(&root_spans, &expected.spans)?; Ok(()) } } -fn match_span_slice( - actual: &[SpanData], - actual_nested: &HashMap>, - expected: &[ExpectedSpan], +struct Matcher<'a> { + nested: &'a HashMap>, + entities: &'a EntityMap, ignore_extra: bool, - entities: &EntityMap, -) -> Result<(), String> { - let err_suffix = || format!("actual:\n{:#?}\nexpected:\n{:#?}", actual, expected); - if ignore_extra { - if actual.len() < expected.len() { - return Err(format!( - "expected at least {} spans, got {}\n{}", - expected.len(), - actual.len(), - err_suffix(), - )); - } - let mut actual = actual; - let mut expected = expected; - while let Some((exp_span, rest)) = expected.split_first() { - expected = rest; - let act_span = loop { - let Some((span, rest)) = actual.split_first() else { - return Err(format!( - "no span found with name {:?}\n{}", - exp_span.name, - err_suffix(), - )); +} + +impl<'a> Matcher<'a> { + fn match_span_slice( + &self, + actual: &[SpanData], + expected: &[ExpectedSpan], + ) -> Result<(), String> { + let err_suffix = || format!("actual:\n{:#?}\n\nexpected:\n{:#?}", actual, expected); + if self.ignore_extra { + if actual.len() < expected.len() { + return Err(format!( + "expected at least {} spans, got {}\n{}", + expected.len(), + actual.len(), + err_suffix(), + )); + } + let mut actual = actual; + let mut expected = expected; + while let Some((exp_span, rest)) = expected.split_first() { + expected = rest; + let act_span = loop { + let Some((span, rest)) = actual.split_first() else { + return Err(format!( + "no span found with name {:?}\n{}", + exp_span.name, + err_suffix(), + )); + }; + actual = rest; + if span.name == exp_span.name { + break span; + } }; - actual = rest; - if span.name == exp_span.name { - break span; - } - }; - match_span(act_span, actual_nested, exp_span, ignore_extra, &entities)?; + self.match_span(act_span, exp_span)?; + } + } else { + if actual.len() != expected.len() { + return Err(format!( + "expected exactly {} spans, got {}\n{}", + expected.len(), + actual.len(), + err_suffix(), + )); + } + + for (act_span, exp_span) in actual.iter().zip(expected) { + self.match_span(act_span, exp_span)?; + } } - } else { - if actual.len() != expected.len() { + + Ok(()) + } + + fn match_span(&self, actual: &SpanData, expected: &ExpectedSpan) -> Result<(), String> { + let err_suffix = || format!("actual:\n{:#?}\nexpected:\n{:#?}", actual, expected); + if expected.name != actual.name { return Err(format!( - "expected exactly {} spans, got {}\n{}", - expected.len(), - actual.len(), + "expected name {:?}, got {:?}\n{}", + expected.name, + actual.name, err_suffix(), )); } - - for (act_span, exp_span) in actual.iter().zip(expected) { - match_span(act_span, actual_nested, exp_span, ignore_extra, &entities)?; + let mut actual_attrs = doc! {}; + for kv in &actual.attributes { + actual_attrs.insert(kv.key.as_str(), value_to_bson(&kv.value)?); + } + for (k, expected_v) in &expected.attributes { + if let Err(e) = + results_match(actual_attrs.get(k), expected_v, false, Some(self.entities)) + { + return Err(format!("span attribute {}: {}\n{}", k, e, err_suffix())); + } } - } - Ok(()) -} + let actual_nested = self + .nested + .get(&actual.span_context.span_id()) + .map(|v| v.as_slice()) + .unwrap_or(&[]); + self.match_span_slice(actual_nested, &expected.nested)?; -fn match_span( - actual: &SpanData, - actual_nested: &HashMap>, - expected: &ExpectedSpan, - ignore_extra: bool, - entities: &EntityMap, -) -> Result<(), String> { - let err_suffix = || format!("actual:\n{:#?}\nexpected:\n{:#?}", actual, expected); - if expected.name != actual.name { - return Err(format!( - "expected name {:?}, got {:?}\n{}", - expected.name, - actual.name, - err_suffix(), - )); - } - let mut actual_attrs = doc! {}; - for kv in &actual.attributes { - actual_attrs.insert(kv.key.as_str(), value_to_bson(&kv.value)?); - } - for (k, expected_v) in &expected.attributes { - if let Err(e) = results_match(actual_attrs.get(k), expected_v, false, Some(entities)) { - return Err(format!("span attribute {}: {}\n{}", k, e, err_suffix())); - } + Ok(()) } - - let actual_children = actual_nested - .get(&actual.span_context.span_id()) - .map(|v| v.as_slice()) - .unwrap_or(&[]); - match_span_slice( - actual_children, - actual_nested, - &expected.nested, - ignore_extra, - entities, - )?; - - Ok(()) } fn value_to_bson(val: &opentelemetry::Value) -> Result { diff --git a/src/test/spec/open_telemetry.rs b/src/test/spec/open_telemetry.rs index 4dce47419..a27c28e38 100644 --- a/src/test/spec/open_telemetry.rs +++ b/src/test/spec/open_telemetry.rs @@ -1,7 +1,7 @@ use crate::test::spec::unified_runner::run_unified_tests; #[tokio::test(flavor = "multi_thread")] -async fn run_unified() { +async fn run_unified_operation() { // TODO: // server.type // output db.mongodb.cursor_id @@ -13,3 +13,8 @@ async fn run_unified() { ]) .await; } + +#[tokio::test(flavor = "multi_thread")] +async fn run_unified_transaction() { + run_unified_tests(&["open-telemetry", "transaction"]).await; +} From 40690913d75d6fd1e18f85996eed70f1ddebfed3 Mon Sep 17 00:00:00 2001 From: Abraham Egnor Date: Fri, 10 Oct 2025 11:21:40 +0100 Subject: [PATCH 18/30] special-case db.mongodb.lsid --- src/otel/testing.rs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/otel/testing.rs b/src/otel/testing.rs index 15f918813..3d993d47e 100644 --- a/src/otel/testing.rs +++ b/src/otel/testing.rs @@ -165,7 +165,21 @@ impl<'a> Matcher<'a> { } let mut actual_attrs = doc! {}; for kv in &actual.attributes { - actual_attrs.insert(kv.key.as_str(), value_to_bson(&kv.value)?); + let key = kv.key.as_str(); + let value = match key { + "db.mongodb.lsid" => match &kv.value { + opentelemetry::Value::String(s) => { + let doc: Bson = serde_json::from_str::(s.as_str()) + .map_err(|e| format!("serde_json error: {}", e))? + .try_into() + .map_err(|e| format!("json value error: {}", e))?; + doc + } + _ => return Err(format!("unexpected type for {:?}: {:?}", key, kv.value)), + }, + _ => value_to_bson(&kv.value)?, + }; + actual_attrs.insert(key, value); } for (k, expected_v) in &expected.attributes { if let Err(e) = From 478951e916cbd1861455fbe9ad70bf88b8182933 Mon Sep 17 00:00:00 2001 From: Abraham Egnor Date: Fri, 10 Oct 2025 11:26:03 +0100 Subject: [PATCH 19/30] update stub --- src/otel_stub.rs | 8 ++++++-- src/test/spec/open_telemetry.rs | 1 + 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/otel_stub.rs b/src/otel_stub.rs index 1404be240..30413f3f5 100644 --- a/src/otel_stub.rs +++ b/src/otel_stub.rs @@ -1,9 +1,13 @@ -use crate::{operation::Operation, Client}; +use crate::{operation::Operation, Client, ClientSession}; type Context = (); impl Client { - pub(crate) fn start_operation_span(&self, _op: &impl Operation) -> Context { + pub(crate) fn start_operation_span( + &self, + _op: &impl Operation, + _session: Option<&ClientSession>, + ) -> Context { () } } diff --git a/src/test/spec/open_telemetry.rs b/src/test/spec/open_telemetry.rs index a27c28e38..df37003e5 100644 --- a/src/test/spec/open_telemetry.rs +++ b/src/test/spec/open_telemetry.rs @@ -5,6 +5,7 @@ async fn run_unified_operation() { // TODO: // server.type // output db.mongodb.cursor_id + // test parsing for db.mongodb.lsid run_unified_tests(&["open-telemetry", "operation"]) .skip_tests(&[ "List collections", // expects `cursor: {}` in `db.query.text` From 83390c87644d7d204d25fa9b20a3148f3b606778 Mon Sep 17 00:00:00 2001 From: Abraham Egnor Date: Fri, 10 Oct 2025 11:45:09 +0100 Subject: [PATCH 20/30] post-merge fix --- .evergreen/run-tests.sh | 2 +- src/trace.rs | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.evergreen/run-tests.sh b/.evergreen/run-tests.sh index b88a5cc36..50b7013e7 100755 --- a/.evergreen/run-tests.sh +++ b/.evergreen/run-tests.sh @@ -6,7 +6,7 @@ set -o pipefail source .evergreen/env.sh source .evergreen/cargo-test.sh -FEATURE_FLAGS+=("tracing-unstable" "cert-key-password") +FEATURE_FLAGS+=("tracing-unstable" "cert-key-password" "opentelemetry") if [ "$OPENSSL" = true ]; then FEATURE_FLAGS+=("openssl-tls") diff --git a/src/trace.rs b/src/trace.rs index e034d0439..bf806aeab 100644 --- a/src/trace.rs +++ b/src/trace.rs @@ -1,6 +1,9 @@ #[cfg(feature = "bson-3")] use crate::bson_compat::RawDocumentBufExt; -use crate::client::options::{ServerAddress, DEFAULT_PORT}; +use crate::{ + bson_util::{doc_to_json_str, truncate_on_char_boundary}, + client::options::{ServerAddress, DEFAULT_PORT}, +}; pub(crate) mod command; pub(crate) mod connection; @@ -38,7 +41,7 @@ impl crate::error::Error { ); if let Some(server_response) = self.server_response() { let server_response_string = match server_response.to_document() { - Ok(document) => serialize_command_or_reply(document, max_document_length), + Ok(document) => doc_to_json_str(document, max_document_length), Err(_) => { let mut hex_string = hex::encode(server_response.as_bytes()); truncate_on_char_boundary(&mut hex_string, max_document_length); From 2e223908ac87ff08dcb60bc262505dcfb259e487 Mon Sep 17 00:00:00 2001 From: Abraham Egnor Date: Mon, 13 Oct 2025 12:05:14 +0100 Subject: [PATCH 21/30] msrv and clippy --- src/client/executor.rs | 124 ++++++++++++----------- src/operation.rs | 75 ++------------ src/operation/abort_transaction.rs | 5 +- src/operation/aggregate.rs | 13 +-- src/operation/aggregate/change_stream.rs | 3 +- src/operation/bulk_write.rs | 5 +- src/operation/commit_transaction.rs | 5 +- src/operation/count.rs | 3 +- src/operation/count_documents.rs | 3 +- src/operation/create.rs | 4 +- src/operation/create_indexes.rs | 3 +- src/operation/delete.rs | 3 +- src/operation/distinct.rs | 3 +- src/operation/drop_collection.rs | 4 +- src/operation/drop_database.rs | 3 +- src/operation/drop_indexes.rs | 3 +- src/operation/find.rs | 4 +- src/operation/find_and_modify.rs | 3 +- src/operation/get_more.rs | 4 +- src/operation/insert.rs | 3 +- src/operation/list_collections.rs | 4 +- src/operation/list_databases.rs | 5 +- src/operation/list_indexes.rs | 4 +- src/operation/raw_output.rs | 3 +- src/operation/run_command.rs | 3 +- src/operation/run_cursor_command.rs | 6 +- src/operation/search_index.rs | 9 +- src/operation/update.rs | 3 +- src/otel.rs | 93 ++++++++++++++++- src/otel_stub.rs | 5 +- 30 files changed, 236 insertions(+), 172 deletions(-) diff --git a/src/client/executor.rs b/src/client/executor.rs index 28835fe8c..84c0a8336 100644 --- a/src/client/executor.rs +++ b/src/client/executor.rs @@ -110,72 +110,78 @@ impl Client { op: &mut T, session: impl Into>, ) -> Result> { - let mut session = session.into(); + let session = session.into(); let ctx = self.start_operation_span(op, session.as_deref()); - let result = (async move || { - // Validate inputs that can be checked before server selection and connection - // checkout. - if self.inner.shutdown.executed.load(Ordering::SeqCst) { - return Err(ErrorKind::Shutdown.into()); + let result = self + .execute_operation_with_details_inner(op, session) + .with_context(ctx.clone()) + .await; + #[cfg(feature = "opentelemetry")] + self.record_error(&ctx, &result); + + result + } + + async fn execute_operation_with_details_inner( + &self, + op: &mut T, + mut session: Option<&mut ClientSession>, + ) -> Result> { + // Validate inputs that can be checked before server selection and connection + // checkout. + if self.inner.shutdown.executed.load(Ordering::SeqCst) { + return Err(ErrorKind::Shutdown.into()); + } + // TODO RUST-9: remove this validation + if !op.is_acknowledged() { + return Err(ErrorKind::InvalidArgument { + message: "Unacknowledged write concerns are not supported".to_string(), } - // TODO RUST-9: remove this validation - if !op.is_acknowledged() { - return Err(ErrorKind::InvalidArgument { - message: "Unacknowledged write concerns are not supported".to_string(), + .into()); + } + if let Some(write_concern) = op.write_concern() { + write_concern.validate()?; + } + + // Validate the session and update its transaction status if needed. + if let Some(ref mut session) = session { + if !TrackingArc::ptr_eq(&self.inner, &session.client().inner) { + return Err(Error::invalid_argument( + "the session provided to an operation must be created from the same client as \ + the collection/database on which the operation is being performed", + )); + } + if op + .selection_criteria() + .and_then(|sc| sc.as_read_pref()) + .is_some_and(|rp| rp != &ReadPreference::Primary) + && session.in_transaction() + { + return Err(ErrorKind::Transaction { + message: "read preference in a transaction must be primary".into(), } .into()); } - if let Some(write_concern) = op.write_concern() { - write_concern.validate()?; - } - - // Validate the session and update its transaction status if needed. - if let Some(ref mut session) = session { - if !TrackingArc::ptr_eq(&self.inner, &session.client().inner) { - return Err(Error::invalid_argument( - "the session provided to an operation must be created from the same \ - client as the collection/database on which the operation is being \ - performed", - )); - } - if op - .selection_criteria() - .and_then(|sc| sc.as_read_pref()) - .is_some_and(|rp| rp != &ReadPreference::Primary) - && session.in_transaction() - { - return Err(ErrorKind::Transaction { - message: "read preference in a transaction must be primary".into(), - } - .into()); - } - // If the current transaction has been committed/aborted and it is not being - // re-committed/re-aborted, reset the transaction's state to None. - if matches!( - session.transaction.state, - TransactionState::Committed { .. } - ) && op.name() != CommitTransaction::NAME - || session.transaction.state == TransactionState::Aborted - && op.name() != AbortTransaction::NAME - { - session.transaction.reset(); - } + // If the current transaction has been committed/aborted and it is not being + // re-committed/re-aborted, reset the transaction's state to None. + if matches!( + session.transaction.state, + TransactionState::Committed { .. } + ) && op.name() != CommitTransaction::NAME + || session.transaction.state == TransactionState::Aborted + && op.name() != AbortTransaction::NAME + { + session.transaction.reset(); } + } - Box::pin(async { - self.execute_operation_with_retry(op, session) - .with_current_context() - .await - }) - .with_current_context() - .await - })() - .with_context(ctx.clone()) - .await; - #[cfg(feature = "opentelemetry")] - self.record_error(&ctx, &result); - - result + Box::pin(async { + self.execute_operation_with_retry(op, session) + .with_current_context() + .await + }) + .with_current_context() + .await } /// Execute the given operation, returning the cursor created by the operation. diff --git a/src/operation.rs b/src/operation.rs index 7184d1190..bceeb110d 100644 --- a/src/operation.rs +++ b/src/operation.rs @@ -177,49 +177,8 @@ pub(crate) trait Operation { /// The name of the server side command associated with this operation. fn name(&self) -> &CStr; - /// The name to use for telemetry purposes. - #[allow(dead_code)] - fn log_name(&self) -> &str; - - #[allow(dead_code)] - fn target(&self) -> OperationTarget<'_>; - - #[allow(dead_code)] - fn cursor_id(&self) -> Option; - - #[allow(dead_code)] - fn output_cursor_id(output: &Self::O) -> Option; -} - -#[allow(dead_code)] -pub(crate) struct OperationTarget<'a> { - pub(crate) database: &'a str, - pub(crate) collection: Option<&'a str>, -} - -impl OperationTarget<'static> { - pub(crate) const ADMIN: Self = OperationTarget { - database: "admin", - collection: None, - }; -} - -impl<'a> From<&'a str> for OperationTarget<'a> { - fn from(value: &'a str) -> Self { - OperationTarget { - database: value, - collection: None, - } - } -} - -impl<'a> From<&'a Namespace> for OperationTarget<'a> { - fn from(value: &'a Namespace) -> Self { - OperationTarget { - database: &value.db, - collection: Some(&value.coll), - } - } + #[cfg(feature = "opentelemetry")] + crate::otel::op_methods!(); } pub(crate) type OverrideCriteriaFn = @@ -322,20 +281,8 @@ pub(crate) trait OperationWithDefaults: Send + Sync { Self::NAME } - /// The name to use for telemetry purposes. - fn log_name(&self) -> &str { - crate::bson_compat::cstr_to_str(self.name()) - } - - fn target(&self) -> OperationTarget<'_>; - - fn cursor_id(&self) -> Option { - None - } - - fn output_cursor_id(_output: &Self::O) -> Option { - None - } + #[cfg(feature = "opentelemetry")] + crate::otel::op_methods_defaults!(); } impl Operation for T @@ -390,18 +337,8 @@ where fn name(&self) -> &CStr { self.name() } - fn log_name(&self) -> &str { - self.log_name() - } - fn target(&self) -> OperationTarget<'_> { - self.target() - } - fn cursor_id(&self) -> Option { - self.cursor_id() - } - fn output_cursor_id(output: &Self::O) -> Option { - Self::output_cursor_id(output) - } + #[cfg(feature = "opentelemetry")] + crate::otel::op_methods_default_impl!(); } fn should_redact_body(body: &RawDocumentBuf) -> bool { diff --git a/src/operation/abort_transaction.rs b/src/operation/abort_transaction.rs index 6bd31764d..d550cc82f 100644 --- a/src/operation/abort_transaction.rs +++ b/src/operation/abort_transaction.rs @@ -81,7 +81,8 @@ impl OperationWithDefaults for AbortTransaction { self.pinned = None; } - fn target(&self) -> super::OperationTarget { - "admin".into() + #[cfg(feature = "opentelemetry")] + fn target(&self) -> crate::otel::OperationTarget<'_> { + crate::otel::OperationTarget::ADMIN } } diff --git a/src/operation/aggregate.rs b/src/operation/aggregate.rs index 328b30aaa..2137393aa 100644 --- a/src/operation/aggregate.rs +++ b/src/operation/aggregate.rs @@ -154,11 +154,13 @@ impl OperationWithDefaults for Aggregate { } } + #[cfg(feature = "opentelemetry")] fn output_cursor_id(output: &Self::O) -> Option { Some(output.id()) } - fn target(&self) -> super::OperationTarget<'_> { + #[cfg(feature = "opentelemetry")] + fn target(&self) -> crate::otel::OperationTarget<'_> { (&self.target).into() } } @@ -209,12 +211,3 @@ impl From for AggregateTarget { AggregateTarget::Database(db_name) } } - -impl<'a> From<&'a AggregateTarget> for super::OperationTarget<'a> { - fn from(value: &'a AggregateTarget) -> Self { - match value { - AggregateTarget::Database(db) => db.as_str().into(), - AggregateTarget::Collection(ns) => ns.into(), - } - } -} diff --git a/src/operation/aggregate/change_stream.rs b/src/operation/aggregate/change_stream.rs index 99cfbd43e..3a9ebcf17 100644 --- a/src/operation/aggregate/change_stream.rs +++ b/src/operation/aggregate/change_stream.rs @@ -136,7 +136,8 @@ impl OperationWithDefaults for ChangeStreamAggregate { self.inner.retryability() } - fn target(&self) -> crate::operation::OperationTarget<'_> { + #[cfg(feature = "opentelemetry")] + fn target(&self) -> crate::otel::OperationTarget<'_> { self.inner.target() } } diff --git a/src/operation/bulk_write.rs b/src/operation/bulk_write.rs index 0832fe5c9..cf4baaf31 100644 --- a/src/operation/bulk_write.rs +++ b/src/operation/bulk_write.rs @@ -486,7 +486,8 @@ where } } - fn target(&self) -> super::OperationTarget<'_> { - super::OperationTarget::ADMIN + #[cfg(feature = "opentelemetry")] + fn target(&self) -> crate::otel::OperationTarget<'_> { + crate::otel::OperationTarget::ADMIN } } diff --git a/src/operation/commit_transaction.rs b/src/operation/commit_transaction.rs index db3664369..d64f70422 100644 --- a/src/operation/commit_transaction.rs +++ b/src/operation/commit_transaction.rs @@ -81,7 +81,8 @@ impl OperationWithDefaults for CommitTransaction { } } - fn target(&self) -> super::OperationTarget<'_> { - super::OperationTarget::ADMIN + #[cfg(feature = "opentelemetry")] + fn target(&self) -> crate::otel::OperationTarget<'_> { + crate::otel::OperationTarget::ADMIN } } diff --git a/src/operation/count.rs b/src/operation/count.rs index 5c968d190..e0983eb71 100644 --- a/src/operation/count.rs +++ b/src/operation/count.rs @@ -76,7 +76,8 @@ impl OperationWithDefaults for Count { Retryability::Read } - fn target(&self) -> super::OperationTarget<'_> { + #[cfg(feature = "opentelemetry")] + fn target(&self) -> crate::otel::OperationTarget<'_> { (&self.ns).into() } } diff --git a/src/operation/count_documents.rs b/src/operation/count_documents.rs index 760251017..cda7d13df 100644 --- a/src/operation/count_documents.rs +++ b/src/operation/count_documents.rs @@ -111,7 +111,8 @@ impl OperationWithDefaults for CountDocuments { self.aggregate.supports_read_concern(description) } - fn target(&self) -> super::OperationTarget<'_> { + #[cfg(feature = "opentelemetry")] + fn target(&self) -> crate::otel::OperationTarget<'_> { self.aggregate.target() } } diff --git a/src/operation/create.rs b/src/operation/create.rs index 32d52bd1d..2abecb757 100644 --- a/src/operation/create.rs +++ b/src/operation/create.rs @@ -53,11 +53,13 @@ impl OperationWithDefaults for Create { .and_then(|opts| opts.write_concern.as_ref()) } + #[cfg(feature = "opentelemetry")] fn log_name(&self) -> &str { "createCollection" } - fn target(&self) -> super::OperationTarget<'_> { + #[cfg(feature = "opentelemetry")] + fn target(&self) -> crate::otel::OperationTarget<'_> { (&self.ns).into() } } diff --git a/src/operation/create_indexes.rs b/src/operation/create_indexes.rs index 8240d732f..028ea8a4d 100644 --- a/src/operation/create_indexes.rs +++ b/src/operation/create_indexes.rs @@ -84,7 +84,8 @@ impl OperationWithDefaults for CreateIndexes { .and_then(|opts| opts.write_concern.as_ref()) } - fn target(&self) -> super::OperationTarget<'_> { + #[cfg(feature = "opentelemetry")] + fn target(&self) -> crate::otel::OperationTarget<'_> { (&self.ns).into() } } diff --git a/src/operation/delete.rs b/src/operation/delete.rs index 8847d05e0..c726a4c39 100644 --- a/src/operation/delete.rs +++ b/src/operation/delete.rs @@ -100,7 +100,8 @@ impl OperationWithDefaults for Delete { } } - fn target(&self) -> super::OperationTarget<'_> { + #[cfg(feature = "opentelemetry")] + fn target(&self) -> crate::otel::OperationTarget<'_> { (&self.ns).into() } } diff --git a/src/operation/distinct.rs b/src/operation/distinct.rs index bae8af091..78d251c96 100644 --- a/src/operation/distinct.rs +++ b/src/operation/distinct.rs @@ -90,7 +90,8 @@ impl OperationWithDefaults for Distinct { true } - fn target(&self) -> super::OperationTarget<'_> { + #[cfg(feature = "opentelemetry")] + fn target(&self) -> crate::otel::OperationTarget<'_> { (&self.ns).into() } } diff --git a/src/operation/drop_collection.rs b/src/operation/drop_collection.rs index 501aeddfd..d80c41444 100644 --- a/src/operation/drop_collection.rs +++ b/src/operation/drop_collection.rs @@ -61,11 +61,13 @@ impl OperationWithDefaults for DropCollection { .and_then(|opts| opts.write_concern.as_ref()) } + #[cfg(feature = "opentelemetry")] fn log_name(&self) -> &str { "dropCollection" } - fn target(&self) -> super::OperationTarget<'_> { + #[cfg(feature = "opentelemetry")] + fn target(&self) -> crate::otel::OperationTarget<'_> { (&self.ns).into() } } diff --git a/src/operation/drop_database.rs b/src/operation/drop_database.rs index aedbd2a42..351bf8b2a 100644 --- a/src/operation/drop_database.rs +++ b/src/operation/drop_database.rs @@ -53,7 +53,8 @@ impl OperationWithDefaults for DropDatabase { .and_then(|opts| opts.write_concern.as_ref()) } - fn target(&self) -> super::OperationTarget<'_> { + #[cfg(feature = "opentelemetry")] + fn target(&self) -> crate::otel::OperationTarget<'_> { self.target_db.as_str().into() } } diff --git a/src/operation/drop_indexes.rs b/src/operation/drop_indexes.rs index d88972b03..a61c89328 100644 --- a/src/operation/drop_indexes.rs +++ b/src/operation/drop_indexes.rs @@ -52,7 +52,8 @@ impl OperationWithDefaults for DropIndexes { .and_then(|opts| opts.write_concern.as_ref()) } - fn target(&self) -> super::OperationTarget<'_> { + #[cfg(feature = "opentelemetry")] + fn target(&self) -> crate::otel::OperationTarget<'_> { (&self.ns).into() } } diff --git a/src/operation/find.rs b/src/operation/find.rs index 90aa989c7..8fae20af6 100644 --- a/src/operation/find.rs +++ b/src/operation/find.rs @@ -129,10 +129,12 @@ impl OperationWithDefaults for Find { Retryability::Read } - fn target(&self) -> super::OperationTarget<'_> { + #[cfg(feature = "opentelemetry")] + fn target(&self) -> crate::otel::OperationTarget<'_> { (&self.ns).into() } + #[cfg(feature = "opentelemetry")] fn output_cursor_id(output: &Self::O) -> Option { Some(output.id()) } diff --git a/src/operation/find_and_modify.rs b/src/operation/find_and_modify.rs index 3d700f451..6a86c4b90 100644 --- a/src/operation/find_and_modify.rs +++ b/src/operation/find_and_modify.rs @@ -119,7 +119,8 @@ impl OperationWithDefaults for FindAndModify { Retryability::Write } - fn target(&self) -> super::OperationTarget<'_> { + #[cfg(feature = "opentelemetry")] + fn target(&self) -> crate::otel::OperationTarget<'_> { (&self.ns).into() } } diff --git a/src/operation/get_more.rs b/src/operation/get_more.rs index 35cd10d07..b389c3962 100644 --- a/src/operation/get_more.rs +++ b/src/operation/get_more.rs @@ -106,10 +106,12 @@ impl OperationWithDefaults for GetMore<'_> { self.pinned_connection } - fn target(&self) -> super::OperationTarget<'_> { + #[cfg(feature = "opentelemetry")] + fn target(&self) -> crate::otel::OperationTarget<'_> { (&self.ns).into() } + #[cfg(feature = "opentelemetry")] fn cursor_id(&self) -> Option { Some(self.cursor_id) } diff --git a/src/operation/insert.rs b/src/operation/insert.rs index eb9e25424..4e0d218ca 100644 --- a/src/operation/insert.rs +++ b/src/operation/insert.rs @@ -178,7 +178,8 @@ impl OperationWithDefaults for Insert<'_> { Retryability::Write } - fn target(&self) -> super::OperationTarget<'_> { + #[cfg(feature = "opentelemetry")] + fn target(&self) -> crate::otel::OperationTarget<'_> { (&self.ns).into() } } diff --git a/src/operation/list_collections.rs b/src/operation/list_collections.rs index 8f1ae8c94..be7b0fdaf 100644 --- a/src/operation/list_collections.rs +++ b/src/operation/list_collections.rs @@ -82,11 +82,13 @@ impl OperationWithDefaults for ListCollections { Retryability::Read } + #[cfg(feature = "opentelemetry")] fn output_cursor_id(output: &Self::O) -> Option { Some(output.id()) } - fn target(&self) -> super::OperationTarget<'_> { + #[cfg(feature = "opentelemetry")] + fn target(&self) -> crate::otel::OperationTarget<'_> { self.db.as_str().into() } } diff --git a/src/operation/list_databases.rs b/src/operation/list_databases.rs index e1e420f84..4e1d77066 100644 --- a/src/operation/list_databases.rs +++ b/src/operation/list_databases.rs @@ -58,8 +58,9 @@ impl OperationWithDefaults for ListDatabases { Retryability::Read } - fn target(&self) -> super::OperationTarget<'_> { - super::OperationTarget::ADMIN + #[cfg(feature = "opentelemetry")] + fn target(&self) -> crate::otel::OperationTarget<'_> { + crate::otel::OperationTarget::ADMIN } } diff --git a/src/operation/list_indexes.rs b/src/operation/list_indexes.rs index d481d46c3..37be43922 100644 --- a/src/operation/list_indexes.rs +++ b/src/operation/list_indexes.rs @@ -70,11 +70,13 @@ impl OperationWithDefaults for ListIndexes { Retryability::Read } + #[cfg(feature = "opentelemetry")] fn output_cursor_id(output: &Self::O) -> Option { Some(output.id()) } - fn target(&self) -> super::OperationTarget<'_> { + #[cfg(feature = "opentelemetry")] + fn target(&self) -> crate::otel::OperationTarget<'_> { (&self.ns).into() } } diff --git a/src/operation/raw_output.rs b/src/operation/raw_output.rs index ee1f32358..67c64447d 100644 --- a/src/operation/raw_output.rs +++ b/src/operation/raw_output.rs @@ -85,7 +85,8 @@ impl Operation for RawOutput { self.0.log_name() } - fn target(&self) -> super::OperationTarget<'_> { + #[cfg(feature = "opentelemetry")] + fn target(&self) -> crate::otel::OperationTarget<'_> { self.0.target() } diff --git a/src/operation/run_command.rs b/src/operation/run_command.rs index 3b5f5b50b..3c2b0ad9c 100644 --- a/src/operation/run_command.rs +++ b/src/operation/run_command.rs @@ -95,7 +95,8 @@ impl OperationWithDefaults for RunCommand<'_> { self.pinned_connection } - fn target(&self) -> super::OperationTarget<'_> { + #[cfg(feature = "opentelemetry")] + fn target(&self) -> crate::otel::OperationTarget<'_> { self.db.as_str().into() } } diff --git a/src/operation/run_cursor_command.rs b/src/operation/run_cursor_command.rs index b1ae7538d..07e91adba 100644 --- a/src/operation/run_cursor_command.rs +++ b/src/operation/run_cursor_command.rs @@ -92,11 +92,13 @@ impl Operation for RunCursorCommand<'_> { self.run_command.name() } + #[cfg(feature = "opentelemetry")] fn log_name(&self) -> &str { self.run_command.log_name() } - fn target(&self) -> super::OperationTarget<'_> { + #[cfg(feature = "opentelemetry")] + fn target(&self) -> crate::otel::OperationTarget<'_> { self.run_command.target() } @@ -128,10 +130,12 @@ impl Operation for RunCursorCommand<'_> { .boxed() } + #[cfg(feature = "opentelemetry")] fn cursor_id(&self) -> Option { self.run_command.cursor_id() } + #[cfg(feature = "opentelemetry")] fn output_cursor_id(output: &Self::O) -> Option { Some(output.id()) } diff --git a/src/operation/search_index.rs b/src/operation/search_index.rs index 5c7f7715d..15869eb67 100644 --- a/src/operation/search_index.rs +++ b/src/operation/search_index.rs @@ -74,7 +74,8 @@ impl OperationWithDefaults for CreateSearchIndexes { false } - fn target(&self) -> super::OperationTarget<'_> { + #[cfg(feature = "opentelemetry")] + fn target(&self) -> crate::otel::OperationTarget<'_> { (&self.ns).into() } } @@ -132,7 +133,8 @@ impl OperationWithDefaults for UpdateSearchIndex { false } - fn target(&self) -> super::OperationTarget<'_> { + #[cfg(feature = "opentelemetry")] + fn target(&self) -> crate::otel::OperationTarget<'_> { (&self.ns).into() } } @@ -188,7 +190,8 @@ impl OperationWithDefaults for DropSearchIndex { false } - fn target(&self) -> super::OperationTarget<'_> { + #[cfg(feature = "opentelemetry")] + fn target(&self) -> crate::otel::OperationTarget<'_> { (&self.ns).into() } } diff --git a/src/operation/update.rs b/src/operation/update.rs index 5c1989506..110efe93b 100644 --- a/src/operation/update.rs +++ b/src/operation/update.rs @@ -213,7 +213,8 @@ impl OperationWithDefaults for Update { } } - fn target(&self) -> super::OperationTarget<'_> { + #[cfg(feature = "opentelemetry")] + fn target(&self) -> crate::otel::OperationTarget<'_> { (&self.ns).into() } } diff --git a/src/otel.rs b/src/otel.rs index fd2f7efb6..b4aa3dd95 100644 --- a/src/otel.rs +++ b/src/otel.rs @@ -15,10 +15,11 @@ use crate::{ bson::Bson, cmap::{conn::wire::Message, Command, ConnectionInfo, StreamDescription}, error::{ErrorKind, Result}, - operation::Operation, + operation::{aggregate::AggregateTarget, Operation}, options::{ClientOptions, ServerAddress, DEFAULT_PORT}, Client, ClientSession, + Namespace, }; #[cfg(test)] @@ -303,3 +304,93 @@ impl CommandAttributes { } } } + +macro_rules! op_methods { + () => { + fn log_name(&self) -> &str; + + fn target(&self) -> crate::otel::OperationTarget<'_>; + + fn cursor_id(&self) -> Option; + + fn output_cursor_id(output: &Self::O) -> Option; + }; +} +pub(crate) use op_methods; + +#[allow(dead_code)] +pub(crate) struct OperationTarget<'a> { + pub(crate) database: &'a str, + pub(crate) collection: Option<&'a str>, +} + +impl OperationTarget<'static> { + pub(crate) const ADMIN: Self = OperationTarget { + database: "admin", + collection: None, + }; +} + +impl<'a> From<&'a str> for OperationTarget<'a> { + fn from(value: &'a str) -> Self { + OperationTarget { + database: value, + collection: None, + } + } +} + +impl<'a> From<&'a Namespace> for OperationTarget<'a> { + fn from(value: &'a Namespace) -> Self { + OperationTarget { + database: &value.db, + collection: Some(&value.coll), + } + } +} + +impl<'a> From<&'a AggregateTarget> for OperationTarget<'a> { + fn from(value: &'a AggregateTarget) -> Self { + match value { + AggregateTarget::Database(db) => db.as_str().into(), + AggregateTarget::Collection(ns) => ns.into(), + } + } +} + +macro_rules! op_methods_defaults { + () => { + fn log_name(&self) -> &str { + crate::bson_compat::cstr_to_str(self.name()) + } + + fn target(&self) -> crate::otel::OperationTarget<'_>; + + fn cursor_id(&self) -> Option { + None + } + + fn output_cursor_id(_output: &Self::O) -> Option { + None + } + }; +} +pub(crate) use op_methods_defaults; + +macro_rules! op_methods_default_impl { + () => { + fn log_name(&self) -> &str { + self.log_name() + } + fn target(&self) -> crate::otel::OperationTarget<'_> { + self.target() + } + fn cursor_id(&self) -> Option { + self.cursor_id() + } + fn output_cursor_id(output: &Self::O) -> Option { + Self::output_cursor_id(output) + } + }; +} +pub(crate) use op_methods_default_impl; diff --git a/src/otel_stub.rs b/src/otel_stub.rs index 30413f3f5..2079a2fb7 100644 --- a/src/otel_stub.rs +++ b/src/otel_stub.rs @@ -1,6 +1,7 @@ use crate::{operation::Operation, Client, ClientSession}; -type Context = (); +#[derive(Clone)] +pub(crate) struct Context; impl Client { pub(crate) fn start_operation_span( @@ -8,7 +9,7 @@ impl Client { _op: &impl Operation, _session: Option<&ClientSession>, ) -> Context { - () + Context } } From 3c19ac5c1c768feed65762528fdd9db855c71982 Mon Sep 17 00:00:00 2001 From: Abraham Egnor Date: Mon, 13 Oct 2025 12:13:24 +0100 Subject: [PATCH 22/30] fix tests --- src/otel.rs | 1 + src/test/spec/open_telemetry.rs | 17 +++++++++++------ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/otel.rs b/src/otel.rs index b4aa3dd95..6cda442cd 100644 --- a/src/otel.rs +++ b/src/otel.rs @@ -163,6 +163,7 @@ impl Client { KeyValue::new("network.transport", "tcp"), ]); } + #[cfg(unix)] ServerAddress::Unix { path } => { attrs.extend([ KeyValue::new("server.address", path.to_string_lossy().into_owned()), diff --git a/src/test/spec/open_telemetry.rs b/src/test/spec/open_telemetry.rs index df37003e5..7d74a87bb 100644 --- a/src/test/spec/open_telemetry.rs +++ b/src/test/spec/open_telemetry.rs @@ -1,4 +1,4 @@ -use crate::test::spec::unified_runner::run_unified_tests; +use crate::test::{log_uncaptured, spec::unified_runner::run_unified_tests}; #[tokio::test(flavor = "multi_thread")] async fn run_unified_operation() { @@ -6,12 +6,17 @@ async fn run_unified_operation() { // server.type // output db.mongodb.cursor_id // test parsing for db.mongodb.lsid + let mut skip = vec![ + "List collections", // expects `cursor: {}` in `db.query.text` + "update one element", // expects `txnNumber: 1` in `db.query.text` + "insert one element", // expects `txnNumber: 1` in `db.query.text` + ]; + if crate::test::server_version_lte(4, 2).await { + log_uncaptured("skipping \"findOneAndUpdate\" on server 4.2"); + skip.push("findOneAndUpdate"); // uses unsupported `comment` field + } run_unified_tests(&["open-telemetry", "operation"]) - .skip_tests(&[ - "List collections", // expects `cursor: {}` in `db.query.text` - "update one element", // expects `txnNumber: 1` in `db.query.text` - "insert one element", // expects `txnNumber: 1` in `db.query.text` - ]) + .skip_tests(&skip) .await; } From a6e49136e185e7e4c1278eef82573225171379f4 Mon Sep 17 00:00:00 2001 From: Abraham Egnor Date: Mon, 13 Oct 2025 12:23:30 +0100 Subject: [PATCH 23/30] fix raw output --- src/operation/raw_output.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/operation/raw_output.rs b/src/operation/raw_output.rs index 67c64447d..6a1cd39a3 100644 --- a/src/operation/raw_output.rs +++ b/src/operation/raw_output.rs @@ -81,6 +81,7 @@ impl Operation for RawOutput { self.0.name() } + #[cfg(feature = "opentelemetry")] fn log_name(&self) -> &str { self.0.log_name() } @@ -90,10 +91,12 @@ impl Operation for RawOutput { self.0.target() } + #[cfg(feature = "opentelemetry")] fn cursor_id(&self) -> Option { self.0.cursor_id() } + #[cfg(feature = "opentelemetry")] fn output_cursor_id(_output: &Self::O) -> Option { None } From f18f8ff10c8e00f50efe3a3437a0ab2e3774d946 Mon Sep 17 00:00:00 2001 From: Abraham Egnor Date: Wed, 15 Oct 2025 12:09:03 +0100 Subject: [PATCH 24/30] witness instead of macro --- src/operation.rs | 14 ++- src/operation/abort_transaction.rs | 5 + src/operation/aggregate.rs | 6 +- src/operation/aggregate/change_stream.rs | 5 + src/operation/bulk_write.rs | 5 + src/operation/commit_transaction.rs | 5 + src/operation/count.rs | 5 + src/operation/count_documents.rs | 5 + src/operation/create.rs | 6 +- src/operation/create_indexes.rs | 5 + src/operation/delete.rs | 5 + src/operation/distinct.rs | 5 + src/operation/drop_collection.rs | 5 + src/operation/drop_database.rs | 5 + src/operation/drop_indexes.rs | 5 + src/operation/find.rs | 6 +- src/operation/find_and_modify.rs | 5 + src/operation/get_more.rs | 5 + src/operation/insert.rs | 5 + src/operation/list_collections.rs | 5 + src/operation/list_databases.rs | 5 + src/operation/list_indexes.rs | 6 +- src/operation/raw_output.rs | 16 ++-- src/operation/run_command.rs | 5 + src/operation/run_cursor_command.rs | 28 +++--- src/operation/search_index.rs | 15 +++ src/operation/update.rs | 5 + src/otel.rs | 111 +++++++++++++---------- 28 files changed, 227 insertions(+), 76 deletions(-) diff --git a/src/operation.rs b/src/operation.rs index bceeb110d..04d31a6e1 100644 --- a/src/operation.rs +++ b/src/operation.rs @@ -177,8 +177,16 @@ pub(crate) trait Operation { /// The name of the server side command associated with this operation. fn name(&self) -> &CStr; + //#[cfg(feature = "opentelemetry")] + //type Otel: crate::otel::OtelInfo; + + #[cfg(feature = "opentelemetry")] + type Otel: crate::otel::OtelWitness; + #[cfg(feature = "opentelemetry")] - crate::otel::op_methods!(); + fn otel(&self) -> &impl crate::otel::OtelInfo { + ::otel(&self) + } } pub(crate) type OverrideCriteriaFn = @@ -282,7 +290,7 @@ pub(crate) trait OperationWithDefaults: Send + Sync { } #[cfg(feature = "opentelemetry")] - crate::otel::op_methods_defaults!(); + type Otel: crate::otel::OtelWitness; } impl Operation for T @@ -338,7 +346,7 @@ where self.name() } #[cfg(feature = "opentelemetry")] - crate::otel::op_methods_default_impl!(); + type Otel = ::Otel; } fn should_redact_body(body: &RawDocumentBuf) -> bool { diff --git a/src/operation/abort_transaction.rs b/src/operation/abort_transaction.rs index d550cc82f..295966477 100644 --- a/src/operation/abort_transaction.rs +++ b/src/operation/abort_transaction.rs @@ -82,6 +82,11 @@ impl OperationWithDefaults for AbortTransaction { } #[cfg(feature = "opentelemetry")] + type Otel = crate::otel::Witness; +} + +#[cfg(feature = "opentelemetry")] +impl crate::otel::OtelInfoDefaults for AbortTransaction { fn target(&self) -> crate::otel::OperationTarget<'_> { crate::otel::OperationTarget::ADMIN } diff --git a/src/operation/aggregate.rs b/src/operation/aggregate.rs index 2137393aa..e04b17e0d 100644 --- a/src/operation/aggregate.rs +++ b/src/operation/aggregate.rs @@ -155,11 +155,15 @@ impl OperationWithDefaults for Aggregate { } #[cfg(feature = "opentelemetry")] + type Otel = crate::otel::Witness; +} + +#[cfg(feature = "opentelemetry")] +impl crate::otel::OtelInfoDefaults for Aggregate { fn output_cursor_id(output: &Self::O) -> Option { Some(output.id()) } - #[cfg(feature = "opentelemetry")] fn target(&self) -> crate::otel::OperationTarget<'_> { (&self.target).into() } diff --git a/src/operation/aggregate/change_stream.rs b/src/operation/aggregate/change_stream.rs index 3a9ebcf17..f1f7abb0f 100644 --- a/src/operation/aggregate/change_stream.rs +++ b/src/operation/aggregate/change_stream.rs @@ -137,6 +137,11 @@ impl OperationWithDefaults for ChangeStreamAggregate { } #[cfg(feature = "opentelemetry")] + type Otel = crate::otel::Witness; +} + +#[cfg(feature = "opentelemetry")] +impl crate::otel::OtelInfoDefaults for ChangeStreamAggregate { fn target(&self) -> crate::otel::OperationTarget<'_> { self.inner.target() } diff --git a/src/operation/bulk_write.rs b/src/operation/bulk_write.rs index cf4baaf31..60f78b77a 100644 --- a/src/operation/bulk_write.rs +++ b/src/operation/bulk_write.rs @@ -487,6 +487,11 @@ where } #[cfg(feature = "opentelemetry")] + type Otel = crate::otel::Witness; +} + +#[cfg(feature = "opentelemetry")] +impl crate::otel::OtelInfoDefaults for BulkWrite<'_, R> { fn target(&self) -> crate::otel::OperationTarget<'_> { crate::otel::OperationTarget::ADMIN } diff --git a/src/operation/commit_transaction.rs b/src/operation/commit_transaction.rs index d64f70422..41ff4877e 100644 --- a/src/operation/commit_transaction.rs +++ b/src/operation/commit_transaction.rs @@ -82,6 +82,11 @@ impl OperationWithDefaults for CommitTransaction { } #[cfg(feature = "opentelemetry")] + type Otel = crate::otel::Witness; +} + +#[cfg(feature = "opentelemetry")] +impl crate::otel::OtelInfoDefaults for CommitTransaction { fn target(&self) -> crate::otel::OperationTarget<'_> { crate::otel::OperationTarget::ADMIN } diff --git a/src/operation/count.rs b/src/operation/count.rs index e0983eb71..a2baa0750 100644 --- a/src/operation/count.rs +++ b/src/operation/count.rs @@ -77,6 +77,11 @@ impl OperationWithDefaults for Count { } #[cfg(feature = "opentelemetry")] + type Otel = crate::otel::Witness; +} + +#[cfg(feature = "opentelemetry")] +impl crate::otel::OtelInfoDefaults for Count { fn target(&self) -> crate::otel::OperationTarget<'_> { (&self.ns).into() } diff --git a/src/operation/count_documents.rs b/src/operation/count_documents.rs index cda7d13df..19f1ae513 100644 --- a/src/operation/count_documents.rs +++ b/src/operation/count_documents.rs @@ -112,6 +112,11 @@ impl OperationWithDefaults for CountDocuments { } #[cfg(feature = "opentelemetry")] + type Otel = crate::otel::Witness; +} + +#[cfg(feature = "opentelemetry")] +impl crate::otel::OtelInfoDefaults for CountDocuments { fn target(&self) -> crate::otel::OperationTarget<'_> { self.aggregate.target() } diff --git a/src/operation/create.rs b/src/operation/create.rs index 2abecb757..47b587883 100644 --- a/src/operation/create.rs +++ b/src/operation/create.rs @@ -54,11 +54,15 @@ impl OperationWithDefaults for Create { } #[cfg(feature = "opentelemetry")] + type Otel = crate::otel::Witness; +} + +#[cfg(feature = "opentelemetry")] +impl crate::otel::OtelInfoDefaults for Create { fn log_name(&self) -> &str { "createCollection" } - #[cfg(feature = "opentelemetry")] fn target(&self) -> crate::otel::OperationTarget<'_> { (&self.ns).into() } diff --git a/src/operation/create_indexes.rs b/src/operation/create_indexes.rs index 028ea8a4d..4f05e0534 100644 --- a/src/operation/create_indexes.rs +++ b/src/operation/create_indexes.rs @@ -85,6 +85,11 @@ impl OperationWithDefaults for CreateIndexes { } #[cfg(feature = "opentelemetry")] + type Otel = crate::otel::Witness; +} + +#[cfg(feature = "opentelemetry")] +impl crate::otel::OtelInfoDefaults for CreateIndexes { fn target(&self) -> crate::otel::OperationTarget<'_> { (&self.ns).into() } diff --git a/src/operation/delete.rs b/src/operation/delete.rs index c726a4c39..62354b782 100644 --- a/src/operation/delete.rs +++ b/src/operation/delete.rs @@ -101,6 +101,11 @@ impl OperationWithDefaults for Delete { } #[cfg(feature = "opentelemetry")] + type Otel = crate::otel::Witness; +} + +#[cfg(feature = "opentelemetry")] +impl crate::otel::OtelInfoDefaults for Delete { fn target(&self) -> crate::otel::OperationTarget<'_> { (&self.ns).into() } diff --git a/src/operation/distinct.rs b/src/operation/distinct.rs index 78d251c96..dea795850 100644 --- a/src/operation/distinct.rs +++ b/src/operation/distinct.rs @@ -91,6 +91,11 @@ impl OperationWithDefaults for Distinct { } #[cfg(feature = "opentelemetry")] + type Otel = crate::otel::Witness; +} + +#[cfg(feature = "opentelemetry")] +impl crate::otel::OtelInfoDefaults for Distinct { fn target(&self) -> crate::otel::OperationTarget<'_> { (&self.ns).into() } diff --git a/src/operation/drop_collection.rs b/src/operation/drop_collection.rs index d80c41444..5880bed3a 100644 --- a/src/operation/drop_collection.rs +++ b/src/operation/drop_collection.rs @@ -62,6 +62,11 @@ impl OperationWithDefaults for DropCollection { } #[cfg(feature = "opentelemetry")] + type Otel = crate::otel::Witness; +} + +#[cfg(feature = "opentelemetry")] +impl crate::otel::OtelInfoDefaults for DropCollection { fn log_name(&self) -> &str { "dropCollection" } diff --git a/src/operation/drop_database.rs b/src/operation/drop_database.rs index 351bf8b2a..350afdc92 100644 --- a/src/operation/drop_database.rs +++ b/src/operation/drop_database.rs @@ -54,6 +54,11 @@ impl OperationWithDefaults for DropDatabase { } #[cfg(feature = "opentelemetry")] + type Otel = crate::otel::Witness; +} + +#[cfg(feature = "opentelemetry")] +impl crate::otel::OtelInfoDefaults for DropDatabase { fn target(&self) -> crate::otel::OperationTarget<'_> { self.target_db.as_str().into() } diff --git a/src/operation/drop_indexes.rs b/src/operation/drop_indexes.rs index a61c89328..86554a001 100644 --- a/src/operation/drop_indexes.rs +++ b/src/operation/drop_indexes.rs @@ -53,6 +53,11 @@ impl OperationWithDefaults for DropIndexes { } #[cfg(feature = "opentelemetry")] + type Otel = crate::otel::Witness; +} + +#[cfg(feature = "opentelemetry")] +impl crate::otel::OtelInfoDefaults for DropIndexes { fn target(&self) -> crate::otel::OperationTarget<'_> { (&self.ns).into() } diff --git a/src/operation/find.rs b/src/operation/find.rs index 8fae20af6..4277c393c 100644 --- a/src/operation/find.rs +++ b/src/operation/find.rs @@ -130,11 +130,15 @@ impl OperationWithDefaults for Find { } #[cfg(feature = "opentelemetry")] + type Otel = crate::otel::Witness; +} + +#[cfg(feature = "opentelemetry")] +impl crate::otel::OtelInfoDefaults for Find { fn target(&self) -> crate::otel::OperationTarget<'_> { (&self.ns).into() } - #[cfg(feature = "opentelemetry")] fn output_cursor_id(output: &Self::O) -> Option { Some(output.id()) } diff --git a/src/operation/find_and_modify.rs b/src/operation/find_and_modify.rs index 6a86c4b90..57bffae36 100644 --- a/src/operation/find_and_modify.rs +++ b/src/operation/find_and_modify.rs @@ -120,6 +120,11 @@ impl OperationWithDefaults for FindAndModify { } #[cfg(feature = "opentelemetry")] + type Otel = crate::otel::Witness; +} + +#[cfg(feature = "opentelemetry")] +impl crate::otel::OtelInfoDefaults for FindAndModify { fn target(&self) -> crate::otel::OperationTarget<'_> { (&self.ns).into() } diff --git a/src/operation/get_more.rs b/src/operation/get_more.rs index b389c3962..e79edbe54 100644 --- a/src/operation/get_more.rs +++ b/src/operation/get_more.rs @@ -107,6 +107,11 @@ impl OperationWithDefaults for GetMore<'_> { } #[cfg(feature = "opentelemetry")] + type Otel = crate::otel::Witness; +} + +#[cfg(feature = "opentelemetry")] +impl crate::otel::OtelInfoDefaults for GetMore<'_> { fn target(&self) -> crate::otel::OperationTarget<'_> { (&self.ns).into() } diff --git a/src/operation/insert.rs b/src/operation/insert.rs index 4e0d218ca..d80618d77 100644 --- a/src/operation/insert.rs +++ b/src/operation/insert.rs @@ -179,6 +179,11 @@ impl OperationWithDefaults for Insert<'_> { } #[cfg(feature = "opentelemetry")] + type Otel = crate::otel::Witness; +} + +#[cfg(feature = "opentelemetry")] +impl crate::otel::OtelInfoDefaults for Insert<'_> { fn target(&self) -> crate::otel::OperationTarget<'_> { (&self.ns).into() } diff --git a/src/operation/list_collections.rs b/src/operation/list_collections.rs index be7b0fdaf..4198e5ff6 100644 --- a/src/operation/list_collections.rs +++ b/src/operation/list_collections.rs @@ -83,6 +83,11 @@ impl OperationWithDefaults for ListCollections { } #[cfg(feature = "opentelemetry")] + type Otel = crate::otel::Witness; +} + +#[cfg(feature = "opentelemetry")] +impl crate::otel::OtelInfoDefaults for ListCollections { fn output_cursor_id(output: &Self::O) -> Option { Some(output.id()) } diff --git a/src/operation/list_databases.rs b/src/operation/list_databases.rs index 4e1d77066..d15117d96 100644 --- a/src/operation/list_databases.rs +++ b/src/operation/list_databases.rs @@ -59,6 +59,11 @@ impl OperationWithDefaults for ListDatabases { } #[cfg(feature = "opentelemetry")] + type Otel = crate::otel::Witness; +} + +#[cfg(feature = "opentelemetry")] +impl crate::otel::OtelInfoDefaults for ListDatabases { fn target(&self) -> crate::otel::OperationTarget<'_> { crate::otel::OperationTarget::ADMIN } diff --git a/src/operation/list_indexes.rs b/src/operation/list_indexes.rs index 37be43922..b40ae1209 100644 --- a/src/operation/list_indexes.rs +++ b/src/operation/list_indexes.rs @@ -71,11 +71,15 @@ impl OperationWithDefaults for ListIndexes { } #[cfg(feature = "opentelemetry")] + type Otel = crate::otel::Witness; +} + +#[cfg(feature = "opentelemetry")] +impl crate::otel::OtelInfoDefaults for ListIndexes { fn output_cursor_id(output: &Self::O) -> Option { Some(output.id()) } - #[cfg(feature = "opentelemetry")] fn target(&self) -> crate::otel::OperationTarget<'_> { (&self.ns).into() } diff --git a/src/operation/raw_output.rs b/src/operation/raw_output.rs index 6a1cd39a3..d7b7f83bf 100644 --- a/src/operation/raw_output.rs +++ b/src/operation/raw_output.rs @@ -82,22 +82,24 @@ impl Operation for RawOutput { } #[cfg(feature = "opentelemetry")] + type Otel = crate::otel::Witness; +} + +#[cfg(feature = "opentelemetry")] +impl crate::otel::OtelInfo for RawOutput { fn log_name(&self) -> &str { - self.0.log_name() + self.0.otel().log_name() } - #[cfg(feature = "opentelemetry")] fn target(&self) -> crate::otel::OperationTarget<'_> { - self.0.target() + self.0.otel().target() } - #[cfg(feature = "opentelemetry")] fn cursor_id(&self) -> Option { - self.0.cursor_id() + self.0.otel().cursor_id() } - #[cfg(feature = "opentelemetry")] - fn output_cursor_id(_output: &Self::O) -> Option { + fn output_cursor_id(_output: &::O) -> Option { None } } diff --git a/src/operation/run_command.rs b/src/operation/run_command.rs index 3c2b0ad9c..7e11af549 100644 --- a/src/operation/run_command.rs +++ b/src/operation/run_command.rs @@ -96,6 +96,11 @@ impl OperationWithDefaults for RunCommand<'_> { } #[cfg(feature = "opentelemetry")] + type Otel = crate::otel::Witness; +} + +#[cfg(feature = "opentelemetry")] +impl crate::otel::OtelInfoDefaults for RunCommand<'_> { fn target(&self) -> crate::otel::OperationTarget<'_> { self.db.as_str().into() } diff --git a/src/operation/run_cursor_command.rs b/src/operation/run_cursor_command.rs index 07e91adba..e326145d9 100644 --- a/src/operation/run_cursor_command.rs +++ b/src/operation/run_cursor_command.rs @@ -92,16 +92,6 @@ impl Operation for RunCursorCommand<'_> { self.run_command.name() } - #[cfg(feature = "opentelemetry")] - fn log_name(&self) -> &str { - self.run_command.log_name() - } - - #[cfg(feature = "opentelemetry")] - fn target(&self) -> crate::otel::OperationTarget<'_> { - self.run_command.target() - } - fn handle_response<'a>( &'a self, response: &'a RawCommandResponse, @@ -131,12 +121,24 @@ impl Operation for RunCursorCommand<'_> { } #[cfg(feature = "opentelemetry")] + type Otel = crate::otel::Witness; +} + +#[cfg(feature = "opentelemetry")] +impl crate::otel::OtelInfo for RunCursorCommand<'_> { + fn log_name(&self) -> &str { + self.run_command.otel().log_name() + } + + fn target(&self) -> crate::otel::OperationTarget<'_> { + self.run_command.otel().target() + } + fn cursor_id(&self) -> Option { - self.run_command.cursor_id() + self.run_command.otel().cursor_id() } - #[cfg(feature = "opentelemetry")] - fn output_cursor_id(output: &Self::O) -> Option { + fn output_cursor_id(output: &::O) -> Option { Some(output.id()) } } diff --git a/src/operation/search_index.rs b/src/operation/search_index.rs index 15869eb67..c43e1f932 100644 --- a/src/operation/search_index.rs +++ b/src/operation/search_index.rs @@ -75,6 +75,11 @@ impl OperationWithDefaults for CreateSearchIndexes { } #[cfg(feature = "opentelemetry")] + type Otel = crate::otel::Witness; +} + +#[cfg(feature = "opentelemetry")] +impl crate::otel::OtelInfoDefaults for CreateSearchIndexes { fn target(&self) -> crate::otel::OperationTarget<'_> { (&self.ns).into() } @@ -134,6 +139,11 @@ impl OperationWithDefaults for UpdateSearchIndex { } #[cfg(feature = "opentelemetry")] + type Otel = crate::otel::Witness; +} + +#[cfg(feature = "opentelemetry")] +impl crate::otel::OtelInfoDefaults for UpdateSearchIndex { fn target(&self) -> crate::otel::OperationTarget<'_> { (&self.ns).into() } @@ -191,6 +201,11 @@ impl OperationWithDefaults for DropSearchIndex { } #[cfg(feature = "opentelemetry")] + type Otel = crate::otel::Witness; +} + +#[cfg(feature = "opentelemetry")] +impl crate::otel::OtelInfoDefaults for DropSearchIndex { fn target(&self) -> crate::otel::OperationTarget<'_> { (&self.ns).into() } diff --git a/src/operation/update.rs b/src/operation/update.rs index 110efe93b..cd323ba04 100644 --- a/src/operation/update.rs +++ b/src/operation/update.rs @@ -214,6 +214,11 @@ impl OperationWithDefaults for Update { } #[cfg(feature = "opentelemetry")] + type Otel = crate::otel::Witness; +} + +#[cfg(feature = "opentelemetry")] +impl crate::otel::OtelInfoDefaults for Update { fn target(&self) -> crate::otel::OperationTarget<'_> { (&self.ns).into() } diff --git a/src/otel.rs b/src/otel.rs index 6cda442cd..f56d299da 100644 --- a/src/otel.rs +++ b/src/otel.rs @@ -111,6 +111,7 @@ impl Client { op: &impl Operation, session: Option<&ClientSession>, ) -> Context { + let op = op.otel(); if !self.options().otel_enabled() { return Context::current(); } @@ -140,6 +141,7 @@ impl Client { message: &Message, cmd_attrs: CommandAttributes, ) -> Context { + let op = op.otel(); if !self.options().otel_enabled() || cmd_attrs.should_redact { return Context::current(); } @@ -246,7 +248,7 @@ impl Client { if let Ok(out) = result { // tests don't match the spec here if false { - if let Some(cursor_id) = Op::output_cursor_id(out) { + if let Some(cursor_id) = ::output_cursor_id(out) { let span = context.span(); span.set_attribute(KeyValue::new("db.mongodb.cursor_id", cursor_id)); } @@ -256,7 +258,7 @@ impl Client { } } -fn op_target(op: &impl Operation) -> String { +fn op_target(op: &impl OtelInfo) -> String { let target = op.target(); if let Some(coll) = target.collection { format!("{}.{}", target.database, coll) @@ -265,7 +267,7 @@ fn op_target(op: &impl Operation) -> String { } } -fn common_attrs(op: &impl Operation) -> Vec { +fn common_attrs(op: &impl OtelInfo) -> Vec { let target = op.target(); let mut attrs = vec![ KeyValue::new("db.system", "mongodb"), @@ -306,20 +308,68 @@ impl CommandAttributes { } } -macro_rules! op_methods { - () => { - fn log_name(&self) -> &str; +pub(crate) trait OtelWitness { + type Op: OtelInfo; + fn otel(op: &Self::Op) -> &impl OtelInfo { + op + } + fn output_cursor_id(output: &::O) -> Option { + Self::Op::output_cursor_id(output) + } +} - fn target(&self) -> crate::otel::OperationTarget<'_>; +pub(crate) struct Witness { + _t: std::marker::PhantomData, +} - fn cursor_id(&self) -> Option; +impl OtelWitness for Witness { + type Op = T; +} - fn output_cursor_id(output: &Self::O) -> Option; - }; +pub(crate) trait OtelInfo: Operation { + fn log_name(&self) -> &str; + + fn target(&self) -> crate::otel::OperationTarget<'_>; + + fn cursor_id(&self) -> Option; + + fn output_cursor_id(output: &::O) -> Option; +} + +pub(crate) trait OtelInfoDefaults: Operation { + fn log_name(&self) -> &str { + crate::bson_compat::cstr_to_str(self.name()) + } + + fn target(&self) -> crate::otel::OperationTarget<'_>; + + fn cursor_id(&self) -> Option { + None + } + + fn output_cursor_id(_output: &::O) -> Option { + None + } +} + +impl OtelInfo for T { + fn log_name(&self) -> &str { + self.log_name() + } + + fn target(&self) -> crate::otel::OperationTarget<'_> { + self.target() + } + + fn cursor_id(&self) -> Option { + self.cursor_id() + } + + fn output_cursor_id(output: &::O) -> Option { + T::output_cursor_id(output) + } } -pub(crate) use op_methods; -#[allow(dead_code)] pub(crate) struct OperationTarget<'a> { pub(crate) database: &'a str, pub(crate) collection: Option<&'a str>, @@ -358,40 +408,3 @@ impl<'a> From<&'a AggregateTarget> for OperationTarget<'a> { } } } - -macro_rules! op_methods_defaults { - () => { - fn log_name(&self) -> &str { - crate::bson_compat::cstr_to_str(self.name()) - } - - fn target(&self) -> crate::otel::OperationTarget<'_>; - - fn cursor_id(&self) -> Option { - None - } - - fn output_cursor_id(_output: &Self::O) -> Option { - None - } - }; -} -pub(crate) use op_methods_defaults; - -macro_rules! op_methods_default_impl { - () => { - fn log_name(&self) -> &str { - self.log_name() - } - fn target(&self) -> crate::otel::OperationTarget<'_> { - self.target() - } - fn cursor_id(&self) -> Option { - self.cursor_id() - } - fn output_cursor_id(output: &Self::O) -> Option { - Self::output_cursor_id(output) - } - }; -} -pub(crate) use op_methods_default_impl; From a291f61ca8c9050b0d473830e1aba1446b26b0cc Mon Sep 17 00:00:00 2001 From: Abraham Egnor Date: Wed, 15 Oct 2025 12:17:09 +0100 Subject: [PATCH 25/30] remove stale --- src/operation.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/operation.rs b/src/operation.rs index 04d31a6e1..44dc7380d 100644 --- a/src/operation.rs +++ b/src/operation.rs @@ -177,9 +177,6 @@ pub(crate) trait Operation { /// The name of the server side command associated with this operation. fn name(&self) -> &CStr; - //#[cfg(feature = "opentelemetry")] - //type Otel: crate::otel::OtelInfo; - #[cfg(feature = "opentelemetry")] type Otel: crate::otel::OtelWitness; From 34673894b6e427818eec471055361d370bf16a69 Mon Sep 17 00:00:00 2001 From: Abraham Egnor Date: Wed, 15 Oct 2025 12:30:29 +0100 Subject: [PATCH 26/30] clippy --- src/operation.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/operation.rs b/src/operation.rs index 44dc7380d..b55fa6b69 100644 --- a/src/operation.rs +++ b/src/operation.rs @@ -182,7 +182,7 @@ pub(crate) trait Operation { #[cfg(feature = "opentelemetry")] fn otel(&self) -> &impl crate::otel::OtelInfo { - ::otel(&self) + ::otel(self) } } From 7f03cdbbfb31ba3c6d38132cbd1f2b5fb9779329 Mon Sep 17 00:00:00 2001 From: Abraham Egnor Date: Mon, 20 Oct 2025 17:02:16 +0100 Subject: [PATCH 27/30] concrete spans --- src/client/executor.rs | 10 ++-- src/client/options.rs | 2 +- src/client/session.rs | 12 ++--- src/otel.rs | 117 ++++++++++++++++++++++++++--------------- src/otel/testing.rs | 4 +- 5 files changed, 90 insertions(+), 55 deletions(-) diff --git a/src/client/executor.rs b/src/client/executor.rs index 84c0a8336..2de719ab6 100644 --- a/src/client/executor.rs +++ b/src/client/executor.rs @@ -111,13 +111,13 @@ impl Client { session: impl Into>, ) -> Result> { let session = session.into(); - let ctx = self.start_operation_span(op, session.as_deref()); + let span = self.start_operation_span(op, session.as_deref()); let result = self .execute_operation_with_details_inner(op, session) - .with_context(ctx.clone()) + .with_context(span.context.clone()) .await; #[cfg(feature = "opentelemetry")] - self.record_error(&ctx, &result); + span.record_error(&result); result } @@ -531,7 +531,7 @@ impl Client { message.request_id = Some(request_id); #[cfg(feature = "opentelemetry")] - let ctx = self.start_command_span( + let span = self.start_command_span( op, &connection_info, connection.stream_description()?, @@ -670,7 +670,7 @@ impl Client { } }; #[cfg(feature = "opentelemetry")] - self.record_command_result::(&ctx, &result); + span.record_command_result::(&result); if result .as_ref() diff --git a/src/client/options.rs b/src/client/options.rs index 1c3b50904..b06a817af 100644 --- a/src/client/options.rs +++ b/src/client/options.rs @@ -614,7 +614,7 @@ pub struct ClientOptions { /// Configuration for opentelemetry. #[cfg(feature = "opentelemetry")] - pub tracing: Option, + pub tracing: Option, /// Information from the SRV URI that generated these client options, if applicable. #[builder(setter(skip))] diff --git a/src/client/session.rs b/src/client/session.rs index a73caf90a..b14360a0b 100644 --- a/src/client/session.rs +++ b/src/client/session.rs @@ -119,21 +119,21 @@ pub(crate) struct Transaction { pub(crate) pinned: Option, pub(crate) recovery_token: Option, #[cfg(feature = "opentelemetry")] - pub(crate) otel_ctx: Option, + pub(crate) otel_span: Option, } impl Transaction { pub(crate) fn start( &mut self, options: Option, - #[cfg(feature = "opentelemetry")] otel_ctx: opentelemetry::Context, + #[cfg(feature = "opentelemetry")] otel_span: crate::otel::TxnSpan, ) { self.state = TransactionState::Starting; self.options = options; self.recovery_token = None; #[cfg(feature = "opentelemetry")] { - self.otel_ctx = Some(otel_ctx); + self.otel_span = Some(otel_span); } } @@ -158,7 +158,7 @@ impl Transaction { pub(crate) fn drop_span(&mut self) { #[cfg(feature = "opentelemetry")] { - self.otel_ctx = None; + self.otel_span = None; } } @@ -188,7 +188,7 @@ impl Transaction { pinned: self.pinned.take(), recovery_token: self.recovery_token.take(), #[cfg(feature = "opentelemetry")] - otel_ctx: self.otel_ctx.take(), + otel_span: self.otel_span.take(), } } } @@ -201,7 +201,7 @@ impl Default for Transaction { pinned: None, recovery_token: None, #[cfg(feature = "opentelemetry")] - otel_ctx: None, + otel_span: None, } } } diff --git a/src/otel.rs b/src/otel.rs index f56d299da..8bf85a3ed 100644 --- a/src/otel.rs +++ b/src/otel.rs @@ -6,7 +6,7 @@ use derive_where::derive_where; use opentelemetry::{ global::{BoxedTracer, ObjectSafeTracerProvider}, - trace::{Span, SpanKind, TraceContextExt, Tracer, TracerProvider}, + trace::{SpanKind, TraceContextExt, Tracer, TracerProvider}, Context, KeyValue, }; @@ -31,7 +31,7 @@ pub(crate) mod testing; #[builder(field_defaults(default, setter(into)))] #[serde(rename_all = "camelCase")] #[non_exhaustive] -pub struct Options { +pub struct OpentelemetryOptions { /// Enables or disables OpenTelemtry for this client instance. If unset, will use the value of /// the `OTEL_RUST_INSTRUMENTATION_MONGODB_ENABLED` environment variable. pub enabled: Option, @@ -46,7 +46,7 @@ pub struct Options { setter( fn transform(provider: P) -> Option> where - S: Span + Send + Sync + 'static, + S: opentelemetry::trace::Span + Send + Sync + 'static, T: Tracer + Send + Sync + 'static, P: TracerProvider + Send + Sync + 'static, { @@ -110,10 +110,13 @@ impl Client { &self, op: &impl Operation, session: Option<&ClientSession>, - ) -> Context { + ) -> OpSpan { let op = op.otel(); if !self.options().otel_enabled() { - return Context::current(); + return OpSpan { + context: Context::current(), + enabled: false, + }; } let span_name = format!("{} {}", op.log_name(), op_target(op)); let mut attrs = common_attrs(op); @@ -126,10 +129,16 @@ impl Client { .span_builder(span_name) .with_kind(SpanKind::Client) .with_attributes(attrs); - if let Some(txn_ctx) = session.and_then(|s| s.transaction.otel_ctx.as_ref()) { + let context = if let Some(TxnSpan(txn_ctx)) = + session.and_then(|s| s.transaction.otel_span.as_ref()) + { txn_ctx.with_span(builder.start_with_context(self.tracer(), txn_ctx)) } else { Context::current_with_span(builder.start(self.tracer())) + }; + OpSpan { + context, + enabled: true, } } @@ -140,10 +149,13 @@ impl Client { stream_desc: &StreamDescription, message: &Message, cmd_attrs: CommandAttributes, - ) -> Context { + ) -> CmdSpan { let op = op.otel(); if !self.options().otel_enabled() || cmd_attrs.should_redact { - return Context::current(); + return CmdSpan { + context: Context::current(), + enabled: false, + }; } let otel_driver_conn_id: i64 = conn_info.id.into(); let mut attrs = common_attrs(op); @@ -196,12 +208,15 @@ impl Client { .with_kind(SpanKind::Client) .with_attributes(attrs) .start(self.tracer()); - Context::current_with_span(span) + CmdSpan { + context: Context::current_with_span(span), + enabled: true, + } } - pub(crate) fn start_transaction_span(&self) -> Context { + pub(crate) fn start_transaction_span(&self) -> TxnSpan { if !self.options().otel_enabled() { - return Context::current(); + return TxnSpan(Context::current()); } let span = self .tracer() @@ -209,53 +224,73 @@ impl Client { .with_kind(SpanKind::Client) .with_attributes([KeyValue::new("db.system", "mongodb")]) .start(self.tracer()); - Context::current_with_span(span) + TxnSpan(Context::current_with_span(span)) } +} - pub(crate) fn record_error(&self, context: &Context, result: &Result) { - if !self.options().otel_enabled() { +pub(crate) struct OpSpan { + pub(crate) context: Context, + enabled: bool, +} + +impl OpSpan { + pub(crate) fn record_error(&self, result: &Result) { + if !self.enabled { return; } - if let Err(error) = result { - let span = context.span(); - span.set_attributes([ - KeyValue::new("exception.message", error.to_string()), - KeyValue::new("exception.type", error.kind.name()), - #[cfg(test)] - KeyValue::new("exception.stacktrace", error.bt.to_string()), - ]); - if let ErrorKind::Command(cmd_err) = &*error.kind { - span.set_attribute(KeyValue::new( - "db.response.status_code", - cmd_err.code.to_string(), - )); - } - span.record_error(error); - span.set_status(opentelemetry::trace::Status::Error { - description: error.to_string().into(), - }); - } + record_error(&self.context, result); } +} - pub(crate) fn record_command_result( - &self, - context: &Context, - result: &Result, - ) { - if !self.options().otel_enabled() { +pub(crate) struct CmdSpan { + context: Context, + enabled: bool, +} + +impl CmdSpan { + pub(crate) fn record_command_result(&self, result: &Result) { + if !self.enabled { return; } if let Ok(out) = result { // tests don't match the spec here if false { if let Some(cursor_id) = ::output_cursor_id(out) { - let span = context.span(); + let span = self.context.span(); span.set_attribute(KeyValue::new("db.mongodb.cursor_id", cursor_id)); } } } - self.record_error(context, result); + record_error(&self.context, result); + } +} + +#[derive(Debug)] +pub(crate) struct TxnSpan(Context); + +fn record_error(context: &Context, result: &Result) { + let error = if let Err(error) = result { + error + } else { + return; + }; + let span = context.span(); + span.set_attributes([ + KeyValue::new("exception.message", error.to_string()), + KeyValue::new("exception.type", error.kind.name()), + #[cfg(test)] + KeyValue::new("exception.stacktrace", error.bt.to_string()), + ]); + if let ErrorKind::Command(cmd_err) = &*error.kind { + span.set_attribute(KeyValue::new( + "db.response.status_code", + cmd_err.code.to_string(), + )); } + span.record_error(error); + span.set_status(opentelemetry::trace::Status::Error { + description: error.to_string().into(), + }); } fn op_target(op: &impl OtelInfo) -> String { diff --git a/src/otel/testing.rs b/src/otel/testing.rs index 3d993d47e..6c717b96a 100644 --- a/src/otel/testing.rs +++ b/src/otel/testing.rs @@ -45,12 +45,12 @@ pub(crate) struct ClientTracing { } impl ClientTracing { - pub(crate) fn new(observe: &ObserveTracingMessages) -> (Self, super::Options) { + pub(crate) fn new(observe: &ObserveTracingMessages) -> (Self, super::OpentelemetryOptions) { let exporter = InMemorySpanExporterBuilder::new().build(); let provider = SdkTracerProvider::builder() .with_span_processor(BatchSpanProcessor::builder(exporter.clone()).build()) .build(); - let mut options = super::Options::builder() + let mut options = super::OpentelemetryOptions::builder() .enabled(true) .tracer_provider(provider.clone()) .build(); From 604ad7d27b4a3442026d6baae691eb60578fd874 Mon Sep 17 00:00:00 2001 From: Abraham Egnor Date: Mon, 20 Oct 2025 17:10:52 +0100 Subject: [PATCH 28/30] fix stub --- src/client/executor.rs | 9 +++++---- src/otel_stub.rs | 19 ------------------- 2 files changed, 5 insertions(+), 23 deletions(-) diff --git a/src/client/executor.rs b/src/client/executor.rs index 2de719ab6..674eeca94 100644 --- a/src/client/executor.rs +++ b/src/client/executor.rs @@ -111,11 +111,12 @@ impl Client { session: impl Into>, ) -> Result> { let session = session.into(); + #[cfg(feature = "opentelemetry")] let span = self.start_operation_span(op, session.as_deref()); - let result = self - .execute_operation_with_details_inner(op, session) - .with_context(span.context.clone()) - .await; + let inner = self.execute_operation_with_details_inner(op, session); + #[cfg(feature = "opentelemetry")] + let inner = inner.with_context(span.context.clone()); + let result = inner.await; #[cfg(feature = "opentelemetry")] span.record_error(&result); diff --git a/src/otel_stub.rs b/src/otel_stub.rs index 2079a2fb7..2d4903c0d 100644 --- a/src/otel_stub.rs +++ b/src/otel_stub.rs @@ -1,23 +1,4 @@ -use crate::{operation::Operation, Client, ClientSession}; - -#[derive(Clone)] -pub(crate) struct Context; - -impl Client { - pub(crate) fn start_operation_span( - &self, - _op: &impl Operation, - _session: Option<&ClientSession>, - ) -> Context { - Context - } -} - pub(crate) trait OtelFutureStub: Sized { - fn with_context(self, _ctx: Context) -> Self { - self - } - fn with_current_context(self) -> Self { self } From 692301452456759490adc537f4565b43c89d55ec Mon Sep 17 00:00:00 2001 From: Abraham Egnor Date: Mon, 20 Oct 2025 17:21:08 +0100 Subject: [PATCH 29/30] error-backtrace --- Cargo.toml | 3 +++ README.md | 1 + src/error.rs | 16 ++++++++-------- src/otel.rs | 4 ++-- 4 files changed, 14 insertions(+), 10 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3d584263d..faf6e1d43 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -81,6 +81,9 @@ text-indexes-unstable = [] # Enable support for opentelemetry instrumentation opentelemetry = ["dep:opentelemetry"] +# Capture backtraces in errors. This can be slow, memory intensive, and very verbose. +error-backtrace = [] + [dependencies] base64 = "0.22" bitflags = "2" diff --git a/README.md b/README.md index a3e580712..1c51b6538 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,7 @@ features = ["sync"] | `azure-oidc` | Enable support for Azure OIDC environment authentication. | | `gcp-oidc` | Enable support for GCP OIDC environment authentication. | | `text-indexes-unstable` | Enables support for text indexes in explicit encryption. This feature is in preview and should be used for experimental workloads only. This feature is unstable and its security is not guaranteed until released as Generally Available (GA). The GA version of this feature may not be backwards compatible with the preview version. | +| `error-backtrace` | Capture backtraces in `Error` values. This can be slow, memory intensive, and very verbose. | ## Web Framework Examples diff --git a/src/error.rs b/src/error.rs index f00006320..4fb3578ea 100644 --- a/src/error.rs +++ b/src/error.rs @@ -52,14 +52,14 @@ pub type Result = std::result::Result; /// cloned. #[derive(Clone, Debug, Error)] #[cfg_attr( - test, + feature = "error-backtrace", error( - "Kind: {kind}, labels: {labels:?}, source: {source:?}, backtrace: {bt}, server response: \ - {server_response:?}" + "Kind: {kind}, labels: {labels:?}, source: {source:?}, server response: \ + {server_response:?}, backtrace: {backtrace}" ) )] #[cfg_attr( - not(test), + not(feature = "error-backtrace"), error( "Kind: {kind}, labels: {labels:?}, source: {source:?}, server response: \ {server_response:?}" @@ -79,8 +79,8 @@ pub struct Error { pub(crate) server_response: Option>, - #[cfg(test)] - pub(crate) bt: Arc, + #[cfg(feature = "error-backtrace")] + pub(crate) backtrace: Arc, } impl Error { @@ -113,8 +113,8 @@ impl Error { wire_version: None, source: None, server_response: None, - #[cfg(test)] - bt: Arc::new(std::backtrace::Backtrace::capture()), + #[cfg(feature = "error-backtrace")] + backtrace: Arc::new(std::backtrace::Backtrace::capture()), } } diff --git a/src/otel.rs b/src/otel.rs index 8bf85a3ed..d76d84a8e 100644 --- a/src/otel.rs +++ b/src/otel.rs @@ -278,8 +278,8 @@ fn record_error(context: &Context, result: &Result) { span.set_attributes([ KeyValue::new("exception.message", error.to_string()), KeyValue::new("exception.type", error.kind.name()), - #[cfg(test)] - KeyValue::new("exception.stacktrace", error.bt.to_string()), + #[cfg(feature = "error-backtrace")] + KeyValue::new("exception.stacktrace", error.backtrace.to_string()), ]); if let ErrorKind::Command(cmd_err) = &*error.kind { span.set_attribute(KeyValue::new( From dc984f22d36b321119947ed432ffb9f480c56e10 Mon Sep 17 00:00:00 2001 From: Abraham Egnor Date: Tue, 21 Oct 2025 10:04:41 +0100 Subject: [PATCH 30/30] test with backtraces --- .evergreen/run-tests.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.evergreen/run-tests.sh b/.evergreen/run-tests.sh index 50b7013e7..318aee8b3 100755 --- a/.evergreen/run-tests.sh +++ b/.evergreen/run-tests.sh @@ -6,7 +6,7 @@ set -o pipefail source .evergreen/env.sh source .evergreen/cargo-test.sh -FEATURE_FLAGS+=("tracing-unstable" "cert-key-password" "opentelemetry") +FEATURE_FLAGS+=("tracing-unstable" "cert-key-password" "opentelemetry" "error-backtrace") if [ "$OPENSSL" = true ]; then FEATURE_FLAGS+=("openssl-tls")