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
1 change: 1 addition & 0 deletions .changelog/5365.added
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
`opentelemetry-sdk`: wire top-level `attribute_limits` into per-signal providers via declarative config; add `log_record_limits` support to `LoggerProvider`
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
ConfigurationError,
MissingDependencyError,
)
from opentelemetry.configuration.models import (
AttributeLimits,
)
from opentelemetry.configuration.models import (
BatchLogRecordProcessor as BatchLogRecordProcessorConfig,
)
Expand All @@ -28,6 +31,9 @@
from opentelemetry.configuration.models import (
LogRecordExporter as LogRecordExporterConfig,
)
from opentelemetry.configuration.models import (
LogRecordLimits as LogRecordLimitsConfig,
)
from opentelemetry.configuration.models import (
LogRecordProcessor as LogRecordProcessorConfig,
)
Expand All @@ -41,6 +47,7 @@
SimpleLogRecordProcessor as SimpleLogRecordProcessorConfig,
)
from opentelemetry.sdk._logs import LoggerProvider
from opentelemetry.sdk._logs._internal import LogRecordLimits
from opentelemetry.sdk._logs._internal.export import (
BatchLogRecordProcessor,
ConsoleLogRecordExporter,
Expand All @@ -51,6 +58,8 @@

_logger = logging.getLogger(__name__)

_DEFAULT_OTEL_LOG_ATTRIBUTE_COUNT_LIMIT = 128

# BatchLogRecordProcessor defaults per OTel spec (milliseconds).
_DEFAULT_SCHEDULE_DELAY_MILLIS = 1000
_DEFAULT_EXPORT_TIMEOUT_MILLIS = 30000
Expand Down Expand Up @@ -216,9 +225,38 @@ def _create_log_record_processor(
)


def _create_log_record_limits(
config: LogRecordLimitsConfig,
global_limits: AttributeLimits | None = None,
) -> LogRecordLimits:
"""Create LogRecordLimits from config.

Absent fields fall back to global_limits (if provided), then to OTel spec
defaults (128 for counts, unlimited for lengths).
Explicit values suppress env-var reading — matching Java SDK behavior.
"""
attribute_count_limit = config.attribute_count_limit
if attribute_count_limit is None and global_limits is not None:
attribute_count_limit = global_limits.attribute_count_limit

attribute_value_length_limit = config.attribute_value_length_limit
if attribute_value_length_limit is None and global_limits is not None:
attribute_value_length_limit = global_limits.attribute_value_length_limit

return LogRecordLimits(
max_attributes=(
attribute_count_limit if attribute_count_limit is not None else _DEFAULT_OTEL_LOG_ATTRIBUTE_COUNT_LIMIT
),
max_attribute_length=(
attribute_value_length_limit if attribute_value_length_limit is not None else LogRecordLimits.UNSET
),
)


def create_logger_provider(
config: LoggerProviderConfig | None,
resource: Resource | None = None,
global_attribute_limits: AttributeLimits | None = None,
) -> LoggerProvider:
"""Create an SDK LoggerProvider from declarative config.

Expand All @@ -228,21 +266,25 @@ def create_logger_provider(
Args:
config: LoggerProvider config from the parsed config file, or None.
resource: Resource to attach to the provider.
global_attribute_limits: Top-level attribute_limits from the root config,
used as a fallback when per-signal limits are not specified.

Returns:
A configured LoggerProvider.
"""
provider = LoggerProvider(resource=resource)
if config is not None and config.limits is not None:
limits = config.limits

else:
limits = LogRecordLimitsConfig()

log_record_limits = _create_log_record_limits(limits, global_attribute_limits)

provider = LoggerProvider(resource=resource, log_record_limits=log_record_limits)

if config is None:
return provider

if config.limits is not None:
_logger.warning(
"log_record_limits are specified in config but are not supported "
"by the Python SDK LoggerProvider constructor; limits will be ignored."
)

for processor_config in config.processors:
provider.add_log_record_processor(_create_log_record_processor(processor_config))

Expand All @@ -252,6 +294,7 @@ def create_logger_provider(
def configure_logger_provider(
config: LoggerProviderConfig | None,
resource: Resource | None = None,
global_attribute_limits: AttributeLimits | None = None,
) -> None:
"""Configure the global LoggerProvider from declarative config.

Expand All @@ -261,7 +304,9 @@ def configure_logger_provider(
Args:
config: LoggerProvider config from the parsed config file, or None.
resource: Resource to attach to the provider.
global_attribute_limits: Top-level attribute_limits from the root config,
used as a fallback when per-signal limits are not specified.
"""
if config is None:
return
set_logger_provider(create_logger_provider(config, resource))
set_logger_provider(create_logger_provider(config, resource, global_attribute_limits))
Original file line number Diff line number Diff line change
Expand Up @@ -99,9 +99,10 @@ def configure_sdk(config: OpenTelemetryConfiguration) -> None:
level = _SEVERITY_TO_LOGGING_LEVEL.get(config.log_level, INFO)
getLogger("opentelemetry").setLevel(level)

global_attribute_limits = config.attribute_limits
resource = create_resource(config.resource)
configure_tracer_provider(config.tracer_provider, resource)
configure_tracer_provider(config.tracer_provider, resource, global_attribute_limits)
configure_meter_provider(config.meter_provider, resource)
configure_logger_provider(config.logger_provider, resource)
configure_logger_provider(config.logger_provider, resource, global_attribute_limits)
configure_propagator(config.propagator)
configure_instrumentation(config.instrumentation_development)
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
ConfigurationError,
MissingDependencyError,
)
from opentelemetry.configuration.models import (
AttributeLimits,
)
from opentelemetry.configuration.models import (
ExperimentalComposableRuleBasedSampler as RuleBasedSamplerConfig,
)
Expand Down Expand Up @@ -357,17 +360,27 @@ def _create_parent_based_sampler(config: ParentBasedSamplerConfig) -> Sampler:
return ParentBased(**kwargs)


def _create_span_limits(config: SpanLimitsConfig) -> SpanLimits:
def _create_span_limits(
config: SpanLimitsConfig,
global_limits: AttributeLimits | None = None,
) -> SpanLimits:
"""Create SpanLimits from config.

Absent fields use the OTel spec defaults (128 for counts, unlimited for lengths).
Absent fields fall back to global_limits (if provided), then to OTel spec
defaults (128 for counts, unlimited for lengths).
Explicit values suppress env-var reading — matching Java SDK behavior.
"""
attribute_count_limit = config.attribute_count_limit
if attribute_count_limit is None and global_limits is not None:
attribute_count_limit = global_limits.attribute_count_limit

attribute_value_length_limit = config.attribute_value_length_limit
if attribute_value_length_limit is None and global_limits is not None:
attribute_value_length_limit = global_limits.attribute_value_length_limit

return SpanLimits(
max_span_attributes=(
config.attribute_count_limit
if config.attribute_count_limit is not None
else _DEFAULT_OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT
attribute_count_limit if attribute_count_limit is not None else _DEFAULT_OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT
),
max_events=(
config.event_count_limit if config.event_count_limit is not None else _DEFAULT_OTEL_SPAN_EVENT_COUNT_LIMIT
Expand All @@ -385,13 +398,19 @@ def _create_span_limits(config: SpanLimitsConfig) -> SpanLimits:
if config.link_attribute_count_limit is not None
else _DEFAULT_OTEL_LINK_ATTRIBUTE_COUNT_LIMIT
),
max_attribute_length=config.attribute_value_length_limit,
max_attribute_length=(
attribute_value_length_limit if attribute_value_length_limit is not None else SpanLimits.UNSET
),
max_span_attribute_length=(
attribute_value_length_limit if attribute_value_length_limit is not None else SpanLimits.UNSET
),
)


def create_tracer_provider(
config: TracerProviderConfig | None,
resource: Resource | None = None,
global_attribute_limits: AttributeLimits | None = None,
) -> TracerProvider:
"""Create an SDK TracerProvider from declarative config.

Expand All @@ -402,6 +421,8 @@ def create_tracer_provider(
Args:
config: TracerProvider config from the parsed config file, or None.
resource: Resource to attach to the provider.
global_attribute_limits: Top-level attribute_limits from the root config,
used as a fallback when per-signal limits are not specified.

Returns:
A configured TracerProvider.
Expand All @@ -410,17 +431,13 @@ def create_tracer_provider(
id_generator = (
_create_id_generator(config.id_generator) if config is not None and config.id_generator is not None else None
)
span_limits = (
_create_span_limits(config.limits)
if config is not None and config.limits is not None
else SpanLimits(
max_span_attributes=_DEFAULT_OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT,
max_events=_DEFAULT_OTEL_SPAN_EVENT_COUNT_LIMIT,
max_links=_DEFAULT_OTEL_SPAN_LINK_COUNT_LIMIT,
max_event_attributes=_DEFAULT_OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT,
max_link_attributes=_DEFAULT_OTEL_LINK_ATTRIBUTE_COUNT_LIMIT,
)
)
if config is not None and config.limits is not None:
Comment thread
ocelotl marked this conversation as resolved.
limits = config.limits

else:
limits = SpanLimitsConfig()

span_limits = _create_span_limits(limits, global_attribute_limits)

provider = TracerProvider(
resource=resource,
Expand All @@ -439,6 +456,7 @@ def create_tracer_provider(
def configure_tracer_provider(
config: TracerProviderConfig | None,
resource: Resource | None = None,
global_attribute_limits: AttributeLimits | None = None,
) -> None:
"""Configure the global TracerProvider from declarative config.

Expand All @@ -449,7 +467,9 @@ def configure_tracer_provider(
Args:
config: TracerProvider config from the parsed config file, or None.
resource: Resource to attach to the provider.
global_attribute_limits: Top-level attribute_limits from the root config,
used as a fallback when per-signal limits are not specified.
"""
if config is None:
return
trace.set_tracer_provider(create_tracer_provider(config, resource))
trace.set_tracer_provider(create_tracer_provider(config, resource, global_attribute_limits))
69 changes: 55 additions & 14 deletions opentelemetry-configuration/tests/test_logger_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
# Tests access private members of SDK classes to assert correct configuration.
# pylint: disable=protected-access

import os
import sys
import unittest
from unittest.mock import MagicMock, patch
Expand All @@ -22,6 +23,10 @@
create_logger_provider,
)
from opentelemetry.configuration.file._loader import ConfigurationError
from opentelemetry.configuration.models import (
AttributeLimits,
NameStringValuePair,
)
from opentelemetry.configuration.models import (
BatchLogRecordProcessor as BatchLogRecordProcessorConfig,
)
Expand All @@ -40,9 +45,6 @@
from opentelemetry.configuration.models import (
LogRecordProcessor as LogRecordProcessorConfig,
)
from opentelemetry.configuration.models import (
NameStringValuePair,
)
from opentelemetry.configuration.models import (
OtlpGrpcExporter as OtlpGrpcExporterConfig,
)
Expand Down Expand Up @@ -409,20 +411,34 @@ def test_otlp_grpc_exporter_endpoint(self):


class TestLogRecordLimits(unittest.TestCase):
def test_limits_logs_warning(self):
def test_default_limits(self):
provider = create_logger_provider(None)
self.assertEqual(provider._log_record_limits.max_attributes, 128)
self.assertIsNone(provider._log_record_limits.max_attribute_length)

def test_default_limits_do_not_read_env_vars(self):
with patch.dict(
os.environ,
{
"OTEL_ATTRIBUTE_COUNT_LIMIT": "1",
"OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT": "2",
},
):
provider = create_logger_provider(None)
self.assertEqual(provider._log_record_limits.max_attributes, 128)
self.assertIsNone(provider._log_record_limits.max_attribute_length)

def test_limits_from_config(self):
config = LoggerProviderConfig(
processors=[],
limits=LogRecordLimitsConfig(attribute_count_limit=64),
)
with self.assertLogs(
"opentelemetry.configuration._logger_provider",
level="WARNING",
) as cm:
create_logger_provider(config)
self.assertTrue(
any("limits" in msg for msg in cm.output),
"Expected warning about unsupported limits",
limits=LogRecordLimitsConfig(
attribute_count_limit=64,
attribute_value_length_limit=256,
),
)
provider = create_logger_provider(config)
self.assertEqual(provider._log_record_limits.max_attributes, 64)
self.assertEqual(provider._log_record_limits.max_attribute_length, 256)

@staticmethod
def test_no_limits_no_warning():
Expand All @@ -431,6 +447,31 @@ def test_no_limits_no_warning():
create_logger_provider(config)
mock_logger.warning.assert_not_called()

def test_global_attribute_count_limit_used_when_no_per_signal_limits(self):
global_limits = AttributeLimits(attribute_count_limit=42)
provider = create_logger_provider(None, global_attribute_limits=global_limits)
self.assertEqual(provider._log_record_limits.max_attributes, 42)

def test_global_attribute_value_length_limit_used_when_no_per_signal_limits(
self,
):
global_limits = AttributeLimits(attribute_value_length_limit=64)
provider = create_logger_provider(None, global_attribute_limits=global_limits)
self.assertEqual(provider._log_record_limits.max_attribute_length, 64)

def test_per_signal_limits_override_global(self):
global_limits = AttributeLimits(attribute_count_limit=100, attribute_value_length_limit=200)
config = LoggerProviderConfig(
processors=[],
limits=LogRecordLimitsConfig(
attribute_count_limit=7,
attribute_value_length_limit=16,
),
)
provider = create_logger_provider(config, global_attribute_limits=global_limits)
self.assertEqual(provider._log_record_limits.max_attributes, 7)
self.assertEqual(provider._log_record_limits.max_attribute_length, 16)


if __name__ == "__main__":
unittest.main()
4 changes: 2 additions & 2 deletions opentelemetry-configuration/tests/test_sdk.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,9 @@ def test_calls_each_signal_with_resource(
configure_sdk(config)

mock_create_resource.assert_called_once_with(resource_cfg)
mock_tracer.assert_called_once_with(tracer_cfg, sentinel_resource)
mock_tracer.assert_called_once_with(tracer_cfg, sentinel_resource, None)
mock_meter.assert_called_once_with(None, sentinel_resource)
mock_logger.assert_called_once_with(None, sentinel_resource)
mock_logger.assert_called_once_with(None, sentinel_resource, None)
mock_propagator.assert_called_once_with(propagator_cfg)

@patch("opentelemetry.configuration._sdk.configure_propagator")
Expand Down
Loading
Loading