Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
5c26f00
config: add resource and propagator creation from declarative config
MikeGoldsmith Mar 13, 2026
8232012
update changelog with PR number
MikeGoldsmith Mar 13, 2026
8329ae4
fix pylint, pyright and ruff errors in resource/propagator config
MikeGoldsmith Mar 13, 2026
506d816
address review feedback: use _DEFAULT_RESOURCE, fix bool_array coercion
MikeGoldsmith Mar 16, 2026
8232d48
fix linter
MikeGoldsmith Mar 16, 2026
6ed3425
Merge branch 'main' of github.com:open-telemetry/opentelemetry-python…
MikeGoldsmith Mar 16, 2026
99753f9
address review feedback: single coercion table, simplify attributes m…
MikeGoldsmith Mar 16, 2026
8ba91d8
use Callable type annotation on _array helper
MikeGoldsmith Mar 17, 2026
516aecc
Merge remote-tracking branch 'upstream/main' into mike/config-resourc…
MikeGoldsmith Mar 20, 2026
9cfdcce
add detection infrastructure foundations for resource detectors
MikeGoldsmith Mar 20, 2026
103ff08
move service.name default into base resource
MikeGoldsmith Mar 20, 2026
7f51034
remove unused logging import from _propagator.py
MikeGoldsmith Mar 20, 2026
c98f016
add create_logger_provider/configure_logger_provider for declarative …
MikeGoldsmith Mar 17, 2026
da00c92
add changelog entry for logger provider declarative config (#4990)
MikeGoldsmith Mar 17, 2026
31933ef
fix linter errors
MikeGoldsmith Mar 17, 2026
d7b4616
merge upstream/main and use shared _parse_headers from _common
MikeGoldsmith Apr 1, 2026
14ba572
address review feedback: simplify resource filter and propagator loading
MikeGoldsmith Apr 1, 2026
14b1c9b
fix ruff formatting
MikeGoldsmith Apr 1, 2026
c0906d4
fix pyright: wrap EntryPoints in iter() for next() compatibility
MikeGoldsmith Apr 1, 2026
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.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## Unreleased

- `opentelemetry-sdk`: Add `create_logger_provider`/`configure_logger_provider` to declarative file configuration, enabling LoggerProvider instantiation from config files without reading env vars
([#4990](https://github.com/open-telemetry/opentelemetry-python/pull/4990))
- `opentelemetry-sdk`: Add shared `_parse_headers` helper for declarative config OTLP exporters
([#5021](https://github.com/open-telemetry/opentelemetry-python/pull/5021))
- `opentelemetry-api`: Replace a broad exception in attribute cleaning tests to satisfy pylint in the `lint-opentelemetry-api` CI job
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,271 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from __future__ import annotations

import logging
from typing import Optional

from opentelemetry._logs import set_logger_provider
from opentelemetry.sdk._configuration._common import _parse_headers
from opentelemetry.sdk._configuration._exceptions import ConfigurationError
from opentelemetry.sdk._configuration.models import (
BatchLogRecordProcessor as BatchLogRecordProcessorConfig,
)
from opentelemetry.sdk._configuration.models import (
LoggerProvider as LoggerProviderConfig,
)
from opentelemetry.sdk._configuration.models import (
LogRecordExporter as LogRecordExporterConfig,
)
from opentelemetry.sdk._configuration.models import (
LogRecordProcessor as LogRecordProcessorConfig,
)
from opentelemetry.sdk._configuration.models import (
OtlpGrpcExporter as OtlpGrpcExporterConfig,
)
from opentelemetry.sdk._configuration.models import (
OtlpHttpExporter as OtlpHttpExporterConfig,
)
from opentelemetry.sdk._configuration.models import (
SimpleLogRecordProcessor as SimpleLogRecordProcessorConfig,
)
from opentelemetry.sdk._logs import LoggerProvider
from opentelemetry.sdk._logs._internal.export import (
BatchLogRecordProcessor,
ConsoleLogRecordExporter,
LogRecordExporter,
SimpleLogRecordProcessor,
)
from opentelemetry.sdk.resources import Resource

_logger = logging.getLogger(__name__)

# BatchLogRecordProcessor defaults per OTel spec (milliseconds).
# Note: The Python SDK reads OTEL_BLRP_SCHEDULE_DELAY with an incorrect default
# of 5000ms. The spec mandates 1000ms. We pass 1000 explicitly here to match
# the spec and suppress env var reading.
_DEFAULT_SCHEDULE_DELAY_MILLIS = 1000
_DEFAULT_EXPORT_TIMEOUT_MILLIS = 30000
_DEFAULT_MAX_QUEUE_SIZE = 2048
_DEFAULT_MAX_EXPORT_BATCH_SIZE = 512


def _map_compression(
value: Optional[str], compression_enum: type
) -> Optional[object]:
"""Map a compression string to the given Compression enum value."""
if value is None or value.lower() == "none":
return None
if value.lower() == "gzip":
return compression_enum.Gzip # type: ignore[attr-defined]
raise ConfigurationError(
f"Unsupported compression value '{value}'. Supported values: 'gzip', 'none'."
)


def _create_console_log_exporter() -> ConsoleLogRecordExporter:
"""Create a ConsoleLogRecordExporter."""
return ConsoleLogRecordExporter()


def _create_otlp_http_log_exporter(
config: OtlpHttpExporterConfig,
) -> LogRecordExporter:
"""Create an OTLP HTTP log exporter from config."""
try:
# pylint: disable=import-outside-toplevel,no-name-in-module
from opentelemetry.exporter.otlp.proto.http import ( # type: ignore[import-untyped] # noqa: PLC0415
Compression,
)
from opentelemetry.exporter.otlp.proto.http._log_exporter import ( # type: ignore[import-untyped] # noqa: PLC0415
OTLPLogExporter,
)
except ImportError as exc:
raise ConfigurationError(
"otlp_http log exporter requires 'opentelemetry-exporter-otlp-proto-http'. "
"Install it with: pip install opentelemetry-exporter-otlp-proto-http"
) from exc

compression = _map_compression(config.compression, Compression)
headers = _parse_headers(config.headers, config.headers_list)
timeout = (config.timeout / 1000.0) if config.timeout is not None else None

return OTLPLogExporter( # type: ignore[return-value]
endpoint=config.endpoint,
headers=headers,
timeout=timeout,
compression=compression, # type: ignore[arg-type]
)


def _create_otlp_grpc_log_exporter(
config: OtlpGrpcExporterConfig,
) -> LogRecordExporter:
"""Create an OTLP gRPC log exporter from config."""
try:
# pylint: disable=import-outside-toplevel,no-name-in-module
import grpc # type: ignore[import-untyped] # noqa: PLC0415

from opentelemetry.exporter.otlp.proto.grpc._log_exporter import ( # type: ignore[import-untyped] # noqa: PLC0415
OTLPLogExporter,
)
except ImportError as exc:
raise ConfigurationError(
"otlp_grpc log exporter requires 'opentelemetry-exporter-otlp-proto-grpc'. "
"Install it with: pip install opentelemetry-exporter-otlp-proto-grpc"
) from exc

compression = _map_compression(config.compression, grpc.Compression)
headers = _parse_headers(config.headers, config.headers_list)
timeout = (config.timeout / 1000.0) if config.timeout is not None else None

return OTLPLogExporter( # type: ignore[return-value]
endpoint=config.endpoint,
headers=headers,
timeout=timeout,
compression=compression, # type: ignore[arg-type]
)


def _create_log_record_exporter(
config: LogRecordExporterConfig,
) -> LogRecordExporter:
"""Create a log record exporter from config."""
if config.console is not None:
return _create_console_log_exporter()
if config.otlp_http is not None:
return _create_otlp_http_log_exporter(config.otlp_http)
if config.otlp_grpc is not None:
return _create_otlp_grpc_log_exporter(config.otlp_grpc)
if config.otlp_file_development is not None:
raise ConfigurationError(
"otlp_file_development log exporter is experimental and not yet supported."
)
raise ConfigurationError(
"No exporter type specified in log record exporter config. "
"Supported types: console, otlp_http, otlp_grpc."
)


def _create_batch_log_record_processor(
config: BatchLogRecordProcessorConfig,
) -> BatchLogRecordProcessor:
"""Create a BatchLogRecordProcessor from config.

Passes explicit defaults to suppress OTEL_BLRP_* env var reading.
"""
exporter = _create_log_record_exporter(config.exporter)
schedule_delay = (
config.schedule_delay
if config.schedule_delay is not None
else _DEFAULT_SCHEDULE_DELAY_MILLIS
)
export_timeout = (
config.export_timeout
if config.export_timeout is not None
else _DEFAULT_EXPORT_TIMEOUT_MILLIS
)
max_queue_size = (
config.max_queue_size
if config.max_queue_size is not None
else _DEFAULT_MAX_QUEUE_SIZE
)
max_export_batch_size = (
config.max_export_batch_size
if config.max_export_batch_size is not None
else _DEFAULT_MAX_EXPORT_BATCH_SIZE
)
return BatchLogRecordProcessor(
exporter=exporter,
schedule_delay_millis=float(schedule_delay),
export_timeout_millis=float(export_timeout),
max_queue_size=max_queue_size,
max_export_batch_size=max_export_batch_size,
)


def _create_simple_log_record_processor(
config: SimpleLogRecordProcessorConfig,
) -> SimpleLogRecordProcessor:
"""Create a SimpleLogRecordProcessor from config."""
exporter = _create_log_record_exporter(config.exporter)
return SimpleLogRecordProcessor(exporter)


def _create_log_record_processor(
config: LogRecordProcessorConfig,
) -> BatchLogRecordProcessor | SimpleLogRecordProcessor:
"""Create a log record processor from config."""
if config.batch is not None:
return _create_batch_log_record_processor(config.batch)
if config.simple is not None:
return _create_simple_log_record_processor(config.simple)
raise ConfigurationError(
"No processor type specified in log record processor config. "
"Supported types: batch, simple."
)


def create_logger_provider(
config: Optional[LoggerProviderConfig],
resource: Optional[Resource] = None,
) -> LoggerProvider:
"""Create an SDK LoggerProvider from declarative config.

Does NOT read OTEL_BLRP_* or other env vars for values explicitly
controlled by the config. Absent config values use OTel spec defaults.

Args:
config: LoggerProvider config from the parsed config file, or None.
resource: Resource to attach to the provider.

Returns:
A configured LoggerProvider.
"""
provider = LoggerProvider(resource=resource)

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)
)

return provider


def configure_logger_provider(
config: Optional[LoggerProviderConfig],
resource: Optional[Resource] = None,
) -> None:
"""Configure the global LoggerProvider from declarative config.

When config is None (logger_provider section absent from config file),
the global is not set — matching Java/JS SDK behavior.

Args:
config: LoggerProvider config from the parsed config file, or None.
resource: Resource to attach to the provider.
"""
if config is None:
return
set_logger_provider(create_logger_provider(config, resource))
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,16 @@
def _load_entry_point_propagator(name: str) -> TextMapPropagator:
"""Load a propagator by name from the opentelemetry_propagator entry point group."""
try:
eps = list(entry_points(group="opentelemetry_propagator", name=name))
if not eps:
ep = next(
iter(entry_points(group="opentelemetry_propagator", name=name)),
None,
)
if not ep:
raise ConfigurationError(
f"Propagator '{name}' not found. "
"It may not be installed or may be misspelled."
)
return eps[0].load()()
return ep.load()()
except ConfigurationError:
raise
except Exception as exc:
Expand Down Expand Up @@ -85,29 +88,24 @@ def create_propagator(
if config is None:
return CompositePropagator([])

propagators: list[TextMapPropagator] = []
seen_types: set[type] = set()

def _add_deduped(propagator: TextMapPropagator) -> None:
if type(propagator) not in seen_types:
seen_types.add(type(propagator))
propagators.append(propagator)
propagators: dict[type[TextMapPropagator], TextMapPropagator] = {}

# Process structured composite list
if config.composite:
for entry in config.composite:
for propagator in _propagators_from_textmap_config(entry):
_add_deduped(propagator)
propagators.setdefault(type(propagator), propagator)

# Process composite_list (comma-separated propagator names via entry_points)
if config.composite_list:
for name in config.composite_list.split(","):
name = name.strip()
if not name or name.lower() == "none":
continue
_add_deduped(_load_entry_point_propagator(name))
propagator = _load_entry_point_propagator(name)
propagators.setdefault(type(propagator), propagator)

return CompositePropagator(propagators)
return CompositePropagator(list(propagators.values()))


def configure_propagator(config: Optional[PropagatorConfig]) -> None:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,13 +170,9 @@ def _filter_attributes(
if not included and not excluded:
return attrs

effective_included = included if included else None # [] → include all

result: dict[str, object] = {}
for key, value in attrs.items():
if effective_included is not None and not any(
fnmatch.fnmatch(key, pat) for pat in effective_included
):
if included and not any(fnmatch.fnmatch(key, pat) for pat in included):
continue
if excluded and any(fnmatch.fnmatch(key, pat) for pat in excluded):
continue
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@
"""

from opentelemetry.sdk._configuration._exceptions import ConfigurationError
from opentelemetry.sdk._configuration._logger_provider import (
configure_logger_provider,
create_logger_provider,
)
from opentelemetry.sdk._configuration._propagator import (
configure_propagator,
create_propagator,
Expand All @@ -44,4 +48,6 @@
"create_resource",
"create_propagator",
"configure_propagator",
"create_logger_provider",
"configure_logger_provider",
]
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ def _get_schema() -> dict:
_logger = logging.getLogger(__name__)


# Re-export for backwards compatibility
__all__ = ["ConfigurationError", "load_config_file"]


def load_config_file(file_path: str) -> OpenTelemetryConfiguration:
"""Load and parse an OpenTelemetry configuration file.

Expand Down
Loading
Loading