Skip to content

OTEL attribute count limit not respected, causing columns dropped #4677

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ def _from_env_if_absent(
cls, value: int | None, env_var: str, default: int | None = None
) -> int | None:
if value == cls.UNSET:
return None
value = None # continue to read the limit from env

err_msg = "{} must be a non-negative integer but got {}"

Expand Down Expand Up @@ -629,6 +629,7 @@ def _translate(self, record: logging.LogRecord) -> LogRecord:
body=body,
resource=logger.resource,
attributes=attributes,
limits=self._logger_provider._log_limits,
)

def emit(self, record: logging.LogRecord) -> None:
Expand Down Expand Up @@ -691,6 +692,7 @@ def __init__(
multi_log_record_processor: SynchronousMultiLogRecordProcessor
| ConcurrentMultiLogRecordProcessor
| None = None,
log_limits: LogLimits | None = _UnsetLogLimits,
):
if resource is None:
self._resource = Resource.create({})
Expand All @@ -706,6 +708,7 @@ def __init__(
self._at_exit_handler = atexit.register(self.shutdown)
self._logger_cache = {}
self._logger_cache_lock = Lock()
self._log_limits = log_limits

@property
def resource(self):
Expand Down
213 changes: 213 additions & 0 deletions opentelemetry-sdk/tests/logs/test_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
LoggingHandler,
LogRecordProcessor,
)
from opentelemetry.sdk._logs._internal import LogLimits, _UnsetLogLimits
from opentelemetry.sdk.environment_variables import OTEL_ATTRIBUTE_COUNT_LIMIT
from opentelemetry.semconv._incubating.attributes import code_attributes
from opentelemetry.semconv.attributes import exception_attributes
from opentelemetry.trace import (
Expand Down Expand Up @@ -367,6 +369,217 @@ def test_handler_root_logger_with_disabled_sdk_does_not_go_into_recursion_error(

self.assertEqual(processor.emit_count(), 0)

@patch.dict(os.environ, {OTEL_ATTRIBUTE_COUNT_LIMIT: "3"})
def test_otel_attribute_count_limit_respected_in_logging_handler(self):
"""Test that OTEL_ATTRIBUTE_COUNT_LIMIT is properly respected by LoggingHandler."""
# Store original values to restore later
original_max_attributes = _UnsetLogLimits.max_attributes
original_max_attribute_length = _UnsetLogLimits.max_attribute_length

try:
# Force _UnsetLogLimits to re-read the environment variable
_UnsetLogLimits.max_attributes = (
_UnsetLogLimits._from_env_if_absent(
LogLimits.UNSET, OTEL_ATTRIBUTE_COUNT_LIMIT
)
)

processor, logger = set_up_test_logging(logging.WARNING)

# Create a log record with many extra attributes
extra_attrs = {f"custom_attr_{i}": f"value_{i}" for i in range(10)}

with self.assertLogs(level=logging.WARNING):
logger.warning(
"Test message with many attributes", extra=extra_attrs
)

log_record = processor.get_log_record(0)

# With OTEL_ATTRIBUTE_COUNT_LIMIT=3, should have exactly 3 attributes
total_attrs = len(log_record.attributes)
self.assertEqual(
total_attrs,
3,
f"Should have exactly 3 attributes due to limit, got {total_attrs}",
)

# Should have 10 dropped attributes (10 custom + 3 code - 3 kept = 10 dropped)
self.assertEqual(
log_record.dropped_attributes,
10,
f"Should have 10 dropped attributes, got {log_record.dropped_attributes}",
)
finally:
# Restore original values
_UnsetLogLimits.max_attributes = original_max_attributes
_UnsetLogLimits.max_attribute_length = (
original_max_attribute_length
)

@patch.dict(os.environ, {OTEL_ATTRIBUTE_COUNT_LIMIT: "5"})
def test_otel_attribute_count_limit_includes_code_attributes(self):
"""Test that OTEL_ATTRIBUTE_COUNT_LIMIT applies to all attributes including code attributes."""
# Import _UnsetLogLimits directly

# Store original values to restore later
original_max_attributes = _UnsetLogLimits.max_attributes
original_max_attribute_length = _UnsetLogLimits.max_attribute_length

try:
# Force _UnsetLogLimits to re-read the environment variable
_UnsetLogLimits.max_attributes = (
_UnsetLogLimits._from_env_if_absent(
LogLimits.UNSET, OTEL_ATTRIBUTE_COUNT_LIMIT
)
)

# Now proceed with the test
processor, logger = set_up_test_logging(logging.WARNING)

# Create a log record with some extra attributes
extra_attrs = {f"user_attr_{i}": f"value_{i}" for i in range(8)}

with self.assertLogs(level=logging.WARNING):
logger.warning("Test message", extra=extra_attrs)

log_record = processor.get_log_record(0)

# With OTEL_ATTRIBUTE_COUNT_LIMIT=5, should have exactly 5 attributes
total_attrs = len(log_record.attributes)
self.assertEqual(
total_attrs,
5,
f"Should have exactly 5 attributes due to limit, got {total_attrs}",
)

# Should have 6 dropped attributes (8 user + 3 code - 5 kept = 6 dropped)
self.assertEqual(
log_record.dropped_attributes,
6,
f"Should have 6 dropped attributes, got {log_record.dropped_attributes}",
)
finally:
# Restore original values
_UnsetLogLimits.max_attributes = original_max_attributes
_UnsetLogLimits.max_attribute_length = (
original_max_attribute_length
)

def test_logging_handler_without_env_var_uses_default_limit(self):
"""Test that without OTEL_ATTRIBUTE_COUNT_LIMIT, default limit (128) should apply."""
processor, logger = set_up_test_logging(logging.WARNING)

# Create a log record with many attributes (more than default limit of 128)
extra_attrs = {f"attr_{i}": f"value_{i}" for i in range(150)}

with self.assertLogs(level=logging.WARNING):
logger.warning(
"Test message with many attributes", extra=extra_attrs
)

log_record = processor.get_log_record(0)

# Should be limited to default limit (128) total attributes
total_attrs = len(log_record.attributes)
self.assertEqual(
total_attrs,
128,
f"Should have exactly 128 attributes (default limit), got {total_attrs}",
)

# Should have 25 dropped attributes (150 user + 3 code - 128 kept = 25 dropped)
self.assertEqual(
log_record.dropped_attributes,
25,
f"Should have 25 dropped attributes, got {log_record.dropped_attributes}",
)

def test_custom_log_limits_from_logger_provider(self):
"""Test that LoggingHandler uses custom LogLimits from LoggerProvider."""
# Create a LoggerProvider with custom LogLimits (max_attributes=4)
logger_provider = LoggerProvider(
log_limits=LogLimits(max_attributes=4)
)

# Set up logging with this provider
processor = FakeProcessor()
logger_provider.add_log_record_processor(processor)
logger = logging.getLogger("custom_limits_test")
handler = LoggingHandler(
level=logging.WARNING, logger_provider=logger_provider
)
logger.addHandler(handler)

# Create a log record with many extra attributes
extra_attrs = {f"custom_attr_{i}": f"value_{i}" for i in range(10)}

with self.assertLogs(level=logging.WARNING):
logger.warning(
"Test message with custom limits", extra=extra_attrs
)

log_record = processor.get_log_record(0)

# With custom max_attributes=4, should have exactly 4 attributes
total_attrs = len(log_record.attributes)
self.assertEqual(
total_attrs,
4,
f"Should have exactly 4 attributes due to custom limit, got {total_attrs}",
)

# Should have 9 dropped attributes (10 custom + 3 code - 4 kept = 9 dropped)
self.assertEqual(
log_record.dropped_attributes,
9,
f"Should have 9 dropped attributes, got {log_record.dropped_attributes}",
)

@patch.dict(os.environ, {OTEL_ATTRIBUTE_COUNT_LIMIT: "10"})
def test_custom_log_limits_override_env_vars(self):
"""Test that custom LogLimits from LoggerProvider override environment variables."""
# Create a LoggerProvider with custom LogLimits (max_attributes=3)
# This should override the OTEL_ATTRIBUTE_COUNT_LIMIT=10 from the environment
logger_provider = LoggerProvider(
log_limits=LogLimits(max_attributes=3)
)

# Set up logging with this provider
processor = FakeProcessor()
logger_provider.add_log_record_processor(processor)
logger = logging.getLogger("override_env_test")
handler = LoggingHandler(
level=logging.WARNING, logger_provider=logger_provider
)
logger.addHandler(handler)

# Create a log record with many extra attributes
extra_attrs = {f"custom_attr_{i}": f"value_{i}" for i in range(8)}

with self.assertLogs(level=logging.WARNING):
logger.warning(
"Test message with custom limits", extra=extra_attrs
)

log_record = processor.get_log_record(0)

# With custom max_attributes=3, should have exactly 3 attributes
# (even though OTEL_ATTRIBUTE_COUNT_LIMIT=10)
total_attrs = len(log_record.attributes)
self.assertEqual(
total_attrs,
3,
f"Should have exactly 3 attributes due to custom limit, got {total_attrs}",
)

# Should have 8 dropped attributes (8 custom + 3 code - 3 kept = 8 dropped)
self.assertEqual(
log_record.dropped_attributes,
8,
f"Should have 8 dropped attributes, got {log_record.dropped_attributes}",
)


def set_up_test_logging(level, formatter=None, root_logger=False):
logger_provider = LoggerProvider()
Expand Down