@@ -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,80 @@ 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
+ 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
80
117
}
81
118
}
82
119
}
83
120
84
- #[ derive( Clone , Copy , Debug , Serialize , Deserialize ) ]
121
+ #[ derive( Clone , Copy , Debug , Serialize , Deserialize , PartialEq ) ]
85
122
pub enum OtelProtocol {
86
123
Grpc ,
87
124
Http ,
88
125
}
89
126
127
+ // Represents https://opentelemetry.io/docs/concepts/signals/
128
+ enum OtelSignal {
129
+ Traces ,
130
+ Metrics ,
131
+ Logs ,
132
+ }
133
+
90
134
impl Default for OtelProtocol {
91
135
fn default ( ) -> Self {
92
136
Self :: Http
@@ -109,3 +153,104 @@ impl FromStr for OtelProtocol {
109
153
110
154
/// Environment settings for initializing a capability provider
111
155
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