Skip to content

Commit eb900b3

Browse files
committed
feat: opentelemetry-etw-logs: with resources
1 parent 471aa06 commit eb900b3

File tree

6 files changed

+191
-19
lines changed

6 files changed

+191
-19
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.9.1
615

716
- Added `Processor::builder_etw_compat_only()` method that builds a processor using a provider name that is fully compatible with ETW requirements (dropping UserEvents provider name compatibility) by allowing hyphens (`-`).

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,9 +83,10 @@ pub(crate) mod test_utils {
8383

8484
use crate::exporter::options::Options;
8585
use crate::exporter::ETWExporter;
86+
use std::collections::HashSet;
8687

8788
pub(crate) fn new_etw_exporter() -> ETWExporter {
88-
ETWExporter::new(test_options())
89+
ETWExporter::new(test_options(), HashSet::new())
8990
}
9091

9192
pub(crate) fn new_instrumentation_scope() -> opentelemetry::InstrumentationScope {

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

Lines changed: 45 additions & 10 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

8-
use opentelemetry::logs::Severity;
9-
use opentelemetry::Key;
10+
use opentelemetry::logs::{AnyValue, Severity};
11+
use opentelemetry::{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(
@@ -49,7 +53,10 @@ fn enabled_callback_noop(
4953
impl ETWExporter {
5054
const KEYWORD: u64 = 1;
5155

52-
pub(crate) fn new(options: Options) -> Self {
56+
pub(crate) fn new(
57+
options: Options,
58+
resource_attribute_keys: HashSet<Cow<'static, str>>,
59+
) -> Self {
5360
let mut provider_options = tld::Provider::options();
5461

5562
provider_options.callback(enabled_callback_noop, 0x0);
@@ -69,6 +76,7 @@ impl ETWExporter {
6976
provider,
7077
resource: Default::default(),
7178
options,
79+
resource_attribute_keys,
7280
}
7381
}
7482

@@ -110,7 +118,12 @@ impl ETWExporter {
110118

111119
part_a::populate_part_a(event, &self.resource, log_record, field_tag);
112120

113-
let event_id = part_c::populate_part_c(event, log_record, field_tag);
121+
let event_id = part_c::populate_part_c(
122+
event,
123+
log_record,
124+
&self.resource.attributes_from_resource,
125+
field_tag,
126+
);
114127

115128
part_b::populate_part_b(event, log_record, otel_level, event_id);
116129

@@ -150,12 +163,24 @@ impl opentelemetry_sdk::logs::LogExporter for ETWExporter {
150163
}
151164

152165
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());
166+
// Clear previous resource attributes
167+
self.resource.attributes_from_resource.clear();
168+
169+
// Add attributes from resource to the attributes_from_resource
170+
for (key, value) in resource.iter() {
171+
// Special handling for cloud role and instance
172+
// as they are used in PartA of the Common Schema format.
173+
if key.as_str() == "service.name" {
174+
self.resource.cloud_role = Some(value.to_string());
175+
} else if key.as_str() == "service.instance.id" {
176+
self.resource.cloud_role_instance = Some(value.to_string());
177+
} else if self.resource_attribute_keys.contains(key.as_str()) {
178+
self.resource
179+
.attributes_from_resource
180+
.push((key.clone(), val_to_any_value(value)));
181+
}
182+
// Other attributes are ignored
183+
}
159184
}
160185

161186
fn shutdown(&self) -> OTelSdkResult {
@@ -169,6 +194,16 @@ impl opentelemetry_sdk::logs::LogExporter for ETWExporter {
169194
}
170195
}
171196

197+
fn val_to_any_value(val: &Value) -> AnyValue {
198+
match val {
199+
Value::Bool(b) => AnyValue::Boolean(*b),
200+
Value::I64(i) => AnyValue::Int(*i),
201+
Value::F64(f) => AnyValue::Double(*f),
202+
Value::String(s) => AnyValue::String(s.clone()),
203+
_ => AnyValue::String("".into()),
204+
}
205+
}
206+
172207
#[cfg(test)]
173208
mod tests {
174209
use opentelemetry_sdk::logs::LogExporter;

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

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
use opentelemetry::logs::AnyValue;
2+
use opentelemetry::Key;
23
use tracelogging_dynamic as tld;
34

45
pub(crate) const EVENT_ID: &str = "event_id";
56

67
pub(crate) fn populate_part_c(
78
event: &mut tld::EventBuilder,
89
log_record: &opentelemetry_sdk::logs::SdkLogRecord,
10+
resource_attributes: &[(Key, AnyValue)],
911
field_tag: u32,
1012
) -> Option<i64> {
1113
//populate CS PartC
@@ -25,10 +27,14 @@ pub(crate) fn populate_part_c(
2527
}
2628
}
2729

30+
// Add count of resource attributes
31+
cs_c_count += resource_attributes.len() as u16;
32+
2833
// If there are additional PartC attributes, add them to the event
2934
if cs_c_count > 0 {
30-
event.add_struct("PartC", cs_c_count, field_tag);
35+
event.add_struct("PartC", cs_c_count as u8, field_tag);
3136

37+
// Add log record attributes first
3238
// TODO: This 2nd iteration is not optimal, and can be optimized
3339
for (key, value) in log_record.attributes_iter() {
3440
match (key.as_str(), &value) {
@@ -40,6 +46,11 @@ pub(crate) fn populate_part_c(
4046
}
4147
}
4248
}
49+
50+
// Add resource attributes
51+
for (key, value) in resource_attributes {
52+
super::common::add_attribute_to_event(event, key, value);
53+
}
4354
}
4455
event_id
4556
}

opentelemetry-etw-logs/src/lib.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,44 @@
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+
//! ```
342
443
#![warn(missing_debug_implementations, missing_docs)]
544

opentelemetry-etw-logs/src/processor.rs

Lines changed: 84 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ use opentelemetry::InstrumentationScope;
22
use opentelemetry_sdk::error::OTelSdkResult;
33
use opentelemetry_sdk::logs::{LogBatch, LogExporter, SdkLogRecord};
44
use opentelemetry_sdk::Resource;
5+
use std::borrow::Cow;
6+
use std::collections::HashSet;
57
use std::error::Error;
68
use std::fmt::Debug;
79

@@ -59,8 +61,11 @@ impl Processor {
5961
}
6062

6163
/// Creates a new instance of the [`Processor`] using the given options.
62-
pub(crate) fn new(options: Options) -> Self {
63-
let exporter: ETWExporter = ETWExporter::new(options);
64+
pub(crate) fn new(
65+
options: Options,
66+
resource_attribute_keys: HashSet<Cow<'static, str>>,
67+
) -> Self {
68+
let exporter: ETWExporter = ETWExporter::new(options, resource_attribute_keys);
6469
Processor {
6570
event_exporter: exporter,
6671
}
@@ -113,6 +118,7 @@ impl opentelemetry_sdk::logs::LogProcessor for Processor {
113118
pub struct ProcessorBuilder {
114119
options: Options,
115120
provider_name_compat_mode: ProviderNameCompatMode,
121+
resource_attribute_keys: HashSet<Cow<'static, str>>,
116122
}
117123

118124
impl ProcessorBuilder {
@@ -125,6 +131,7 @@ impl ProcessorBuilder {
125131
ProcessorBuilder {
126132
options: Options::new(provider_name.to_string()),
127133
provider_name_compat_mode: ProviderNameCompatMode::CrossCompat,
134+
resource_attribute_keys: HashSet::new(),
128135
}
129136
}
130137

@@ -137,6 +144,7 @@ impl ProcessorBuilder {
137144
ProcessorBuilder {
138145
options: Options::new(provider_name.to_string()),
139146
provider_name_compat_mode: ProviderNameCompatMode::EtwCompatOnly,
147+
resource_attribute_keys: HashSet::new(),
140148
}
141149
}
142150

@@ -153,11 +161,48 @@ impl ProcessorBuilder {
153161
self
154162
}
155163

164+
/// Sets the resource attributes for the processor.
165+
///
166+
/// This specifies which resource attributes should be exported with each log record.
167+
///
168+
/// # Performance Considerations
169+
///
170+
/// **Warning**: Each specified resource attribute will be serialized and sent
171+
/// with EVERY log record. This is different from OTLP exporters where resource
172+
/// attributes are serialized once per batch. Consider the performance impact
173+
/// when selecting which attributes to export.
174+
///
175+
/// # Best Practices for ETW
176+
///
177+
/// **Recommendation**: Be selective about which resource attributes to export.
178+
/// Since ETW requires a local listener/agent, the agent can often deduce many
179+
/// resource attributes without requiring them to be sent with each log:
180+
///
181+
/// - **Infrastructure attributes** (datacenter, region, availability zone) can
182+
/// be determined by the local agent.
183+
/// - **Host attributes** (hostname, IP address, OS version) are available locally.
184+
/// - **Deployment attributes** (environment, cluster) may be known to the agent.
185+
///
186+
/// Focus on attributes that are truly specific to your application instance
187+
/// and cannot be easily determined by the local agent.
188+
///
189+
/// Nevertheless, if there are attributes that are fixed and must be emitted
190+
/// with every log, modeling them as Resource attributes and using this method
191+
/// is much more efficient than emitting them explicitly with every log.
192+
pub fn with_resource_attributes<I, S>(mut self, attributes: I) -> Self
193+
where
194+
I: IntoIterator<Item = S>,
195+
S: Into<Cow<'static, str>>,
196+
{
197+
self.resource_attribute_keys = attributes.into_iter().map(|s| s.into()).collect();
198+
self
199+
}
200+
156201
/// Builds the processor with given options, returning `Error` if it fails.
157202
pub fn build(self) -> Result<Processor, Box<dyn Error>> {
158203
self.validate()?;
159204

160-
Ok(Processor::new(self.options))
205+
Ok(Processor::new(self.options, self.resource_attribute_keys))
161206
}
162207

163208
fn validate(&self) -> Result<(), Box<dyn Error>> {
@@ -214,21 +259,21 @@ mod tests {
214259

215260
#[test]
216261
fn test_shutdown() {
217-
let processor = Processor::new(test_options());
262+
let processor = Processor::new(test_options(), HashSet::new());
218263

219264
assert!(processor.shutdown().is_ok());
220265
}
221266

222267
#[test]
223268
fn test_force_flush() {
224-
let processor = Processor::new(test_options());
269+
let processor = Processor::new(test_options(), HashSet::new());
225270

226271
assert!(processor.force_flush().is_ok());
227272
}
228273

229274
#[test]
230275
fn test_emit() {
231-
let processor: Processor = Processor::new(test_options());
276+
let processor: Processor = Processor::new(test_options(), HashSet::new());
232277

233278
let mut record = SdkLoggerProvider::builder()
234279
.build()
@@ -241,7 +286,7 @@ mod tests {
241286
#[test]
242287
#[cfg(feature = "spec_unstable_logs_enabled")]
243288
fn test_event_enabled() {
244-
let processor = Processor::new(test_options());
289+
let processor = Processor::new(test_options(), HashSet::new());
245290

246291
// Unit test are forced to return true as there is no ETW session listening for the event
247292
assert!(processor.event_enabled(opentelemetry::logs::Severity::Info, "test", Some("test")));
@@ -427,4 +472,36 @@ mod tests {
427472
);
428473
assert!(result.is_ok());
429474
}
475+
476+
#[test]
477+
fn test_resource_attributes() {
478+
use opentelemetry::logs::LogRecord;
479+
use opentelemetry::logs::Logger;
480+
use opentelemetry::logs::LoggerProvider;
481+
use opentelemetry::KeyValue;
482+
use opentelemetry_sdk::logs::SdkLoggerProvider;
483+
use opentelemetry_sdk::Resource;
484+
485+
let processor = Processor::builder("test_provider")
486+
.with_resource_attributes(vec!["resource_attribute1", "resource_attribute2"])
487+
.build()
488+
.unwrap();
489+
490+
let logger_provider = SdkLoggerProvider::builder()
491+
.with_resource(
492+
Resource::builder()
493+
.with_service_name("test_service")
494+
.with_attribute(KeyValue::new("resource_attribute1", "value1"))
495+
.with_attribute(KeyValue::new("resource_attribute2", "value2"))
496+
.with_attribute(KeyValue::new("resource_attribute3", "value3")) // This should not be exported
497+
.build(),
498+
)
499+
.with_log_processor(processor)
500+
.build();
501+
502+
let logger = logger_provider.logger("test_logger");
503+
let mut log_record = logger.create_log_record();
504+
log_record.add_attribute("log_attribute", "log_value");
505+
logger.emit(log_record);
506+
}
430507
}

0 commit comments

Comments
 (0)