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
2 changes: 2 additions & 0 deletions .changelog/42.added
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
`LoggerProvider` now accepts a `limits` argument (a `LogRecordLimits`) that bounds the
number and value length of log-record attributes, mirroring `SpanLimits` on `TracerProvider`
Original file line number Diff line number Diff line change
Expand Up @@ -327,12 +327,16 @@ def _from_api_log_record(
record: LogRecord,
resource: Resource,
instrumentation_scope: InstrumentationScope | None = None,
limits: LogRecordLimits | None = None,
) -> ReadWriteLogRecord:
return cls(
log_record=record,
resource=resource,
instrumentation_scope=instrumentation_scope,
)
kwargs = {
"log_record": record,
"resource": resource,
"instrumentation_scope": instrumentation_scope,
}
if limits is not None:
kwargs["limits"] = limits
return cls(**kwargs)


class LogRecordProcessor(abc.ABC):
Expand Down Expand Up @@ -703,6 +707,7 @@ def __init__(
*,
logger_metrics: LoggerMetricsT,
_logger_config: _LoggerConfig,
limits: LogRecordLimits | None = None,
):
super().__init__(
instrumentation_scope.name,
Expand All @@ -715,6 +720,7 @@ def __init__(
self._instrumentation_scope = instrumentation_scope
self._logger_metrics = logger_metrics
self._logger_config = _logger_config
self._limits = limits or LogRecordLimits()

def _is_enabled(self) -> bool:
return self._logger_config.is_enabled
Expand Down Expand Up @@ -763,6 +769,7 @@ def emit(
record=record,
resource=self._resource,
instrumentation_scope=self._instrumentation_scope,
limits=self._limits,
)
else:
_set_log_record_exception_attributes(record.log_record)
Expand All @@ -785,6 +792,7 @@ def emit(
record=log_record,
resource=self._resource,
instrumentation_scope=self._instrumentation_scope,
limits=self._limits,
)

self._logger_metrics.emit_log()
Expand Down Expand Up @@ -816,13 +824,15 @@ def __init__(
| ConcurrentMultiLogRecordProcessor
| None = None,
*,
limits: LogRecordLimits | None = None,
meter_provider: MeterProvider | None = None,
_logger_configurator: _LoggerConfiguratorT | None = None,
):
if resource is None:
self._resource = Resource.create({})
else:
self._resource = resource
self._limits = limits or LogRecordLimits()
self._multi_log_record_processor = (
multi_log_record_processor or SynchronousMultiLogRecordProcessor()
)
Expand Down Expand Up @@ -884,6 +894,7 @@ def _get_logger_no_cache(
scope,
logger_metrics=self._logger_metrics,
_logger_config=self._apply_logger_configurator(scope),
limits=self._limits,
)

def _get_logger_cached(
Expand Down
77 changes: 76 additions & 1 deletion opentelemetry-sdk/tests/logs/test_log_limits.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,16 @@
import unittest
from unittest.mock import patch

from opentelemetry.sdk._logs import LogRecordLimits
from opentelemetry._logs import LogRecord as APILogRecord
from opentelemetry._logs import SeverityNumber
from opentelemetry.sdk._logs import LoggerProvider, LogRecordLimits
from opentelemetry.sdk._logs._internal import (
_DEFAULT_OTEL_ATTRIBUTE_COUNT_LIMIT,
)
from opentelemetry.sdk._logs.export import (
InMemoryLogRecordExporter,
SimpleLogRecordProcessor,
)
from opentelemetry.sdk.environment_variables import (
OTEL_ATTRIBUTE_COUNT_LIMIT,
OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT,
Expand Down Expand Up @@ -140,3 +146,72 @@ def test_invalid_env_vars_raise(self):
str(error.exception),
f"Unexpected error message for {env_var}={bad_value}",
)


class TestLoggerProviderLimits(unittest.TestCase):
@staticmethod
def _emit_and_get_record(provider, attributes):
exporter = InMemoryLogRecordExporter()
provider.add_log_record_processor(SimpleLogRecordProcessor(exporter))
logger = provider.get_logger("test_logger_provider_limits")
logger.emit(
APILogRecord(
body="body",
severity_number=SeverityNumber.WARN,
attributes=attributes,
)
)
finished = exporter.get_finished_logs()
assert len(finished) == 1
return finished[0]

def test_default_provider_uses_default_attribute_count_limit(self):
provider = LoggerProvider()
attributes = {
f"key_{index}": index
for index in range(_DEFAULT_OTEL_ATTRIBUTE_COUNT_LIMIT + 5)
}

record = self._emit_and_get_record(provider, attributes)

self.assertEqual(
len(record.log_record.attributes),
_DEFAULT_OTEL_ATTRIBUTE_COUNT_LIMIT,
)
self.assertEqual(record.dropped_attributes, 5)

def test_programmatic_limits_bound_attribute_count(self):
provider = LoggerProvider(
limits=LogRecordLimits(max_log_record_attributes=2)
)
attributes = {"a": 1, "b": 2, "c": 3, "d": 4}

record = self._emit_and_get_record(provider, attributes)

self.assertEqual(len(record.log_record.attributes), 2)
self.assertEqual(record.dropped_attributes, 2)

def test_programmatic_limits_bound_attribute_value_length(self):
provider = LoggerProvider(
limits=LogRecordLimits(max_log_record_attribute_length=3)
)
attributes = {"key": "abcdefgh"}

record = self._emit_and_get_record(provider, attributes)

self.assertEqual(record.log_record.attributes["key"], "abc")

def test_programmatic_limits_via_global_fallbacks(self):
provider = LoggerProvider(
limits=LogRecordLimits(max_attributes=1, max_attribute_length=2)
)
attributes = {"first": "abcd", "second": "efgh"}

record = self._emit_and_get_record(provider, attributes)

# max_attributes / max_attribute_length act as global fallbacks for the
# log-record-specific limits when the latter are unset.
self.assertEqual(len(record.log_record.attributes), 1)
# BoundedAttributes evicts the oldest key when full, so "second"
# survives and its value is truncated to the length limit.
self.assertEqual(record.log_record.attributes["second"], "ef")
Loading