Skip to content
Merged
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-user-events-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.13.0

Released 2025-May-27
Expand Down
62 changes: 59 additions & 3 deletions opentelemetry-user-events-logs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,46 @@
//!
//! This will create a logger provider with the `user_events` exporter enabled.
//!
//! ## Resource Attribute Handling
//!
//! **Important**: By default, resource attributes are NOT exported with log records.
//! The user_events 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_user_events_logs::Processor;
//! use opentelemetry::KeyValue;
//!
//! let user_event_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_attribute2", "value3")) // This won't be exported
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cijothomas is there a typo here?

.with_attribute(KeyValue::new("custom_attribute3", "value3")) // This won't be exported

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good catch! yes. Will send a fix

//! .build(),
//! )
//! .with_log_processor(user_event_processor)
//! .build();
//! ```
//!
//!
//! ## Listening to Exported Events
//!
//! Tools like `perf` or `ftrace` can be used to listen to the exported events.
Expand All @@ -101,7 +141,7 @@ mod tests {
use crate::Processor;
use opentelemetry::trace::Tracer;
use opentelemetry::trace::{TraceContextExt, TracerProvider};
use opentelemetry::Key;
use opentelemetry::{Key, KeyValue};
use opentelemetry_appender_tracing::layer;
use opentelemetry_sdk::Resource;
use opentelemetry_sdk::{
Expand All @@ -124,10 +164,20 @@ mod tests {

// Basic check if user_events are available
check_user_events_available().expect("Kernel does not support user_events. Verify your distribution/kernel supports user_events: https://docs.kernel.org/trace/user_events.html.");
let user_event_processor = Processor::builder("myprovider").build().unwrap();
let user_event_processor = Processor::builder("myprovider")
.with_resource_attributes(vec!["resource_attribute1", "resource_attribute2"])
.build()
.unwrap();

let logger_provider = LoggerProviderBuilder::default()
.with_resource(Resource::builder().with_service_name("myrolename").build())
.with_resource(
Resource::builder()
.with_service_name("myrolename")
.with_attribute(KeyValue::new("resource_attribute1", "v1"))
.with_attribute(KeyValue::new("resource_attribute2", "v2"))
.with_attribute(KeyValue::new("resource_attribute3", "v3"))
.build(),
)
.with_log_processor(user_event_processor)
.build();

Expand Down Expand Up @@ -233,6 +283,12 @@ mod tests {
// Validate PartC
let part_c = &event["PartC"];
assert_eq!(part_c["user_name"].as_str().unwrap(), "otel user");
assert_eq!(part_c["resource_attribute1"].as_str().unwrap(), "v1");
assert_eq!(part_c["resource_attribute2"].as_str().unwrap(), "v2");
assert!(
part_c.get("resource_attribute3").is_none(),
"resource_attribute3 should not be present"
);
assert_eq!(
part_c["user_email"].as_str().unwrap(),
"[email protected]"
Expand Down
57 changes: 48 additions & 9 deletions opentelemetry-user-events-logs/src/logs/exporter.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
use eventheader::{FieldFormat, Level};
use eventheader_dynamic::{EventBuilder, EventSet, Provider};
use opentelemetry::{otel_debug, otel_info};
use opentelemetry::{otel_debug, otel_info, Value};
use opentelemetry_sdk::Resource;
use std::borrow::Cow;
use std::collections::HashSet;
use std::sync::Arc;
use std::{fmt::Debug, sync::Mutex};

Expand All @@ -18,6 +20,8 @@ pub(crate) struct UserEventsExporter {
event_sets: Vec<Arc<EventSet>>,
cloud_role: Option<String>,
cloud_role_instance: Option<String>,
attributes_from_resource: Vec<(Key, AnyValue)>,
resource_attribute_keys: HashSet<Cow<'static, str>>,
}

// Constants for the UserEventsExporter
Expand Down Expand Up @@ -114,7 +118,10 @@ const fn get_severity_level(severity: Severity) -> Level {

impl UserEventsExporter {
/// Create instance of the exporter
pub(crate) fn new(provider_name: &str) -> Self {
pub(crate) fn new(
provider_name: &str,
resource_attributes: HashSet<Cow<'static, str>>,
) -> Self {
let mut eventheader_provider: Provider =
Provider::new(provider_name, &Provider::new_options());
let event_sets = register_events(&mut eventheader_provider);
Expand All @@ -126,6 +133,8 @@ impl UserEventsExporter {
event_sets,
cloud_role: None,
cloud_role_instance: None,
resource_attribute_keys: resource_attributes,
attributes_from_resource: Vec::new(),
}
}

Expand Down Expand Up @@ -279,6 +288,18 @@ impl UserEventsExporter {
}
}

if !self.attributes_from_resource.is_empty() {
if !is_part_c_present {
eb.add_struct_with_bookmark("PartC", 1, 0, &mut cs_c_bookmark);
is_part_c_present = true;
}

for (key, value) in &self.attributes_from_resource {
self.add_attribute_to_event(&mut eb, (key, value));
cs_c_count += 1;
}
}

if is_part_c_present {
eb.set_struct_field_count(cs_c_bookmark, cs_c_count);
}
Expand Down Expand Up @@ -408,12 +429,30 @@ impl opentelemetry_sdk::logs::LogExporter for UserEventsExporter {
}

fn set_resource(&mut self, resource: &Resource) {
self.cloud_role = resource
.get(&Key::from_static_str("service.name"))
.map(|v| v.to_string());
self.cloud_role_instance = resource
.get(&Key::from_static_str("service.instance.id"))
.map(|v| v.to_string());
// add attributes from resource to the attributes_from_resource
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.cloud_role = Some(value.to_string());
} else if key.as_str() == "service.instance.id" {
self.cloud_role_instance = Some(value.to_string());
} else if self.resource_attribute_keys.contains(key.as_str()) {
self.attributes_from_resource
.push((key.clone(), val_to_any_value(value)));
}
// Other attributes are ignored
}
}
}

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()),
}
}

Expand All @@ -422,7 +461,7 @@ mod tests {
use super::*;
#[test]
fn exporter_debug() {
let exporter = UserEventsExporter::new("test_provider");
let exporter = UserEventsExporter::new("test_provider", HashSet::new());
assert_eq!(
format!("{exporter:?}"),
"user_events log exporter (provider name: test_provider)"
Expand Down
48 changes: 46 additions & 2 deletions opentelemetry-user-events-logs/src/logs/processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ use opentelemetry_sdk::{
error::OTelSdkResult,
logs::{LogBatch, SdkLogRecord},
};
use std::borrow::Cow;
use std::collections::HashSet;
use std::error::Error;
use std::fmt::Debug;

Expand Down Expand Up @@ -66,6 +68,7 @@ impl opentelemetry_sdk::logs::LogProcessor for Processor {
#[derive(Debug)]
pub struct ProcessorBuilder<'a> {
provider_name: &'a str,
resource_attribute_keys: HashSet<Cow<'static, str>>,
}

impl<'a> ProcessorBuilder<'a> {
Expand Down Expand Up @@ -93,7 +96,48 @@ impl<'a> ProcessorBuilder<'a> {
/// For example the following will capture level 2 (Error) and 3(Warning) events:
/// perf record -e user_events:myprovider_L2K1,user_events:myprovider_L3K1
pub(crate) fn new(provider_name: &'a str) -> Self {
Self { provider_name }
Self {
provider_name,
resource_attribute_keys: HashSet::new(),
}
}

/// Sets the resource attributes for the processor.
///
/// This specifies which resource attributes should be exported with each log record.
///
/// # Performance Considerations
///
/// **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.
///
/// # Best Practices for user_events
///
/// **Recommendation**: Be selective about which resource attributes to export.
/// Since user_events 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.
///
/// Nevertheless, if there are attributes that are fixed and must be emitted
/// with every log, modeling them as Resource attributes and using this method
/// is much more efficient than emitting them explicitly with every log.
pub 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
}

/// Builds the processor with the configured options
Expand All @@ -117,7 +161,7 @@ impl<'a> ProcessorBuilder<'a> {
return Err("Provider name must contain only ASCII letters, digits, and '_'.".into());
}

let exporter = UserEventsExporter::new(self.provider_name);
let exporter = UserEventsExporter::new(self.provider_name, self.resource_attribute_keys);
Ok(Processor { exporter })
}
}
Expand Down
Loading