Skip to content

Commit 95e2a56

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 95e2a56

File tree

3 files changed

+184
-25
lines changed

3 files changed

+184
-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

+182-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,92 @@ 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+
match Url::parse(&endpoint) {
107+
Ok(url) => {
108+
if url.path() == "/" {
109+
format!("{}{}", url.as_str().trim_end_matches('/'), signal)
110+
} else {
111+
endpoint
112+
}
113+
}
114+
Err(_) => endpoint,
80115
}
81116
}
82117
}
83118

84-
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
119+
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
85120
pub enum OtelProtocol {
86121
Grpc,
87122
Http,
88123
}
89124

125+
// Represents https://opentelemetry.io/docs/concepts/signals/
126+
enum OtelSignal {
127+
Traces,
128+
Metrics,
129+
Logs,
130+
}
131+
132+
impl std::fmt::Display for OtelSignal {
133+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
134+
write!(
135+
f,
136+
"/v1/{}",
137+
match self {
138+
OtelSignal::Traces => "traces",
139+
OtelSignal::Metrics => "metrics",
140+
OtelSignal::Logs => "logs",
141+
}
142+
)
143+
}
144+
}
145+
90146
impl Default for OtelProtocol {
91147
fn default() -> Self {
92148
Self::Http
@@ -109,3 +165,104 @@ impl FromStr for OtelProtocol {
109165

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

0 commit comments

Comments
 (0)