Skip to content

Commit 0026a82

Browse files
authored
feat: add support for Opentelemetry Metrics (#249)
* create metrics feature & meter provider init implementation * add env vars for meter initialization and test in axum otlp example * delete superfluous old file metrics.rs * rename environment variables for metrics * apply PR suggestions and update README.md
1 parent e83242d commit 0026a82

File tree

8 files changed

+290
-107
lines changed

8 files changed

+290
-107
lines changed

examples/axum-otlp/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ axum-tracing-opentelemetry = { path = "../../axum-tracing-opentelemetry" }
1313
init-tracing-opentelemetry = { path = "../../init-tracing-opentelemetry", features = [
1414
"otlp",
1515
"tracing_subscriber_ext",
16+
"metrics"
1617
] }
1718
opentelemetry = { workspace = true }
1819
opentelemetry-otlp = { workspace = true, default-features = false, features = [

examples/axum-otlp/src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ async fn health() -> impl IntoResponse {
4545

4646
#[tracing::instrument]
4747
async fn index() -> impl IntoResponse {
48+
tracing::info!(monotonic_counter.index = 1);
4849
let trace_id = find_current_trace_id();
4950
dbg!(&trace_id);
5051
//std::thread::sleep(std::time::Duration::from_secs(1));

init-tracing-opentelemetry/Cargo.toml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,7 @@ opentelemetry-otlp = { workspace = true, optional = true, features = [
2020
"trace",
2121
] }
2222
# opentelemetry-resource-detectors = { workspace = true } //FIXME enable when available for opentelemetry >= 0.25
23-
opentelemetry-stdout = { workspace = true, features = [
24-
"trace",
25-
], optional = true }
23+
opentelemetry-stdout = { workspace = true, features = ["trace"], optional = true }
2624
opentelemetry-semantic-conventions = { workspace = true, optional = true }
2725
opentelemetry-zipkin = { workspace = true, features = [], optional = true }
2826
opentelemetry_sdk = { workspace = true }
@@ -74,3 +72,4 @@ tls = ["opentelemetry-otlp/tls", "tonic"]
7472
tls-roots = ["opentelemetry-otlp/tls-roots"]
7573
tls-webpki-roots = ["opentelemetry-otlp/tls-webpki-roots"]
7674
logfmt = ["dep:tracing-logfmt"]
75+
metrics = ["opentelemetry-otlp/metrics", "tracing-opentelemetry/metrics", "opentelemetry-stdout/metrics"]

init-tracing-opentelemetry/README.md

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@ async fn main() -> Result<(), axum::BoxError> {
1717
}
1818
```
1919

20-
The `init_subscribers` function returns a `TracingGuard` instance. Following the guard pattern, this struct provides no functions but, when dropped, ensures that any pending traces are sent before it exits. The syntax `let _guard` is suggested to ensure that Rust does not drop the struct until the application exits.
20+
The `init_subscribers` function returns a `OtelGuard` instance. Following the guard pattern, this struct provides no functions but, when dropped, ensures that any pending traces/metrics are sent before it exits. The syntax `let _guard` is suggested to ensure that Rust does not drop the struct until the application exits.
2121

22-
To configure opentelemetry tracer & tracing, you can use the functions from `init_tracing_opentelemetry::tracing_subscriber_ext`, but they are very opinionated (and WIP to make them more customizable and friendly), so we recommend making your composition, but look at the code (to avoid some issue) and share your feedback.
22+
To configure opentelemetry tracer & tracing (& metrics), you can use the functions from `init_tracing_opentelemetry::tracing_subscriber_ext`, but they are very opinionated (and WIP to make them more customizable and friendly), so we recommend making your composition, but look at the code (to avoid some issue) and share your feedback.
2323

2424
```txt
2525
pub fn build_loglevel_filter_layer() -> tracing_subscriber::filter::EnvFilter {
@@ -161,6 +161,20 @@ spec:
161161
- check the environment variables of opentelemetry `OTEL_EXPORTER...` and `OTEL_TRACES_SAMPLER` (values are logged on target `otel::setup` )
162162
- check that log target `otel::tracing` enable log level `trace` (or `info` if you use `tracing_level_info` feature) to generate span to send to opentelemetry collector.
163163
164+
## Metrics
165+
166+
To configure opentelemetry metrics, enable the `metrics` feature, this will initialize a `SdkMeterProvider`, set it globally and add a a [`MetricsLayer`](https://docs.rs/tracing-opentelemetry/latest/tracing_opentelemetry/struct.MetricsLayer.html) to allow using `tracing` events to produce metrics.
167+
168+
The `opentelemetry_sdk` can still be used to produce metrics as well, since we configured the `SdkMeterProvider` globally, so any Axum/Tonic middleware that does not use `tracing` but directly [opentelemetry::metrics](https://docs.rs/opentelemetry/latest/opentelemetry/metrics/struct.Meter.html) will work.
169+
170+
Configure the following set of environment variables to configure the metrics exporter (on top of those configured above):
171+
172+
- `OTEL_EXPORTER_OTLP_METRICS_ENDPOINT` override to `OTEL_EXPORTER_OTLP_ENDPOINT` for the url of the exporter / collector
173+
- `OTEL_EXPORTER_OTLP_METRICS_PROTOCOL` override to `OTEL_EXPORTER_OTLP_PROTOCOL`, fallback to auto-detection based on ENDPOINT port
174+
- `OTEL_EXPORTER_OTLP_METRICS_TIMEOUT` to set the timeout for the connection to the exporter
175+
- `OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE` to set the temporality preference for the exporter
176+
- `OTEL_METRIC_EXPORT_INTERVAL` to set frequence of metrics export in __*milliseconds*__, defaults to 60s
177+
164178
## Changelog - History
165179
166180
[CHANGELOG.md](https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/blob/main/CHANGELOG.md)
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
use super::infer_protocol;
2+
use crate::resource::DetectResource;
3+
use crate::Error;
4+
use opentelemetry::global;
5+
use opentelemetry_otlp::{ExporterBuildError, MetricExporter, WithExportConfig};
6+
use opentelemetry_sdk::metrics::{
7+
MeterProviderBuilder, PeriodicReader, SdkMeterProvider, Temporality,
8+
};
9+
use opentelemetry_sdk::Resource;
10+
use std::env;
11+
use std::time::Duration;
12+
use tracing::Subscriber;
13+
use tracing_opentelemetry::MetricsLayer;
14+
use tracing_subscriber::registry::LookupSpan;
15+
#[cfg(feature = "tls")]
16+
use {opentelemetry_otlp::WithTonicConfig, tonic::transport::ClientTlsConfig};
17+
18+
pub fn build_metrics_layer<S>() -> Result<(MetricsLayer<S>, SdkMeterProvider), Error>
19+
where
20+
S: Subscriber + for<'a> LookupSpan<'a>,
21+
{
22+
let otel_rsrc = DetectResource::default().build();
23+
let meter_provider = init_meterprovider(otel_rsrc, identity)?;
24+
global::set_meter_provider(meter_provider.clone());
25+
let layer = tracing_opentelemetry::MetricsLayer::new(meter_provider.clone());
26+
Ok((layer, meter_provider))
27+
}
28+
29+
#[must_use]
30+
pub fn identity(v: MeterProviderBuilder) -> MeterProviderBuilder {
31+
v
32+
}
33+
34+
pub fn init_meterprovider<F>(
35+
resource: Resource,
36+
transform: F,
37+
) -> Result<SdkMeterProvider, ExporterBuildError>
38+
where
39+
F: FnOnce(MeterProviderBuilder) -> MeterProviderBuilder,
40+
{
41+
let (maybe_protocol, maybe_endpoint) = read_protocol_and_endpoint_from_env();
42+
let protocol = infer_protocol(maybe_protocol.as_deref(), maybe_endpoint.as_deref());
43+
let timeout = env::var("OTEL_EXPORTER_OTLP_METRICS_TIMEOUT")
44+
.ok()
45+
.and_then(|var| var.parse::<u64>().ok())
46+
.map_or(Duration::from_secs(10), Duration::from_secs);
47+
let temporality = env::var("OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE")
48+
.ok()
49+
.and_then(|var| match var.to_lowercase().as_str() {
50+
"delta" => Some(Temporality::Delta),
51+
"cumulative" => Some(Temporality::Cumulative),
52+
unknown => {
53+
tracing::warn!("unknown '{unknown}' env var set for OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY; defaulting to cumulative");
54+
None
55+
},
56+
})
57+
.unwrap_or_default();
58+
let export_interval = env::var("OTEL_METRIC_EXPORT_INTERVAL")
59+
.ok()
60+
.and_then(|var| var.parse::<u64>().ok())
61+
.map_or(Duration::from_secs(60), Duration::from_millis);
62+
63+
let exporter = match protocol.as_deref() {
64+
Some("http/protobuf") => Some(
65+
MetricExporter::builder()
66+
.with_http()
67+
.with_temporality(temporality)
68+
.with_timeout(timeout)
69+
.build()?,
70+
),
71+
#[cfg(feature = "tls")]
72+
Some("grpc/tls") => Some(
73+
MetricExporter::builder()
74+
.with_tonic()
75+
.with_tls_config(ClientTlsConfig::new().with_enabled_roots())
76+
.with_temporality(temporality)
77+
.with_timeout(timeout)
78+
.build()?,
79+
),
80+
Some("grpc") => Some(
81+
MetricExporter::builder()
82+
.with_tonic()
83+
.with_temporality(temporality)
84+
.with_timeout(timeout)
85+
.build()?,
86+
),
87+
Some(x) => {
88+
tracing::warn!("unknown '{x}' env var set or infered for OTEL_EXPORTER_OTLP_METRICS_PROTOCOL or OTEL_EXPORTER_OTLP_PROTOCOL; no metric exporter will be created");
89+
None
90+
}
91+
None => {
92+
tracing::warn!("no env var set or infered for OTEL_EXPORTER_OTLP_METRICS_PROTOCOL or OTEL_EXPORTER_OTLP_PROTOCOL; no metric exporter will be created");
93+
None
94+
}
95+
};
96+
let mut meter_provider = SdkMeterProvider::builder().with_resource(resource);
97+
if let Some(exporter) = exporter {
98+
let reader = PeriodicReader::builder(exporter)
99+
.with_interval(export_interval)
100+
.build();
101+
meter_provider = meter_provider.with_reader(reader);
102+
}
103+
meter_provider = transform(meter_provider);
104+
Ok(meter_provider.build())
105+
}
106+
107+
fn read_protocol_and_endpoint_from_env() -> (Option<String>, Option<String>) {
108+
let maybe_protocol = std::env::var("OTEL_EXPORTER_OTLP_METRICS_PROTOCOL")
109+
.or_else(|_| std::env::var("OTEL_EXPORTER_OTLP_PROTOCOL"))
110+
.ok();
111+
let maybe_endpoint = std::env::var("OTEL_EXPORTER_OTLP_METRICS_ENDPOINT")
112+
.or_else(|_| {
113+
std::env::var("OTEL_EXPORTER_OTLP_ENDPOINT").map(|endpoint| match &maybe_protocol {
114+
Some(protocol) if protocol.contains("http") => {
115+
format!("{endpoint}/v1/metrics")
116+
}
117+
_ => endpoint,
118+
})
119+
})
120+
.ok();
121+
(maybe_protocol, maybe_endpoint)
122+
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
#[cfg(feature = "metrics")]
2+
pub mod metrics;
3+
pub mod traces;
4+
5+
#[cfg(feature = "metrics")]
6+
use opentelemetry::metrics::MeterProvider;
7+
#[cfg(feature = "metrics")]
8+
use opentelemetry_sdk::metrics::SdkMeterProvider;
9+
10+
use opentelemetry::trace::TracerProvider;
11+
use opentelemetry_sdk::trace::SdkTracerProvider;
12+
13+
#[must_use = "Recommend holding with 'let _guard = ' pattern to ensure final traces/metrics are sent to the server"]
14+
/// On Drop of the `OtelGuard` instance,
15+
/// the wrapped Tracer/Meter Provider is force to flush and to shutdown (ignoring error).
16+
pub struct OtelGuard {
17+
#[cfg(feature = "metrics")]
18+
pub(crate) meter_provider: SdkMeterProvider,
19+
pub(crate) tracer_provider: SdkTracerProvider,
20+
}
21+
22+
impl OtelGuard {
23+
#[must_use]
24+
pub fn tracer_provider(&self) -> &impl TracerProvider {
25+
&self.tracer_provider
26+
}
27+
28+
#[cfg(feature = "metrics")]
29+
#[must_use]
30+
pub fn meter_provider(&self) -> &impl MeterProvider {
31+
&self.meter_provider
32+
}
33+
}
34+
35+
impl Drop for OtelGuard {
36+
#[allow(unused_must_use)]
37+
fn drop(&mut self) {
38+
let _ = self.tracer_provider.force_flush();
39+
let _ = self.tracer_provider.shutdown();
40+
#[cfg(feature = "metrics")]
41+
{
42+
let _ = self.meter_provider.force_flush();
43+
let _ = self.meter_provider.shutdown();
44+
}
45+
}
46+
}
47+
48+
#[allow(unused_mut)]
49+
pub(crate) fn infer_protocol(
50+
maybe_protocol: Option<&str>,
51+
maybe_endpoint: Option<&str>,
52+
) -> Option<String> {
53+
let mut maybe_protocol = match (maybe_protocol, maybe_endpoint) {
54+
(Some(protocol), _) => Some(protocol.to_string()),
55+
(None, Some(endpoint)) => {
56+
if endpoint.contains(":4317") {
57+
Some("grpc".to_string())
58+
} else {
59+
Some("http/protobuf".to_string())
60+
}
61+
}
62+
_ => None,
63+
};
64+
#[cfg(feature = "tls")]
65+
if maybe_protocol.as_deref() == Some("grpc")
66+
&& maybe_endpoint.is_some_and(|e| e.starts_with("https"))
67+
{
68+
maybe_protocol = Some("grpc/tls".to_string());
69+
}
70+
71+
maybe_protocol
72+
}
73+
74+
#[cfg(test)]
75+
mod tests {
76+
use assert2::assert;
77+
use rstest::rstest;
78+
79+
use super::*;
80+
81+
#[rstest]
82+
#[case(None, None, None)] //Devskim: ignore DS137138
83+
#[case(Some("http/protobuf"), None, Some("http/protobuf"))] //Devskim: ignore DS137138
84+
#[case(Some("grpc"), None, Some("grpc"))] //Devskim: ignore DS137138
85+
#[case(None, Some("http://localhost:4317"), Some("grpc"))] //Devskim: ignore DS137138
86+
#[cfg_attr(
87+
feature = "tls",
88+
case(None, Some("https://localhost:4317"), Some("grpc/tls"))
89+
)]
90+
#[cfg_attr(
91+
feature = "tls",
92+
case(Some("grpc/tls"), Some("https://localhost:4317"), Some("grpc/tls"))
93+
)]
94+
#[case(
95+
Some("http/protobuf"),
96+
Some("http://localhost:4318/v1/traces"), //Devskim: ignore DS137138
97+
Some("http/protobuf"),
98+
)]
99+
#[case(
100+
Some("http/protobuf"),
101+
Some("https://examples.com:4318/v1/traces"),
102+
Some("http/protobuf")
103+
)]
104+
#[case(
105+
Some("http/protobuf"),
106+
Some("https://examples.com:4317"),
107+
Some("http/protobuf")
108+
)]
109+
fn test_infer_protocol(
110+
#[case] traces_protocol: Option<&str>,
111+
#[case] traces_endpoint: Option<&str>,
112+
#[case] expected_protocol: Option<&str>,
113+
) {
114+
assert!(infer_protocol(traces_protocol, traces_endpoint).as_deref() == expected_protocol);
115+
}
116+
}
Lines changed: 1 addition & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use super::infer_protocol;
12
use opentelemetry_otlp::{ExporterBuildError, SpanExporter};
23
use opentelemetry_sdk::{trace::SdkTracerProvider, trace::TracerProviderBuilder, Resource};
34
#[cfg(feature = "tls")]
@@ -70,70 +71,3 @@ fn read_protocol_and_endpoint_from_env() -> (Option<String>, Option<String>) {
7071
.ok();
7172
(maybe_protocol, maybe_endpoint)
7273
}
73-
74-
#[allow(unused_mut)]
75-
fn infer_protocol(maybe_protocol: Option<&str>, maybe_endpoint: Option<&str>) -> Option<String> {
76-
let mut maybe_protocol = match (maybe_protocol, maybe_endpoint) {
77-
(Some(protocol), _) => Some(protocol.to_string()),
78-
(None, Some(endpoint)) => {
79-
if endpoint.contains(":4317") {
80-
Some("grpc".to_string())
81-
} else {
82-
Some("http/protobuf".to_string())
83-
}
84-
}
85-
_ => None,
86-
};
87-
#[cfg(feature = "tls")]
88-
if maybe_protocol.as_deref() == Some("grpc")
89-
&& maybe_endpoint.is_some_and(|e| e.starts_with("https"))
90-
{
91-
maybe_protocol = Some("grpc/tls".to_string());
92-
}
93-
94-
maybe_protocol
95-
}
96-
97-
#[cfg(test)]
98-
mod tests {
99-
use assert2::assert;
100-
use rstest::rstest;
101-
102-
use super::*;
103-
104-
#[rstest]
105-
#[case(None, None, None)] //Devskim: ignore DS137138
106-
#[case(Some("http/protobuf"), None, Some("http/protobuf"))] //Devskim: ignore DS137138
107-
#[case(Some("grpc"), None, Some("grpc"))] //Devskim: ignore DS137138
108-
#[case(None, Some("http://localhost:4317"), Some("grpc"))] //Devskim: ignore DS137138
109-
#[cfg_attr(
110-
feature = "tls",
111-
case(None, Some("https://localhost:4317"), Some("grpc/tls"))
112-
)]
113-
#[cfg_attr(
114-
feature = "tls",
115-
case(Some("grpc/tls"), Some("https://localhost:4317"), Some("grpc/tls"))
116-
)]
117-
#[case(
118-
Some("http/protobuf"),
119-
Some("http://localhost:4318/v1/traces"), //Devskim: ignore DS137138
120-
Some("http/protobuf"),
121-
)]
122-
#[case(
123-
Some("http/protobuf"),
124-
Some("https://examples.com:4318/v1/traces"),
125-
Some("http/protobuf")
126-
)]
127-
#[case(
128-
Some("http/protobuf"),
129-
Some("https://examples.com:4317"),
130-
Some("http/protobuf")
131-
)]
132-
fn test_infer_protocol(
133-
#[case] traces_protocol: Option<&str>,
134-
#[case] traces_endpoint: Option<&str>,
135-
#[case] expected_protocol: Option<&str>,
136-
) {
137-
assert!(infer_protocol(traces_protocol, traces_endpoint).as_deref() == expected_protocol);
138-
}
139-
}

0 commit comments

Comments
 (0)