1use std::{path::PathBuf, str::FromStr};
6
7use anyhow::bail;
8use serde::{Deserialize, Serialize};
9use url::Url;
10
11use crate::{logging::Level, wit::WitMap};
12
13#[derive(Clone, Debug, Default, Deserialize, Serialize)]
15pub struct OtelConfig {
16 #[serde(default)]
18 pub enable_observability: bool,
19 #[serde(skip_serializing_if = "Option::is_none")]
21 pub enable_traces: Option<bool>,
22 #[serde(skip_serializing_if = "Option::is_none")]
24 pub enable_metrics: Option<bool>,
25 #[serde(skip_serializing_if = "Option::is_none")]
27 pub enable_logs: Option<bool>,
28 #[serde(skip_serializing_if = "Option::is_none")]
30 pub observability_endpoint: Option<String>,
31 #[serde(skip_serializing_if = "Option::is_none")]
33 pub traces_endpoint: Option<String>,
34 #[serde(skip_serializing_if = "Option::is_none")]
36 pub metrics_endpoint: Option<String>,
37 #[serde(skip_serializing_if = "Option::is_none")]
39 pub logs_endpoint: Option<String>,
40 #[serde(default)]
42 pub protocol: OtelProtocol,
43 #[serde(default)]
45 pub additional_ca_paths: Vec<PathBuf>,
46 #[serde(default)]
48 pub trace_level: Level,
49 #[serde(skip_serializing_if = "Option::is_none")]
52 pub traces_sampler: Option<String>,
53 #[serde(skip_serializing_if = "Option::is_none")]
56 pub traces_sampler_arg: Option<String>,
57 #[serde(skip_serializing_if = "Option::is_none")]
62 pub max_batch_queue_size: Option<usize>,
63 #[serde(skip_serializing_if = "Option::is_none")]
70 pub concurrent_exports: Option<usize>,
71}
72
73impl OtelConfig {
74 pub fn logs_endpoint(&self) -> String {
75 self.resolve_endpoint(OtelSignal::Logs, self.logs_endpoint.clone())
76 }
77
78 pub fn metrics_endpoint(&self) -> String {
79 self.resolve_endpoint(OtelSignal::Metrics, self.metrics_endpoint.clone())
80 }
81
82 pub fn traces_endpoint(&self) -> String {
83 self.resolve_endpoint(OtelSignal::Traces, self.traces_endpoint.clone())
84 }
85
86 pub fn logs_enabled(&self) -> bool {
87 self.enable_logs.unwrap_or(self.enable_observability)
88 }
89
90 pub fn metrics_enabled(&self) -> bool {
91 self.enable_metrics.unwrap_or(self.enable_observability)
92 }
93
94 pub fn traces_enabled(&self) -> bool {
95 self.enable_traces.unwrap_or(self.enable_observability)
96 }
97
98 fn resolve_endpoint(
104 &self,
105 signal: OtelSignal,
106 signal_endpoint_override: Option<String>,
107 ) -> String {
108 if let Some(endpoint) = signal_endpoint_override {
110 return endpoint;
111 }
112 if let Some(endpoint) = self.observability_endpoint.clone() {
113 return match self.protocol {
114 OtelProtocol::Grpc => self.resolve_grpc_endpoint(endpoint),
115 OtelProtocol::Http => self.resolve_http_endpoint(signal, endpoint),
116 };
117 }
118 match self.protocol {
120 OtelProtocol::Grpc => "http://127.0.0.1:4317".to_string(),
121 OtelProtocol::Http => format!("http://127.0.0.1:4318{signal}"),
122 }
123 }
124
125 fn resolve_grpc_endpoint(&self, endpoint: String) -> String {
128 match Url::parse(&endpoint) {
129 Ok(mut url) => {
130 if let Ok(mut path) = url.path_segments_mut() {
131 path.clear();
132 }
133 url.as_str().trim_end_matches('/').to_string()
134 }
135 Err(_) => endpoint,
136 }
137 }
138
139 fn resolve_http_endpoint(&self, signal: OtelSignal, endpoint: String) -> String {
144 match Url::parse(&endpoint) {
145 Ok(url) => {
146 if url.path() == "/" {
147 format!("{}{}", url.as_str().trim_end_matches('/'), signal)
148 } else {
149 endpoint
150 }
151 }
152 Err(_) => endpoint,
153 }
154 }
155}
156
157#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
158#[derive(Default)]
163pub enum OtelProtocol {
164 #[serde(alias = "grpc", alias = "Grpc")]
165 Grpc,
166 #[serde(alias = "http", alias = "Http")]
167 #[default]
168 Http,
169}
170
171enum OtelSignal {
173 Traces,
174 Metrics,
175 Logs,
176}
177
178impl std::fmt::Display for OtelSignal {
179 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
180 write!(
181 f,
182 "/v1/{}",
183 match self {
184 OtelSignal::Traces => "traces",
185 OtelSignal::Metrics => "metrics",
186 OtelSignal::Logs => "logs",
187 }
188 )
189 }
190}
191
192impl FromStr for OtelProtocol {
193 type Err = anyhow::Error;
194
195 fn from_str(s: &str) -> Result<Self, Self::Err> {
196 match s {
197 "http" => Ok(Self::Http),
198 "grpc" => Ok(Self::Grpc),
199 protocol => {
200 bail!("unsupported protocol: {protocol:?}, did you mean 'http' or 'grpc'?")
201 }
202 }
203 }
204}
205
206pub type TraceContext = WitMap<String>;
208
209#[cfg(test)]
210mod tests {
211 use super::{OtelConfig, OtelProtocol};
212
213 #[test]
214 fn test_grpc_resolves_to_defaults_without_overrides() {
215 let config = OtelConfig {
216 protocol: OtelProtocol::Grpc,
217 ..Default::default()
218 };
219
220 let expected = String::from("http://127.0.0.1:4317");
221
222 assert_eq!(expected, config.traces_endpoint());
223 assert_eq!(expected, config.metrics_endpoint());
224 assert_eq!(expected, config.logs_endpoint());
225 }
226
227 #[test]
228 fn test_grpc_resolves_to_base_url_without_path_components() {
229 let config = OtelConfig {
230 protocol: OtelProtocol::Grpc,
231 observability_endpoint: Some(String::from(
232 "https://example.com:4318/path/does/not/exist",
233 )),
234 ..Default::default()
235 };
236
237 let expected = String::from("https://example.com:4318");
238
239 assert_eq!(expected, config.traces_endpoint());
240 assert_eq!(expected, config.metrics_endpoint());
241 assert_eq!(expected, config.logs_endpoint());
242 }
243
244 #[test]
245 fn test_grpc_resolves_to_signal_specific_overrides_as_provided() {
246 let config = OtelConfig {
247 protocol: OtelProtocol::Grpc,
248 traces_endpoint: Some(String::from("https://example.com:4318/path/does/not/exist")),
249 ..Default::default()
250 };
251
252 let expected_traces = String::from("https://example.com:4318/path/does/not/exist");
253 let expected_others = String::from("http://127.0.0.1:4317");
254
255 assert_eq!(expected_traces, config.traces_endpoint());
256 assert_eq!(expected_others, config.metrics_endpoint());
257 assert_eq!(expected_others, config.logs_endpoint());
258 }
259
260 #[test]
261 fn test_http_resolves_to_defaults_without_overrides() {
262 let config = OtelConfig {
263 protocol: OtelProtocol::Http,
264 ..Default::default()
265 };
266
267 let expected_traces = String::from("http://127.0.0.1:4318/v1/traces");
268 let expected_metrics = String::from("http://127.0.0.1:4318/v1/metrics");
269 let expected_logs = String::from("http://127.0.0.1:4318/v1/logs");
270
271 assert_eq!(expected_traces, config.traces_endpoint());
272 assert_eq!(expected_metrics, config.metrics_endpoint());
273 assert_eq!(expected_logs, config.logs_endpoint());
274 }
275
276 #[test]
277 fn test_http_configuration_for_specific_signal_should_not_affect_other_signals() {
278 let config = OtelConfig {
279 protocol: OtelProtocol::Http,
280 traces_endpoint: Some(String::from(
281 "https://example.com:4318/v1/traces/or/something",
282 )),
283 ..Default::default()
284 };
285
286 let expected_traces = String::from("https://example.com:4318/v1/traces/or/something");
287 let expected_metrics = String::from("http://127.0.0.1:4318/v1/metrics");
288 let expected_logs = String::from("http://127.0.0.1:4318/v1/logs");
289
290 assert_eq!(expected_traces, config.traces_endpoint());
291 assert_eq!(expected_metrics, config.metrics_endpoint());
292 assert_eq!(expected_logs, config.logs_endpoint());
293 }
294
295 #[test]
296 fn test_http_should_be_configurable_across_all_signals_via_observability_endpoint() {
297 let config = OtelConfig {
298 protocol: OtelProtocol::Http,
299 observability_endpoint: Some(String::from("https://example.com:4318")),
300 ..Default::default()
301 };
302
303 let expected_traces = String::from("https://example.com:4318/v1/traces");
304 let expected_metrics = String::from("https://example.com:4318/v1/metrics");
305 let expected_logs = String::from("https://example.com:4318/v1/logs");
306
307 assert_eq!(expected_traces, config.traces_endpoint());
308 assert_eq!(expected_metrics, config.metrics_endpoint());
309 assert_eq!(expected_logs, config.logs_endpoint());
310 }
311}