10
10
//!
11
11
//! [1]: https://opentelemetry.io/
12
12
//! [2]: https://opentelemetry.io/docs/specs/semconv/http/http-spans/
13
- use std:: { future:: Future , net:: SocketAddr , str :: FromStr , task:: Poll } ;
13
+ use std:: { future:: Future , net:: SocketAddr , num :: ParseIntError , task:: Poll } ;
14
14
15
15
use axum:: {
16
- extract:: { ConnectInfo , Host , MatchedPath , Request } ,
17
- http:: { header:: USER_AGENT , HeaderMap } ,
16
+ extract:: { ConnectInfo , MatchedPath , Request } ,
17
+ http:: {
18
+ header:: { HOST , USER_AGENT } ,
19
+ HeaderMap ,
20
+ } ,
18
21
response:: Response ,
19
22
} ;
20
23
use futures_util:: ready;
21
24
use opentelemetry:: trace:: SpanKind ;
22
25
use pin_project:: pin_project;
26
+ use snafu:: { ResultExt , Snafu } ;
23
27
use tower:: { Layer , Service } ;
24
28
use tracing:: { debug, field:: Empty , instrument, trace_span, Span } ;
25
29
use tracing_opentelemetry:: OpenTelemetrySpanExt ;
@@ -30,6 +34,10 @@ mod injector;
30
34
pub use extractor:: * ;
31
35
pub use injector:: * ;
32
36
37
+ const X_FORWARDED_HOST_HEADER_KEY : & str = "X-Forwarded-Host" ;
38
+ const DEFAULT_HTTPS_PORT : u16 = 443 ;
39
+ const DEFAULT_HTTP_PORT : u16 = 80 ;
40
+
33
41
/// A Tower [`Layer`][1] which decorates [`TraceService`].
34
42
///
35
43
/// ### Example with Axum
@@ -163,13 +171,25 @@ where
163
171
}
164
172
}
165
173
174
+ #[ derive( Debug , Snafu ) ]
175
+ pub enum ServerHostError {
176
+ #[ snafu( display( "failed to parse port {port:?} as u16 from string" ) ) ]
177
+ ParsePort { source : ParseIntError , port : String } ,
178
+
179
+ #[ snafu( display( "encountered invalid request scheme {scheme:?}" ) ) ]
180
+ InvalidScheme { scheme : String } ,
181
+
182
+ #[ snafu( display( "failed to extract any host information from request" ) ) ]
183
+ ExtractHost ,
184
+ }
185
+
166
186
/// This trait provides various helper functions to extract data from a
167
187
/// HTTP [`Request`].
168
188
pub trait RequestExt {
169
189
/// Returns the client socket address, if available.
170
190
fn client_socket_address ( & self ) -> Option < SocketAddr > ;
171
191
172
- /// Returns the server socket address , if available.
192
+ /// Returns the server host , if available.
173
193
///
174
194
/// ### Value Selection Strategy
175
195
///
@@ -186,7 +206,7 @@ pub trait RequestExt {
186
206
/// > - The Host header.
187
207
///
188
208
/// [1]: https://opentelemetry.io/docs/specs/semconv/http/http-spans/#setting-serveraddress-and-serverport-attributes
189
- fn server_socket_address ( & self ) -> Option < SocketAddr > ;
209
+ fn server_host ( & self ) -> Result < ( String , u16 ) , ServerHostError > ;
190
210
191
211
/// Returns the matched path, like `/object/:object_id/tags`.
192
212
///
@@ -211,9 +231,33 @@ pub trait RequestExt {
211
231
}
212
232
213
233
impl RequestExt for Request {
214
- fn server_socket_address ( & self ) -> Option < SocketAddr > {
215
- let host = self . extensions ( ) . get :: < Host > ( ) ?;
216
- SocketAddr :: from_str ( & host. 0 ) . ok ( )
234
+ fn server_host ( & self ) -> Result < ( String , u16 ) , ServerHostError > {
235
+ // There is currently no obvious way to use the Host extractor from Axum
236
+ // directly. Using that extractor either requires impossible code (async
237
+ // in the Service's call function, unnecessary cloning or consuming self
238
+ // and returning a newly created request). That's why the following
239
+ // section mirrors the Axum extractor implementation. The implementation
240
+ // currently only looks for the X-Forwarded-Host / Host header and falls
241
+ // back to the request URI host. The Axum implementation also extracts
242
+ // data from the Forwarded header.
243
+
244
+ if let Some ( host) = self
245
+ . headers ( )
246
+ . get ( X_FORWARDED_HOST_HEADER_KEY )
247
+ . and_then ( |host| host. to_str ( ) . ok ( ) )
248
+ {
249
+ return server_host_to_tuple ( host, self . uri ( ) . scheme_str ( ) ) ;
250
+ }
251
+
252
+ if let Some ( host) = self . headers ( ) . get ( HOST ) . and_then ( |host| host. to_str ( ) . ok ( ) ) {
253
+ return server_host_to_tuple ( host, self . uri ( ) . scheme_str ( ) ) ;
254
+ }
255
+
256
+ if let ( Some ( host) , Some ( port) ) = ( self . uri ( ) . host ( ) , self . uri ( ) . port_u16 ( ) ) {
257
+ return Ok ( ( host. to_owned ( ) , port) ) ;
258
+ }
259
+
260
+ ExtractHostSnafu . fail ( )
217
261
}
218
262
219
263
fn client_socket_address ( & self ) -> Option < SocketAddr > {
@@ -242,6 +286,29 @@ impl RequestExt for Request {
242
286
}
243
287
}
244
288
289
+ fn server_host_to_tuple (
290
+ host : & str ,
291
+ scheme : Option < & str > ,
292
+ ) -> Result < ( String , u16 ) , ServerHostError > {
293
+ if let Some ( ( host, port) ) = host. split_once ( ':' ) {
294
+ // First, see if the host header value contains a colon indicating that
295
+ // it includes a non-default port.
296
+ let port: u16 = port. parse ( ) . context ( ParsePortSnafu { port } ) ?;
297
+ Ok ( ( host. to_owned ( ) , port) )
298
+ } else {
299
+ // If there is no port included in the header value, the port is implied.
300
+ // Port 443 for HTTPS and port 80 for HTTP.
301
+ let port = match scheme {
302
+ Some ( "https" ) => DEFAULT_HTTPS_PORT ,
303
+ Some ( "http" ) => DEFAULT_HTTP_PORT ,
304
+ Some ( scheme) => return InvalidSchemeSnafu { scheme } . fail ( ) ,
305
+ _ => return InvalidSchemeSnafu { scheme : "" } . fail ( ) ,
306
+ } ;
307
+
308
+ Ok ( ( host. to_owned ( ) , port) )
309
+ }
310
+ }
311
+
245
312
/// This trait provides various helper functions to create a [`Span`] out of
246
313
/// an HTTP [`Request`].
247
314
pub trait SpanExt {
@@ -319,7 +386,7 @@ impl SpanExt for Span {
319
386
// - https://github.com/tokio-rs/tracing/pull/732
320
387
//
321
388
// Additionally we cannot use consts for field names. There was an
322
- // upstream PR to add support for it, but it was unexpectingly closed.
389
+ // upstream PR to add support for it, but it was unexpectedly closed.
323
390
// See https://github.com/tokio-rs/tracing/pull/2254.
324
391
//
325
392
// If this is eventually supported (maybe with our efforts), we can use
@@ -332,22 +399,21 @@ impl SpanExt for Span {
332
399
debug ! ( "create http span" ) ;
333
400
let span = trace_span ! (
334
401
"HTTP request" ,
335
- otel. name = span_name,
336
- otel. kind = ?SpanKind :: Server ,
337
- otel. status_code = Empty ,
338
- otel. status_message = Empty ,
339
- http. request. method = http_method,
340
- http. response. status_code = Empty ,
341
- url. path = url. path( ) ,
342
- url. query = url. query( ) ,
343
- url. scheme = url. scheme_str( ) . unwrap_or_default( ) ,
344
- user_agent. original = Empty ,
345
- server. address = Empty ,
346
- server. port = Empty ,
347
- client. address = Empty ,
348
- client. port = Empty ,
349
- http. route = Empty ,
350
- http. response. status_code = Empty ,
402
+ "otel.name" = span_name,
403
+ "otel.kind" = ?SpanKind :: Server ,
404
+ "otel.status_code" = Empty ,
405
+ "otel.status_message" = Empty ,
406
+ "http.request.method" = http_method,
407
+ "http.response.status_code" = Empty ,
408
+ "http.route" = Empty ,
409
+ "url.path" = url. path( ) ,
410
+ "url.query" = url. query( ) ,
411
+ "url.scheme" = url. scheme_str( ) . unwrap_or_default( ) ,
412
+ "user_agent.original" = Empty ,
413
+ "server.address" = Empty ,
414
+ "server.port" = Empty ,
415
+ "client.address" = Empty ,
416
+ "client.port" = Empty ,
351
417
// TODO (@Techassi): Add network.protocol.version
352
418
) ;
353
419
@@ -363,9 +429,12 @@ impl SpanExt for Span {
363
429
// Setting server.address and server.port
364
430
// See https://opentelemetry.io/docs/specs/semconv/http/http-spans/#setting-serveraddress-and-serverport-attributes
365
431
366
- if let Some ( server_socket_address) = req. server_socket_address ( ) {
367
- span. record ( "server.address" , server_socket_address. ip ( ) . to_string ( ) )
368
- . record ( "server.port" , server_socket_address. port ( ) ) ;
432
+ if let Ok ( ( host, port) ) = req. server_host ( ) {
433
+ // NOTE (@Techassi): We cast to i64, because otherwise the field
434
+ // will NOT be recorded as a number but as a string. This is likely
435
+ // an issue in the tracing-opentelemetry crate.
436
+ span. record ( "server.address" , host)
437
+ . record ( "server.port" , port as i64 ) ;
369
438
}
370
439
371
440
// Setting fields according to the HTTP server semantic conventions
@@ -375,25 +444,22 @@ impl SpanExt for Span {
375
444
span. record ( "client.address" , client_socket_address. ip ( ) . to_string ( ) ) ;
376
445
377
446
if opt_in {
378
- span. record ( "client.port" , client_socket_address. port ( ) ) ;
447
+ // NOTE (@Techassi): We cast to i64, because otherwise the field
448
+ // will NOT be recorded as a number but as a string. This is
449
+ // likely an issue in the tracing-opentelemetry crate.
450
+ span. record ( "client.port" , client_socket_address. port ( ) as i64 ) ;
379
451
}
380
452
}
381
453
382
454
// Only include the headers if the user opted in, because this might
383
455
// potentially be an expensive operation when many different headers
384
456
// are present. The OpenTelemetry spec also marks this as opt-in.
385
457
386
- // NOTE (@Techassi): Currently, tracing doesn't support recording
387
- // fields which are not registered at span creation which thus makes
388
- // it impossible to record request headers at runtime.
458
+ // FIXME (@Techassi): Currently, tracing doesn't support recording
459
+ // fields which are not registered at span creation which thus makes it
460
+ // impossible to record request headers at runtime.
389
461
// See: https://github.com/tokio-rs/tracing/issues/1343
390
462
391
- // FIXME (@Techassi): Add support for this when tracing allows dynamic
392
- // fields.
393
- // if opt_in {
394
- // span.add_header_fields(req.headers())
395
- // }
396
-
397
463
if let Some ( http_route) = req. matched_path ( ) {
398
464
span. record ( "http.route" , http_route. as_str ( ) ) ;
399
465
}
@@ -420,7 +486,11 @@ impl SpanExt for Span {
420
486
421
487
fn finalize_with_response ( & self , response : & mut Response ) {
422
488
let status_code = response. status ( ) ;
423
- self . record ( "http.response.status_code" , status_code. as_u16 ( ) ) ;
489
+
490
+ // NOTE (@Techassi): We cast to i64, because otherwise the field will
491
+ // NOT be recorded as a number but as a string. This is likely an issue
492
+ // in the tracing-opentelemetry crate.
493
+ self . record ( "http.response.status_code" , status_code. as_u16 ( ) as i64 ) ;
424
494
425
495
// Only set the span status to "Error" when we encountered an server
426
496
// error. See:
0 commit comments