Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
4 changes: 4 additions & 0 deletions tonic-xds/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ tonic-prost = { version = "0.14", optional = true }
rustls = { version = "0.23", default-features = false, features = ["std", "tls12"], optional = true }
rustls-pemfile = { version = "2", optional = true }
x509-parser = { version = "0.17", optional = true }
opentelemetry = { version = "0.32", optional = true, default-features = false, features = ["metrics"] }

[lints]
workspace = true
Expand All @@ -72,6 +73,9 @@ rcgen = "0.14"
[features]
testutil = ["dep:tonic-prost"]

# Bundled OpenTelemetry metrics recorder for the gRFC A78 xDS client metrics.
otel = ["dep:opentelemetry", "xds-client/otel"]

# TLS crypto backend — pick exactly one.
_tls-any = ["dep:rustls", "dep:rustls-pemfile", "dep:x509-parser"]
tls-ring = ["_tls-any", "tonic/tls-ring", "xds-client/tonic-tls-ring"]
Expand Down
54 changes: 50 additions & 4 deletions tonic-xds/src/client/channel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ use std::sync::Arc;
use std::task::{Context, Poll};
use tonic::{body::Body as TonicBody, client::GrpcService, transport::channel::Channel};
use tower::{BoxError, Service, ServiceBuilder, util::BoxCloneSyncService};
use xds_client::{ClientConfig, Node, ProstCodec, TokioRuntime, TonicTransportBuilder, XdsClient};
use xds_client::{
ClientConfig, MetricsRecorder, Node, ProstCodec, TokioRuntime, TonicTransportBuilder, XdsClient,
};

use crate::client::retry::{GrpcRetryPolicy, GrpcRetryPolicyConfig, RetryLayer};

Expand Down Expand Up @@ -151,9 +153,25 @@ const _: fn() = || {
};

/// Builder for creating an [`XdsChannel`] or [`XdsChannelGrpc`].
#[derive(Clone, Debug)]
#[derive(Clone)]
pub struct XdsChannelBuilder {
config: Arc<XdsChannelConfig>,
recorder: Option<Arc<dyn MetricsRecorder>>,
}

impl Debug for XdsChannelBuilder {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("XdsChannelBuilder")
.field("config", &self.config)
.field(
"recorder",
&self
.recorder
.as_ref()
.map_or("None", |_| "Some(Arc<dyn MetricsRecorder>)"),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm... do we really want to do it this way? Maybe use std::any::type_name_of_val ?

)
.finish()
}
}

impl XdsChannelBuilder {
Expand All @@ -162,9 +180,33 @@ impl XdsChannelBuilder {
pub fn new(config: XdsChannelConfig) -> Self {
Self {
config: Arc::new(config),
recorder: None,
}
}

/// Sets the [`MetricsRecorder`] backend that receives the gRFC A78 xDS
/// client metrics emitted by the underlying [`XdsClient`].
///
/// By default no recorder is configured and metric emission is skipped.
/// With the `otel` feature, `with_otel_metrics` provides a one-call
/// OpenTelemetry setup.
#[must_use]
pub fn with_metrics_recorder(mut self, recorder: Arc<dyn MetricsRecorder>) -> Self {
self.recorder = Some(recorder);
self
}

/// Emits the gRFC A78 xDS client metrics through an OpenTelemetry `Meter`.
///
/// Convenience wrapper over
/// [`with_metrics_recorder`](Self::with_metrics_recorder) that installs an
/// [`xds_client::OtelMetricsRecorder`]. Requires the `otel` feature.
#[cfg(feature = "otel")]
#[must_use]
pub fn with_otel_metrics(self, meter: opentelemetry::metrics::Meter) -> Self {
self.with_metrics_recorder(Arc::new(xds_client::OtelMetricsRecorder::new(meter)))
}

fn build_tonic_grpc_channel(&self) -> Result<XdsChannelGrpc, BuildError> {
let bootstrap = match self.config.bootstrap.clone() {
Some(b) => b,
Expand Down Expand Up @@ -199,8 +241,12 @@ impl XdsChannelBuilder {
let node = Node::try_from(bootstrap.node)?;
let client_config =
ClientConfig::new(node, &server_uri).with_target(self.config.target_uri.to_string());
let xds_client =
XdsClient::builder(client_config, transport_builder, ProstCodec, TokioRuntime).build();
let mut client_builder =
XdsClient::builder(client_config, transport_builder, ProstCodec, TokioRuntime);
if let Some(recorder) = self.recorder.clone() {
client_builder = client_builder.with_metrics_recorder(recorder);
}
let xds_client = client_builder.build();

let cache = Arc::new(XdsCache::new());
let resource_manager =
Expand Down
5 changes: 5 additions & 0 deletions tonic-xds/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,5 +150,10 @@ pub use client::channel::{
pub use xds::bootstrap::{BootstrapConfig, BootstrapError};
pub use xds::uri::{XdsUri, XdsUriError};

pub use xds_client::MetricsRecorder;
Comment thread
YutaoMa marked this conversation as resolved.
Outdated

#[cfg(feature = "otel")]
pub use xds_client::OtelMetricsRecorder;

#[cfg(any(test, feature = "testutil"))]
pub mod testutil;
6 changes: 6 additions & 0 deletions xds-client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ http = { version = "1", optional = true }
envoy-types = { version = "0.7", optional = true }
prost = { version = "0.14", optional = true }

# Optional dependency for the bundled OpenTelemetry metrics recorder
opentelemetry = { version = "0.32", optional = true, default-features = false, features = ["metrics"] }
Comment thread
YutaoMa marked this conversation as resolved.
Outdated

[features]
default = ["transport-tonic", "codegen-prost"]
transport-tonic = [
Expand All @@ -38,6 +41,7 @@ tonic-tls-ring = ["transport-tonic", "tonic/tls-ring"]
tonic-tls-aws-lc = ["transport-tonic", "tonic/tls-aws-lc"]
rt-tokio = ["tokio/rt", "tokio/time"]
codegen-prost = ["dep:envoy-types", "dep:prost"]
otel = ["dep:opentelemetry"]
test-util = []

[dev-dependencies]
Expand All @@ -60,4 +64,6 @@ required-features = ["tonic-tls-ring"]
allowed_external_types = [
# major released
"bytes::*",
# exposed only with the `otel` feature (OtelMetricsRecorder::new)
"opentelemetry::*",
]
4 changes: 4 additions & 0 deletions xds-client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
//! - `transport-tonic`: Enables the use of the `tonic` transport. This enables `rt-tokio` and `codegen-prost` features. Enabled by default.
//! - `rt-tokio`: Enables the use of the `tokio` runtime. Enabled by default.
//! - `codegen-prost`: Enables the use of the `prost` codec generated resources. Enabled by default.
//! - `otel`: Enables `OtelMetricsRecorder`, a bundled [`MetricsRecorder`] that forwards measurements to an OpenTelemetry `Meter`. Disabled by default.

pub mod client;
pub mod codec;
Expand All @@ -68,6 +69,9 @@ pub use codec::XdsCodec;
pub use error::{Error, Result};
pub use message::{DiscoveryRequest, DiscoveryResponse, ErrorDetail, Locality, Node, ResourceAny};
pub use metrics::{Instrument, InstrumentKind, KeyValue, MetricsRecorder, StringValue, Value};

#[cfg(feature = "otel")]
pub use metrics::otel::OtelMetricsRecorder;
pub use resource::{DecodeResult, DecodedResource, Resource};
pub use runtime::Runtime;
pub use transport::{Transport, TransportBuilder, TransportStream};
Expand Down
25 changes: 20 additions & 5 deletions xds-client/src/metrics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@
//! to receive metric measurements emitted by the client. Modeled after gRFC A79's
//! `MetricsRecorder` abstraction.
//!
//! No bundled implementation is provided in this crate today; consumers wanting
//! to ship metrics to a real backend must implement [`MetricsRecorder`]
//! themselves. A bundled OpenTelemetry implementation behind an `otel` Cargo
//! feature is planned but not yet implemented.
//! A bundled OpenTelemetry implementation is available behind the `otel` Cargo
//! feature (see `OtelMetricsRecorder`). Consumers that use a different telemetry
//! framework can implement [`MetricsRecorder`] themselves.
//!
//! # Example
//!
Expand All @@ -28,6 +27,9 @@ use std::borrow::Cow;
use std::fmt;
use std::sync::Arc;

#[cfg(feature = "otel")]
pub mod otel;

/// Static descriptor for a metric instrument.
///
/// One `Instrument` is declared per metric as a `pub static` constant. Call
Expand Down Expand Up @@ -266,13 +268,26 @@ pub mod instruments {

/// `grpc.xds_client.resources` — gauge of cached xDS resources, emitted as up-down-counter deltas.
///
/// Use `cache_state` attribute values from [`super::cache_state`].
/// Use `cache_state` attribute values from [`super::attrs::GRPC_XDS_CACHE_STATE`].
pub static XDS_CLIENT_RESOURCES: Instrument = Instrument {
name: "grpc.xds_client.resources",
description: "Number of xDS resources currently cached, broken down by cache state.",
unit: "{resource}",
kind: InstrumentKind::UpDownCounter,
};

/// Every instrument emitted by this crate.
///
/// Backends that pre-register instruments up front (e.g. the bundled
/// `OtelMetricsRecorder`) iterate this slice at construction time instead of
/// creating instruments lazily on the recording path.
pub static ALL: &[&Instrument] = &[
&XDS_CLIENT_CONNECTED,
&XDS_CLIENT_SERVER_FAILURE,
&XDS_CLIENT_RESOURCE_UPDATES_VALID,
&XDS_CLIENT_RESOURCE_UPDATES_INVALID,
&XDS_CLIENT_RESOURCES,
];
}

/// Attribute keys used by the gRFC A78 XdsClient metrics.
Expand Down
Loading
Loading