Skip to content

Commit 8495eb2

Browse files
committed
fix: Add signal-specific path components to OtelConfig's default endpoints
Fixes wasmCloud#2265 Signed-off-by: Joonas Bergius <joonas@cosmonic.com>
1 parent 825ef3a commit 8495eb2

File tree

3 files changed

+172
-25
lines changed

3 files changed

+172
-25
lines changed

Cargo.lock

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/core/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ tokio = { workspace = true }
4545
tower = { workspace = true }
4646
tracing = { workspace = true }
4747
ulid = { workspace = true, features = ["std"] }
48+
url = { workspace = true }
4849
uuid = { workspace = true, features = ["serde"] }
4950
wascap = { workspace = true }
5051
webpki-roots = { workspace = true, optional = true }

crates/core/src/otel.rs

+170-25
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,10 @@ use std::str::FromStr;
44

55
use anyhow::bail;
66
use serde::{Deserialize, Serialize};
7+
use url::Url;
78

89
use crate::wit::WitMap;
910

10-
// We redefine the upstream variables here since they are not exported by the opentelemetry-otlp crate:
11-
// https://github.com/open-telemetry/opentelemetry-rust/blob/opentelemetry-0.23.0/opentelemetry-otlp/src/exporter/mod.rs#L57-L60
12-
const OTEL_EXPORTER_OTLP_GRPC_ENDPOINT_DEFAULT: &str = "http://localhost:4317";
13-
const OTEL_EXPORTER_OTLP_HTTP_ENDPOINT_DEFAULT: &str = "http://localhost:4318";
14-
1511
/// Configuration values for OpenTelemetry
1612
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
1713
pub struct OtelConfig {
@@ -38,27 +34,15 @@ pub struct OtelConfig {
3834

3935
impl OtelConfig {
4036
pub fn logs_endpoint(&self) -> String {
41-
self.logs_endpoint.clone().unwrap_or_else(|| {
42-
self.observability_endpoint
43-
.clone()
44-
.unwrap_or_else(|| self.default_endpoint().to_string())
45-
})
37+
self.resolve_endpoint(OtelSignal::Logs, self.logs_endpoint.clone())
4638
}
4739

4840
pub fn metrics_endpoint(&self) -> String {
49-
self.metrics_endpoint.clone().unwrap_or_else(|| {
50-
self.observability_endpoint
51-
.clone()
52-
.unwrap_or_else(|| self.default_endpoint().to_string())
53-
})
41+
self.resolve_endpoint(OtelSignal::Metrics, self.metrics_endpoint.clone())
5442
}
5543

5644
pub fn traces_endpoint(&self) -> String {
57-
self.traces_endpoint.clone().unwrap_or_else(|| {
58-
self.observability_endpoint
59-
.clone()
60-
.unwrap_or_else(|| self.default_endpoint().to_string())
61-
})
45+
self.resolve_endpoint(OtelSignal::Traces, self.traces_endpoint.clone())
6246
}
6347

6448
pub fn logs_enabled(&self) -> bool {
@@ -73,20 +57,80 @@ impl OtelConfig {
7357
self.enable_traces.unwrap_or(self.enable_observability)
7458
}
7559

76-
fn default_endpoint(&self) -> &str {
77-
match self.protocol {
78-
OtelProtocol::Grpc => OTEL_EXPORTER_OTLP_GRPC_ENDPOINT_DEFAULT,
79-
OtelProtocol::Http => OTEL_EXPORTER_OTLP_HTTP_ENDPOINT_DEFAULT,
60+
// We have 3 potential outcomes depending on the provided configuration:
61+
// 1. We are given a signal-specific endpoint to use, which we'll use as-is.
62+
// 2. We are given an endpoint that each of the signal paths should added to
63+
// 3. We are given nothing, and we should simply default to an empty string,
64+
// which lets the opentelemetry-otlp library handle defaults appropriately.
65+
fn resolve_endpoint(
66+
&self,
67+
signal: OtelSignal,
68+
signal_endpoint_override: Option<String>,
69+
) -> String {
70+
// If we have a signal specific endpoint override, use it as provided.
71+
if let Some(endpoint) = signal_endpoint_override {
72+
return endpoint;
73+
}
74+
75+
if let Some(endpoint) = self.observability_endpoint.clone() {
76+
return match self.protocol {
77+
OtelProtocol::Grpc => self.resolve_grpc_endpoint(endpoint),
78+
OtelProtocol::Http => self.resolve_http_endpoint(signal, endpoint),
79+
};
80+
}
81+
82+
// If we have no match, fall back to empty string to let the opentelemetry-otlp
83+
// library handling turn into the signal-specific default endpoint.
84+
String::new()
85+
}
86+
87+
// opentelemetry-otlp expects the gRPC endpoint to not have path components
88+
// configured, so we're just clearing them out and returning the base url.
89+
fn resolve_grpc_endpoint(&self, endpoint: String) -> String {
90+
match Url::parse(&endpoint) {
91+
Ok(mut url) => {
92+
if let Ok(mut path) = url.path_segments_mut() {
93+
path.clear();
94+
}
95+
url.as_str().trim_end_matches('/').to_string()
96+
}
97+
Err(_) => endpoint,
98+
}
99+
}
100+
101+
// opentelemetry-otlp expects the http endpoint to be fully configured
102+
// including the path, so we check whether there's a path already configured
103+
// and use the url as configured, or append the signal-specific path to the
104+
// provided endpoint.
105+
fn resolve_http_endpoint(&self, signal: OtelSignal, endpoint: String) -> String {
106+
let base_url = endpoint.trim_end_matches('/');
107+
108+
// Check whether the URL passed in has path components, if not, append the signal-appropriate path, otherwise use as is.
109+
if base_url.matches('/').count() == 2 {
110+
match signal {
111+
OtelSignal::Traces => format!("{}/v1/traces", base_url),
112+
OtelSignal::Metrics => format!("{}/v1/metrics", base_url),
113+
OtelSignal::Logs => format!("{}/v1/logs", base_url),
114+
}
115+
} else {
116+
endpoint
80117
}
81118
}
82119
}
83120

84-
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
121+
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
85122
pub enum OtelProtocol {
86123
Grpc,
87124
Http,
88125
}
89126

127+
// Represents https://opentelemetry.io/docs/concepts/signals/
128+
enum OtelSignal {
129+
Traces,
130+
Metrics,
131+
Logs,
132+
}
133+
90134
impl Default for OtelProtocol {
91135
fn default() -> Self {
92136
Self::Http
@@ -109,3 +153,104 @@ impl FromStr for OtelProtocol {
109153

110154
/// Environment settings for initializing a capability provider
111155
pub type TraceContext = WitMap<String>;
156+
157+
#[cfg(test)]
158+
mod tests {
159+
use super::{OtelConfig, OtelProtocol};
160+
161+
#[test]
162+
fn test_grpc_resolves_to_empty_string_without_overrides() {
163+
let config = OtelConfig {
164+
protocol: OtelProtocol::Grpc,
165+
..Default::default()
166+
};
167+
168+
let expected = String::from("");
169+
170+
assert_eq!(expected, config.traces_endpoint());
171+
assert_eq!(expected, config.metrics_endpoint());
172+
assert_eq!(expected, config.logs_endpoint());
173+
}
174+
175+
#[test]
176+
fn test_grpc_resolves_to_base_url_without_path_components() {
177+
let config = OtelConfig {
178+
protocol: OtelProtocol::Grpc,
179+
observability_endpoint: Some(String::from(
180+
"https://example.com:4318/path/does/not/exist",
181+
)),
182+
..Default::default()
183+
};
184+
185+
let expected = String::from("https://example.com:4318");
186+
187+
assert_eq!(expected, config.traces_endpoint());
188+
assert_eq!(expected, config.metrics_endpoint());
189+
assert_eq!(expected, config.logs_endpoint());
190+
}
191+
192+
#[test]
193+
fn test_grpc_resolves_to_signal_specific_overrides_as_provided() {
194+
let config = OtelConfig {
195+
protocol: OtelProtocol::Grpc,
196+
traces_endpoint: Some(String::from("https://example.com:4318/path/does/not/exist")),
197+
..Default::default()
198+
};
199+
200+
let expected_traces = String::from("https://example.com:4318/path/does/not/exist");
201+
let expected_others = String::from("");
202+
203+
assert_eq!(expected_traces, config.traces_endpoint());
204+
assert_eq!(expected_others, config.metrics_endpoint());
205+
assert_eq!(expected_others, config.logs_endpoint());
206+
}
207+
208+
#[test]
209+
fn test_http_resolves_to_empty_string_without_overrides() {
210+
let config = OtelConfig {
211+
protocol: OtelProtocol::Http,
212+
..Default::default()
213+
};
214+
215+
let expected = String::from("");
216+
217+
assert_eq!(expected, config.traces_endpoint());
218+
assert_eq!(expected, config.metrics_endpoint());
219+
assert_eq!(expected, config.logs_endpoint());
220+
}
221+
222+
#[test]
223+
fn test_http_configuration_for_specific_signal_should_not_affect_other_signals() {
224+
let config = OtelConfig {
225+
protocol: OtelProtocol::Http,
226+
traces_endpoint: Some(String::from(
227+
"https://example.com:4318/v1/traces/or/something",
228+
)),
229+
..Default::default()
230+
};
231+
232+
let expected_traces = String::from("https://example.com:4318/v1/traces/or/something");
233+
let expected_others = String::from("");
234+
235+
assert_eq!(expected_traces, config.traces_endpoint());
236+
assert_eq!(expected_others, config.metrics_endpoint());
237+
assert_eq!(expected_others, config.logs_endpoint());
238+
}
239+
240+
#[test]
241+
fn test_http_should_be_configurable_across_all_signals_via_observability_endpoint() {
242+
let config = OtelConfig {
243+
protocol: OtelProtocol::Http,
244+
observability_endpoint: Some(String::from("https://example.com:4318")),
245+
..Default::default()
246+
};
247+
248+
let expected_traces = String::from("https://example.com:4318/v1/traces");
249+
let expected_metrics = String::from("https://example.com:4318/v1/metrics");
250+
let expected_logs = String::from("https://example.com:4318/v1/logs");
251+
252+
assert_eq!(expected_traces, config.traces_endpoint());
253+
assert_eq!(expected_metrics, config.metrics_endpoint());
254+
assert_eq!(expected_logs, config.logs_endpoint());
255+
}
256+
}

0 commit comments

Comments
 (0)