Skip to content
Open
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
8 changes: 8 additions & 0 deletions opentelemetry-sdk/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@

## vNext

- `SdkLoggerProvider` now enforces a maximum attribute count per log record
(default `128`). Attributes beyond the limit are rejected eagerly in
`add_attribute`, so a misbehaving caller cannot grow a record's memory past
the limit, and the number of rejected attributes is tracked and exposed via
`SdkLogRecord::dropped_attributes_count()`. The limit is configurable via
`LoggerProviderBuilder::with_max_attributes_per_log` or the
`OTEL_LOGRECORD_ATTRIBUTE_COUNT_LIMIT` environment variable, which falls back
to `OTEL_ATTRIBUTE_COUNT_LIMIT`.
- Bound instruments are now available for `Gauge` and `UpDownCounter` via the
new `BoundGauge<T>` and `BoundUpDownCounter<T>` types exposed by the
`opentelemetry` crate. Requires the `experimental_metrics_bound_instruments`
Expand Down
6 changes: 6 additions & 0 deletions opentelemetry-sdk/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,12 @@
//! | `OTEL_BSP_MAX_EXPORT_BATCH_SIZE` | Maximum batch size. Must be less than or equal to `OTEL_BSP_MAX_QUEUE_SIZE`. | `512` |
//! | `OTEL_BSP_MAX_CONCURRENT_EXPORTS` | Maximum number of concurrent exports. Honored by `span_processor_with_async_runtime::BatchSpanProcessor`; thread-based `BatchSpanProcessor` exports serially. For concurrent exports, enable `experimental_trace_batch_span_processor_with_async_runtime` and use the async-runtime processor. | `1` |
//!
//! ### Logs: Log Record Limits
//!
//! | Variable | Description | Default |
//! |---|---|---|
//! | `OTEL_LOGRECORD_ATTRIBUTE_COUNT_LIMIT` | Maximum number of attributes allowed on a log record. Falls back to `OTEL_ATTRIBUTE_COUNT_LIMIT` when unset. | `128` |
//!
//! ### Logs: Batch Log Record Processor (BLRP)
//!
//! | Variable | Description | Default |
Expand Down
4 changes: 3 additions & 1 deletion opentelemetry-sdk/src/logs/logger.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ impl opentelemetry::logs::Logger for SdkLogger {
type LogRecord = SdkLogRecord;

fn create_log_record(&self) -> Self::LogRecord {
SdkLogRecord::new()
// Stamp the record with the configured attribute limit so that
// `add_attribute` can enforce it eagerly as attributes are added.
SdkLogRecord::new_with_limit(self.provider.max_attributes_per_log())
}

/// Emit a `LogRecord`.
Expand Down
177 changes: 177 additions & 0 deletions opentelemetry-sdk/src/logs/logger_provider.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use super::record::DEFAULT_MAX_ATTRIBUTES_PER_LOG;
use super::{BatchLogProcessor, LogProcessor, SdkLogger, SimpleLogProcessor};
use crate::error::{OTelSdkError, OTelSdkResult};
use crate::logs::LogExporter;
Expand All @@ -22,10 +23,18 @@ fn noop_logger_provider() -> &'static SdkLoggerProvider {
inner: Arc::new(LoggerProviderInner {
processors: Vec::new(),
is_shutdown: AtomicBool::new(true),
max_attributes_per_log: DEFAULT_MAX_ATTRIBUTES_PER_LOG,
}),
})
}

#[inline]
fn read_u32_env(key: &str) -> Option<u32> {
std::env::var(key)
.ok()
.and_then(|value| value.parse::<u32>().ok())
}

#[derive(Debug, Clone)]
/// Handles the creation and coordination of [`Logger`]s.
///
Expand Down Expand Up @@ -82,6 +91,12 @@ impl SdkLoggerProvider {
&self.inner.processors
}

/// Maximum number of attributes retained per log record. Attributes beyond
/// this limit are dropped when the record is emitted.
pub(crate) fn max_attributes_per_log(&self) -> u32 {
self.inner.max_attributes_per_log
}

/// Force flush all remaining logs in log processors and return results.
pub fn force_flush(&self) -> OTelSdkResult {
let result: Vec<_> = self
Expand Down Expand Up @@ -135,6 +150,7 @@ impl SdkLoggerProvider {
struct LoggerProviderInner {
processors: Vec<Box<dyn LogProcessor>>,
is_shutdown: AtomicBool,
max_attributes_per_log: u32,
}

impl LoggerProviderInner {
Expand Down Expand Up @@ -184,6 +200,7 @@ impl Drop for LoggerProviderInner {
pub struct LoggerProviderBuilder {
processors: Vec<Box<dyn LogProcessor>>,
resource: Option<Resource>,
max_attributes_per_log: Option<u32>,
}

impl LoggerProviderBuilder {
Expand Down Expand Up @@ -287,6 +304,20 @@ impl LoggerProviderBuilder {
LoggerProviderBuilder { resource, ..self }
}

/// Specify the maximum number of attributes retained per log record.
///
/// Attributes added beyond this limit are dropped when the record is
/// emitted. When not set, this is read from the
/// `OTEL_LOGRECORD_ATTRIBUTE_COUNT_LIMIT` environment variable, which falls
/// back to `OTEL_ATTRIBUTE_COUNT_LIMIT`, and otherwise defaults to `128`.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: after this is merged, we should follow up immediately with your other PR so this variable will be consistently applied in spans too.

/// A value set here takes precedence over the environment variables.
pub fn with_max_attributes_per_log(self, max_attributes: u32) -> Self {
LoggerProviderBuilder {
max_attributes_per_log: Some(max_attributes),
..self
}
}

/// Create a new provider from this configuration.
pub fn build(self) -> SdkLoggerProvider {
let resource = self.resource.unwrap_or(Resource::builder().build());
Expand All @@ -295,10 +326,20 @@ impl LoggerProviderBuilder {
processor.set_resource(&resource);
}

// Precedence: explicit builder value, then
// `OTEL_LOGRECORD_ATTRIBUTE_COUNT_LIMIT`, then the general
// `OTEL_ATTRIBUTE_COUNT_LIMIT`, then the default.
let max_attributes_per_log = self
.max_attributes_per_log
.or_else(|| read_u32_env("OTEL_LOGRECORD_ATTRIBUTE_COUNT_LIMIT"))
.or_else(|| read_u32_env("OTEL_ATTRIBUTE_COUNT_LIMIT"))
.unwrap_or(DEFAULT_MAX_ATTRIBUTES_PER_LOG);

let logger_provider = SdkLoggerProvider {
inner: Arc::new(LoggerProviderInner {
processors,
is_shutdown: AtomicBool::new(false),
max_attributes_per_log,
}),
};

Expand Down Expand Up @@ -793,6 +834,7 @@ mod tests {
flush_called.clone(),
))],
is_shutdown: AtomicBool::new(false),
max_attributes_per_log: DEFAULT_MAX_ATTRIBUTES_PER_LOG,
});

{
Expand Down Expand Up @@ -833,6 +875,7 @@ mod tests {
flush_called.clone(),
))],
is_shutdown: AtomicBool::new(false),
max_attributes_per_log: DEFAULT_MAX_ATTRIBUTES_PER_LOG,
});

// Create a scope to test behavior when providers are dropped
Expand Down Expand Up @@ -1007,4 +1050,138 @@ mod tests {
Ok(())
}
}

fn emit_record_with_attributes(provider: &SdkLoggerProvider, count: usize) {
let logger = provider.logger("test");
let mut record = logger.create_log_record();
for i in 0..count {
record.add_attribute(Key::new(format!("key{i}")), AnyValue::Int(i as i64));
}
logger.emit(record);
provider.force_flush().unwrap();
}

#[test]
fn log_record_attributes_capped_at_default_limit() {
let exporter = InMemoryLogExporter::default();
let provider = SdkLoggerProvider::builder()
.with_simple_exporter(exporter.clone())
.build();
assert_eq!(
provider.max_attributes_per_log(),
DEFAULT_MAX_ATTRIBUTES_PER_LOG
);

emit_record_with_attributes(&provider, 200);

let emitted = exporter.get_emitted_logs().unwrap();
assert_eq!(
emitted[0].record.attributes_iter().count(),
DEFAULT_MAX_ATTRIBUTES_PER_LOG as usize
);
assert_eq!(
emitted[0].record.dropped_attributes_count(),
200 - DEFAULT_MAX_ATTRIBUTES_PER_LOG
);
}

#[test]
fn log_record_attributes_within_limit_are_kept() {
let exporter = InMemoryLogExporter::default();
let provider = SdkLoggerProvider::builder()
.with_simple_exporter(exporter.clone())
.build();

emit_record_with_attributes(&provider, 10);

let emitted = exporter.get_emitted_logs().unwrap();
assert_eq!(emitted[0].record.attributes_iter().count(), 10);
assert_eq!(emitted[0].record.dropped_attributes_count(), 0);
}

#[test]
fn builder_max_attributes_per_log_takes_precedence() {
temp_env::with_vars(
[
("OTEL_LOGRECORD_ATTRIBUTE_COUNT_LIMIT", Some("16")),
("OTEL_ATTRIBUTE_COUNT_LIMIT", Some("8")),
],
|| {
let exporter = InMemoryLogExporter::default();
let provider = SdkLoggerProvider::builder()
.with_simple_exporter(exporter.clone())
.with_max_attributes_per_log(4)
.build();
assert_eq!(provider.max_attributes_per_log(), 4);

emit_record_with_attributes(&provider, 10);
let emitted = exporter.get_emitted_logs().unwrap();
assert_eq!(emitted[0].record.attributes_iter().count(), 4);
assert_eq!(emitted[0].record.dropped_attributes_count(), 6);
},
);
}

#[test]
fn logrecord_env_var_takes_precedence_over_general() {
temp_env::with_vars(
[
("OTEL_LOGRECORD_ATTRIBUTE_COUNT_LIMIT", Some("16")),
("OTEL_ATTRIBUTE_COUNT_LIMIT", Some("8")),
],
|| {
let provider = SdkLoggerProvider::builder().build();
assert_eq!(provider.max_attributes_per_log(), 16);
},
);
}

#[test]
fn general_env_var_is_fallback() {
temp_env::with_vars(
[
("OTEL_LOGRECORD_ATTRIBUTE_COUNT_LIMIT", None::<&str>),
("OTEL_ATTRIBUTE_COUNT_LIMIT", Some("8")),
],
|| {
let provider = SdkLoggerProvider::builder().build();
assert_eq!(provider.max_attributes_per_log(), 8);
},
);
}

#[test]
fn invalid_env_var_falls_back_to_default() {
temp_env::with_vars(
[
("OTEL_LOGRECORD_ATTRIBUTE_COUNT_LIMIT", Some("not-a-number")),
("OTEL_ATTRIBUTE_COUNT_LIMIT", None::<&str>),
],
|| {
let provider = SdkLoggerProvider::builder().build();
assert_eq!(
provider.max_attributes_per_log(),
DEFAULT_MAX_ATTRIBUTES_PER_LOG
);
},
);
}

#[test]
fn noop_logger_after_shutdown_uses_default_limit() {
let provider = SdkLoggerProvider::builder().build();
provider.shutdown().unwrap();

// After shutdown, new loggers refer to the no-op provider, which is
// configured with the default attribute limit.
let logger = provider.logger("after-shutdown");
let mut record = logger.create_log_record();
for i in 0..(DEFAULT_MAX_ATTRIBUTES_PER_LOG as usize + 10) {
record.add_attribute(Key::new(format!("k{i}")), AnyValue::Int(i as i64));
}
assert_eq!(
record.attributes_iter().count(),
DEFAULT_MAX_ATTRIBUTES_PER_LOG as usize
);
}
}
Loading
Loading