Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions opentelemetry-etw-logs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@

## vNext

- Added a `with_resource_attributes` method to the processor builder, allowing
users to specify which resource attribute keys are exported with each log
record.
- By default, the Resource attributes `"service.name"` and
`"service.instance.id"` continue to be exported as `cloud.roleName` and
`cloud.roleInstance`.
- This feature enables exporting additional resource attributes beyond the
defaults.

## v0.9.1

- 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 (`-`).
Expand Down
99 changes: 91 additions & 8 deletions opentelemetry-etw-logs/src/exporter/mod.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
use std::borrow::Cow;
use std::cell::RefCell;
use std::collections::HashSet;
use std::fmt::Debug;
use std::pin::Pin;
use std::sync::Arc;

use tracelogging_dynamic as tld;

use opentelemetry::logs::Severity;
use opentelemetry::Key;
use opentelemetry::{logs::AnyValue, Key, Value};
use opentelemetry_sdk::error::{OTelSdkError, OTelSdkResult};

pub(crate) mod common;
Expand All @@ -26,12 +28,14 @@ thread_local! {
struct Resource {
pub cloud_role: Option<String>,
pub cloud_role_instance: Option<String>,
pub attributes_from_resource: Vec<(Key, AnyValue)>,
}

pub(crate) struct ETWExporter {
provider: Pin<Arc<tld::Provider>>,
resource: Resource,
options: Options,
resource_attribute_keys: HashSet<Cow<'static, str>>,
}

fn enabled_callback_noop(
Expand Down Expand Up @@ -65,9 +69,12 @@ impl ETWExporter {
provider.as_ref().register();
}

let resource_attribute_keys = options.resource_attribute_keys().clone();

ETWExporter {
provider,
resource: Default::default(),
resource_attribute_keys,
options,
}
}
Expand Down Expand Up @@ -110,7 +117,7 @@ impl ETWExporter {

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

let event_id = part_c::populate_part_c(event, log_record, field_tag);
let event_id = part_c::populate_part_c(event, log_record, &self.resource, field_tag);

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

Expand Down Expand Up @@ -150,12 +157,24 @@ impl opentelemetry_sdk::logs::LogExporter for ETWExporter {
}

fn set_resource(&mut self, resource: &opentelemetry_sdk::Resource) {
self.resource.cloud_role = resource
.get(&Key::from_static_str("service.name"))
.map(|v| v.to_string());
self.resource.cloud_role_instance = resource
.get(&Key::from_static_str("service.instance.id"))
.map(|v| v.to_string());
// Clear previous resource attributes
self.resource.attributes_from_resource.clear();

// Process resource attributes
for (key, value) in resource.iter() {
// Special handling for cloud role and instance
// as they are used in PartA of the Common Schema format.
if key.as_str() == "service.name" {
self.resource.cloud_role = Some(value.to_string());
} else if key.as_str() == "service.instance.id" {
self.resource.cloud_role_instance = Some(value.to_string());
} else if self.resource_attribute_keys.contains(key.as_str()) {
self.resource
.attributes_from_resource
.push((key.clone(), val_to_any_value(value)));
}
// Other attributes are ignored
}
}

fn shutdown(&self) -> OTelSdkResult {
Expand All @@ -169,6 +188,16 @@ impl opentelemetry_sdk::logs::LogExporter for ETWExporter {
}
}

fn val_to_any_value(val: &Value) -> AnyValue {
match val {
Value::Bool(b) => AnyValue::Boolean(*b),
Value::I64(i) => AnyValue::Int(*i),
Value::F64(f) => AnyValue::Double(*f),
Value::String(s) => AnyValue::String(s.clone()),
_ => AnyValue::String("".into()),
}
}

#[cfg(test)]
mod tests {
use opentelemetry_sdk::logs::LogExporter;
Expand Down Expand Up @@ -224,6 +253,60 @@ mod tests {
assert!(result.is_ok());
}

#[test]
fn test_event_resources_with_custom_attributes() {
use opentelemetry::logs::LogRecord;
use opentelemetry::KeyValue;

let mut log_record = common::test_utils::new_sdk_log_record();
log_record.set_event_name("event-name");

// Create exporter with custom resource attributes
let options = Options::new("test_provider")
.with_resource_attributes(vec!["custom_attribute1", "custom_attribute2"]);

let mut exporter = ETWExporter::new(options);

exporter.set_resource(
&opentelemetry_sdk::Resource::builder()
.with_attributes([
KeyValue::new("service.name", "test-service"),
KeyValue::new("service.instance.id", "test-instance"),
KeyValue::new("custom_attribute1", "value1"),
KeyValue::new("custom_attribute2", "value2"),
KeyValue::new("custom_attribute3", "value3"), // This should be ignored
])
.build(),
);

// Verify that only the configured attributes are stored
assert_eq!(
exporter.resource.cloud_role,
Some("test-service".to_string())
);
assert_eq!(
exporter.resource.cloud_role_instance,
Some("test-instance".to_string())
);
assert_eq!(exporter.resource.attributes_from_resource.len(), 2);

// Check that the correct attributes are stored
let attrs: std::collections::HashMap<String, String> = exporter
.resource
.attributes_from_resource
.iter()
.map(|(k, v)| (k.as_str().to_string(), format!("{:?}", v)))
.collect();
assert!(attrs.contains_key("custom_attribute1"));
assert!(attrs.contains_key("custom_attribute2"));
assert!(!attrs.contains_key("custom_attribute3"));

let instrumentation = common::test_utils::new_instrumentation_scope();
let result = exporter.export_log_data(&log_record, &instrumentation);

assert!(result.is_ok());
}

#[test]
fn test_debug() {
let exporter = common::test_utils::new_etw_exporter();
Expand Down
18 changes: 18 additions & 0 deletions opentelemetry-etw-logs/src/exporter/options.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use opentelemetry_sdk::logs::SdkLogRecord;
use std::borrow::Cow;
use std::collections::HashSet;
use std::error::Error;

type BoxedEventNameCallback = Box<dyn EventNameCallback>;
Expand All @@ -8,13 +9,15 @@ type BoxedEventNameCallback = Box<dyn EventNameCallback>;
pub(crate) struct Options {
provider_name: Cow<'static, str>,
event_name_callback: Option<BoxedEventNameCallback>,
resource_attribute_keys: HashSet<Cow<'static, str>>,
}

impl Options {
pub(crate) fn new(provider_name: impl Into<Cow<'static, str>>) -> Options {
Options {
provider_name: provider_name.into(),
event_name_callback: None,
resource_attribute_keys: HashSet::new(),
}
}

Expand All @@ -23,6 +26,21 @@ impl Options {
&self.provider_name
}

/// Returns the resource attribute keys that will be exported with each log record.
pub(crate) fn resource_attribute_keys(&self) -> &HashSet<Cow<'static, str>> {
&self.resource_attribute_keys
}

/// Sets the resource attributes for the exporter.
pub(crate) fn with_resource_attributes<I, S>(mut self, attributes: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<Cow<'static, str>>,
{
self.resource_attribute_keys = attributes.into_iter().map(|s| s.into()).collect();
self
}

/// Returns the default event name that will be used for the ETW events.
pub(crate) fn default_event_name(&self) -> &str {
"Log"
Expand Down
11 changes: 10 additions & 1 deletion opentelemetry-etw-logs/src/exporter/part_c.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ pub(crate) const EVENT_ID: &str = "event_id";
pub(crate) fn populate_part_c(
event: &mut tld::EventBuilder,
log_record: &opentelemetry_sdk::logs::SdkLogRecord,
resource: &super::Resource,
field_tag: u32,
) -> Option<i64> {
//populate CS PartC
Expand All @@ -25,9 +26,17 @@ pub(crate) fn populate_part_c(
}
}

// Count resource attributes
cs_c_count += resource.attributes_from_resource.len();

// If there are additional PartC attributes, add them to the event
if cs_c_count > 0 {
event.add_struct("PartC", cs_c_count, field_tag);
event.add_struct("PartC", cs_c_count.try_into().unwrap_or(u8::MAX), field_tag);

// Add resource attributes first
for (key, value) in &resource.attributes_from_resource {
super::common::add_attribute_to_event(event, key, value);
}

// TODO: This 2nd iteration is not optimal, and can be optimized
for (key, value) in log_record.attributes_iter() {
Expand Down
59 changes: 59 additions & 0 deletions opentelemetry-etw-logs/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,64 @@
//! The ETW exporter will enable applications to use OpenTelemetry API
//! to capture the telemetry events, and write them to the ETW subsystem.
//!
//! ## Resource Attribute Handling
//!
//! **Important**: By default, resource attributes are NOT exported with log records.
//! The ETW exporter only automatically exports these specific resource attributes:
//!
//! - **`service.name`** → Exported as `cloud.roleName` in PartA of Common Schema
//! - **`service.instance.id`** → Exported as `cloud.roleInstance` in PartA of Common Schema
//!
//! All other resource attributes are ignored unless explicitly specified.
//!
//! ### Opting in to Additional Resource Attributes
//!
//! To export additional resource attributes, use the `with_resource_attributes()` method:
//!
//! ```rust
//! use opentelemetry_sdk::logs::SdkLoggerProvider;
//! use opentelemetry_sdk::Resource;
//! use opentelemetry_etw_logs::Processor;
//! use opentelemetry::KeyValue;
//!
//! let etw_processor = Processor::builder("myprovider")
//! // Only export specific resource attributes
//! .with_resource_attributes(["custom_attribute1", "custom_attribute2"])
//! .build()
//! .unwrap();
//!
//! let provider = SdkLoggerProvider::builder()
//! .with_resource(
//! Resource::builder_empty()
//! .with_service_name("example")
//! .with_attribute(KeyValue::new("custom_attribute1", "value1"))
//! .with_attribute(KeyValue::new("custom_attribute2", "value2"))
//! .with_attribute(KeyValue::new("custom_attribute3", "value3")) // This won't be exported
//! .build(),
//! )
//! .with_log_processor(etw_processor)
//! .build();
//! ```
//!
//! ### Performance Considerations for ETW
//!
//! **Warning**: Each specified resource attribute will be serialized and sent
//! with EVERY log record. This is different from OTLP exporters where resource
//! attributes are serialized once per batch. Consider the performance impact
//! when selecting which attributes to export.
//!
//! **Recommendation**: Be selective about which resource attributes to export.
//! Since ETW writes to a local kernel buffer and requires a local
//! listener/agent, the agent can often deduce many resource attributes without
//! requiring them to be sent with each log:
//!
//! - **Infrastructure attributes** (datacenter, region, availability zone) can
//! be determined by the local agent.
//! - **Host attributes** (hostname, IP address, OS version) are available locally.
//! - **Deployment attributes** (environment, cluster) may be known to the agent.
//!
//! Focus on attributes that are truly specific to your application instance
//! and cannot be easily determined by the local agent.

#![warn(missing_debug_implementations, missing_docs)]

Expand Down
Loading
Loading