Skip to content

Commit 72cc025

Browse files
authored
Merge pull request #2475 from calebschoepp/otel-metrics-via-tracing
Taking a first crack at implementing metrics
2 parents 7d66e91 + 1b0f40d commit 72cc025

File tree

10 files changed

+252
-67
lines changed

10 files changed

+252
-67
lines changed

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/telemetry/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,4 @@ tracing-appender = "0.2.2"
1717
tracing-opentelemetry = "0.23.0"
1818
tracing-subscriber = { version = "0.3.17", features = ["env-filter", "json", "registry"] }
1919
url = "2.2.2"
20+
terminal = { path = "../terminal" }

crates/telemetry/src/env.rs

Lines changed: 65 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,44 @@
1-
/// Returns a boolean indicating if the OTEL layer should be enabled.
1+
use std::env::VarError;
2+
3+
use opentelemetry_otlp::{
4+
OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_EXPORTER_OTLP_METRICS_ENDPOINT, OTEL_EXPORTER_OTLP_PROTOCOL,
5+
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT,
6+
};
7+
8+
const OTEL_SDK_DISABLED: &str = "OTEL_SDK_DISABLED";
9+
const OTEL_EXPORTER_OTLP_TRACES_PROTOCOL: &str = "OTEL_EXPORTER_OTLP_TRACES_PROTOCOL";
10+
const OTEL_EXPORTER_OTLP_METRICS_PROTOCOL: &str = "OTEL_EXPORTER_OTLP_METRICS_PROTOCOL";
11+
12+
/// Returns a boolean indicating if the OTEL tracing layer should be enabled.
213
///
314
/// It is considered enabled if any of the following environment variables are set and not empty:
415
/// - `OTEL_EXPORTER_OTLP_ENDPOINT`
516
/// - `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT`
17+
///
18+
/// Note that this is overridden if OTEL_SDK_DISABLED is set and not empty.
19+
pub(crate) fn otel_tracing_enabled() -> bool {
20+
any_vars_set(&[
21+
OTEL_EXPORTER_OTLP_ENDPOINT,
22+
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT,
23+
]) && !otel_sdk_disabled()
24+
}
25+
26+
/// Returns a boolean indicating if the OTEL metrics layer should be enabled.
27+
///
28+
/// It is considered enabled if any of the following environment variables are set and not empty:
29+
/// - `OTEL_EXPORTER_OTLP_ENDPOINT`
630
/// - `OTEL_EXPORTER_OTLP_METRICS_ENDPOINT`
731
///
832
/// Note that this is overridden if OTEL_SDK_DISABLED is set and not empty.
9-
pub(crate) fn otel_enabled() -> bool {
10-
const ENABLING_VARS: &[&str] = &[
11-
"OTEL_EXPORTER_OTLP_ENDPOINT",
12-
"OTEL_EXPORTER_OTLP_TRACES_ENDPOINT",
13-
"OTEL_EXPORTER_OTLP_METRICS_ENDPOINT",
14-
];
15-
ENABLING_VARS
33+
pub(crate) fn otel_metrics_enabled() -> bool {
34+
any_vars_set(&[
35+
OTEL_EXPORTER_OTLP_ENDPOINT,
36+
OTEL_EXPORTER_OTLP_METRICS_ENDPOINT,
37+
]) && !otel_sdk_disabled()
38+
}
39+
40+
fn any_vars_set(enabling_vars: &[&str]) -> bool {
41+
enabling_vars
1642
.iter()
1743
.any(|key| std::env::var_os(key).is_some_and(|val| !val.is_empty()))
1844
}
@@ -21,7 +47,7 @@ pub(crate) fn otel_enabled() -> bool {
2147
///
2248
/// It is considered disabled if the environment variable `OTEL_SDK_DISABLED` is set and not empty.
2349
pub(crate) fn otel_sdk_disabled() -> bool {
24-
std::env::var_os("OTEL_SDK_DISABLED").is_some_and(|val| !val.is_empty())
50+
std::env::var_os(OTEL_SDK_DISABLED).is_some_and(|val| !val.is_empty())
2551
}
2652

2753
/// The protocol to use for OTLP exporter.
@@ -34,15 +60,41 @@ pub(crate) enum OtlpProtocol {
3460
impl OtlpProtocol {
3561
/// Returns the protocol to be used for exporting traces as defined by the environment.
3662
pub(crate) fn traces_protocol_from_env() -> Self {
37-
let trace_protocol = std::env::var("OTEL_EXPORTER_OTLP_TRACES_PROTOCOL");
38-
let general_protocol = std::env::var("OTEL_EXPORTER_OTLP_PROTOCOL");
39-
let protocol = trace_protocol.unwrap_or(general_protocol.unwrap_or_default());
63+
Self::protocol_from_env(
64+
std::env::var(OTEL_EXPORTER_OTLP_TRACES_PROTOCOL),
65+
std::env::var(OTEL_EXPORTER_OTLP_PROTOCOL),
66+
)
67+
}
68+
69+
/// Returns the protocol to be used for exporting metrics as defined by the environment.
70+
pub(crate) fn metrics_protocol_from_env() -> Self {
71+
Self::protocol_from_env(
72+
std::env::var(OTEL_EXPORTER_OTLP_METRICS_PROTOCOL),
73+
std::env::var(OTEL_EXPORTER_OTLP_PROTOCOL),
74+
)
75+
}
76+
77+
fn protocol_from_env(
78+
specific_protocol: Result<String, VarError>,
79+
general_protocol: Result<String, VarError>,
80+
) -> Self {
81+
let protocol =
82+
specific_protocol.unwrap_or(general_protocol.unwrap_or("http/protobuf".to_string()));
83+
84+
static WARN_ONCE: std::sync::Once = std::sync::Once::new();
4085

4186
match protocol.as_str() {
4287
"grpc" => Self::Grpc,
4388
"http/protobuf" => Self::HttpProtobuf,
4489
"http/json" => Self::HttpJson,
45-
_ => Self::HttpProtobuf,
90+
s => {
91+
WARN_ONCE.call_once(|| {
92+
terminal::warn!(
93+
"'{s}' is not a valid OTLP protocol, defaulting to http/protobuf"
94+
);
95+
});
96+
Self::HttpProtobuf
97+
}
4698
}
4799
}
48100
}

crates/telemetry/src/lib.rs

Lines changed: 45 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
use std::io::IsTerminal;
22

3-
use env::otel_enabled;
4-
use env::otel_sdk_disabled;
3+
use env::otel_metrics_enabled;
4+
use env::otel_tracing_enabled;
55
use opentelemetry_sdk::propagation::TraceContextPropagator;
66
use tracing_subscriber::{fmt, prelude::*, registry, EnvFilter, Layer};
77

88
pub mod detector;
99
mod env;
10+
pub mod metrics;
1011
mod propagation;
1112
mod traces;
1213

@@ -16,9 +17,34 @@ pub use propagation::inject_trace_context;
1617
/// Initializes telemetry for Spin using the [tracing] library.
1718
///
1819
/// Under the hood this involves initializing a [tracing::Subscriber] with multiple [Layer]s. One
19-
/// [Layer] emits [tracing] events to stderr, and another sends spans to an OTEL collector.
20+
/// [Layer] emits [tracing] events to stderr, another sends spans to an OTel collector, and another
21+
/// sends metrics to an OTel collector.
2022
///
21-
/// Configuration is pulled from the environment.
23+
/// Configuration for the OTel layers is pulled from the environment.
24+
///
25+
/// Examples of emitting traces from Spin:
26+
///
27+
/// ```no_run
28+
/// # use tracing::instrument;
29+
/// # use tracing::Level;
30+
/// #[instrument(name = "span_name", err(level = Level::INFO), fields(otel.name = "dynamically set name"))]
31+
/// fn func_you_want_to_trace() -> anyhow::Result<String> {
32+
/// Ok("Hello, world!".to_string())
33+
/// }
34+
/// ```
35+
///
36+
/// Some notes on tracing:
37+
///
38+
/// - If you don't want the span to be collected by default emit it at a trace or debug level.
39+
/// - Make sure you `.in_current_span()` any spawned tasks so the span context is propagated.
40+
/// - Use the otel.name attribute to dynamically set the span name.
41+
/// - Use the err argument to have instrument automatically handle errors.
42+
///
43+
/// Examples of emitting metrics from Spin:
44+
///
45+
/// ```no_run
46+
/// spin_telemetry::metrics::monotonic_counter!(spin.metric_name = 1, metric_attribute = "value");
47+
/// ```
2248
pub fn init(spin_version: String) -> anyhow::Result<ShutdownGuard> {
2349
// This layer will print all tracing library log messages to stderr.
2450
let fmt_layer = fmt::layer()
@@ -30,19 +56,27 @@ pub fn init(spin_version: String) -> anyhow::Result<ShutdownGuard> {
3056
.add_directive("watchexec=off".parse()?),
3157
);
3258

33-
// We only want to build the otel layer if the user passed some endpoint configuration and it wasn't explicitly disabled.
34-
let build_otel_layer = !otel_sdk_disabled() && otel_enabled();
35-
let otel_layer = if build_otel_layer {
36-
// In this case we want to set the error handler to log errors to the tracing layer.
37-
opentelemetry::global::set_error_handler(otel_error_handler)?;
59+
// Even if metrics or tracing aren't enabled we're okay to turn on the global error handler
60+
opentelemetry::global::set_error_handler(otel_error_handler)?;
61+
62+
let otel_tracing_layer = if otel_tracing_enabled() {
63+
Some(traces::otel_tracing_layer(spin_version.clone())?)
64+
} else {
65+
None
66+
};
3867

39-
Some(traces::otel_tracing_layer(spin_version)?)
68+
let otel_metrics_layer = if otel_metrics_enabled() {
69+
Some(metrics::otel_metrics_layer(spin_version)?)
4070
} else {
4171
None
4272
};
4373

4474
// Build a registry subscriber with the layers we want to use.
45-
registry().with(otel_layer).with(fmt_layer).init();
75+
registry()
76+
.with(otel_tracing_layer)
77+
.with(otel_metrics_layer)
78+
.with(fmt_layer)
79+
.init();
4680

4781
// Used to propagate trace information in the standard W3C TraceContext format. Even if the otel
4882
// layer is disabled we still want to propagate trace context.

crates/telemetry/src/metrics.rs

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
use std::time::Duration;
2+
3+
use anyhow::{bail, Result};
4+
use opentelemetry_otlp::MetricsExporterBuilder;
5+
use opentelemetry_sdk::{
6+
metrics::{
7+
reader::{DefaultAggregationSelector, DefaultTemporalitySelector},
8+
PeriodicReader, SdkMeterProvider,
9+
},
10+
resource::{EnvResourceDetector, TelemetryResourceDetector},
11+
runtime, Resource,
12+
};
13+
use tracing_opentelemetry::{MetricsLayer, OpenTelemetryLayer};
14+
use tracing_subscriber::{filter::Filtered, layer::Layered, EnvFilter, Registry};
15+
16+
use crate::{detector::SpinResourceDetector, env::OtlpProtocol};
17+
18+
/// Constructs a layer for the tracing subscriber that sends metrics to an OTEL collector.
19+
///
20+
/// It pulls OTEL configuration from the environment based on the variables defined
21+
/// [here](https://opentelemetry.io/docs/specs/otel/protocol/exporter/) and
22+
/// [here](https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/#general-sdk-configuration).
23+
pub(crate) fn otel_metrics_layer(spin_version: String) -> Result<CustomMetricsLayer> {
24+
let resource = Resource::from_detectors(
25+
Duration::from_secs(5),
26+
vec![
27+
// Set service.name from env OTEL_SERVICE_NAME > env OTEL_RESOURCE_ATTRIBUTES > spin
28+
// Set service.version from Spin metadata
29+
Box::new(SpinResourceDetector::new(spin_version)),
30+
// Sets fields from env OTEL_RESOURCE_ATTRIBUTES
31+
Box::new(EnvResourceDetector::new()),
32+
// Sets telemetry.sdk{name, language, version}
33+
Box::new(TelemetryResourceDetector),
34+
],
35+
);
36+
37+
// This will configure the exporter based on the OTEL_EXPORTER_* environment variables. We
38+
// currently default to using the HTTP exporter but in the future we could select off of the
39+
// combination of OTEL_EXPORTER_OTLP_PROTOCOL and OTEL_EXPORTER_OTLP_TRACES_PROTOCOL to
40+
// determine whether we should use http/protobuf or grpc.
41+
let exporter_builder: MetricsExporterBuilder = match OtlpProtocol::metrics_protocol_from_env() {
42+
OtlpProtocol::Grpc => opentelemetry_otlp::new_exporter().tonic().into(),
43+
OtlpProtocol::HttpProtobuf => opentelemetry_otlp::new_exporter().http().into(),
44+
OtlpProtocol::HttpJson => bail!("http/json OTLP protocol is not supported"),
45+
};
46+
let exporter = exporter_builder.build_metrics_exporter(
47+
Box::new(DefaultTemporalitySelector::new()),
48+
Box::new(DefaultAggregationSelector::new()),
49+
)?;
50+
51+
let reader = PeriodicReader::builder(exporter, runtime::Tokio).build();
52+
let meter_provider = SdkMeterProvider::builder()
53+
.with_reader(reader)
54+
.with_resource(resource)
55+
.build();
56+
57+
Ok(MetricsLayer::new(meter_provider))
58+
}
59+
60+
#[macro_export]
61+
/// Records an increment to the named counter with the given attributes.
62+
///
63+
/// The increment may only be an i64 or f64. You must not mix types for the same metric.
64+
///
65+
/// ```no_run
66+
/// # use spin_telemetry::metrics::counter;
67+
/// counter!(spin.metric_name = 1, metric_attribute = "value");
68+
/// ```
69+
macro_rules! counter {
70+
($metric:ident $(. $suffixes:ident)* = $metric_value:expr $(, $attrs:ident=$values:expr)*) => {
71+
tracing::trace!(counter.$metric $(. $suffixes)* = $metric_value $(, $attrs=$values)*);
72+
}
73+
}
74+
75+
#[macro_export]
76+
/// Adds an additional value to the distribution of the named histogram with the given attributes.
77+
///
78+
/// The increment may only be an i64 or f64. You must not mix types for the same metric.
79+
///
80+
/// ```no_run
81+
/// # use spin_telemetry::metrics::histogram;
82+
/// histogram!(spin.metric_name = 1.5, metric_attribute = "value");
83+
/// ```
84+
macro_rules! histogram {
85+
($metric:ident $(. $suffixes:ident)* = $metric_value:expr $(, $attrs:ident=$values:expr)*) => {
86+
tracing::trace!(histogram.$metric $(. $suffixes)* = $metric_value $(, $attrs=$values)*);
87+
}
88+
}
89+
90+
#[macro_export]
91+
/// Records an increment to the named monotonic counter with the given attributes.
92+
///
93+
/// The increment may only be a positive i64 or f64. You must not mix types for the same metric.
94+
///
95+
/// ```no_run
96+
/// # use spin_telemetry::metrics::monotonic_counter;
97+
/// monotonic_counter!(spin.metric_name = 1, metric_attribute = "value");
98+
/// ```
99+
macro_rules! monotonic_counter {
100+
($metric:ident $(. $suffixes:ident)* = $metric_value:expr $(, $attrs:ident=$values:expr)*) => {
101+
tracing::trace!(monotonic_counter.$metric $(. $suffixes)* = $metric_value $(, $attrs=$values)*);
102+
}
103+
}
104+
105+
pub use counter;
106+
pub use histogram;
107+
pub use monotonic_counter;
108+
109+
/// This really large type alias is require to make the registry.with() pattern happy.
110+
type CustomMetricsLayer = MetricsLayer<
111+
Layered<
112+
Option<
113+
Filtered<
114+
OpenTelemetryLayer<Registry, opentelemetry_sdk::trace::Tracer>,
115+
EnvFilter,
116+
Registry,
117+
>,
118+
>,
119+
Registry,
120+
>,
121+
>;

0 commit comments

Comments
 (0)