@@ -573,4 +573,77 @@ mod tests {
573573 assert ! ( metric_names. contains( & TOTAL_CONNECTIONS ) , "missing total_connections" ) ;
574574 assert ! ( metric_names. contains( & ACTIVE_CONNECTIONS ) , "missing active_connections" ) ;
575575 }
576+
577+ #[ tokio:: test]
578+ async fn middleware_sanitizes_short_id_in_metrics ( ) {
579+ use axum:: body:: Body ;
580+ use axum:: http:: Request ;
581+ use opentelemetry_sdk:: metrics:: data:: { AggregatedMetrics , MetricData } ;
582+ use tower:: ServiceExt ;
583+
584+ let exporter = InMemoryMetricExporter :: default ( ) ;
585+ let reader = PeriodicReader :: builder ( exporter. clone ( ) ) . build ( ) ;
586+ let provider = SdkMeterProvider :: builder ( ) . with_reader ( reader) . build ( ) ;
587+
588+ let tempdir = tempdir ( ) . unwrap ( ) ;
589+ let config = Config :: new (
590+ "[::]:0" . parse ( ) . expect ( "valid listener address" ) ,
591+ tempdir. path ( ) . to_path_buf ( ) ,
592+ Duration :: from_secs ( 2 ) ,
593+ None ,
594+ ) ;
595+
596+ let sentinel_tag = generate_sentinel_tag ( ) ;
597+ let metrics = MetricsService :: new ( Some ( provider. clone ( ) ) ) ;
598+ let services = Services {
599+ directory : init_directory ( & config, sentinel_tag, & metrics) . await . unwrap ( ) ,
600+ relay : crate :: ohttp_relay:: Service :: new ( sentinel_tag) . await ,
601+ metrics,
602+ #[ cfg( feature = "access-control" ) ]
603+ geoip : None ,
604+ } ;
605+
606+ let app = build_app ( services) ;
607+
608+ let short_id = payjoin:: directory:: ShortId ( [ 0u8 ; 8 ] ) . to_string ( ) ;
609+ let uri = format ! ( "/{short_id}" ) ;
610+ let request = Request :: builder ( ) . method ( "GET" ) . uri ( & uri) . body ( Body :: empty ( ) ) . unwrap ( ) ;
611+ let _response = ServiceExt :: < Request < Body > > :: oneshot ( app, request) . await . unwrap ( ) ;
612+
613+ provider. force_flush ( ) . expect ( "flush failed" ) ;
614+
615+ let finished = exporter. get_finished_metrics ( ) . expect ( "metrics" ) ;
616+ println ! ( "finished: {:?}" , finished) ;
617+ let endpoint_attrs: Vec < String > = finished
618+ . iter ( )
619+ . flat_map ( |rm| rm. scope_metrics ( ) )
620+ . flat_map ( |sm| sm. metrics ( ) )
621+ . filter ( |m| m. name ( ) == HTTP_REQUESTS )
622+ . flat_map ( |m| match m. data ( ) {
623+ AggregatedMetrics :: U64 ( MetricData :: Sum ( sum) ) => sum
624+ . data_points ( )
625+ . flat_map ( |dp| dp. attributes ( ) )
626+ . filter_map ( |kv| {
627+ if kv. key . as_str ( ) == "endpoint" {
628+ Some ( kv. value . to_string ( ) )
629+ } else {
630+ None
631+ }
632+ } )
633+ . collect :: < Vec < _ > > ( ) ,
634+ _ => vec ! [ ] ,
635+ } )
636+ . collect ( ) ;
637+
638+ println ! ( "endpoint_attrs: {:?}" , endpoint_attrs) ;
639+
640+ assert ! (
641+ endpoint_attrs. iter( ) . all( |ep| ep == "/{mailbox}" ) ,
642+ "short ID must be sanitized in metrics, got: {endpoint_attrs:?}"
643+ ) ;
644+ assert ! (
645+ endpoint_attrs. iter( ) . all( |ep| !ep. contains( & short_id) ) ,
646+ "actual short ID value must not appear in metrics"
647+ ) ;
648+ }
576649}
0 commit comments