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 @@ -13,6 +13,7 @@
load_entry_point,
)
from opentelemetry.sdk._configuration._exceptions import ConfigurationError
from opentelemetry.sdk._configuration.models import AttributeLimits
from opentelemetry.sdk._configuration.models import (
BatchLogRecordProcessor as BatchLogRecordProcessorConfig,
)
Expand All @@ -25,6 +26,9 @@
from opentelemetry.sdk._configuration.models import (
LogRecordExporter as LogRecordExporterConfig,
)
from opentelemetry.sdk._configuration.models import (
LogRecordLimits as LogRecordLimitsConfig,
)
from opentelemetry.sdk._configuration.models import (
LogRecordProcessor as LogRecordProcessorConfig,
)
Expand All @@ -38,6 +42,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 @@ -48,6 +53,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 @@ -232,9 +239,40 @@ 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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Won't this be handled already by the LogRecordLimits constructor?

),
max_attribute_length=attribute_value_length_limit,
)
Comment on lines +262 to +269


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 @@ -244,21 +282,28 @@ 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:
log_record_limits = _create_log_record_limits(
config.limits, global_attribute_limits
)
else:
log_record_limits = _create_log_record_limits(
LogRecordLimitsConfig(), 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 @@ -270,6 +315,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 @@ -279,7 +325,11 @@ 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)
)
14 changes: 11 additions & 3 deletions opentelemetry-sdk/src/opentelemetry/sdk/_configuration/_sdk.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@
from opentelemetry.sdk._configuration._tracer_provider import (
configure_tracer_provider,
)
from opentelemetry.sdk._configuration.models import OpenTelemetryConfiguration
from opentelemetry.sdk._configuration.models import (
AttributeLimits,
OpenTelemetryConfiguration,
)

_logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -57,8 +60,13 @@ def configure_sdk(config: OpenTelemetryConfiguration) -> None:
)
return

global_attribute_limits: AttributeLimits | None = 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)
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
load_entry_point,
)
from opentelemetry.sdk._configuration._exceptions import ConfigurationError
from opentelemetry.sdk._configuration.models import (
AttributeLimits,
)
from opentelemetry.sdk._configuration.models import (
ExperimentalComposableRuleBasedSampler as RuleBasedSamplerConfig,
)
Expand Down Expand Up @@ -373,16 +376,30 @@ 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
attribute_count_limit
if attribute_count_limit is not None
else _DEFAULT_OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT
),
max_events=(
Expand All @@ -405,13 +422,14 @@ 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,
)


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 @@ -422,6 +440,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 @@ -431,17 +451,14 @@ def create_tracer_provider(
if config is not None and config.sampler is not None
else _DEFAULT_SAMPLER
)
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:
span_limits = _create_span_limits(
config.limits, global_attribute_limits
)
else:
span_limits = _create_span_limits(
SpanLimitsConfig(), global_attribute_limits
)
)

provider = TracerProvider(
resource=resource,
Expand All @@ -459,6 +476,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 @@ -469,7 +487,11 @@ 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)
)
Original file line number Diff line number Diff line change
Expand Up @@ -295,11 +295,13 @@ 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,
**({} if limits is None else {"limits": limits}),
)


Expand Down Expand Up @@ -671,6 +673,7 @@ def __init__(
*,
logger_metrics: LoggerMetricsT,
_logger_config: _LoggerConfig,
log_record_limits: LogRecordLimits | None = None,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Likewise here.

):
super().__init__(
instrumentation_scope.name,
Expand All @@ -683,6 +686,7 @@ def __init__(
self._instrumentation_scope = instrumentation_scope
self._logger_metrics = logger_metrics
self._logger_config = _logger_config
self._log_record_limits = log_record_limits or LogRecordLimits()

def _is_enabled(self) -> bool:
return self._logger_config.is_enabled
Expand Down Expand Up @@ -728,6 +732,7 @@ def emit(
record=record,
resource=self._resource,
instrumentation_scope=self._instrumentation_scope,
limits=self._log_record_limits,
)
else:
_set_log_record_exception_attributes(record.log_record)
Expand All @@ -750,6 +755,7 @@ def emit(
record=log_record,
resource=self._resource,
instrumentation_scope=self._instrumentation_scope,
limits=self._log_record_limits,
)

self._logger_metrics.emit_log()
Expand Down Expand Up @@ -783,6 +789,7 @@ def __init__(
*,
meter_provider: MeterProvider | None = None,
_logger_configurator: _LoggerConfiguratorT | None = None,
log_record_limits: LogRecordLimits | None = None,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Small nit, but I'd prefer moving public keyword arguments before private ones for readability.

):
if resource is None:
self._resource = Resource.create({})
Expand All @@ -802,6 +809,7 @@ def __init__(
self._logger_configurator = (
_logger_configurator or _default_logger_configurator
)
self._log_record_limits = log_record_limits or LogRecordLimits()
self._at_exit_handler = None
if shutdown_on_exit:
self._at_exit_handler = atexit.register(self.shutdown)
Expand Down Expand Up @@ -829,6 +837,7 @@ def _get_logger_no_cache(
scope,
logger_metrics=self._logger_metrics,
_logger_config=self._apply_logger_configurator(scope),
log_record_limits=self._log_record_limits,
)

def _get_logger_cached(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,7 @@ def __init__(
out: typing.IO = sys.stdout,
formatter: collections.abc.Callable[
[ReadableSpan], str
] = lambda span: (span.to_json() + linesep),
] = lambda span: span.to_json() + linesep,
):
self.out = out
self.formatter = formatter
Expand Down
Loading
Loading