@@ -95,6 +95,7 @@ pub struct Context {
9595 #[ cfg( feature = "trace" ) ]
9696 pub ( crate ) span : Option < Arc < SynchronizedSpan > > ,
9797 entries : Option < Arc < EntryMap > > ,
98+ suppress_telemetry : bool ,
9899}
99100
100101type EntryMap = HashMap < TypeId , Arc < dyn Any + Sync + Send > , BuildHasherDefault < IdHasher > > ;
@@ -242,6 +243,7 @@ impl Context {
242243 entries,
243244 #[ cfg( feature = "trace" ) ]
244245 span : self . span . clone ( ) ,
246+ suppress_telemetry : self . suppress_telemetry ,
245247 }
246248 }
247249
@@ -328,19 +330,97 @@ impl Context {
328330 }
329331 }
330332
333+ /// Returns whether telemetry is suppressed in this context.
334+ #[ inline]
335+ pub fn is_telemetry_suppressed ( & self ) -> bool {
336+ self . suppress_telemetry
337+ }
338+
339+ /// Returns a new context with telemetry suppression enabled.
340+ pub fn with_telemetry_suppressed ( & self ) -> Self {
341+ Context {
342+ entries : self . entries . clone ( ) ,
343+ #[ cfg( feature = "trace" ) ]
344+ span : self . span . clone ( ) ,
345+ suppress_telemetry : true ,
346+ }
347+ }
348+
349+ /// Enters a scope where telemetry is suppressed.
350+ ///
351+ /// This method is specifically designed for OpenTelemetry components (like Exporters,
352+ /// Processors etc.) to prevent generating recursive or self-referential
353+ /// telemetry data when performing their own operations.
354+ ///
355+ /// Without suppression, we have a telemetry-induced-telemetry situation
356+ /// where, operations like exporting telemetry could generate new telemetry
357+ /// about the export process itself, potentially causing:
358+ /// - Infinite telemetry feedback loops
359+ /// - Excessive resource consumption
360+ ///
361+ /// This method:
362+ /// 1. Takes the current context
363+ /// 2. Creates a new context from current, with `suppress_telemetry` set to `true`
364+ /// 3. Attaches it to the current thread
365+ /// 4. Returns a guard that restores the previous context when dropped
366+ ///
367+ /// OTel SDK components would check `is_current_telemetry_suppressed()` before
368+ /// generating new telemetry, but not end users.
369+ ///
370+ /// # Examples
371+ ///
372+ /// ```
373+ /// use opentelemetry::Context;
374+ ///
375+ /// // Example: Inside an exporter's implementation
376+ /// fn example_export_function() {
377+ /// // Prevent telemetry-generating operations from creating more telemetry
378+ /// let _guard = Context::enter_telemetry_suppressed_scope();
379+ ///
380+ /// // Verify suppression is active
381+ /// assert_eq!(Context::is_current_telemetry_suppressed(), true);
382+ ///
383+ /// // Here you would normally perform operations that might generate telemetry
384+ /// // but now they won't because the context has suppression enabled
385+ /// }
386+ ///
387+ /// // Demonstrate the function
388+ /// example_export_function();
389+ /// ```
390+ pub fn enter_telemetry_suppressed_scope ( ) -> ContextGuard {
391+ Self :: map_current ( |cx| cx. with_telemetry_suppressed ( ) ) . attach ( )
392+ }
393+
394+ /// Returns whether telemetry is suppressed in the current context.
395+ ///
396+ /// This method is used by OpenTelemetry components to determine whether they should
397+ /// generate new telemetry in the current execution context. It provides a performant
398+ /// way to check the suppression state.
399+ ///
400+ /// End-users generally should not use this method directly, as it is primarily intended for
401+ /// OpenTelemetry SDK components.
402+ ///
403+ ///
404+ #[ inline]
405+ pub fn is_current_telemetry_suppressed ( ) -> bool {
406+ Self :: map_current ( |cx| cx. is_telemetry_suppressed ( ) )
407+ }
408+
331409 #[ cfg( feature = "trace" ) ]
332410 pub ( crate ) fn current_with_synchronized_span ( value : SynchronizedSpan ) -> Self {
333- Context {
411+ Self :: map_current ( |cx| Context {
334412 span : Some ( Arc :: new ( value) ) ,
335- entries : Context :: map_current ( |cx| cx. entries . clone ( ) ) ,
336- }
413+ entries : cx. entries . clone ( ) ,
414+ suppress_telemetry : cx. suppress_telemetry ,
415+ } )
337416 }
338417
339418 #[ cfg( feature = "trace" ) ]
340419 pub ( crate ) fn with_synchronized_span ( & self , value : SynchronizedSpan ) -> Self {
341420 Context {
342421 span : Some ( Arc :: new ( value) ) ,
343422 entries : self . entries . clone ( ) ,
423+ suppress_telemetry : self . suppress_telemetry ,
344424 }
345425 }
346426}
@@ -359,7 +439,9 @@ impl fmt::Debug for Context {
359439 }
360440 }
361441
362- dbg. field ( "entries count" , & entries) . finish ( )
442+ dbg. field ( "entries count" , & entries)
443+ . field ( "suppress_telemetry" , & self . suppress_telemetry )
444+ . finish ( )
363445 }
364446}
365447
@@ -897,4 +979,158 @@ mod tests {
897979 assert_eq ! ( Context :: current( ) . get:: <ValueA >( ) , None ) ;
898980 assert_eq ! ( Context :: current( ) . get:: <ValueB >( ) , None ) ;
899981 }
982+
983+ #[ test]
984+ fn test_is_telemetry_suppressed ( ) {
985+ // Default context has suppression disabled
986+ let cx = Context :: new ( ) ;
987+ assert ! ( !cx. is_telemetry_suppressed( ) ) ;
988+
989+ // With suppression enabled
990+ let suppressed = cx. with_telemetry_suppressed ( ) ;
991+ assert ! ( suppressed. is_telemetry_suppressed( ) ) ;
992+ }
993+
994+ #[ test]
995+ fn test_with_telemetry_suppressed ( ) {
996+ // Start with a normal context
997+ let cx = Context :: new ( ) ;
998+ assert ! ( !cx. is_telemetry_suppressed( ) ) ;
999+
1000+ // Create a suppressed context
1001+ let suppressed = cx. with_telemetry_suppressed ( ) ;
1002+
1003+ // Original should remain unchanged
1004+ assert ! ( !cx. is_telemetry_suppressed( ) ) ;
1005+
1006+ // New context should be suppressed
1007+ assert ! ( suppressed. is_telemetry_suppressed( ) ) ;
1008+
1009+ // Test with values to ensure they're preserved
1010+ let cx_with_value = cx. with_value ( ValueA ( 42 ) ) ;
1011+ let suppressed_with_value = cx_with_value. with_telemetry_suppressed ( ) ;
1012+
1013+ assert ! ( !cx_with_value. is_telemetry_suppressed( ) ) ;
1014+ assert ! ( suppressed_with_value. is_telemetry_suppressed( ) ) ;
1015+ assert_eq ! ( suppressed_with_value. get:: <ValueA >( ) , Some ( & ValueA ( 42 ) ) ) ;
1016+ }
1017+
1018+ #[ test]
1019+ fn test_enter_telemetry_suppressed_scope ( ) {
1020+ // Ensure we start with a clean context
1021+ let _reset_guard = Context :: new ( ) . attach ( ) ;
1022+
1023+ // Default context should not be suppressed
1024+ assert ! ( !Context :: is_current_telemetry_suppressed( ) ) ;
1025+
1026+ // Add an entry to the current context
1027+ let cx_with_value = Context :: current ( ) . with_value ( ValueA ( 42 ) ) ;
1028+ let _guard_with_value = cx_with_value. attach ( ) ;
1029+
1030+ // Verify the entry is present and context is not suppressed
1031+ assert_eq ! ( Context :: current( ) . get:: <ValueA >( ) , Some ( & ValueA ( 42 ) ) ) ;
1032+ assert ! ( !Context :: is_current_telemetry_suppressed( ) ) ;
1033+
1034+ // Enter a suppressed scope
1035+ {
1036+ let _guard = Context :: enter_telemetry_suppressed_scope ( ) ;
1037+
1038+ // Verify suppression is active and the entry is still present
1039+ assert ! ( Context :: is_current_telemetry_suppressed( ) ) ;
1040+ assert ! ( Context :: current( ) . is_telemetry_suppressed( ) ) ;
1041+ assert_eq ! ( Context :: current( ) . get:: <ValueA >( ) , Some ( & ValueA ( 42 ) ) ) ;
1042+ }
1043+
1044+ // After guard is dropped, should be back to unsuppressed and entry should still be present
1045+ assert ! ( !Context :: is_current_telemetry_suppressed( ) ) ;
1046+ assert ! ( !Context :: current( ) . is_telemetry_suppressed( ) ) ;
1047+ assert_eq ! ( Context :: current( ) . get:: <ValueA >( ) , Some ( & ValueA ( 42 ) ) ) ;
1048+ }
1049+
1050+ #[ test]
1051+ fn test_nested_suppression_scopes ( ) {
1052+ // Ensure we start with a clean context
1053+ let _reset_guard = Context :: new ( ) . attach ( ) ;
1054+
1055+ // Default context should not be suppressed
1056+ assert ! ( !Context :: is_current_telemetry_suppressed( ) ) ;
1057+
1058+ // First level suppression
1059+ {
1060+ let _outer = Context :: enter_telemetry_suppressed_scope ( ) ;
1061+ assert ! ( Context :: is_current_telemetry_suppressed( ) ) ;
1062+
1063+ // Second level. This component is unaware of Suppression,
1064+ // and just attaches a new context. Since it is from current,
1065+ // it'll already have suppression enabled.
1066+ {
1067+ let _inner = Context :: current ( ) . with_value ( ValueA ( 1 ) ) . attach ( ) ;
1068+ assert ! ( Context :: is_current_telemetry_suppressed( ) ) ;
1069+ assert_eq ! ( Context :: current( ) . get:: <ValueA >( ) , Some ( & ValueA ( 1 ) ) ) ;
1070+ }
1071+
1072+ // Another scenario. This component is unaware of Suppression,
1073+ // and just attaches a new context, not from Current. Since it is
1074+ // not from current it will not have suppression enabled.
1075+ {
1076+ let _inner = Context :: new ( ) . with_value ( ValueA ( 1 ) ) . attach ( ) ;
1077+ assert ! ( !Context :: is_current_telemetry_suppressed( ) ) ;
1078+ assert_eq ! ( Context :: current( ) . get:: <ValueA >( ) , Some ( & ValueA ( 1 ) ) ) ;
1079+ }
1080+
1081+ // Still suppressed after inner scope
1082+ assert ! ( Context :: is_current_telemetry_suppressed( ) ) ;
1083+ }
1084+
1085+ // Back to unsuppressed
1086+ assert ! ( !Context :: is_current_telemetry_suppressed( ) ) ;
1087+ }
1088+
1089+ #[ tokio:: test( flavor = "multi_thread" , worker_threads = 4 ) ]
1090+ async fn test_async_suppression ( ) {
1091+ async fn nested_operation ( ) {
1092+ assert ! ( Context :: is_current_telemetry_suppressed( ) ) ;
1093+
1094+ let cx_with_additional_value = Context :: current ( ) . with_value ( ValueB ( 24 ) ) ;
1095+
1096+ async {
1097+ assert_eq ! (
1098+ Context :: current( ) . get:: <ValueB >( ) ,
1099+ Some ( & ValueB ( 24 ) ) ,
1100+ "Parent value should still be available after adding new value"
1101+ ) ;
1102+ assert ! ( Context :: is_current_telemetry_suppressed( ) ) ;
1103+
1104+ // Do some async work to simulate real-world scenario
1105+ sleep ( Duration :: from_millis ( 10 ) ) . await ;
1106+
1107+ // Values should still be available after async work
1108+ assert_eq ! (
1109+ Context :: current( ) . get:: <ValueB >( ) ,
1110+ Some ( & ValueB ( 24 ) ) ,
1111+ "Parent value should still be available after adding new value"
1112+ ) ;
1113+ assert ! ( Context :: is_current_telemetry_suppressed( ) ) ;
1114+ }
1115+ . with_context ( cx_with_additional_value)
1116+ . await ;
1117+ }
1118+
1119+ // Set up suppressed context, but don't attach it to current
1120+ let suppressed_parent = Context :: new ( ) . with_telemetry_suppressed ( ) ;
1121+ // Current should not be suppressed as we haven't attached it
1122+ assert ! ( !Context :: is_current_telemetry_suppressed( ) ) ;
1123+
1124+ // Create and run async operation with the suppressed context explicitly propagated
1125+ nested_operation ( )
1126+ . with_context ( suppressed_parent. clone ( ) )
1127+ . await ;
1128+
1129+ // After async operation completes:
1130+ // Suppression should be active
1131+ assert ! ( suppressed_parent. is_telemetry_suppressed( ) ) ;
1132+
1133+ // Current should still be not suppressed
1134+ assert ! ( !Context :: is_current_telemetry_suppressed( ) ) ;
1135+ }
9001136}
0 commit comments