Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions sdk/cosmos/azure_data_cosmos/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
- Added opt-in Cosmos binary JSON encoding for item operations (`create`/`read`/`replace`/`upsert`). Enable it via `CosmosClientBuilder::with_binary_encoding_options` (or the `AZURE_COSMOS_BINARY_ENCODING_ENABLED` environment-variable fallback). Off by default; when disabled, requests and responses are byte-for-byte unchanged. ([#4671](https://github.com/Azure/azure-sdk-for-rust/pull/4671))
- Added a pluggable client-side diagnostics emission layer — the `DiagnosticsHandler` trait and ordered `DiagnosticsHandlerChain` (registered via `CosmosClientBuilder::with_diagnostics_handler`) — invoked once per operation (singleton and paginated, on success and failure) with the completed `DiagnosticsContext` plus an SDK-supplied `CosmosOperationContext`; the empty default chain is a zero-overhead no-op. ([#4789](https://github.com/Azure/azure-sdk-for-rust/pull/4789))
- Added the `metrics`-gated `CosmosMetricsHandler` (with `MetricsOptions`), emitting the stable `db.client.operation.duration` histogram plus per-signal opt-in metrics (`with_request_charge_metric`, `with_returned_rows_metric`) and an opt-in extended attribute set (`with_extended_attributes`); a no-op when no meter provider is registered. ([#4789](https://github.com/Azure/azure-sdk-for-rust/pull/4789))
- Added `MetricsOptions::with_active_instance_metric`, an opt-in `azure.cosmosdb.client.active_instance.count` up-down counter reporting the number of live `CosmosClient` instances per account endpoint, keyed on `server.address` (plus `server.port` for a non-default port). ([#4874](https://github.com/Azure/azure-sdk-for-rust/pull/4874))
- Added `DiagnosticsHandler::on_client_created`, a defaulted hook that lets a handler observe client construction (`CosmosClientInfo`) and return a `ClientLifetimeToken` dropped with the client, for handlers that need to track client lifetime. ([#4874](https://github.com/Azure/azure-sdk-for-rust/pull/4874))
- Added composable tail-sampled emission handlers — a `TracingLogHandler` leaf that writes a compact `tracing` line and a `SamplingLogHandler` wrapper (holding an `Arc<dyn DiagnosticsHandler>`) that applies the sampling gate plus a shared per-window rate limit, defaulting to wrap a `TracingLogHandler` — and the `distributed_tracing`-gated `CosmosTracingHandler` (backdated span tree), also rate-limited so an error storm can't overwhelm exporters. All emit only for operations which fail or breach a configurable `DiagnosticsThresholds`, and stamp *why* they were sampled (a failure, or which threshold) on the emitted line and span. ([#4789](https://github.com/Azure/azure-sdk-for-rust/pull/4789))

### Breaking Changes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ impl ContainerClient {
self.container_ref.account(),
self.container_ref.rid(),
options.operation,
self.operation_context("read_throughput"),
self.operation_context("read_container_throughput"),
)
.await
}
Expand Down Expand Up @@ -222,7 +222,7 @@ impl ContainerClient {
self.container_ref.rid(),
throughput,
options.operation,
self.operation_context("replace_throughput"),
self.operation_context("replace_container_throughput"),
)
.await
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use std::sync::Arc;

use crate::{
clients::{resolve_binary_encoding, ClientContext},
diagnostics::DiagnosticsHandler,
diagnostics::{CosmosClientInfo, DiagnosticsHandler},
options::{
BinaryEncodingOptions, CosmosClientOptions, OperationOptions, PartitionFailoverOptions,
ThroughputControlGroupOptions, UserAgentSuffix,
Expand Down Expand Up @@ -298,6 +298,10 @@ impl CosmosClientBuilder {
let (account_endpoint, credential) = account.into_parts();
let endpoint = account_endpoint.into_url();

// Capture the account coordinates for client-scoped diagnostics before
// the endpoint is moved into the driver account.
let client_info = CosmosClientInfo::from_endpoint(&endpoint);

// Clone credential for the driver before the SDK consumes it for auth policy.
let driver_credential = credential.clone();

Expand All @@ -321,11 +325,12 @@ impl CosmosClientBuilder {
let driver = runtime.into_inner().create_driver(driver_options).await?;

Ok(CosmosClient {
context: ClientContext {
context: ClientContext::new(
driver,
binary_encoding: resolve_binary_encoding(self.options.binary_encoding),
diagnostics_handlers: self.options.diagnostics_handlers,
},
resolve_binary_encoding(self.options.binary_encoding),
self.options.diagnostics_handlers,
&client_info,
),
})
}
}
Expand Down
4 changes: 2 additions & 2 deletions sdk/cosmos/azure_data_cosmos/src/clients/database_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ impl DatabaseClient {
self.context.driver.account(),
&resource_id,
options.operation,
self.operation_context("read_throughput"),
self.operation_context("read_database_throughput"),
)
.await
}
Expand Down Expand Up @@ -292,7 +292,7 @@ impl DatabaseClient {
&resource_id,
throughput,
options.operation,
self.operation_context("replace_throughput"),
self.operation_context("replace_database_throughput"),
)
.await
}
Expand Down
32 changes: 31 additions & 1 deletion sdk/cosmos/azure_data_cosmos/src/clients/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,10 @@ use std::sync::Arc;

use azure_data_cosmos_driver::CosmosDriver;

use crate::diagnostics::{CosmosOperationContext, DiagnosticsContext, DiagnosticsHandlerChain};
use crate::diagnostics::{
ClientLifetimeToken, CosmosClientInfo, CosmosOperationContext, DiagnosticsContext,
DiagnosticsHandlerChain,
};
use crate::models::CosmosResponse;
use crate::options::BinaryEncodingOptions;

Expand All @@ -59,9 +62,36 @@ pub(crate) struct ClientContext {
/// Empty by default, in which case the completion path does nothing beyond
/// checking whether a handler is present.
pub(crate) diagnostics_handlers: DiagnosticsHandlerChain,
/// Lifetime tokens handed back by the handlers when this client was built.
///
/// Never read; held solely so the tokens' `Drop` runs when the last client
/// derived from this context goes away. Because the context is cloned down
/// into every `DatabaseClient`/`ContainerClient`, the shared `Arc` keeps the
/// tokens alive for as long as *any* of those clients is reachable, which is
/// the lifetime handlers are meant to observe.
_client_tokens: Arc<[ClientLifetimeToken]>,
}

impl ClientContext {
/// Builds the shared context for a newly constructed
/// [`CosmosClient`](super::CosmosClient), notifying every registered handler
/// that a client came online and taking ownership of the lifetime tokens
/// they hand back.
pub(crate) fn new(
driver: Arc<CosmosDriver>,
binary_encoding: BinaryEncodingOptions,
diagnostics_handlers: DiagnosticsHandlerChain,
client_info: &CosmosClientInfo,
) -> Self {
let client_tokens = diagnostics_handlers.dispatch_client_created(client_info);
Self {
driver,
binary_encoding,
diagnostics_handlers,
_client_tokens: client_tokens,
}
}

/// Converts a completed driver response into the SDK
/// [`CosmosResponse`](crate::models::CosmosResponse) and invokes the
/// diagnostics handler chain for the operation.
Expand Down
6 changes: 6 additions & 0 deletions sdk/cosmos/azure_data_cosmos/src/diagnostics/attributes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ pub(crate) const DB_RESPONSE_STATUS_CODE: &str = "db.response.status_code";
/// `server.address` — the host contacted for the request.
pub(crate) const SERVER_ADDRESS: &str = "server.address";

/// `server.port` — the port contacted for the request.
///
/// Per semantic conventions this is emitted only when the port differs from the
/// scheme's default (443 for HTTPS).
pub(crate) const SERVER_PORT: &str = "server.port";

/// `error.type` — a low-cardinality identifier of the error (the status code).
pub(crate) const ERROR_TYPE: &str = "error.type";

Expand Down
118 changes: 118 additions & 0 deletions sdk/cosmos/azure_data_cosmos/src/diagnostics/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,91 @@ use azure_core::http::Context;

use crate::diagnostics::DiagnosticsContext;

/// Identity of a Cosmos client instance, handed to
/// [`DiagnosticsHandler::on_client_created`] when a
/// [`CosmosClient`](crate::CosmosClient) is constructed.
///
/// Carries only the account-level coordinates a handler needs to key
/// client-scoped telemetry; it deliberately exposes no credential material.
#[derive(Clone, Debug)]
pub struct CosmosClientInfo {
server_address: Option<String>,
server_port: Option<u16>,
}

impl CosmosClientInfo {
/// Builds the client identity from the account endpoint.
///
/// `server_port` is populated only when the endpoint specifies a port other
/// than the scheme default, matching the `server.port` semantic convention.
pub(crate) fn from_endpoint(endpoint: &azure_core::http::Url) -> Self {
Self {
server_address: endpoint.host_str().map(str::to_owned),
server_port: endpoint.port(),
}
}

/// The account endpoint's host, if the endpoint had one.
///
/// Maps to the `server.address` semantic-convention attribute.
pub fn server_address(&self) -> Option<&str> {
self.server_address.as_deref()
}

/// The account endpoint's port, when it is not the scheme default.
///
/// Maps to the `server.port` semantic-convention attribute, which is only
/// emitted for non-default ports.
pub fn server_port(&self) -> Option<u16> {
self.server_port
}
}

/// An opaque handle that represents one live client's registration with a
/// [`DiagnosticsHandler`].
///
/// A handler returns a token from
/// [`on_client_created`](DiagnosticsHandler::on_client_created) when it needs to
/// observe the end of that client's lifetime. The SDK stores the token in the
/// client's shared state, so it is dropped once the [`CosmosClient`](crate::CosmosClient)
/// and every client derived from it (database, container) have been dropped.
///
/// Handlers use this to keep client-scoped state — such as the
/// `azure.cosmosdb.client.active_instance.count` up-down counter — balanced
/// without tying that state to the handler object's own lifetime (a single
/// handler may be registered on many clients, or on none).
pub struct ClientLifetimeToken {
on_drop: Option<Box<dyn FnOnce() + Send + Sync>>,
}

impl ClientLifetimeToken {
/// Creates a token that runs `on_drop` when the client it is attached to is
/// released.
///
/// `on_drop` runs on whichever thread drops the last client handle, so it
/// must be cheap and non-blocking, and it must not panic.
pub fn new(on_drop: impl FnOnce() + Send + Sync + 'static) -> Self {
Self {
on_drop: Some(Box::new(on_drop)),
}
}
}

impl Drop for ClientLifetimeToken {
fn drop(&mut self) {
if let Some(on_drop) = self.on_drop.take() {
on_drop();
}
}
}

impl fmt::Debug for ClientLifetimeToken {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ClientLifetimeToken")
.finish_non_exhaustive()
}
}

/// A sink that consumes a completed [`DiagnosticsContext`] for a single Cosmos
/// operation.
///
Expand Down Expand Up @@ -67,6 +152,24 @@ pub trait DiagnosticsHandler: Send + Sync {
/// the caller's pipeline/trace context, so read it for operation metadata
/// rather than for trace-context correlation.
fn handle(&self, diagnostics: &DiagnosticsContext, cx: &Context<'_>);

/// Notifies the handler that a new [`CosmosClient`](crate::CosmosClient) was
/// constructed with this handler registered.
///
/// Called exactly once per client, at construction. Return a
/// [`ClientLifetimeToken`] to be notified when that client — and every
/// database/container client derived from it — has been dropped; return
/// `None` (the default) when the handler does not track client lifetimes.
///
/// This is the seam for client-scoped telemetry. A handler object may be
/// shared across several clients or registered on none, so its own lifetime
/// is not a proxy for a live client; this hook and the returned token are.
///
/// * `client` - Account-level identity of the newly created client.
fn on_client_created(&self, client: &CosmosClientInfo) -> Option<ClientLifetimeToken> {
let _ = client;
None
}
}

/// An ordered, cheaply cloneable chain of [`DiagnosticsHandler`]s.
Expand Down Expand Up @@ -143,6 +246,21 @@ impl DiagnosticsHandlerChain {
handler.handle(diagnostics, cx);
}
}

/// Notifies every handler that a client was created, collecting the lifetime
/// tokens they hand back.
///
/// The returned tokens must be stored for the client's lifetime; dropping
/// them is what signals client teardown to the handlers.
pub(crate) fn dispatch_client_created(
&self,
client: &CosmosClientInfo,
) -> Arc<[ClientLifetimeToken]> {
self.handlers
.iter()
.filter_map(|handler| handler.on_client_created(client))
.collect()
}
}

impl Default for DiagnosticsHandlerChain {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ pub const METRIC_OPERATION_REQUEST_CHARGE: &str = "azure.cosmosdb.client.operati
/// Optional histogram (rows): number of rows/items returned by an operation.
pub const METRIC_RESPONSE_RETURNED_ROWS: &str = "db.client.response.returned_rows";

/// Optional up-down counter (instances): number of live
/// [`CosmosMetricsHandler`](super::CosmosMetricsHandler) instances (one per
/// instrumented client, under the intended one-handler-per-client registration).
pub const METRIC_ACTIVE_INSTANCE_COUNT: &str = "azure.cosmosdb.client.active_instance.count";

// =========================================================================
// Instrument units
// =========================================================================
Expand All @@ -49,6 +54,9 @@ pub const UNIT_REQUEST_UNIT: &str = "{request_unit}";
/// Unit for [`METRIC_RESPONSE_RETURNED_ROWS`] — rows.
pub const UNIT_ROW: &str = "{row}";

/// Unit for [`METRIC_ACTIVE_INSTANCE_COUNT`] — client instances.
pub const UNIT_INSTANCE: &str = "{instance}";

// =========================================================================
// Stable attributes (always emitted; operation scope, low cardinality)
//
Expand Down Expand Up @@ -80,6 +88,12 @@ pub const ATTR_ERROR_TYPE: &str = attributes::ERROR_TYPE;
/// `server.address` — host of the contacted endpoint.
pub const ATTR_SERVER_ADDRESS: &str = attributes::SERVER_ADDRESS;

/// `server.port` — port of the contacted endpoint.
///
/// Conditionally required: emitted only when the endpoint uses a non-default
/// port (i.e. anything other than 443 for HTTPS).
pub const ATTR_SERVER_PORT: &str = attributes::SERVER_PORT;

/// Fallback value for [`ATTR_ERROR_TYPE`] when the error is otherwise unknown
/// (per semantic conventions).
pub const ERROR_TYPE_OTHER: &str = attributes::ERROR_TYPE_OTHER;
Expand Down
Loading
Loading