@@ -4,14 +4,10 @@ use std::str::FromStr;
4
4
5
5
use anyhow:: bail;
6
6
use serde:: { Deserialize , Serialize } ;
7
+ use url:: Url ;
7
8
8
9
use crate :: wit:: WitMap ;
9
10
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
-
15
11
/// Configuration values for OpenTelemetry
16
12
#[ derive( Clone , Debug , Default , Deserialize , Serialize ) ]
17
13
pub struct OtelConfig {
@@ -38,27 +34,15 @@ pub struct OtelConfig {
38
34
39
35
impl OtelConfig {
40
36
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 ( ) )
46
38
}
47
39
48
40
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 ( ) )
54
42
}
55
43
56
44
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 ( ) )
62
46
}
63
47
64
48
pub fn logs_enabled ( & self ) -> bool {
@@ -73,20 +57,92 @@ impl OtelConfig {
73
57
self . enable_traces . unwrap_or ( self . enable_observability )
74
58
}
75
59
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,
80
115
}
81
116
}
82
117
}
83
118
84
- #[ derive( Clone , Copy , Debug , Serialize , Deserialize ) ]
119
+ #[ derive( Clone , Copy , Debug , Serialize , Deserialize , PartialEq ) ]
85
120
pub enum OtelProtocol {
86
121
Grpc ,
87
122
Http ,
88
123
}
89
124
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
+
90
146
impl Default for OtelProtocol {
91
147
fn default ( ) -> Self {
92
148
Self :: Http
@@ -109,3 +165,104 @@ impl FromStr for OtelProtocol {
109
165
110
166
/// Environment settings for initializing a capability provider
111
167
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