@@ -13,11 +13,12 @@ use axum::extract::MatchedPath;
13
13
use futures_util:: ready;
14
14
use opentelemetry:: metrics:: { Histogram , Meter , UpDownCounter } ;
15
15
use opentelemetry:: KeyValue ;
16
+ use opentelemetry_semantic_conventions as semconv;
16
17
use pin_project_lite:: pin_project;
17
18
use tower_layer:: Layer ;
18
19
use tower_service:: Service ;
19
20
20
- const HTTP_SERVER_DURATION_METRIC : & str = "http.server.request.duration" ;
21
+ const HTTP_SERVER_DURATION_METRIC : & str = semconv :: metric :: HTTP_SERVER_REQUEST_DURATION ;
21
22
const HTTP_SERVER_DURATION_UNIT : & str = "s" ;
22
23
23
24
const _OTEL_DEFAULT_HTTP_SERVER_DURATION_BOUNDARIES: [ f64 ; 14 ] = [
@@ -31,23 +32,23 @@ const _OTEL_DEFAULT_HTTP_SERVER_DURATION_BOUNDARIES: [f64; 14] = [
31
32
const LIBRARY_DEFAULT_HTTP_SERVER_DURATION_BOUNDARIES : [ f64 ; 14 ] = [
32
33
0.01 , 0.025 , 0.05 , 0.1 , 0.25 , 0.5 , 1.0 , 2.5 , 5.0 , 10.0 , 30.0 , 60.0 , 120.0 , 300.0 ,
33
34
] ;
34
- const HTTP_SERVER_ACTIVE_REQUESTS_METRIC : & str = "http.server.active_requests" ;
35
+ const HTTP_SERVER_ACTIVE_REQUESTS_METRIC : & str = semconv :: metric :: HTTP_SERVER_ACTIVE_REQUESTS ;
35
36
const HTTP_SERVER_ACTIVE_REQUESTS_UNIT : & str = "{request}" ;
36
37
37
- const HTTP_SERVER_REQUEST_BODY_SIZE_METRIC : & str = "http.server.request.body.size" ;
38
+ const HTTP_SERVER_REQUEST_BODY_SIZE_METRIC : & str = semconv :: metric :: HTTP_SERVER_REQUEST_BODY_SIZE ;
38
39
const HTTP_SERVER_REQUEST_BODY_SIZE_UNIT : & str = "By" ;
39
40
40
- const HTTP_SERVER_RESPONSE_BODY_SIZE_METRIC : & str = "http.server.response.body.size" ;
41
+ const HTTP_SERVER_RESPONSE_BODY_SIZE_METRIC : & str = semconv :: metric :: HTTP_SERVER_RESPONSE_BODY_SIZE ;
41
42
const HTTP_SERVER_RESPONSE_BODY_SIZE_UNIT : & str = "By" ;
42
43
43
- const NETWORK_PROTOCOL_NAME_LABEL : & str = "network.protocol.name" ;
44
+ const NETWORK_PROTOCOL_NAME_LABEL : & str = semconv :: attribute :: NETWORK_PROTOCOL_NAME ;
44
45
const NETWORK_PROTOCOL_VERSION_LABEL : & str = "network.protocol.version" ;
45
46
const URL_SCHEME_LABEL : & str = "url.scheme" ;
46
47
47
- const HTTP_REQUEST_METHOD_LABEL : & str = "http.request.method" ;
48
- #[ allow ( dead_code ) ] // cargo check is not smart
49
- const HTTP_ROUTE_LABEL : & str = "http.route" ;
50
- const HTTP_RESPONSE_STATUS_CODE_LABEL : & str = "http.response.status_code" ;
48
+ const HTTP_REQUEST_METHOD_LABEL : & str = semconv :: attribute :: HTTP_REQUEST_METHOD ;
49
+ #[ cfg ( feature = "axum" ) ]
50
+ const HTTP_ROUTE_LABEL : & str = semconv :: attribute :: HTTP_ROUTE ;
51
+ const HTTP_RESPONSE_STATUS_CODE_LABEL : & str = semconv :: attribute :: HTTP_RESPONSE_STATUS_CODE ;
51
52
52
53
/// Trait for extracting custom attributes from HTTP requests
53
54
pub trait RequestAttributeExtractor < B > : Clone + Send + Sync + ' static {
@@ -505,3 +506,259 @@ fn split_and_format_protocol_version(http_version: http::Version) -> (String, St
505
506
} ;
506
507
( String :: from ( "http" ) , String :: from ( version_str) )
507
508
}
509
+
510
+ #[ cfg( test) ]
511
+ mod tests {
512
+ use super :: * ;
513
+ use http:: { Request , Response , StatusCode } ;
514
+ use opentelemetry:: metrics:: MeterProvider ;
515
+ use opentelemetry_sdk:: metrics:: {
516
+ data:: { AggregatedMetrics , MetricData } ,
517
+ InMemoryMetricExporter , PeriodicReader , SdkMeterProvider ,
518
+ } ;
519
+ use std:: time:: Duration ;
520
+ use tower:: Service ;
521
+
522
+ #[ tokio:: test]
523
+ async fn test_metrics_labels ( ) {
524
+ let exporter = InMemoryMetricExporter :: default ( ) ;
525
+ let reader = PeriodicReader :: builder ( exporter. clone ( ) )
526
+ . with_interval ( Duration :: from_millis ( 100 ) )
527
+ . build ( ) ;
528
+ let meter_provider = SdkMeterProvider :: builder ( ) . with_reader ( reader) . build ( ) ;
529
+ let meter = meter_provider. meter ( "test" ) ;
530
+
531
+ let layer = HTTPMetricsLayerBuilder :: builder ( )
532
+ . with_meter ( meter)
533
+ . build ( )
534
+ . unwrap ( ) ;
535
+
536
+ let service = tower:: service_fn ( |_req : Request < String > | async {
537
+ Ok :: < _ , std:: convert:: Infallible > (
538
+ Response :: builder ( )
539
+ . status ( StatusCode :: OK )
540
+ . body ( String :: from ( "Hello, World!" ) )
541
+ . unwrap ( ) ,
542
+ )
543
+ } ) ;
544
+
545
+ let mut service = layer. layer ( service) ;
546
+
547
+ let request = Request :: builder ( )
548
+ . method ( "GET" )
549
+ . uri ( "https://example.com/test" )
550
+ . body ( "test body" . to_string ( ) )
551
+ . unwrap ( ) ;
552
+
553
+ let _response = service. call ( request) . await . unwrap ( ) ;
554
+
555
+ tokio:: time:: sleep ( Duration :: from_millis ( 200 ) ) . await ;
556
+
557
+ let metrics = exporter. get_finished_metrics ( ) . unwrap ( ) ;
558
+ assert ! ( !metrics. is_empty( ) ) ;
559
+
560
+ let resource_metrics = & metrics[ 0 ] ;
561
+ let scope_metrics = resource_metrics
562
+ . scope_metrics ( )
563
+ . next ( )
564
+ . expect ( "Should have scope metrics" ) ;
565
+
566
+ let duration_metric = scope_metrics
567
+ . metrics ( )
568
+ . find ( |m| m. name ( ) == HTTP_SERVER_DURATION_METRIC )
569
+ . expect ( "Duration metric should exist" ) ;
570
+
571
+ if let AggregatedMetrics :: F64 ( MetricData :: Histogram ( histogram) ) = duration_metric. data ( ) {
572
+ let data_point = histogram
573
+ . data_points ( )
574
+ . next ( )
575
+ . expect ( "Should have data point" ) ;
576
+ let attributes: Vec < _ > = data_point. attributes ( ) . collect ( ) ;
577
+
578
+ // Duration metric should have 5 attributes: protocol_name, protocol_version, url_scheme, method, status_code
579
+ assert_eq ! (
580
+ attributes. len( ) ,
581
+ 5 ,
582
+ "Duration metric should have exactly 5 attributes"
583
+ ) ;
584
+
585
+ let protocol_name = attributes
586
+ . iter ( )
587
+ . find ( |kv| kv. key . as_str ( ) == NETWORK_PROTOCOL_NAME_LABEL )
588
+ . expect ( "Protocol name should be present" ) ;
589
+ assert_eq ! ( protocol_name. value. as_str( ) , "http" ) ;
590
+
591
+ let protocol_version = attributes
592
+ . iter ( )
593
+ . find ( |kv| kv. key . as_str ( ) == NETWORK_PROTOCOL_VERSION_LABEL )
594
+ . expect ( "Protocol version should be present" ) ;
595
+ assert_eq ! ( protocol_version. value. as_str( ) , "1.1" ) ;
596
+
597
+ let url_scheme = attributes
598
+ . iter ( )
599
+ . find ( |kv| kv. key . as_str ( ) == URL_SCHEME_LABEL )
600
+ . expect ( "URL scheme should be present" ) ;
601
+ assert_eq ! ( url_scheme. value. as_str( ) , "https" ) ;
602
+
603
+ let method = attributes
604
+ . iter ( )
605
+ . find ( |kv| kv. key . as_str ( ) == HTTP_REQUEST_METHOD_LABEL )
606
+ . expect ( "HTTP method should be present" ) ;
607
+ assert_eq ! ( method. value. as_str( ) , "GET" ) ;
608
+
609
+ let status_code = attributes
610
+ . iter ( )
611
+ . find ( |kv| kv. key . as_str ( ) == HTTP_RESPONSE_STATUS_CODE_LABEL )
612
+ . expect ( "Status code should be present" ) ;
613
+ if let opentelemetry:: Value :: I64 ( code) = & status_code. value {
614
+ assert_eq ! ( * code, 200 ) ;
615
+ } else {
616
+ panic ! ( "Expected i64 status code" ) ;
617
+ }
618
+ } else {
619
+ panic ! ( "Expected histogram data for duration metric" ) ;
620
+ }
621
+
622
+ let request_body_size_metric = scope_metrics
623
+ . metrics ( )
624
+ . find ( |m| m. name ( ) == HTTP_SERVER_REQUEST_BODY_SIZE_METRIC ) ;
625
+
626
+ if let Some ( metric) = request_body_size_metric {
627
+ if let AggregatedMetrics :: F64 ( MetricData :: Histogram ( histogram) ) = metric. data ( ) {
628
+ let data_point = histogram
629
+ . data_points ( )
630
+ . next ( )
631
+ . expect ( "Should have data point" ) ;
632
+ let attributes: Vec < _ > = data_point. attributes ( ) . collect ( ) ;
633
+
634
+ // Request body size metric should have 5 attributes: protocol_name, protocol_version, url_scheme, method, status_code
635
+ assert_eq ! (
636
+ attributes. len( ) ,
637
+ 5 ,
638
+ "Request body size metric should have exactly 5 attributes"
639
+ ) ;
640
+
641
+ let protocol_name = attributes
642
+ . iter ( )
643
+ . find ( |kv| kv. key . as_str ( ) == NETWORK_PROTOCOL_NAME_LABEL )
644
+ . expect ( "Protocol name should be present in request body size" ) ;
645
+ assert_eq ! ( protocol_name. value. as_str( ) , "http" ) ;
646
+
647
+ let protocol_version = attributes
648
+ . iter ( )
649
+ . find ( |kv| kv. key . as_str ( ) == NETWORK_PROTOCOL_VERSION_LABEL )
650
+ . expect ( "Protocol version should be present in request body size" ) ;
651
+ assert_eq ! ( protocol_version. value. as_str( ) , "1.1" ) ;
652
+
653
+ let url_scheme = attributes
654
+ . iter ( )
655
+ . find ( |kv| kv. key . as_str ( ) == URL_SCHEME_LABEL )
656
+ . expect ( "URL scheme should be present in request body size" ) ;
657
+ assert_eq ! ( url_scheme. value. as_str( ) , "https" ) ;
658
+
659
+ let method = attributes
660
+ . iter ( )
661
+ . find ( |kv| kv. key . as_str ( ) == HTTP_REQUEST_METHOD_LABEL )
662
+ . expect ( "HTTP method should be present in request body size" ) ;
663
+ assert_eq ! ( method. value. as_str( ) , "GET" ) ;
664
+
665
+ let status_code = attributes
666
+ . iter ( )
667
+ . find ( |kv| kv. key . as_str ( ) == HTTP_RESPONSE_STATUS_CODE_LABEL )
668
+ . expect ( "Status code should be present in request body size" ) ;
669
+ if let opentelemetry:: Value :: I64 ( code) = & status_code. value {
670
+ assert_eq ! ( * code, 200 ) ;
671
+ } else {
672
+ panic ! ( "Expected i64 status code" ) ;
673
+ }
674
+ }
675
+ }
676
+
677
+ // Test response body size metric
678
+ let response_body_size_metric = scope_metrics
679
+ . metrics ( )
680
+ . find ( |m| m. name ( ) == HTTP_SERVER_RESPONSE_BODY_SIZE_METRIC ) ;
681
+
682
+ if let Some ( metric) = response_body_size_metric {
683
+ if let AggregatedMetrics :: F64 ( MetricData :: Histogram ( histogram) ) = metric. data ( ) {
684
+ let data_point = histogram
685
+ . data_points ( )
686
+ . next ( )
687
+ . expect ( "Should have data point" ) ;
688
+ let attributes: Vec < _ > = data_point. attributes ( ) . collect ( ) ;
689
+
690
+ // Response body size metric should have 5 attributes: protocol_name, protocol_version, url_scheme, method, status_code
691
+ assert_eq ! (
692
+ attributes. len( ) ,
693
+ 5 ,
694
+ "Response body size metric should have exactly 5 attributes"
695
+ ) ;
696
+
697
+ let protocol_name = attributes
698
+ . iter ( )
699
+ . find ( |kv| kv. key . as_str ( ) == NETWORK_PROTOCOL_NAME_LABEL )
700
+ . expect ( "Protocol name should be present in response body size" ) ;
701
+ assert_eq ! ( protocol_name. value. as_str( ) , "http" ) ;
702
+
703
+ let protocol_version = attributes
704
+ . iter ( )
705
+ . find ( |kv| kv. key . as_str ( ) == NETWORK_PROTOCOL_VERSION_LABEL )
706
+ . expect ( "Protocol version should be present in response body size" ) ;
707
+ assert_eq ! ( protocol_version. value. as_str( ) , "1.1" ) ;
708
+
709
+ let url_scheme = attributes
710
+ . iter ( )
711
+ . find ( |kv| kv. key . as_str ( ) == URL_SCHEME_LABEL )
712
+ . expect ( "URL scheme should be present in response body size" ) ;
713
+ assert_eq ! ( url_scheme. value. as_str( ) , "https" ) ;
714
+
715
+ let method = attributes
716
+ . iter ( )
717
+ . find ( |kv| kv. key . as_str ( ) == HTTP_REQUEST_METHOD_LABEL )
718
+ . expect ( "HTTP method should be present in response body size" ) ;
719
+ assert_eq ! ( method. value. as_str( ) , "GET" ) ;
720
+
721
+ let status_code = attributes
722
+ . iter ( )
723
+ . find ( |kv| kv. key . as_str ( ) == HTTP_RESPONSE_STATUS_CODE_LABEL )
724
+ . expect ( "Status code should be present in response body size" ) ;
725
+ if let opentelemetry:: Value :: I64 ( code) = & status_code. value {
726
+ assert_eq ! ( * code, 200 ) ;
727
+ } else {
728
+ panic ! ( "Expected i64 status code" ) ;
729
+ }
730
+ }
731
+ }
732
+
733
+ // Test active requests metric
734
+ let active_requests_metric = scope_metrics
735
+ . metrics ( )
736
+ . find ( |m| m. name ( ) == HTTP_SERVER_ACTIVE_REQUESTS_METRIC ) ;
737
+
738
+ if let Some ( metric) = active_requests_metric {
739
+ if let AggregatedMetrics :: I64 ( MetricData :: Sum ( sum) ) = metric. data ( ) {
740
+ let data_point = sum. data_points ( ) . next ( ) . expect ( "Should have data point" ) ;
741
+ let attributes: Vec < _ > = data_point. attributes ( ) . collect ( ) ;
742
+
743
+ // Active requests metric should have 2 attributes: method, url_scheme
744
+ assert_eq ! (
745
+ attributes. len( ) ,
746
+ 2 ,
747
+ "Active requests metric should have exactly 2 attributes"
748
+ ) ;
749
+
750
+ let method = attributes
751
+ . iter ( )
752
+ . find ( |kv| kv. key . as_str ( ) == HTTP_REQUEST_METHOD_LABEL )
753
+ . expect ( "HTTP method should be present in active requests" ) ;
754
+ assert_eq ! ( method. value. as_str( ) , "GET" ) ;
755
+
756
+ let url_scheme = attributes
757
+ . iter ( )
758
+ . find ( |kv| kv. key . as_str ( ) == URL_SCHEME_LABEL )
759
+ . expect ( "URL scheme should be present in active requests" ) ;
760
+ assert_eq ! ( url_scheme. value. as_str( ) , "https" ) ;
761
+ }
762
+ }
763
+ }
764
+ }
0 commit comments