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,44 @@ 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
Comment thread
ocelotl marked this conversation as resolved.
),
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 @@ -244,21 +286,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 +319,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 +329,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,23 @@ 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 @@ -422,6 +449,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 +460,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 +485,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 +496,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 @@ -112,7 +112,7 @@ class LogRecordLimits:
This class does not enforce any limits itself. It only provides a way to read limits from env,
default values and from user provided arguments.

All limit arguments must be either a non-negative integer or ``None``.
All limit arguments must be either a non-negative integer, ``None`` or ``LogRecordLimits.UNSET``.

- All limit arguments are optional.
- If a limit argument is not set, the class will try to read its value from the corresponding
Expand All @@ -134,6 +134,8 @@ class LogRecordLimits:
the specified length will be truncated.
"""

UNSET = -1

def __init__(
self,
max_attributes: int | None = None,
Expand Down Expand Up @@ -162,6 +164,9 @@ def __repr__(self):
def _from_env_if_absent(
cls, value: int | None, env_var: str, default: int | None = None
) -> int | None:
if value == cls.UNSET:
return None

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

# if no value is provided for the limit, try to load it from env
Expand Down Expand Up @@ -295,11 +300,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 @@ -670,6 +677,7 @@ def __init__(
instrumentation_scope: InstrumentationScope,
*,
logger_metrics: LoggerMetricsT,
log_record_limits: LogRecordLimits | None = None,
Comment thread
ocelotl marked this conversation as resolved.
_logger_config: _LoggerConfig,
):
super().__init__(
Expand All @@ -683,6 +691,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 +737,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 +760,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 @@ -782,6 +793,7 @@ def __init__(
| None = None,
*,
meter_provider: MeterProvider | None = None,
log_record_limits: LogRecordLimits | None = None,
Comment thread
ocelotl marked this conversation as resolved.
_logger_configurator: _LoggerConfiguratorT | None = None,
):
if resource is None:
Expand All @@ -802,6 +814,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 +842,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