@@ -5364,4 +5364,168 @@ mod tests {
53645364 ) ;
53655365 assert_eq ! ( sum. data_points[ 0 ] . value, 100 ) ;
53665366 }
5367+
5368+ #[ cfg( feature = "experimental_metrics_bound_instruments" ) ]
5369+ #[ tokio:: test( flavor = "multi_thread" , worker_threads = 1 ) ]
5370+ async fn bound_histogram_empty_attributes_shares_with_unbound ( ) {
5371+ let mut test_context = TestContext :: new ( Temporality :: Cumulative ) ;
5372+ let histogram = test_context
5373+ . meter ( )
5374+ . u64_histogram ( "my_histogram" )
5375+ . with_boundaries ( vec ! [ 5.0 , 10.0 , 25.0 ] )
5376+ . build ( ) ;
5377+ let bound = histogram. bind ( & [ ] ) ;
5378+
5379+ histogram. record ( 3 , & [ ] ) ;
5380+ bound. record ( 7 ) ;
5381+ histogram. record ( 20 , & [ ] ) ;
5382+ test_context. flush_metrics ( ) ;
5383+
5384+ let MetricData :: Histogram ( hist) = test_context. get_aggregation :: < u64 > ( "my_histogram" , None )
5385+ else {
5386+ unreachable ! ( )
5387+ } ;
5388+
5389+ assert_eq ! (
5390+ hist. data_points. len( ) ,
5391+ 1 ,
5392+ "Bound and unbound with empty attributes must share the same data point"
5393+ ) ;
5394+ let dp = & hist. data_points [ 0 ] ;
5395+ assert ! ( dp. attributes. is_empty( ) ) ;
5396+ assert_eq ! ( dp. count, 3 ) ;
5397+ assert_eq ! ( dp. sum, 30 ) ;
5398+ }
5399+
5400+ #[ cfg( feature = "experimental_metrics_bound_instruments" ) ]
5401+ #[ cfg( feature = "spec_unstable_metrics_views" ) ]
5402+ #[ tokio:: test( flavor = "multi_thread" , worker_threads = 1 ) ]
5403+ async fn bound_counter_view_filters_attributes_at_bind_time ( ) {
5404+ use opentelemetry:: Key ;
5405+
5406+ let exporter = InMemoryMetricExporter :: default ( ) ;
5407+ let view = |i : & Instrument | {
5408+ if i. name ( ) == "my_counter" {
5409+ Stream :: builder ( )
5410+ . with_allowed_attribute_keys ( vec ! [ Key :: new( "k1" ) , Key :: new( "k2" ) ] )
5411+ . build ( )
5412+ . ok ( )
5413+ } else {
5414+ None
5415+ }
5416+ } ;
5417+ let meter_provider = SdkMeterProvider :: builder ( )
5418+ . with_periodic_exporter ( exporter. clone ( ) )
5419+ . with_view ( view)
5420+ . build ( ) ;
5421+ let meter = meter_provider. meter ( "test" ) ;
5422+ let counter = meter. u64_counter ( "my_counter" ) . build ( ) ;
5423+
5424+ // bind with k3 included — view should drop it at bind time
5425+ let bound = counter. bind ( & [
5426+ KeyValue :: new ( "k1" , "v1" ) ,
5427+ KeyValue :: new ( "k2" , "v2" ) ,
5428+ KeyValue :: new ( "k3" , "v3" ) ,
5429+ ] ) ;
5430+ bound. add ( 10 ) ;
5431+ bound. add ( 20 ) ;
5432+
5433+ // unbound call with a *different* k3 value: after view filtering both
5434+ // bound and unbound must collapse into the same data point.
5435+ counter. add (
5436+ 7 ,
5437+ & [
5438+ KeyValue :: new ( "k1" , "v1" ) ,
5439+ KeyValue :: new ( "k2" , "v2" ) ,
5440+ KeyValue :: new ( "k3" , "different" ) ,
5441+ ] ,
5442+ ) ;
5443+
5444+ meter_provider. force_flush ( ) . unwrap ( ) ;
5445+ let resource_metrics = exporter
5446+ . get_finished_metrics ( )
5447+ . expect ( "metrics are expected to be exported." ) ;
5448+ let metric = & resource_metrics[ 0 ] . scope_metrics [ 0 ] . metrics [ 0 ] ;
5449+ let data:: AggregatedMetrics :: U64 ( MetricData :: Sum ( sum) ) = & metric. data else {
5450+ unreachable ! ( )
5451+ } ;
5452+
5453+ assert_eq ! (
5454+ sum. data_points. len( ) ,
5455+ 1 ,
5456+ "view should filter k3, leaving bound+unbound to aggregate together"
5457+ ) ;
5458+ assert_eq ! ( sum. data_points[ 0 ] . value, 37 ) ;
5459+ let attrs = & sum. data_points [ 0 ] . attributes ;
5460+ assert_eq ! ( attrs. len( ) , 2 ) ;
5461+ assert ! ( attrs. iter( ) . any( |kv| kv. key. as_str( ) == "k1" ) ) ;
5462+ assert ! ( attrs. iter( ) . any( |kv| kv. key. as_str( ) == "k2" ) ) ;
5463+ assert ! ( !attrs. iter( ) . any( |kv| kv. key. as_str( ) == "k3" ) ) ;
5464+ }
5465+
5466+ #[ cfg( feature = "experimental_metrics_bound_instruments" ) ]
5467+ #[ cfg( feature = "spec_unstable_metrics_views" ) ]
5468+ #[ tokio:: test( flavor = "multi_thread" , worker_threads = 1 ) ]
5469+ async fn bound_histogram_view_filters_attributes_at_bind_time ( ) {
5470+ use opentelemetry:: Key ;
5471+
5472+ let exporter = InMemoryMetricExporter :: default ( ) ;
5473+ let view = |i : & Instrument | {
5474+ if i. name ( ) == "my_hist" {
5475+ Stream :: builder ( )
5476+ . with_allowed_attribute_keys ( vec ! [ Key :: new( "k1" ) , Key :: new( "k2" ) ] )
5477+ . build ( )
5478+ . ok ( )
5479+ } else {
5480+ None
5481+ }
5482+ } ;
5483+ let meter_provider = SdkMeterProvider :: builder ( )
5484+ . with_periodic_exporter ( exporter. clone ( ) )
5485+ . with_view ( view)
5486+ . build ( ) ;
5487+ let meter = meter_provider. meter ( "test" ) ;
5488+ let histogram = meter
5489+ . u64_histogram ( "my_hist" )
5490+ . with_boundaries ( vec ! [ 5.0 , 10.0 , 25.0 ] )
5491+ . build ( ) ;
5492+
5493+ let bound = histogram. bind ( & [
5494+ KeyValue :: new ( "k1" , "v1" ) ,
5495+ KeyValue :: new ( "k2" , "v2" ) ,
5496+ KeyValue :: new ( "k3" , "v3" ) ,
5497+ ] ) ;
5498+ bound. record ( 3 ) ;
5499+ bound. record ( 20 ) ;
5500+ histogram. record (
5501+ 7 ,
5502+ & [
5503+ KeyValue :: new ( "k1" , "v1" ) ,
5504+ KeyValue :: new ( "k2" , "v2" ) ,
5505+ KeyValue :: new ( "k3" , "different" ) ,
5506+ ] ,
5507+ ) ;
5508+
5509+ meter_provider. force_flush ( ) . unwrap ( ) ;
5510+ let resource_metrics = exporter
5511+ . get_finished_metrics ( )
5512+ . expect ( "metrics are expected to be exported." ) ;
5513+ let metric = & resource_metrics[ 0 ] . scope_metrics [ 0 ] . metrics [ 0 ] ;
5514+ let data:: AggregatedMetrics :: U64 ( MetricData :: Histogram ( hist) ) = & metric. data else {
5515+ unreachable ! ( )
5516+ } ;
5517+
5518+ assert_eq ! (
5519+ hist. data_points. len( ) ,
5520+ 1 ,
5521+ "view should filter k3, leaving bound+unbound to aggregate together"
5522+ ) ;
5523+ let dp = & hist. data_points [ 0 ] ;
5524+ assert_eq ! ( dp. count, 3 ) ;
5525+ assert_eq ! ( dp. sum, 30 ) ;
5526+ assert_eq ! ( dp. attributes. len( ) , 2 ) ;
5527+ assert ! ( dp. attributes. iter( ) . any( |kv| kv. key. as_str( ) == "k1" ) ) ;
5528+ assert ! ( dp. attributes. iter( ) . any( |kv| kv. key. as_str( ) == "k2" ) ) ;
5529+ assert ! ( !dp. attributes. iter( ) . any( |kv| kv. key. as_str( ) == "k3" ) ) ;
5530+ }
53675531}
0 commit comments