1+ use std:: borrow:: Cow ;
12use std:: cell:: RefCell ;
3+ use std:: collections:: HashSet ;
24use std:: fmt:: Debug ;
35use std:: pin:: Pin ;
46use std:: sync:: Arc ;
57
68use tracelogging_dynamic as tld;
79
810use opentelemetry:: logs:: Severity ;
9- use opentelemetry:: Key ;
11+ use opentelemetry:: { logs :: AnyValue , otel_debug , Key , Value } ;
1012use opentelemetry_sdk:: error:: { OTelSdkError , OTelSdkResult } ;
1113
1214pub ( crate ) mod common;
@@ -26,12 +28,14 @@ thread_local! {
2628struct Resource {
2729 pub cloud_role : Option < String > ,
2830 pub cloud_role_instance : Option < String > ,
31+ pub attributes_from_resource : Vec < ( Key , AnyValue ) > ,
2932}
3033
3134pub ( crate ) struct ETWExporter {
3235 provider : Pin < Arc < tld:: Provider > > ,
3336 resource : Resource ,
3437 options : Options ,
38+ resource_attribute_keys : HashSet < Cow < ' static , str > > ,
3539}
3640
3741fn enabled_callback_noop (
@@ -65,9 +69,12 @@ impl ETWExporter {
6569 provider. as_ref ( ) . register ( ) ;
6670 }
6771
72+ let resource_attribute_keys = options. resource_attribute_keys ( ) . clone ( ) ;
73+
6874 ETWExporter {
6975 provider,
7076 resource : Default :: default ( ) ,
77+ resource_attribute_keys,
7178 options,
7279 }
7380 }
@@ -110,7 +117,7 @@ impl ETWExporter {
110117
111118 part_a:: populate_part_a ( event, & self . resource , log_record, field_tag) ;
112119
113- let event_id = part_c:: populate_part_c ( event, log_record, field_tag) ;
120+ let event_id = part_c:: populate_part_c ( event, log_record, & self . resource , field_tag) ;
114121
115122 part_b:: populate_part_b ( event, log_record, otel_level, event_id) ;
116123
@@ -150,12 +157,26 @@ impl opentelemetry_sdk::logs::LogExporter for ETWExporter {
150157 }
151158
152159 fn set_resource ( & mut self , resource : & opentelemetry_sdk:: Resource ) {
153- self . resource . cloud_role = resource
154- . get ( & Key :: from_static_str ( "service.name" ) )
155- . map ( |v| v. to_string ( ) ) ;
156- self . resource . cloud_role_instance = resource
157- . get ( & Key :: from_static_str ( "service.instance.id" ) )
158- . map ( |v| v. to_string ( ) ) ;
160+ // Clear previous resource attributes
161+ self . resource . attributes_from_resource . clear ( ) ;
162+
163+ // Process resource attributes
164+ for ( key, value) in resource. iter ( ) {
165+ // Special handling for cloud role and instance
166+ // as they are used in PartA of the Common Schema format.
167+ if key. as_str ( ) == "service.name" {
168+ self . resource . cloud_role = Some ( value. to_string ( ) ) ;
169+ } else if key. as_str ( ) == "service.instance.id" {
170+ self . resource . cloud_role_instance = Some ( value. to_string ( ) ) ;
171+ } else if self . resource_attribute_keys . contains ( key. as_str ( ) ) {
172+ self . resource
173+ . attributes_from_resource
174+ . push ( ( key. clone ( ) , val_to_any_value ( value) ) ) ;
175+ } else {
176+ // Other attributes are ignored
177+ otel_debug ! ( name: "UserEvents.ResourceAttributeIgnored" , key = key. as_str( ) , message = "To include this attribute, add it via with_resource_attributes() method in the processor builder." ) ;
178+ }
179+ }
159180 }
160181
161182 fn shutdown ( & self ) -> OTelSdkResult {
@@ -169,6 +190,16 @@ impl opentelemetry_sdk::logs::LogExporter for ETWExporter {
169190 }
170191}
171192
193+ fn val_to_any_value ( val : & Value ) -> AnyValue {
194+ match val {
195+ Value :: Bool ( b) => AnyValue :: Boolean ( * b) ,
196+ Value :: I64 ( i) => AnyValue :: Int ( * i) ,
197+ Value :: F64 ( f) => AnyValue :: Double ( * f) ,
198+ Value :: String ( s) => AnyValue :: String ( s. clone ( ) ) ,
199+ _ => AnyValue :: String ( "" . into ( ) ) ,
200+ }
201+ }
202+
172203#[ cfg( test) ]
173204mod tests {
174205 use opentelemetry_sdk:: logs:: LogExporter ;
@@ -224,6 +255,60 @@ mod tests {
224255 assert ! ( result. is_ok( ) ) ;
225256 }
226257
258+ #[ test]
259+ fn test_event_resources_with_custom_attributes ( ) {
260+ use opentelemetry:: logs:: LogRecord ;
261+ use opentelemetry:: KeyValue ;
262+
263+ let mut log_record = common:: test_utils:: new_sdk_log_record ( ) ;
264+ log_record. set_event_name ( "event-name" ) ;
265+
266+ // Create exporter with custom resource attributes
267+ let options = Options :: new ( "test_provider" )
268+ . with_resource_attributes ( vec ! [ "custom_attribute1" , "custom_attribute2" ] ) ;
269+
270+ let mut exporter = ETWExporter :: new ( options) ;
271+
272+ exporter. set_resource (
273+ & opentelemetry_sdk:: Resource :: builder ( )
274+ . with_attributes ( [
275+ KeyValue :: new ( "service.name" , "test-service" ) ,
276+ KeyValue :: new ( "service.instance.id" , "test-instance" ) ,
277+ KeyValue :: new ( "custom_attribute1" , "value1" ) ,
278+ KeyValue :: new ( "custom_attribute2" , "value2" ) ,
279+ KeyValue :: new ( "custom_attribute3" , "value3" ) , // This should be ignored
280+ ] )
281+ . build ( ) ,
282+ ) ;
283+
284+ // Verify that only the configured attributes are stored
285+ assert_eq ! (
286+ exporter. resource. cloud_role,
287+ Some ( "test-service" . to_string( ) )
288+ ) ;
289+ assert_eq ! (
290+ exporter. resource. cloud_role_instance,
291+ Some ( "test-instance" . to_string( ) )
292+ ) ;
293+ assert_eq ! ( exporter. resource. attributes_from_resource. len( ) , 2 ) ;
294+
295+ // Check that the correct attributes are stored
296+ let attrs: std:: collections:: HashMap < String , String > = exporter
297+ . resource
298+ . attributes_from_resource
299+ . iter ( )
300+ . map ( |( k, v) | ( k. as_str ( ) . to_string ( ) , format ! ( "{:?}" , v) ) )
301+ . collect ( ) ;
302+ assert ! ( attrs. contains_key( "custom_attribute1" ) ) ;
303+ assert ! ( attrs. contains_key( "custom_attribute2" ) ) ;
304+ assert ! ( !attrs. contains_key( "custom_attribute3" ) ) ;
305+
306+ let instrumentation = common:: test_utils:: new_instrumentation_scope ( ) ;
307+ let result = exporter. export_log_data ( & log_record, & instrumentation) ;
308+
309+ assert ! ( result. is_ok( ) ) ;
310+ }
311+
227312 #[ test]
228313 fn test_debug ( ) {
229314 let exporter = common:: test_utils:: new_etw_exporter ( ) ;
0 commit comments