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)]
158pub enum OtelProtocol {
163 #[serde(alias = "grpc", alias = "Grpc")]
164 Grpc,
165 #[serde(alias = "http", alias = "Http")]
166 Http,
167}
168
169enum OtelSignal {
171 Traces,
172 Metrics,
173 Logs,
174}
175
176impl std::fmt::Display for OtelSignal {
177 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
178 write!(
179 f,
180 "/v1/{}",
181 match self {
182 OtelSignal::Traces => "traces",
183 OtelSignal::Metrics => "metrics",
184 OtelSignal::Logs => "logs",
185 }
186 )
187 }
188}
189
190impl Default for OtelProtocol {
191 fn default() -> Self {
192 Self::Http
193 }
194}
195
196impl FromStr for OtelProtocol {
197 type Err = anyhow::Error;
198
199 fn from_str(s: &str) -> Result<Self, Self::Err> {
200 match s {
201 "http" => Ok(Self::Http),
202 "grpc" => Ok(Self::Grpc),
203 protocol => {
204 bail!("unsupported protocol: {protocol:?}, did you mean 'http' or 'grpc'?")
205 }
206 }
207 }
208}
209
210pub type TraceContext = WitMap<String>;
212
213#[cfg(test)]
214mod tests {
215 use super::{OtelConfig, OtelProtocol};
216
217 #[test]
218 fn test_grpc_resolves_to_defaults_without_overrides() {
219 let config = OtelConfig {
220 protocol: OtelProtocol::Grpc,
221 ..Default::default()
222 };
223
224 let expected = String::from("http://127.0.0.1:4317");
225
226 assert_eq!(expected, config.traces_endpoint());
227 assert_eq!(expected, config.metrics_endpoint());
228 assert_eq!(expected, config.logs_endpoint());
229 }
230
231 #[test]
232 fn test_grpc_resolves_to_base_url_without_path_components() {
233 let config = OtelConfig {
234 protocol: OtelProtocol::Grpc,
235 observability_endpoint: Some(String::from(
236 "https://example.com:4318/path/does/not/exist",
237 )),
238 ..Default::default()
239 };
240
241 let expected = String::from("https://example.com:4318");
242
243 assert_eq!(expected, config.traces_endpoint());
244 assert_eq!(expected, config.metrics_endpoint());
245 assert_eq!(expected, config.logs_endpoint());
246 }
247
248 #[test]
249 fn test_grpc_resolves_to_signal_specific_overrides_as_provided() {
250 let config = OtelConfig {
251 protocol: OtelProtocol::Grpc,
252 traces_endpoint: Some(String::from("https://example.com:4318/path/does/not/exist")),
253 ..Default::default()
254 };
255
256 let expected_traces = String::from("https://example.com:4318/path/does/not/exist");
257 let expected_others = String::from("http://127.0.0.1:4317");
258
259 assert_eq!(expected_traces, config.traces_endpoint());
260 assert_eq!(expected_others, config.metrics_endpoint());
261 assert_eq!(expected_others, config.logs_endpoint());
262 }
263
264 #[test]
265 fn test_http_resolves_to_defaults_without_overrides() {
266 let config = OtelConfig {
267 protocol: OtelProtocol::Http,
268 ..Default::default()
269 };
270
271 let expected_traces = String::from("http://127.0.0.1:4318/v1/traces");
272 let expected_metrics = String::from("http://127.0.0.1:4318/v1/metrics");
273 let expected_logs = String::from("http://127.0.0.1:4318/v1/logs");
274
275 assert_eq!(expected_traces, config.traces_endpoint());
276 assert_eq!(expected_metrics, config.metrics_endpoint());
277 assert_eq!(expected_logs, config.logs_endpoint());
278 }
279
280 #[test]
281 fn test_http_configuration_for_specific_signal_should_not_affect_other_signals() {
282 let config = OtelConfig {
283 protocol: OtelProtocol::Http,
284 traces_endpoint: Some(String::from(
285 "https://example.com:4318/v1/traces/or/something",
286 )),
287 ..Default::default()
288 };
289
290 let expected_traces = String::from("https://example.com:4318/v1/traces/or/something");
291 let expected_metrics = String::from("http://127.0.0.1:4318/v1/metrics");
292 let expected_logs = String::from("http://127.0.0.1:4318/v1/logs");
293
294 assert_eq!(expected_traces, config.traces_endpoint());
295 assert_eq!(expected_metrics, config.metrics_endpoint());
296 assert_eq!(expected_logs, config.logs_endpoint());
297 }
298
299 #[test]
300 fn test_http_should_be_configurable_across_all_signals_via_observability_endpoint() {
301 let config = OtelConfig {
302 protocol: OtelProtocol::Http,
303 observability_endpoint: Some(String::from("https://example.com:4318")),
304 ..Default::default()
305 };
306
307 let expected_traces = String::from("https://example.com:4318/v1/traces");
308 let expected_metrics = String::from("https://example.com:4318/v1/metrics");
309 let expected_logs = String::from("https://example.com:4318/v1/logs");
310
311 assert_eq!(expected_traces, config.traces_endpoint());
312 assert_eq!(expected_metrics, config.metrics_endpoint());
313 assert_eq!(expected_logs, config.logs_endpoint());
314 }
315}