Skip to content

Commit d88641c

Browse files
Copilotcijothomaslalitb
authored
feat: Add support for Resource attributes in ETW logs exporter (#410)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: cijothomas <5232798+cijothomas@users.noreply.github.com> Co-authored-by: Cijo Thomas <cijo.thomas@gmail.com> Co-authored-by: Lalit Kumar Bhasin <lalit_fin@yahoo.com>
1 parent 304a140 commit d88641c

File tree

6 files changed

+294
-9
lines changed

6 files changed

+294
-9
lines changed

opentelemetry-etw-logs/CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,15 @@
22

33
## vNext
44

5+
- Added a `with_resource_attributes` method to the processor builder, allowing
6+
users to specify which resource attribute keys are exported with each log
7+
record.
8+
- By default, the Resource attributes `"service.name"` and
9+
`"service.instance.id"` continue to be exported as `cloud.roleName` and
10+
`cloud.roleInstance`.
11+
- This feature enables exporting additional resource attributes beyond the
12+
defaults.
13+
514
## v0.10.0
615

716
- Bump opentelemetry and opentelemetry_sdk versions to 0.31

opentelemetry-etw-logs/src/exporter/mod.rs

Lines changed: 93 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
1+
use std::borrow::Cow;
12
use std::cell::RefCell;
3+
use std::collections::HashSet;
24
use std::fmt::Debug;
35
use std::pin::Pin;
46
use std::sync::Arc;
57

68
use tracelogging_dynamic as tld;
79

810
use opentelemetry::logs::Severity;
9-
use opentelemetry::Key;
11+
use opentelemetry::{logs::AnyValue, otel_debug, Key, Value};
1012
use opentelemetry_sdk::error::{OTelSdkError, OTelSdkResult};
1113

1214
pub(crate) mod common;
@@ -26,12 +28,14 @@ thread_local! {
2628
struct Resource {
2729
pub cloud_role: Option<String>,
2830
pub cloud_role_instance: Option<String>,
31+
pub attributes_from_resource: Vec<(Key, AnyValue)>,
2932
}
3033

3134
pub(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

3741
fn 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)]
173204
mod 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();

opentelemetry-etw-logs/src/exporter/options.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use opentelemetry_sdk::logs::SdkLogRecord;
22
use std::borrow::Cow;
3+
use std::collections::HashSet;
34
use std::error::Error;
45

56
type BoxedEventNameCallback = Box<dyn EventNameCallback>;
@@ -8,13 +9,15 @@ type BoxedEventNameCallback = Box<dyn EventNameCallback>;
89
pub(crate) struct Options {
910
provider_name: Cow<'static, str>,
1011
event_name_callback: Option<BoxedEventNameCallback>,
12+
resource_attribute_keys: HashSet<Cow<'static, str>>,
1113
}
1214

1315
impl Options {
1416
pub(crate) fn new(provider_name: impl Into<Cow<'static, str>>) -> Options {
1517
Options {
1618
provider_name: provider_name.into(),
1719
event_name_callback: None,
20+
resource_attribute_keys: HashSet::new(),
1821
}
1922
}
2023

@@ -23,6 +26,21 @@ impl Options {
2326
&self.provider_name
2427
}
2528

29+
/// Returns the resource attribute keys that will be exported with each log record.
30+
pub(crate) fn resource_attribute_keys(&self) -> &HashSet<Cow<'static, str>> {
31+
&self.resource_attribute_keys
32+
}
33+
34+
/// Sets the resource attributes for the exporter.
35+
pub(crate) fn with_resource_attributes<I, S>(mut self, attributes: I) -> Self
36+
where
37+
I: IntoIterator<Item = S>,
38+
S: Into<Cow<'static, str>>,
39+
{
40+
self.resource_attribute_keys = attributes.into_iter().map(|s| s.into()).collect();
41+
self
42+
}
43+
2644
/// Returns the default event name that will be used for the ETW events.
2745
pub(crate) fn default_event_name(&self) -> &str {
2846
"Log"

opentelemetry-etw-logs/src/exporter/part_c.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ pub(crate) const EVENT_ID: &str = "event_id";
66
pub(crate) fn populate_part_c(
77
event: &mut tld::EventBuilder,
88
log_record: &opentelemetry_sdk::logs::SdkLogRecord,
9+
resource: &super::Resource,
910
field_tag: u32,
1011
) -> Option<i64> {
1112
//populate CS PartC
@@ -25,9 +26,17 @@ pub(crate) fn populate_part_c(
2526
}
2627
}
2728

29+
// Count resource attributes
30+
cs_c_count += resource.attributes_from_resource.len();
31+
2832
// If there are additional PartC attributes, add them to the event
2933
if cs_c_count > 0 {
30-
event.add_struct("PartC", cs_c_count, field_tag);
34+
event.add_struct("PartC", cs_c_count.try_into().unwrap_or(u8::MAX), field_tag);
35+
36+
// Add resource attributes first
37+
for (key, value) in &resource.attributes_from_resource {
38+
super::common::add_attribute_to_event(event, key, value);
39+
}
3140

3241
// TODO: This 2nd iteration is not optimal, and can be optimized
3342
for (key, value) in log_record.attributes_iter() {

opentelemetry-etw-logs/src/lib.rs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,64 @@
11
//! The ETW exporter will enable applications to use OpenTelemetry API
22
//! to capture the telemetry events, and write them to the ETW subsystem.
3+
//!
4+
//! ## Resource Attribute Handling
5+
//!
6+
//! **Important**: By default, resource attributes are NOT exported with log records.
7+
//! The ETW exporter only automatically exports these specific resource attributes:
8+
//!
9+
//! - **`service.name`** → Exported as `cloud.roleName` in PartA of Common Schema
10+
//! - **`service.instance.id`** → Exported as `cloud.roleInstance` in PartA of Common Schema
11+
//!
12+
//! All other resource attributes are ignored unless explicitly specified.
13+
//!
14+
//! ### Opting in to Additional Resource Attributes
15+
//!
16+
//! To export additional resource attributes, use the `with_resource_attributes()` method:
17+
//!
18+
//! ```rust
19+
//! use opentelemetry_sdk::logs::SdkLoggerProvider;
20+
//! use opentelemetry_sdk::Resource;
21+
//! use opentelemetry_etw_logs::Processor;
22+
//! use opentelemetry::KeyValue;
23+
//!
24+
//! let etw_processor = Processor::builder("myprovider")
25+
//! // Only export specific resource attributes
26+
//! .with_resource_attributes(["custom_attribute1", "custom_attribute2"])
27+
//! .build()
28+
//! .unwrap();
29+
//!
30+
//! let provider = SdkLoggerProvider::builder()
31+
//! .with_resource(
32+
//! Resource::builder_empty()
33+
//! .with_service_name("example")
34+
//! .with_attribute(KeyValue::new("custom_attribute1", "value1"))
35+
//! .with_attribute(KeyValue::new("custom_attribute2", "value2"))
36+
//! .with_attribute(KeyValue::new("custom_attribute3", "value3")) // This won't be exported
37+
//! .build(),
38+
//! )
39+
//! .with_log_processor(etw_processor)
40+
//! .build();
41+
//! ```
42+
//!
43+
//! ### Performance Considerations for ETW
44+
//!
45+
//! **Warning**: Each specified resource attribute will be serialized and sent
46+
//! with EVERY log record. This is different from OTLP exporters where resource
47+
//! attributes are serialized once per batch. Consider the performance impact
48+
//! when selecting which attributes to export.
49+
//!
50+
//! **Recommendation**: Be selective about which resource attributes to export.
51+
//! Since ETW writes to a local kernel buffer and requires a local
52+
//! listener/agent, the agent can often deduce many resource attributes without
53+
//! requiring them to be sent with each log:
54+
//!
55+
//! - **Infrastructure attributes** (datacenter, region, availability zone) can
56+
//! be determined by the local agent.
57+
//! - **Host attributes** (hostname, IP address, OS version) are available locally.
58+
//! - **Deployment attributes** (environment, cluster) may be known to the agent.
59+
//!
60+
//! Focus on attributes that are truly specific to your application instance
61+
//! and cannot be easily determined by the local agent.
362
463
#![warn(missing_debug_implementations, missing_docs)]
564

0 commit comments

Comments
 (0)