@@ -13,11 +13,12 @@ use axum::extract::MatchedPath;
1313use futures_util:: ready;
1414use opentelemetry:: metrics:: { Histogram , Meter , UpDownCounter } ;
1515use opentelemetry:: KeyValue ;
16+ use opentelemetry_semantic_conventions as semconv;
1617use pin_project_lite:: pin_project;
1718use tower_layer:: Layer ;
1819use tower_service:: Service ;
1920
20- const HTTP_SERVER_DURATION_METRIC : & str = "http.server.request.duration" ;
21+ const HTTP_SERVER_DURATION_METRIC : & str = semconv :: metric :: HTTP_SERVER_REQUEST_DURATION ;
2122const HTTP_SERVER_DURATION_UNIT : & str = "s" ;
2223
2324const _OTEL_DEFAULT_HTTP_SERVER_DURATION_BOUNDARIES: [ f64 ; 14 ] = [
@@ -31,23 +32,23 @@ const _OTEL_DEFAULT_HTTP_SERVER_DURATION_BOUNDARIES: [f64; 14] = [
3132const LIBRARY_DEFAULT_HTTP_SERVER_DURATION_BOUNDARIES : [ f64 ; 14 ] = [
3233 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 ,
3334] ;
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 ;
3536const HTTP_SERVER_ACTIVE_REQUESTS_UNIT : & str = "{request}" ;
3637
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 ;
3839const HTTP_SERVER_REQUEST_BODY_SIZE_UNIT : & str = "By" ;
3940
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 ;
4142const HTTP_SERVER_RESPONSE_BODY_SIZE_UNIT : & str = "By" ;
4243
43- const NETWORK_PROTOCOL_NAME_LABEL : & str = "network.protocol.name" ;
44+ const NETWORK_PROTOCOL_NAME_LABEL : & str = semconv :: attribute :: NETWORK_PROTOCOL_NAME ;
4445const NETWORK_PROTOCOL_VERSION_LABEL : & str = "network.protocol.version" ;
4546const URL_SCHEME_LABEL : & str = "url.scheme" ;
4647
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 ;
5152
5253/// Trait for extracting custom attributes from HTTP requests
5354pub trait RequestAttributeExtractor < B > : Clone + Send + Sync + ' static {
@@ -505,3 +506,259 @@ fn split_and_format_protocol_version(http_version: http::Version) -> (String, St
505506 } ;
506507 ( String :: from ( "http" ) , String :: from ( version_str) )
507508}
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