-
Notifications
You must be signed in to change notification settings - Fork 821
feat(config): add resource and propagator creation from declarative config #4979
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
MikeGoldsmith
wants to merge
18
commits into
open-telemetry:main
Choose a base branch
from
MikeGoldsmith:mike/config-resource-propagator
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+1,529
−9
Open
Changes from all commits
Commits
Show all changes
18 commits
Select commit
Hold shift + click to select a range
5c26f00
config: add resource and propagator creation from declarative config
MikeGoldsmith 8232012
update changelog with PR number
MikeGoldsmith 8329ae4
fix pylint, pyright and ruff errors in resource/propagator config
MikeGoldsmith 506d816
address review feedback: use _DEFAULT_RESOURCE, fix bool_array coercion
MikeGoldsmith 8232d48
fix linter
MikeGoldsmith 6ed3425
Merge branch 'main' of github.com:open-telemetry/opentelemetry-python…
MikeGoldsmith 99753f9
address review feedback: single coercion table, simplify attributes m…
MikeGoldsmith 6b9cad4
add create_logger_provider/configure_logger_provider for declarative …
MikeGoldsmith 1b93f64
add changelog entry for logger provider declarative config (#4990)
MikeGoldsmith f817f97
fix linter errors
MikeGoldsmith 430ae83
use Callable type annotation on _array helper
MikeGoldsmith f36b484
Merge remote-tracking branch 'upstream/main' into mike/config-resourc…
MikeGoldsmith f708be9
replace Dict -> dict
MikeGoldsmith 15431b4
Apply suggestion from @herin049
MikeGoldsmith 7d2379d
Merge branch 'mike/config-resource-propagator' of github.com:mikegold…
MikeGoldsmith e1c16a6
revert Protocol-based typing for _map_compression
MikeGoldsmith b3a3980
set default service.name before building Resource
MikeGoldsmith a7c17a0
Merge branch 'main' into mike/config-resource-propagator
MikeGoldsmith File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
25 changes: 25 additions & 0 deletions
25
opentelemetry-sdk/src/opentelemetry/sdk/_configuration/_exceptions.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| # 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. | ||
|
|
||
|
|
||
| class ConfigurationError(Exception): | ||
| """Raised when configuration loading, parsing, validation, or instantiation fails. | ||
|
|
||
| This includes errors from: | ||
| - File not found or inaccessible | ||
| - Invalid YAML/JSON syntax | ||
| - Schema validation failures | ||
| - Environment variable substitution errors | ||
| - Missing required SDK extensions (e.g., propagator packages not installed) | ||
| """ |
299 changes: 299 additions & 0 deletions
299
opentelemetry-sdk/src/opentelemetry/sdk/_configuration/_logger_provider.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,299 @@ | ||
| # 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._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 _parse_headers( | ||
| headers: Optional[list], | ||
| headers_list: Optional[str], | ||
| ) -> Optional[dict[str, str]]: | ||
| """Merge headers struct and headers_list into a dict. | ||
|
|
||
| Returns None if neither is set, letting the exporter read env vars. | ||
| headers struct takes priority over headers_list for the same key. | ||
| """ | ||
| if headers is None and headers_list is None: | ||
| return None | ||
| result: dict[str, str] = {} | ||
| if headers_list: | ||
| for item in headers_list.split(","): | ||
| item = item.strip() | ||
| if "=" in item: | ||
| key, value = item.split("=", 1) | ||
| result[key.strip()] = value.strip() | ||
| elif item: | ||
| _logger.warning( | ||
| "Invalid header pair in headers_list (missing '='): %s", | ||
| item, | ||
| ) | ||
| if headers: | ||
| for pair in headers: | ||
| result[pair.name] = pair.value or "" | ||
| return result | ||
|
|
||
|
|
||
| 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)) | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the suggestion @herin049.
I tried using
Protocol[T]but ran into a Pyright limitation: we pass the enum class itself (e.g.grpc.Compression) ascompression_enum, not an instance of it.This means:
I reverted back to
compression_enum: typewith# type: ignore[attr-defined].If you know a way to express this that Pyright accepts, happy to revisit — but I couldn't find a clean path.
Thanks