From 4e5fe40e6a16c3acabd732357bd53821bcc136e8 Mon Sep 17 00:00:00 2001 From: silanhe Date: Wed, 5 Aug 2026 20:57:40 +0000 Subject: [PATCH 01/10] feat(otel): InvocationOtelPlugin uses config Replace InvocationOtelPlugin's legacy positional kwargs with a single OtelPluginConfig, mirroring ExecutionOtelPlugin and the JS SDK plugins. This adds use_default_tracer_provider, enable_http_instrumentation, exporter_config, and propagators to the Invocation plugin. Also align the default provider with JS: when no provider is supplied the plugin auto-configures an OTLP provider (default_use_global=False) instead of the global/ADOT provider. Pass use_default_tracer_provider =True for the global (e.g. ADOT) provider. Migrated test call sites and README snippets to OtelPluginConfig. 105/105 otel tests pass; ruff and mypy clean. BREAKING CHANGE: InvocationOtelPlugin no longer accepts positional or keyword arguments; pass an OtelPluginConfig instead, e.g. InvocationOtelPlugin(OtelPluginConfig(tracer_provider=...)). --- .../README.md | 44 ++++++---- .../invocation_plugin.py | 87 +++++++++++++------ .../tests/test_invocation_plugin.py | 15 ++-- .../test_invocation_plugin_integration.py | 32 ++++--- .../tests/test_log_filter.py | 9 +- 5 files changed, 123 insertions(+), 64 deletions(-) diff --git a/packages/aws-durable-execution-sdk-python-otel/README.md b/packages/aws-durable-execution-sdk-python-otel/README.md index a6de879d..62a687ad 100644 --- a/packages/aws-durable-execution-sdk-python-otel/README.md +++ b/packages/aws-durable-execution-sdk-python-otel/README.md @@ -168,21 +168,25 @@ See the [ADOT sampling configuration](https://aws-otel.github.io/docs/getting-st ```python from aws_durable_execution_sdk_python_otel import ( InvocationOtelPlugin, + OtelPluginConfig, xray_context_extractor, ) plugin = InvocationOtelPlugin( - # Provide your own TracerProvider if you already have one configured. - # Defaults to the globally configured tracer provider. - trace_provider=None, - # Use a custom context extractor (default: xray_context_extractor). - context_extractor=xray_context_extractor, - # Custom instrumentation scope name - # (default: "aws-durable-execution-sdk-python"). - instrument_name="my-service", - # Install a root-logger filter that stamps trace context onto every - # log record (default: True). - enrich_logger=True, + OtelPluginConfig( + # Provide your own TracerProvider if you already have one configured. + # When omitted, an OTLP provider is auto-configured (like ExecutionOtelPlugin); + # set use_default_tracer_provider=True to use the global (e.g. ADOT) provider. + tracer_provider=None, + # Use a custom context extractor (default: xray_context_extractor). + context_extractor=xray_context_extractor, + # Custom instrumentation scope name + # (default: "aws-durable-execution-sdk-python"). + instrument_name="my-service", + # Install a root-logger filter that stamps trace context onto every + # log record (default: True). + enrich_logger=True, + ) ) ``` @@ -193,15 +197,16 @@ The plugin supports multiple strategies for extracting upstream trace context: ```python from aws_durable_execution_sdk_python_otel import ( InvocationOtelPlugin, + OtelPluginConfig, w3c_client_context_extractor, xray_context_extractor, ) # Default: X-Ray trace header (recommended for most Lambda deployments) -InvocationOtelPlugin(context_extractor=xray_context_extractor) +InvocationOtelPlugin(OtelPluginConfig(context_extractor=xray_context_extractor)) # W3C Trace Context via clientContext (requires backend propagation support) -InvocationOtelPlugin(context_extractor=w3c_client_context_extractor) +InvocationOtelPlugin(OtelPluginConfig(context_extractor=w3c_client_context_extractor)) ``` ### Log Correlation @@ -251,10 +256,15 @@ The main plugin class. Implements `DurableInstrumentationPlugin` from `aws_durab ```python InvocationOtelPlugin( - trace_provider=None, - context_extractor=None, - instrument_name="aws-durable-execution-sdk-python", - enrich_logger=True, + OtelPluginConfig( + tracer_provider=None, + context_extractor=None, + instrument_name="aws-durable-execution-sdk-python", + enrich_logger=True, + workflow_span_name="Workflow", + # ...and the rest of OtelPluginConfig (use_default_tracer_provider, + # enable_http_instrumentation, exporter_config, propagators). + ) ) ``` diff --git a/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/invocation_plugin.py b/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/invocation_plugin.py index 4f2744d5..1569088d 100644 --- a/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/invocation_plugin.py +++ b/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/invocation_plugin.py @@ -45,7 +45,11 @@ ) from aws_durable_execution_sdk_python_otel.log_filter import install_log_filter from aws_durable_execution_sdk_python_otel.otel_plugin_config import ( - DEFAULT_WORKFLOW_SPAN_NAME, + OtelPluginConfig, +) +from aws_durable_execution_sdk_python_otel.provider import create_tracer_provider +from aws_durable_execution_sdk_python_otel.instrumentations import ( + register_standalone_instrumentations, ) @@ -80,42 +84,60 @@ class InvocationOtelPlugin(DurableInstrumentationPlugin): original logical operation. Args: - trace_provider: OpenTelemetry tracer provider used to create spans. - Optional; when omitted, the globally configured tracer provider - (``opentelemetry.trace.get_tracer_provider()``) is used. - context_extractor: Optional extractor for upstream context. Defaults to - AWS X-Ray header extraction. - instrument_name: Instrumentation scope name registered with the tracer. + config: Shared plugin configuration (the same OtelPluginConfig accepted + by ExecutionOtelPlugin). When omitted, defaults are used (X-Ray + extractor, "Workflow" span name, log enrichment on). When no provider + is configured, an OTLP provider is auto-configured (matching + ExecutionOtelPlugin and the JS SDK plugins); set + ``use_default_tracer_provider=True`` on the config to use the globally + configured tracer provider instead (e.g. the ADOT Lambda layer). """ DEFAULT_INSTRUMENT_NAME = "aws-durable-execution-sdk-python" - def __init__( - self, - trace_provider: SdkTracerProvider | None = None, - context_extractor: ContextExtractor | None = None, - instrument_name: str = DEFAULT_INSTRUMENT_NAME, - enrich_logger: bool = True, - workflow_span_name: str = DEFAULT_WORKFLOW_SPAN_NAME, - ) -> None: - """Initialize the plugin with an OpenTelemetry tracer provider. + def __init__(self, config: OtelPluginConfig | None = None) -> None: + """Initialize the plugin from a shared OtelPluginConfig. + + Accepts the same OtelPluginConfig as ExecutionOtelPlugin so both plugins + share one configuration surface (context extractor, instrumentation + name, provider selection, exporter/propagator settings, log + enrichment). Like ExecutionOtelPlugin and the JS SDK plugins, it + auto-configures an OTLP provider when nothing is supplied; pass + ``use_default_tracer_provider=True`` to use the globally configured + (e.g. ADOT) provider instead. The tracer provider is configured with this plugin's deterministic ID generator so spans for a durable execution share stable trace and - logical operation identifiers. When no provider is supplied, the - globally configured tracer provider is used. + logical operation identifiers. - When enrich_logger is enabled (default), the plugin installs a logging - filter on the root logger at invocation start that stamps the active - OTel trace context onto every emitted log record. + When ``enrich_logger`` is enabled (default), the plugin installs a + logging filter that stamps the active OTel trace context onto every + emitted log record. """ - self._enrich_logger = enrich_logger - self._workflow_span_name = workflow_span_name + self._config = config or OtelPluginConfig() self._context_extractor: ContextExtractor = ( - context_extractor or xray_context_extractor + self._config.context_extractor or xray_context_extractor ) + self._workflow_span_name = self._config.workflow_span_name + self._enrich_logger = self._config.enrich_logger + + # Like ExecutionOtelPlugin (and the JS SDK plugins), InvocationOtelPlugin + # auto-configures an OTLP provider when nothing is supplied. Resolve the + # effective "use the global provider" decision up front so that + # instrumentation registration matches what create_tracer_provider does. + self._use_default = self._config.use_default_tracer_provider + if self._use_default is None: + self._use_default = False + + self._id_generator = DeterministicIdGenerator() + result = create_tracer_provider( + self._config, + id_generator=self._id_generator, + default_use_global=False, + ) + self._provider = result.tracer_provider + self._owns_provider = result.owns_provider - self._provider = trace_provider or trace.get_tracer_provider() # Deterministic trace stitching requires the SDK TracerProvider, which # exposes id_generator/sampler. The API's default ProxyTracerProvider # (returned before an SDK provider is configured) does not. Rather than @@ -128,16 +150,25 @@ def __init__( self._provider ) else: - self._id_generator = DeterministicIdGenerator() logger.warning( "InvocationOtelPlugin expected an SDK TracerProvider " "(opentelemetry.sdk.trace.TracerProvider) but got %s. Spans will " "not use deterministic IDs. " "Ensure the OpenTelemetry SDK is configured (e.g. via the ADOT " - "Lambda layer) or pass an explicit trace_provider.", + "Lambda layer) or pass an explicit tracer_provider.", type(self._provider).__name__, ) - self._tracer: Tracer = self._provider.get_tracer(instrument_name) + self._tracer: Tracer = self._provider.get_tracer(self._config.instrument_name) + + try: + register_standalone_instrumentations( + self._config, + self._provider if self._owns_provider else None, + owns_provider=self._owns_provider, + use_default_tracer_provider=self._use_default, + ) + except Exception: + logger.exception("Failed to register standalone instrumentations") # per invocation status: self._execution_arn = "" diff --git a/packages/aws-durable-execution-sdk-python-otel/tests/test_invocation_plugin.py b/packages/aws-durable-execution-sdk-python-otel/tests/test_invocation_plugin.py index c24f091e..2e1b5a6b 100644 --- a/packages/aws-durable-execution-sdk-python-otel/tests/test_invocation_plugin.py +++ b/packages/aws-durable-execution-sdk-python-otel/tests/test_invocation_plugin.py @@ -36,6 +36,7 @@ operation_id_to_span_id, ) from aws_durable_execution_sdk_python_otel.invocation_plugin import InvocationOtelPlugin +from aws_durable_execution_sdk_python_otel.otel_plugin_config import OtelPluginConfig START_TIME = datetime(2024, 1, 2, 3, 4, 5, tzinfo=UTC) @@ -63,8 +64,10 @@ def _create_plugin() -> tuple[InvocationOtelPlugin, InMemorySpanExporter]: trace_provider = TracerProvider() trace_provider.add_span_processor(SimpleSpanProcessor(exporter)) plugin = InvocationOtelPlugin( - trace_provider=trace_provider, - context_extractor=lambda _: Context(), + OtelPluginConfig( + tracer_provider=trace_provider, + context_extractor=lambda _: Context(), + ) ) return plugin, exporter @@ -1083,9 +1086,11 @@ def test_workflow_span_name_is_configurable(): trace_provider = TracerProvider() trace_provider.add_span_processor(SimpleSpanProcessor(exporter)) plugin = InvocationOtelPlugin( - trace_provider=trace_provider, - context_extractor=lambda _: Context(), - workflow_span_name="MyExecution", + OtelPluginConfig( + tracer_provider=trace_provider, + context_extractor=lambda _: Context(), + workflow_span_name="MyExecution", + ) ) plugin.on_invocation_start(_invocation_start_info()) plugin.on_invocation_end(_invocation_end_info()) diff --git a/packages/aws-durable-execution-sdk-python-otel/tests/test_invocation_plugin_integration.py b/packages/aws-durable-execution-sdk-python-otel/tests/test_invocation_plugin_integration.py index 7b7d5c87..64aa1437 100644 --- a/packages/aws-durable-execution-sdk-python-otel/tests/test_invocation_plugin_integration.py +++ b/packages/aws-durable-execution-sdk-python-otel/tests/test_invocation_plugin_integration.py @@ -45,6 +45,7 @@ operation_id_to_span_id, ) from aws_durable_execution_sdk_python_otel.invocation_plugin import InvocationOtelPlugin +from aws_durable_execution_sdk_python_otel.otel_plugin_config import OtelPluginConfig START_TIME = datetime(2024, 1, 2, 3, 4, 5, tzinfo=UTC) @@ -188,9 +189,11 @@ def _assert_hierarchy(exporter: InMemorySpanExporter) -> None: def test_community_layer_full_lifecycle_uses_supplied_provider(): provider, exporter = _provider() plugin = InvocationOtelPlugin( - trace_provider=provider, - context_extractor=lambda _: Context(), - enrich_logger=False, + OtelPluginConfig( + tracer_provider=provider, + context_extractor=lambda _: Context(), + enrich_logger=False, + ) ) plugin.on_invocation_start(_invocation_start()) @@ -209,8 +212,11 @@ def test_adot_layer_full_lifecycle_uses_global_provider(monkeypatch): monkeypatch.setattr(trace, "get_tracer_provider", lambda: provider) plugin = InvocationOtelPlugin( - context_extractor=lambda _: Context(), - enrich_logger=False, + OtelPluginConfig( + use_default_tracer_provider=True, + context_extractor=lambda _: Context(), + enrich_logger=False, + ) ) plugin.on_invocation_start(_invocation_start()) @@ -225,14 +231,18 @@ def test_second_plugin_configures_cached_tracer_generator(monkeypatch): """A second handler's Workflow span uses its deterministic trace ID.""" provider, exporter = _provider() first_plugin = InvocationOtelPlugin( - trace_provider=provider, - context_extractor=lambda _: Context(), - enrich_logger=False, + OtelPluginConfig( + tracer_provider=provider, + context_extractor=lambda _: Context(), + enrich_logger=False, + ) ) target_plugin = InvocationOtelPlugin( - trace_provider=provider, - context_extractor=lambda _: Context(), - enrich_logger=False, + OtelPluginConfig( + tracer_provider=provider, + context_extractor=lambda _: Context(), + enrich_logger=False, + ) ) monkeypatch.setenv("_X_AMZN_TRACE_ID", XRAY_TRACE_HEADER) diff --git a/packages/aws-durable-execution-sdk-python-otel/tests/test_log_filter.py b/packages/aws-durable-execution-sdk-python-otel/tests/test_log_filter.py index 39933338..29645b85 100644 --- a/packages/aws-durable-execution-sdk-python-otel/tests/test_log_filter.py +++ b/packages/aws-durable-execution-sdk-python-otel/tests/test_log_filter.py @@ -23,6 +23,7 @@ install_log_filter, ) from aws_durable_execution_sdk_python_otel.invocation_plugin import InvocationOtelPlugin +from aws_durable_execution_sdk_python_otel.otel_plugin_config import OtelPluginConfig START_TIME = datetime(2024, 1, 2, 3, 4, 5, tzinfo=UTC) @@ -37,9 +38,11 @@ def _create_plugin( trace_provider = TracerProvider() trace_provider.add_span_processor(SimpleSpanProcessor(exporter)) plugin = InvocationOtelPlugin( - trace_provider=trace_provider, - context_extractor=lambda _: Context(), - enrich_logger=enrich_logger, + OtelPluginConfig( + tracer_provider=trace_provider, + context_extractor=lambda _: Context(), + enrich_logger=enrich_logger, + ) ) return plugin, exporter From 1b0e6e03d8329de68396319b09521be2a72729b0 Mon Sep 17 00:00:00 2001 From: silanhe Date: Wed, 5 Aug 2026 21:09:06 +0000 Subject: [PATCH 02/10] fix(otel): example handlers use global provider The two OTel example handlers used InvocationOtelPlugin() with no config. Now that the plugin defaults to auto-configuring an OTLP provider (localhost:4318), the example tests time out when no collector is running. Pass OtelPluginConfig(use_default_tracer_provider=True) so the examples use the globally configured provider (a no-op proxy under test), matching the conformance handler and the prior no-arg behavior. --- .../src/otel/otel_logger_example.py | 9 +++++++-- .../src/plugin/execution_with_otel.py | 9 +++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/packages/aws-durable-execution-sdk-python-examples/src/otel/otel_logger_example.py b/packages/aws-durable-execution-sdk-python-examples/src/otel/otel_logger_example.py index a727bf73..44dccb42 100644 --- a/packages/aws-durable-execution-sdk-python-examples/src/otel/otel_logger_example.py +++ b/packages/aws-durable-execution-sdk-python-examples/src/otel/otel_logger_example.py @@ -16,7 +16,10 @@ from typing import Any -from aws_durable_execution_sdk_python_otel import InvocationOtelPlugin +from aws_durable_execution_sdk_python_otel import ( + InvocationOtelPlugin, + OtelPluginConfig, +) from aws_durable_execution_sdk_python import StepContext from aws_durable_execution_sdk_python.context import ( @@ -44,7 +47,9 @@ def greet_in_child(child_context: DurableContext, name: str) -> str: return result -@durable_execution(plugins=[InvocationOtelPlugin()]) +@durable_execution( + plugins=[InvocationOtelPlugin(OtelPluginConfig(use_default_tracer_provider=True))] +) def handler(_event: Any, context: DurableContext) -> str: # Logged at the top level: enriched with the invocation span_id. context.logger.info("Workflow started") diff --git a/packages/aws-durable-execution-sdk-python-examples/src/plugin/execution_with_otel.py b/packages/aws-durable-execution-sdk-python-examples/src/plugin/execution_with_otel.py index 3d001d46..9044db7e 100644 --- a/packages/aws-durable-execution-sdk-python-examples/src/plugin/execution_with_otel.py +++ b/packages/aws-durable-execution-sdk-python-examples/src/plugin/execution_with_otel.py @@ -2,7 +2,10 @@ from typing import Any -from aws_durable_execution_sdk_python_otel import InvocationOtelPlugin +from aws_durable_execution_sdk_python_otel import ( + InvocationOtelPlugin, + OtelPluginConfig, +) from aws_durable_execution_sdk_python import StepContext from aws_durable_execution_sdk_python.config import Duration @@ -32,7 +35,9 @@ def add_numbers_in_child(child_context: DurableContext, a: int, b: int): return result -@durable_execution(plugins=[InvocationOtelPlugin()]) +@durable_execution( + plugins=[InvocationOtelPlugin(OtelPluginConfig(use_default_tracer_provider=True))] +) def handler(_event: Any, context: DurableContext) -> int: result = 0 for i in range(3): From b70c4f9eac9b75266bd82d1724d3316bc512793a Mon Sep 17 00:00:00 2001 From: silanhe Date: Wed, 5 Aug 2026 22:04:02 +0000 Subject: [PATCH 03/10] refactor(otel): resolve provider source via enum Simplify the tracer-provider selection by resolving it once into an explicit ProviderSource (EXPLICIT / GLOBAL / AUTO_OTLP) instead of recomputing booleans in two places. - Drop the default_use_global parameter from create_tracer_provider; both plugins now share the same auto-OTLP default, so the seam is dead weight. - Make OtelPluginConfig.use_default_tracer_provider a plain bool (default False) instead of a tri-state Optional[bool]. - ProviderResult carries source; owns_provider is a derived property. - register_standalone_instrumentations switches on result.source rather than (config.tracer_provider, use_default_tracer_provider). Behavior is unchanged. 105/105 otel tests pass; ruff clean; mypy clean (pre-existing optional-instrumentation import warnings only). --- .../__init__.py | 2 + .../execution_plugin.py | 14 ++--- .../instrumentations.py | 31 +++++----- .../invocation_plugin.py | 18 ++---- .../otel_plugin_config.py | 6 +- .../provider.py | 57 ++++++++++++------- .../tests/test_provider.py | 16 ++++-- 7 files changed, 75 insertions(+), 69 deletions(-) diff --git a/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/__init__.py b/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/__init__.py index d44dfb56..890076a7 100644 --- a/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/__init__.py +++ b/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/__init__.py @@ -30,6 +30,7 @@ ) from aws_durable_execution_sdk_python_otel.provider import ( ProviderResult, + ProviderSource, create_tracer_provider, ) @@ -44,6 +45,7 @@ "InvocationOtelPlugin", "OtelContextLogFilter", "ProviderResult", + "ProviderSource", "create_tracer_provider", "derive_workflow_span_id", "install_log_filter", diff --git a/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/execution_plugin.py b/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/execution_plugin.py index 354b5ccf..5b113ed1 100644 --- a/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/execution_plugin.py +++ b/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/execution_plugin.py @@ -101,16 +101,19 @@ def __init__(self, config: OtelPluginConfig | None = None) -> None: self._config.context_extractor or xray_context_extractor ) self._workflow_span_name = self._config.workflow_span_name - self._use_default = bool(self._config.use_default_tracer_provider) self._id_generator = DeterministicIdGenerator() result = create_tracer_provider( self._config, id_generator=self._id_generator, - default_use_global=False, ) self._provider = result.tracer_provider self._owns_provider = result.owns_provider + # Global (ADOT) mode changes Invocation-span parenting (see + # _start_invocation_span). This reflects the user's opt-in flag, not the + # resolved source: a test may supply an explicit provider *and* set the + # flag to exercise the ambient-parenting path. + self._use_default = self._config.use_default_tracer_provider # Deterministic stitching requires an SDK provider exposing id_generator. from opentelemetry.sdk.trace import TracerProvider as SdkTracerProvider @@ -129,12 +132,7 @@ def __init__(self, config: OtelPluginConfig | None = None) -> None: self._tracer: Tracer = self._provider.get_tracer(self._config.instrument_name) try: - register_standalone_instrumentations( - self._config, - self._provider if self._owns_provider else None, - owns_provider=self._owns_provider, - use_default_tracer_provider=self._use_default, - ) + register_standalone_instrumentations(self._config, result) except Exception: logger.exception("Failed to register standalone instrumentations") diff --git a/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/instrumentations.py b/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/instrumentations.py index a9813808..8ddb1f92 100644 --- a/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/instrumentations.py +++ b/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/instrumentations.py @@ -22,13 +22,14 @@ import os from typing import TYPE_CHECKING, Any +from aws_durable_execution_sdk_python_otel.provider import ProviderSource -if TYPE_CHECKING: - from opentelemetry.trace import TracerProvider +if TYPE_CHECKING: from aws_durable_execution_sdk_python_otel.otel_plugin_config import ( OtelPluginConfig, ) + from aws_durable_execution_sdk_python_otel.provider import ProviderResult logger = logging.getLogger(__name__) @@ -98,31 +99,25 @@ def request_hook(span, pool, request_info) -> None: # noqa: ANN001 def register_standalone_instrumentations( config: OtelPluginConfig, - tracer_provider: TracerProvider | None, - *, - owns_provider: bool, - use_default_tracer_provider: bool, + result: ProviderResult, ) -> None: - """Register AWS SDK and HTTP instrumentations per the shared policy. + """Register AWS SDK and HTTP instrumentations per the resolved source. Args: config: Shared plugin configuration. - tracer_provider: The resolved provider (may be the global provider). - owns_provider: True when the plugin created/owns the provider. - use_default_tracer_provider: True when the global provider is in use. + result: The resolved provider and its :class:`ProviderSource`. """ - # A custom, explicitly-supplied provider means the caller manages their own - # instrumentation: skip everything. - if config.tracer_provider is not None: + if result.source is ProviderSource.EXPLICIT: + # Caller manages their own instrumentation: skip everything. return - if use_default_tracer_provider: + if result.source is ProviderSource.GLOBAL: # Global provider: register AWS instrumentation only. _register_aws_instrumentation(None) return - # Auto-configured, plugin-owned provider: AWS SDK always; HTTP unless - # explicitly disabled. - _register_aws_instrumentation(tracer_provider) + # AUTO_OTLP: auto-configured, plugin-owned provider -> AWS SDK always; HTTP + # unless explicitly disabled. + _register_aws_instrumentation(result.tracer_provider) if config.enable_http_instrumentation: - _register_http_instrumentation(tracer_provider) + _register_http_instrumentation(result.tracer_provider) diff --git a/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/invocation_plugin.py b/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/invocation_plugin.py index 1569088d..163485f3 100644 --- a/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/invocation_plugin.py +++ b/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/invocation_plugin.py @@ -122,18 +122,13 @@ def __init__(self, config: OtelPluginConfig | None = None) -> None: self._enrich_logger = self._config.enrich_logger # Like ExecutionOtelPlugin (and the JS SDK plugins), InvocationOtelPlugin - # auto-configures an OTLP provider when nothing is supplied. Resolve the - # effective "use the global provider" decision up front so that - # instrumentation registration matches what create_tracer_provider does. - self._use_default = self._config.use_default_tracer_provider - if self._use_default is None: - self._use_default = False - + # auto-configures an OTLP provider when nothing is supplied; pass + # use_default_tracer_provider=True on the config for the global (ADOT) + # provider. self._id_generator = DeterministicIdGenerator() result = create_tracer_provider( self._config, id_generator=self._id_generator, - default_use_global=False, ) self._provider = result.tracer_provider self._owns_provider = result.owns_provider @@ -161,12 +156,7 @@ def __init__(self, config: OtelPluginConfig | None = None) -> None: self._tracer: Tracer = self._provider.get_tracer(self._config.instrument_name) try: - register_standalone_instrumentations( - self._config, - self._provider if self._owns_provider else None, - owns_provider=self._owns_provider, - use_default_tracer_provider=self._use_default, - ) + register_standalone_instrumentations(self._config, result) except Exception: logger.exception("Failed to register standalone instrumentations") diff --git a/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/otel_plugin_config.py b/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/otel_plugin_config.py index 76dc8843..ef417ea2 100644 --- a/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/otel_plugin_config.py +++ b/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/otel_plugin_config.py @@ -47,7 +47,9 @@ class OtelPluginConfig: set, the plugin does not own or modify it and skips instrumentation registration. use_default_tracer_provider: When True (and no explicit provider), - resolve the globally configured provider via ``trace.get_tracer_provider()``. + resolve the globally configured provider via ``trace.get_tracer_provider()`` + (e.g. the ADOT Lambda layer). Defaults to False, in which case the + plugin auto-configures its own OTLP provider. context_extractor: Upstream trace-context extractor. Defaults to the X-Ray extractor when omitted. instrument_name: Instrumentation scope name. @@ -61,7 +63,7 @@ class OtelPluginConfig: """ tracer_provider: SdkTracerProvider | None = None - use_default_tracer_provider: bool | None = None + use_default_tracer_provider: bool = False context_extractor: ContextExtractor | None = None instrument_name: str = DEFAULT_INSTRUMENT_NAME enable_http_instrumentation: bool = True diff --git a/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/provider.py b/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/provider.py index a0533b4e..e14ee11b 100644 --- a/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/provider.py +++ b/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/provider.py @@ -1,13 +1,14 @@ """Shared TracerProvider factory for the durable-execution OTel plugins. -Implements the 3-level provider resolution used by both plugins: +Implements the 3-tier provider resolution used by both plugins, reported as a +:class:`ProviderSource`: -1. An explicit ``tracer_provider`` in config is used as-is (``owns_provider=False``). -2. Otherwise, when ``use_default_tracer_provider`` resolves to True, the globally - configured provider is used (``owns_provider=False``). +1. An explicit ``tracer_provider`` in config is used as-is (``EXPLICIT``). +2. Otherwise, when ``use_default_tracer_provider`` is True, the globally + configured provider is used (``GLOBAL``). 3. Otherwise a fully auto-configured SDK provider is created with an OTLP exporter, batch processor, sampler and Lambda resource attributes - (``owns_provider=True``). + (``AUTO_OTLP``); this is the only tier the plugin owns/flushes. """ from __future__ import annotations @@ -15,6 +16,7 @@ import logging import os from dataclasses import dataclass +from enum import Enum from typing import TYPE_CHECKING from opentelemetry import propagate, trace @@ -38,12 +40,25 @@ OTLP_ENDPOINT_ENV = "OTEL_EXPORTER_OTLP_ENDPOINT" +class ProviderSource(Enum): + """Which of the three resolution tiers produced the tracer provider.""" + + EXPLICIT = "explicit" # caller supplied config.tracer_provider + GLOBAL = "global" # use_default_tracer_provider -> trace.get_tracer_provider() + AUTO_OTLP = "auto_otlp" # default: plugin builds and owns an OTLP provider + + @dataclass class ProviderResult: - """Result of provider resolution: the provider and whether we own it.""" + """Result of provider resolution: the provider and how it was chosen.""" tracer_provider: TracerProvider - owns_provider: bool + source: ProviderSource + + @property + def owns_provider(self) -> bool: + """True only when the plugin created (and therefore manages) the provider.""" + return self.source is ProviderSource.AUTO_OTLP def _resolve_endpoint(config: OtelPluginConfig) -> str: @@ -151,31 +166,31 @@ def create_tracer_provider( config: OtelPluginConfig, *, id_generator: IdGenerator | None = None, - default_use_global: bool = False, ) -> ProviderResult: - """Resolve a TracerProvider using the shared 3-level priority. + """Resolve a TracerProvider using the shared 3-tier priority. + + The chosen tier is reported as :class:`ProviderSource` so callers make the + instrumentation/flush decision once, off a single value: + + 1. ``config.tracer_provider`` set -> ``EXPLICIT`` (used as-is) + 2. ``config.use_default_tracer_provider`` -> ``GLOBAL`` (global provider) + 3. otherwise (default) -> ``AUTO_OTLP`` (plugin-owned OTLP) Args: config: Shared plugin configuration. id_generator: Deterministic ID generator injected into an auto-configured provider so cross-invocation trace stitching works. - default_use_global: The value ``use_default_tracer_provider`` defaults to - when it is unset in config. ``ExecutionOtelPlugin`` passes ``False``; - ``InvocationOtelPlugin`` passes ``True`` to preserve its historical - behaviour of using the global provider. Returns: A :class:`ProviderResult`. """ if config.tracer_provider is not None: # Explicit provider: use as-is, never wrap/modify. - return ProviderResult(config.tracer_provider, False) + return ProviderResult(config.tracer_provider, ProviderSource.EXPLICIT) - use_default = config.use_default_tracer_provider - if use_default is None: - use_default = default_use_global + if config.use_default_tracer_provider: + return ProviderResult(trace.get_tracer_provider(), ProviderSource.GLOBAL) - if use_default: - return ProviderResult(trace.get_tracer_provider(), False) - - return ProviderResult(_create_auto_provider(config, id_generator), True) + return ProviderResult( + _create_auto_provider(config, id_generator), ProviderSource.AUTO_OTLP + ) diff --git a/packages/aws-durable-execution-sdk-python-otel/tests/test_provider.py b/packages/aws-durable-execution-sdk-python-otel/tests/test_provider.py index dd06a6f2..bfff9ab5 100644 --- a/packages/aws-durable-execution-sdk-python-otel/tests/test_provider.py +++ b/packages/aws-durable-execution-sdk-python-otel/tests/test_provider.py @@ -13,6 +13,7 @@ ) from aws_durable_execution_sdk_python_otel.provider import ( SAMPLING_RATIO_ENV, + ProviderSource, _build_resource, _build_sampler, _resolve_endpoint, @@ -24,21 +25,23 @@ def test_explicit_provider_is_used_and_not_owned(): provider = TracerProvider() result = create_tracer_provider(OtelPluginConfig(tracer_provider=provider)) assert result.tracer_provider is provider + assert result.source is ProviderSource.EXPLICIT assert result.owns_provider is False def test_use_default_provider_returns_global_and_not_owned(): result = create_tracer_provider(OtelPluginConfig(use_default_tracer_provider=True)) assert result.tracer_provider is trace.get_tracer_provider() + assert result.source is ProviderSource.GLOBAL assert result.owns_provider is False -def test_default_use_global_flag_applies_when_unset(): - # InvocationOtelPlugin passes default_use_global=True so an unset config - # resolves to the global provider. - result = create_tracer_provider(OtelPluginConfig(), default_use_global=True) - assert result.tracer_provider is trace.get_tracer_provider() - assert result.owns_provider is False +def test_unset_config_defaults_to_owned_auto_otlp_provider(): + # Both plugins share this default: no explicit provider and + # use_default_tracer_provider left False -> plugin builds/owns an OTLP provider. + result = create_tracer_provider(OtelPluginConfig()) + assert result.source is ProviderSource.AUTO_OTLP + assert result.owns_provider is True def test_auto_configured_provider_is_owned_sdk_provider(): @@ -53,6 +56,7 @@ def test_explicit_provider_takes_precedence_over_use_default(): OtelPluginConfig(tracer_provider=provider, use_default_tracer_provider=True) ) assert result.tracer_provider is provider + assert result.source is ProviderSource.EXPLICIT assert result.owns_provider is False From 4bd4a75ce11465a9ce5a51699dd82bb85445db9f Mon Sep 17 00:00:00 2001 From: silanhe Date: Wed, 5 Aug 2026 22:49:59 +0000 Subject: [PATCH 04/10] refactor(otel): move ProviderSource enum into config Move the ProviderSource enum into otel_plugin_config and add resolve_provider_source() so the explicit > global > auto-OTLP precedence lives in one place. create_tracer_provider becomes a straight switch on the resolved source. Remove the redundant ProviderResult.owns_provider property and the dead self._owns_provider fields in both plugins. --- .../__init__.py | 4 +- .../execution_plugin.py | 1 - .../instrumentations.py | 2 +- .../invocation_plugin.py | 1 - .../otel_plugin_config.py | 33 ++++++++++++ .../provider.py | 52 +++++++++---------- .../tests/test_provider.py | 41 ++++++++++++--- 7 files changed, 97 insertions(+), 37 deletions(-) diff --git a/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/__init__.py b/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/__init__.py index 890076a7..a989ef01 100644 --- a/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/__init__.py +++ b/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/__init__.py @@ -17,6 +17,8 @@ from aws_durable_execution_sdk_python_otel.otel_plugin_config import ( OtelPluginConfig, ExporterConfig, + ProviderSource, + resolve_provider_source, ) from aws_durable_execution_sdk_python_otel.instrumentations import ( register_standalone_instrumentations, @@ -30,7 +32,6 @@ ) from aws_durable_execution_sdk_python_otel.provider import ( ProviderResult, - ProviderSource, create_tracer_provider, ) @@ -47,6 +48,7 @@ "ProviderResult", "ProviderSource", "create_tracer_provider", + "resolve_provider_source", "derive_workflow_span_id", "install_log_filter", "operation_id_to_span_id", diff --git a/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/execution_plugin.py b/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/execution_plugin.py index 5b113ed1..349e1901 100644 --- a/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/execution_plugin.py +++ b/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/execution_plugin.py @@ -108,7 +108,6 @@ def __init__(self, config: OtelPluginConfig | None = None) -> None: id_generator=self._id_generator, ) self._provider = result.tracer_provider - self._owns_provider = result.owns_provider # Global (ADOT) mode changes Invocation-span parenting (see # _start_invocation_span). This reflects the user's opt-in flag, not the # resolved source: a test may supply an explicit provider *and* set the diff --git a/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/instrumentations.py b/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/instrumentations.py index 8ddb1f92..a30beebc 100644 --- a/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/instrumentations.py +++ b/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/instrumentations.py @@ -22,7 +22,7 @@ import os from typing import TYPE_CHECKING, Any -from aws_durable_execution_sdk_python_otel.provider import ProviderSource +from aws_durable_execution_sdk_python_otel.otel_plugin_config import ProviderSource if TYPE_CHECKING: diff --git a/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/invocation_plugin.py b/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/invocation_plugin.py index 163485f3..f20e1e66 100644 --- a/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/invocation_plugin.py +++ b/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/invocation_plugin.py @@ -131,7 +131,6 @@ def __init__(self, config: OtelPluginConfig | None = None) -> None: id_generator=self._id_generator, ) self._provider = result.tracer_provider - self._owns_provider = result.owns_provider # Deterministic trace stitching requires the SDK TracerProvider, which # exposes id_generator/sampler. The API's default ProxyTracerProvider diff --git a/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/otel_plugin_config.py b/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/otel_plugin_config.py index ef417ea2..8684cf09 100644 --- a/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/otel_plugin_config.py +++ b/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/otel_plugin_config.py @@ -8,6 +8,7 @@ from __future__ import annotations from dataclasses import dataclass, field +from enum import Enum from typing import TYPE_CHECKING, Sequence @@ -27,6 +28,19 @@ DEFAULT_OTLP_ENDPOINT = "http://localhost:4318" +class ProviderSource(Enum): + """Which of the three resolution tiers an :class:`OtelPluginConfig` selects. + + Resolved once via :func:`resolve_provider_source` and used by + ``create_tracer_provider`` (to build the provider) and by the plugins (to + make the instrumentation and flush decisions off a single value). + """ + + EXPLICIT = "explicit" # caller supplied config.tracer_provider + GLOBAL = "global" # use_default_tracer_provider -> trace.get_tracer_provider() + AUTO_OTLP = "auto_otlp" # default: plugin builds and owns an OTLP provider + + @dataclass class ExporterConfig: """OTLP exporter configuration for the auto-configured TracerProvider.""" @@ -71,3 +85,22 @@ class OtelPluginConfig: propagators: Sequence[TextMapPropagator] | None = None workflow_span_name: str = DEFAULT_WORKFLOW_SPAN_NAME enrich_logger: bool = True + + +def resolve_provider_source(config: OtelPluginConfig) -> ProviderSource: + """Map a config to its :class:`ProviderSource`, encoding precedence once. + + Precedence (highest first): + + 1. An explicit ``tracer_provider`` -> :attr:`ProviderSource.EXPLICIT`. + 2. ``use_default_tracer_provider`` -> :attr:`ProviderSource.GLOBAL`. + 3. Otherwise (default) -> :attr:`ProviderSource.AUTO_OTLP`. + + Keeping this in one place means ``create_tracer_provider`` and the plugins + all agree on how a config resolves. + """ + if config.tracer_provider is not None: + return ProviderSource.EXPLICIT + if config.use_default_tracer_provider: + return ProviderSource.GLOBAL + return ProviderSource.AUTO_OTLP diff --git a/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/provider.py b/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/provider.py index e14ee11b..530a6a8e 100644 --- a/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/provider.py +++ b/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/provider.py @@ -16,7 +16,6 @@ import logging import os from dataclasses import dataclass -from enum import Enum from typing import TYPE_CHECKING from opentelemetry import propagate, trace @@ -25,6 +24,8 @@ from aws_durable_execution_sdk_python_otel.otel_plugin_config import ( DEFAULT_OTLP_ENDPOINT, OtelPluginConfig, + ProviderSource, + resolve_provider_source, ) @@ -40,26 +41,18 @@ OTLP_ENDPOINT_ENV = "OTEL_EXPORTER_OTLP_ENDPOINT" -class ProviderSource(Enum): - """Which of the three resolution tiers produced the tracer provider.""" - - EXPLICIT = "explicit" # caller supplied config.tracer_provider - GLOBAL = "global" # use_default_tracer_provider -> trace.get_tracer_provider() - AUTO_OTLP = "auto_otlp" # default: plugin builds and owns an OTLP provider - - @dataclass class ProviderResult: - """Result of provider resolution: the provider and how it was chosen.""" + """Result of provider resolution: the provider and how it was chosen. + + ``source`` is the single value callers key their instrumentation/flush + decisions off; the plugin owns (and flushes) the provider only when it is + :attr:`ProviderSource.AUTO_OTLP`. + """ tracer_provider: TracerProvider source: ProviderSource - @property - def owns_provider(self) -> bool: - """True only when the plugin created (and therefore manages) the provider.""" - return self.source is ProviderSource.AUTO_OTLP - def _resolve_endpoint(config: OtelPluginConfig) -> str: """Resolve the OTLP traces endpoint (config -> env -> default). @@ -169,8 +162,10 @@ def create_tracer_provider( ) -> ProviderResult: """Resolve a TracerProvider using the shared 3-tier priority. - The chosen tier is reported as :class:`ProviderSource` so callers make the - instrumentation/flush decision once, off a single value: + The tier is resolved once (:func:`resolve_provider_source`) and this function + is a straight switch on it. The chosen tier is reported back as + :class:`ProviderSource` so callers make the instrumentation/flush decision + off a single value: 1. ``config.tracer_provider`` set -> ``EXPLICIT`` (used as-is) 2. ``config.use_default_tracer_provider`` -> ``GLOBAL`` (global provider) @@ -184,13 +179,16 @@ def create_tracer_provider( Returns: A :class:`ProviderResult`. """ - if config.tracer_provider is not None: - # Explicit provider: use as-is, never wrap/modify. - return ProviderResult(config.tracer_provider, ProviderSource.EXPLICIT) - - if config.use_default_tracer_provider: - return ProviderResult(trace.get_tracer_provider(), ProviderSource.GLOBAL) - - return ProviderResult( - _create_auto_provider(config, id_generator), ProviderSource.AUTO_OTLP - ) + source = resolve_provider_source(config) + + if source is ProviderSource.EXPLICIT: + # Explicit provider: use as-is, never wrap/modify. resolve_provider_source + # only returns EXPLICIT when tracer_provider is set. + assert config.tracer_provider is not None + provider: TracerProvider = config.tracer_provider + elif source is ProviderSource.GLOBAL: + provider = trace.get_tracer_provider() + else: # AUTO_OTLP + provider = _create_auto_provider(config, id_generator) + + return ProviderResult(provider, source) diff --git a/packages/aws-durable-execution-sdk-python-otel/tests/test_provider.py b/packages/aws-durable-execution-sdk-python-otel/tests/test_provider.py index bfff9ab5..03488a15 100644 --- a/packages/aws-durable-execution-sdk-python-otel/tests/test_provider.py +++ b/packages/aws-durable-execution-sdk-python-otel/tests/test_provider.py @@ -10,10 +10,11 @@ from aws_durable_execution_sdk_python_otel.otel_plugin_config import ( OtelPluginConfig, ExporterConfig, + ProviderSource, + resolve_provider_source, ) from aws_durable_execution_sdk_python_otel.provider import ( SAMPLING_RATIO_ENV, - ProviderSource, _build_resource, _build_sampler, _resolve_endpoint, @@ -26,14 +27,12 @@ def test_explicit_provider_is_used_and_not_owned(): result = create_tracer_provider(OtelPluginConfig(tracer_provider=provider)) assert result.tracer_provider is provider assert result.source is ProviderSource.EXPLICIT - assert result.owns_provider is False def test_use_default_provider_returns_global_and_not_owned(): result = create_tracer_provider(OtelPluginConfig(use_default_tracer_provider=True)) assert result.tracer_provider is trace.get_tracer_provider() assert result.source is ProviderSource.GLOBAL - assert result.owns_provider is False def test_unset_config_defaults_to_owned_auto_otlp_provider(): @@ -41,12 +40,11 @@ def test_unset_config_defaults_to_owned_auto_otlp_provider(): # use_default_tracer_provider left False -> plugin builds/owns an OTLP provider. result = create_tracer_provider(OtelPluginConfig()) assert result.source is ProviderSource.AUTO_OTLP - assert result.owns_provider is True def test_auto_configured_provider_is_owned_sdk_provider(): result = create_tracer_provider(OtelPluginConfig()) - assert result.owns_provider is True + assert result.source is ProviderSource.AUTO_OTLP assert isinstance(result.tracer_provider, TracerProvider) @@ -57,7 +55,38 @@ def test_explicit_provider_takes_precedence_over_use_default(): ) assert result.tracer_provider is provider assert result.source is ProviderSource.EXPLICIT - assert result.owns_provider is False + + +# --------------------------------------------------------------------------- +# Provider-source resolution (precedence encoded in one place) +# --------------------------------------------------------------------------- +def test_resolve_provider_source_explicit(): + assert ( + resolve_provider_source(OtelPluginConfig(tracer_provider=TracerProvider())) + is ProviderSource.EXPLICIT + ) + + +def test_resolve_provider_source_global(): + assert ( + resolve_provider_source(OtelPluginConfig(use_default_tracer_provider=True)) + is ProviderSource.GLOBAL + ) + + +def test_resolve_provider_source_defaults_to_auto_otlp(): + assert resolve_provider_source(OtelPluginConfig()) is ProviderSource.AUTO_OTLP + + +def test_resolve_provider_source_explicit_wins_over_default(): + assert ( + resolve_provider_source( + OtelPluginConfig( + tracer_provider=TracerProvider(), use_default_tracer_provider=True + ) + ) + is ProviderSource.EXPLICIT + ) # --------------------------------------------------------------------------- From 8fc36146b8546524d477d61282d832befc4e21a4 Mon Sep 17 00:00:00 2001 From: silanhe Date: Wed, 5 Aug 2026 23:02:06 +0000 Subject: [PATCH 05/10] refactor(otel): make config fully provider_source-driven Replace the use_default_tracer_provider boolean with a provider_source: ProviderSource field as the single driver of provider selection. tracer_provider becomes the EXPLICIT-only field; __post_init__ validates that EXPLICIT has a tracer_provider and GLOBAL/AUTO_OTLP do not. create_tracer_provider switches on config.provider_source; the redundant resolve_provider_source helper is removed. ExecutionOtelPlugin ambient-parenting now keys off ProviderSource.GLOBAL. Tests migrate the explicit-provider+flag shortcut to monkeypatching trace.get_tracer_provider with provider_source=GLOBAL, matching the existing InvocationOtelPlugin integration test. Examples updated to provider_source=GLOBAL. BREAKING CHANGE: use_default_tracer_provider is removed; pass provider_source=ProviderSource.GLOBAL (ADOT) or ProviderSource.EXPLICIT with a tracer_provider instead. --- .../src/otel/otel_logger_example.py | 3 +- .../src/plugin/execution_with_otel.py | 3 +- .../__init__.py | 2 - .../execution_plugin.py | 12 ++-- .../instrumentations.py | 2 +- .../invocation_plugin.py | 13 ++-- .../otel_plugin_config.py | 67 ++++++++++--------- .../provider.py | 39 +++++------ .../tests/test_execution_plugin.py | 27 +++++--- .../test_execution_plugin_integration.py | 22 +++--- .../tests/test_invocation_plugin.py | 7 +- .../test_invocation_plugin_integration.py | 10 ++- .../tests/test_log_filter.py | 6 +- .../tests/test_provider.py | 67 +++++++------------ 14 files changed, 145 insertions(+), 135 deletions(-) diff --git a/packages/aws-durable-execution-sdk-python-examples/src/otel/otel_logger_example.py b/packages/aws-durable-execution-sdk-python-examples/src/otel/otel_logger_example.py index 44dccb42..44f3755d 100644 --- a/packages/aws-durable-execution-sdk-python-examples/src/otel/otel_logger_example.py +++ b/packages/aws-durable-execution-sdk-python-examples/src/otel/otel_logger_example.py @@ -19,6 +19,7 @@ from aws_durable_execution_sdk_python_otel import ( InvocationOtelPlugin, OtelPluginConfig, + ProviderSource, ) from aws_durable_execution_sdk_python import StepContext @@ -48,7 +49,7 @@ def greet_in_child(child_context: DurableContext, name: str) -> str: @durable_execution( - plugins=[InvocationOtelPlugin(OtelPluginConfig(use_default_tracer_provider=True))] + plugins=[InvocationOtelPlugin(OtelPluginConfig(provider_source=ProviderSource.GLOBAL))] ) def handler(_event: Any, context: DurableContext) -> str: # Logged at the top level: enriched with the invocation span_id. diff --git a/packages/aws-durable-execution-sdk-python-examples/src/plugin/execution_with_otel.py b/packages/aws-durable-execution-sdk-python-examples/src/plugin/execution_with_otel.py index 9044db7e..26cc1b64 100644 --- a/packages/aws-durable-execution-sdk-python-examples/src/plugin/execution_with_otel.py +++ b/packages/aws-durable-execution-sdk-python-examples/src/plugin/execution_with_otel.py @@ -5,6 +5,7 @@ from aws_durable_execution_sdk_python_otel import ( InvocationOtelPlugin, OtelPluginConfig, + ProviderSource, ) from aws_durable_execution_sdk_python import StepContext @@ -36,7 +37,7 @@ def add_numbers_in_child(child_context: DurableContext, a: int, b: int): @durable_execution( - plugins=[InvocationOtelPlugin(OtelPluginConfig(use_default_tracer_provider=True))] + plugins=[InvocationOtelPlugin(OtelPluginConfig(provider_source=ProviderSource.GLOBAL))] ) def handler(_event: Any, context: DurableContext) -> int: result = 0 diff --git a/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/__init__.py b/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/__init__.py index a989ef01..b9f1257e 100644 --- a/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/__init__.py +++ b/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/__init__.py @@ -18,7 +18,6 @@ OtelPluginConfig, ExporterConfig, ProviderSource, - resolve_provider_source, ) from aws_durable_execution_sdk_python_otel.instrumentations import ( register_standalone_instrumentations, @@ -48,7 +47,6 @@ "ProviderResult", "ProviderSource", "create_tracer_provider", - "resolve_provider_source", "derive_workflow_span_id", "install_log_filter", "operation_id_to_span_id", diff --git a/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/execution_plugin.py b/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/execution_plugin.py index 349e1901..32fb0872 100644 --- a/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/execution_plugin.py +++ b/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/execution_plugin.py @@ -62,6 +62,7 @@ ) from aws_durable_execution_sdk_python_otel.otel_plugin_config import ( OtelPluginConfig, + ProviderSource, ) from aws_durable_execution_sdk_python_otel.instrumentations import ( register_standalone_instrumentations, @@ -108,11 +109,10 @@ def __init__(self, config: OtelPluginConfig | None = None) -> None: id_generator=self._id_generator, ) self._provider = result.tracer_provider - # Global (ADOT) mode changes Invocation-span parenting (see - # _start_invocation_span). This reflects the user's opt-in flag, not the - # resolved source: a test may supply an explicit provider *and* set the - # flag to exercise the ambient-parenting path. - self._use_default = self._config.use_default_tracer_provider + # GLOBAL (ADOT) mode parents the Invocation span to the ambient Lambda + # invocation span instead of the Workflow span (see + # _start_invocation_span). + self._provider_source = result.source # Deterministic stitching requires an SDK provider exposing id_generator. from opentelemetry.sdk.trace import TracerProvider as SdkTracerProvider @@ -241,7 +241,7 @@ def _start_workflow_span(self, info: InvocationStartInfo) -> None: def _start_invocation_span(self, info: InvocationStartInfo) -> None: self._id_generator.set_next_span_id(None) attributes: dict[str, Any] - if self._use_default: + if self._provider_source is ProviderSource.GLOBAL: # Default-provider mode: parent the Invocation span to the ambient # Lambda invocation span (from the ADOT layer or other # auto-instrumentation), which is still the active context here (the diff --git a/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/instrumentations.py b/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/instrumentations.py index a30beebc..5629ae56 100644 --- a/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/instrumentations.py +++ b/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/instrumentations.py @@ -3,7 +3,7 @@ Mirrors the JS ``registerStandaloneInstrumentations``: * A custom (explicit) provider skips ALL instrumentation registration. -* When the global provider is in use (``use_default_tracer_provider``), only the +* When the global provider is in use (``ProviderSource.GLOBAL``), only the AWS SDK instrumentation is registered (not HTTP). * When the plugin owns an auto-configured provider, both AWS SDK and (optionally) HTTP instrumentation are registered against that provider. diff --git a/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/invocation_plugin.py b/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/invocation_plugin.py index f20e1e66..266fca16 100644 --- a/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/invocation_plugin.py +++ b/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/invocation_plugin.py @@ -89,8 +89,9 @@ class InvocationOtelPlugin(DurableInstrumentationPlugin): extractor, "Workflow" span name, log enrichment on). When no provider is configured, an OTLP provider is auto-configured (matching ExecutionOtelPlugin and the JS SDK plugins); set - ``use_default_tracer_provider=True`` on the config to use the globally - configured tracer provider instead (e.g. the ADOT Lambda layer). + ``provider_source=ProviderSource.GLOBAL`` on the config to use the + globally configured tracer provider instead (e.g. the ADOT Lambda + layer). """ DEFAULT_INSTRUMENT_NAME = "aws-durable-execution-sdk-python" @@ -103,7 +104,7 @@ def __init__(self, config: OtelPluginConfig | None = None) -> None: name, provider selection, exporter/propagator settings, log enrichment). Like ExecutionOtelPlugin and the JS SDK plugins, it auto-configures an OTLP provider when nothing is supplied; pass - ``use_default_tracer_provider=True`` to use the globally configured + ``provider_source=ProviderSource.GLOBAL`` to use the globally configured (e.g. ADOT) provider instead. The tracer provider is configured with this plugin's deterministic ID @@ -122,9 +123,9 @@ def __init__(self, config: OtelPluginConfig | None = None) -> None: self._enrich_logger = self._config.enrich_logger # Like ExecutionOtelPlugin (and the JS SDK plugins), InvocationOtelPlugin - # auto-configures an OTLP provider when nothing is supplied; pass - # use_default_tracer_provider=True on the config for the global (ADOT) - # provider. + # auto-configures an OTLP provider when nothing is supplied; set + # provider_source=ProviderSource.GLOBAL on the config for the global + # (ADOT) provider. self._id_generator = DeterministicIdGenerator() result = create_tracer_provider( self._config, diff --git a/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/otel_plugin_config.py b/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/otel_plugin_config.py index 8684cf09..0816e54d 100644 --- a/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/otel_plugin_config.py +++ b/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/otel_plugin_config.py @@ -29,15 +29,14 @@ class ProviderSource(Enum): - """Which of the three resolution tiers an :class:`OtelPluginConfig` selects. + """Which tracer-provider tier an :class:`OtelPluginConfig` selects. - Resolved once via :func:`resolve_provider_source` and used by - ``create_tracer_provider`` (to build the provider) and by the plugins (to - make the instrumentation and flush decisions off a single value). + The single value that drives provider construction (``create_tracer_provider``) + and the plugins' instrumentation, span-parenting and flush decisions. """ - EXPLICIT = "explicit" # caller supplied config.tracer_provider - GLOBAL = "global" # use_default_tracer_provider -> trace.get_tracer_provider() + EXPLICIT = "explicit" # use config.tracer_provider as-is + GLOBAL = "global" # use the global provider (trace.get_tracer_provider()) AUTO_OTLP = "auto_otlp" # default: plugin builds and owns an OTLP provider @@ -57,13 +56,15 @@ class OtelPluginConfig: are ignored without error by :class:`InvocationOtelPlugin`. Attributes: - tracer_provider: Explicit provider to use as-is. Highest priority; when - set, the plugin does not own or modify it and skips instrumentation - registration. - use_default_tracer_provider: When True (and no explicit provider), - resolve the globally configured provider via ``trace.get_tracer_provider()`` - (e.g. the ADOT Lambda layer). Defaults to False, in which case the - plugin auto-configures its own OTLP provider. + provider_source: Selects how the tracer provider is obtained + (:class:`ProviderSource`). Defaults to ``AUTO_OTLP`` (the plugin + builds and owns an OTLP provider). ``GLOBAL`` uses the globally + configured provider (e.g. the ADOT Lambda layer) via + ``trace.get_tracer_provider()``. ``EXPLICIT`` uses ``tracer_provider`` + as-is and skips instrumentation registration. + tracer_provider: The provider used when ``provider_source`` is + ``EXPLICIT``. Required in that case and must be left unset for + ``GLOBAL`` / ``AUTO_OTLP``. context_extractor: Upstream trace-context extractor. Defaults to the X-Ray extractor when omitted. instrument_name: Instrumentation scope name. @@ -76,8 +77,8 @@ class OtelPluginConfig: enrich_logger: Install the root-logger OTel context filter. """ + provider_source: ProviderSource = ProviderSource.AUTO_OTLP tracer_provider: SdkTracerProvider | None = None - use_default_tracer_provider: bool = False context_extractor: ContextExtractor | None = None instrument_name: str = DEFAULT_INSTRUMENT_NAME enable_http_instrumentation: bool = True @@ -86,21 +87,23 @@ class OtelPluginConfig: workflow_span_name: str = DEFAULT_WORKFLOW_SPAN_NAME enrich_logger: bool = True - -def resolve_provider_source(config: OtelPluginConfig) -> ProviderSource: - """Map a config to its :class:`ProviderSource`, encoding precedence once. - - Precedence (highest first): - - 1. An explicit ``tracer_provider`` -> :attr:`ProviderSource.EXPLICIT`. - 2. ``use_default_tracer_provider`` -> :attr:`ProviderSource.GLOBAL`. - 3. Otherwise (default) -> :attr:`ProviderSource.AUTO_OTLP`. - - Keeping this in one place means ``create_tracer_provider`` and the plugins - all agree on how a config resolves. - """ - if config.tracer_provider is not None: - return ProviderSource.EXPLICIT - if config.use_default_tracer_provider: - return ProviderSource.GLOBAL - return ProviderSource.AUTO_OTLP + def __post_init__(self) -> None: + """Validate that each provider source has the fields it requires. + + The config is fully driven by :attr:`provider_source`; ``tracer_provider`` + is the one source-specific field, so it must be present for ``EXPLICIT`` + and absent for ``GLOBAL`` / ``AUTO_OTLP`` (where it would be silently + ignored). + """ + if self.provider_source is ProviderSource.EXPLICIT: + if self.tracer_provider is None: + raise ValueError( + "provider_source=EXPLICIT requires a tracer_provider." + ) + elif self.tracer_provider is not None: + raise ValueError( + "tracer_provider is only valid with provider_source=EXPLICIT; " + f"got provider_source={self.provider_source.name}. Set " + "ProviderSource.EXPLICIT, or drop tracer_provider for " + "GLOBAL / AUTO_OTLP." + ) diff --git a/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/provider.py b/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/provider.py index 530a6a8e..216d9528 100644 --- a/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/provider.py +++ b/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/provider.py @@ -1,14 +1,13 @@ """Shared TracerProvider factory for the durable-execution OTel plugins. -Implements the 3-tier provider resolution used by both plugins, reported as a -:class:`ProviderSource`: - -1. An explicit ``tracer_provider`` in config is used as-is (``EXPLICIT``). -2. Otherwise, when ``use_default_tracer_provider`` is True, the globally - configured provider is used (``GLOBAL``). -3. Otherwise a fully auto-configured SDK provider is created with an OTLP - exporter, batch processor, sampler and Lambda resource attributes - (``AUTO_OTLP``); this is the only tier the plugin owns/flushes. +Builds the tracer provider selected by the config's +:class:`~aws_durable_execution_sdk_python_otel.otel_plugin_config.ProviderSource`: + +1. ``EXPLICIT`` - the config's ``tracer_provider`` is used as-is. +2. ``GLOBAL`` - the globally configured provider is used (e.g. ADOT layer). +3. ``AUTO_OTLP`` - a fully auto-configured SDK provider is created with an OTLP + exporter, batch processor, sampler and Lambda resource attributes; this is + the only tier the plugin owns/flushes. """ from __future__ import annotations @@ -25,7 +24,6 @@ DEFAULT_OTLP_ENDPOINT, OtelPluginConfig, ProviderSource, - resolve_provider_source, ) @@ -160,16 +158,15 @@ def create_tracer_provider( *, id_generator: IdGenerator | None = None, ) -> ProviderResult: - """Resolve a TracerProvider using the shared 3-tier priority. + """Resolve a TracerProvider from the config's :attr:`provider_source`. - The tier is resolved once (:func:`resolve_provider_source`) and this function - is a straight switch on it. The chosen tier is reported back as - :class:`ProviderSource` so callers make the instrumentation/flush decision - off a single value: + A straight switch on ``config.provider_source``; the chosen tier is reported + back as :class:`ProviderSource` so callers make the instrumentation/flush + decision off a single value: - 1. ``config.tracer_provider`` set -> ``EXPLICIT`` (used as-is) - 2. ``config.use_default_tracer_provider`` -> ``GLOBAL`` (global provider) - 3. otherwise (default) -> ``AUTO_OTLP`` (plugin-owned OTLP) + 1. ``EXPLICIT`` -> ``config.tracer_provider`` used as-is + 2. ``GLOBAL`` -> the globally configured provider + 3. ``AUTO_OTLP`` -> a plugin-owned, auto-configured OTLP provider Args: config: Shared plugin configuration. @@ -179,11 +176,11 @@ def create_tracer_provider( Returns: A :class:`ProviderResult`. """ - source = resolve_provider_source(config) + source = config.provider_source if source is ProviderSource.EXPLICIT: - # Explicit provider: use as-is, never wrap/modify. resolve_provider_source - # only returns EXPLICIT when tracer_provider is set. + # Explicit provider: use as-is, never wrap/modify. OtelPluginConfig + # validation guarantees tracer_provider is set for EXPLICIT. assert config.tracer_provider is not None provider: TracerProvider = config.tracer_provider elif source is ProviderSource.GLOBAL: diff --git a/packages/aws-durable-execution-sdk-python-otel/tests/test_execution_plugin.py b/packages/aws-durable-execution-sdk-python-otel/tests/test_execution_plugin.py index 8b7aedd1..75488662 100644 --- a/packages/aws-durable-execution-sdk-python-otel/tests/test_execution_plugin.py +++ b/packages/aws-durable-execution-sdk-python-otel/tests/test_execution_plugin.py @@ -35,6 +35,7 @@ from aws_durable_execution_sdk_python_otel.execution_plugin import ExecutionOtelPlugin from aws_durable_execution_sdk_python_otel.otel_plugin_config import ( OtelPluginConfig, + ProviderSource, ) @@ -64,6 +65,7 @@ def _create_plugin() -> tuple[ExecutionOtelPlugin, InMemorySpanExporter]: provider.add_span_processor(SimpleSpanProcessor(exporter)) plugin = ExecutionOtelPlugin( OtelPluginConfig( + provider_source=ProviderSource.EXPLICIT, tracer_provider=provider, context_extractor=lambda _: Context(), enrich_logger=False, @@ -389,19 +391,22 @@ def test_step_attempt_span_omits_operation_status(): # --------------------------------------------------------------------------- # Default-provider mode: invocation span # --------------------------------------------------------------------------- -def _create_default_mode_plugin() -> tuple[ExecutionOtelPlugin, InMemorySpanExporter]: - """ExecutionOtelPlugin in default-provider mode wired to an in-memory exporter. - - Passing an explicit ``tracer_provider`` lets the test capture spans while - ``use_default_tracer_provider=True`` still selects the default-mode code path. +def _create_default_mode_plugin( + monkeypatch, +) -> tuple[ExecutionOtelPlugin, InMemorySpanExporter]: + """ExecutionOtelPlugin in GLOBAL (ADOT) mode wired to an in-memory exporter. + + The capture provider is installed as the global provider so + ``provider_source=GLOBAL`` resolves to it, letting the test assert spans + while exercising the ambient-parenting path. """ exporter = InMemorySpanExporter() provider = TracerProvider() provider.add_span_processor(SimpleSpanProcessor(exporter)) + monkeypatch.setattr(trace, "get_tracer_provider", lambda: provider) plugin = ExecutionOtelPlugin( OtelPluginConfig( - tracer_provider=provider, - use_default_tracer_provider=True, + provider_source=ProviderSource.GLOBAL, context_extractor=lambda _: Context(), enrich_logger=False, ) @@ -409,8 +414,8 @@ def _create_default_mode_plugin() -> tuple[ExecutionOtelPlugin, InMemorySpanExpo return plugin, exporter -def test_default_mode_creates_invocation_span(): - plugin, exporter = _create_default_mode_plugin() +def test_default_mode_creates_invocation_span(monkeypatch): + plugin, exporter = _create_default_mode_plugin(monkeypatch) plugin.on_invocation_start(_invocation_start_info()) plugin.on_invocation_end(_invocation_end_info()) @@ -423,8 +428,8 @@ def test_default_mode_creates_invocation_span(): assert invocation.attributes["durable.invocation.first"] is True -def test_default_mode_invocation_span_parented_to_ambient_span(): - plugin, exporter = _create_default_mode_plugin() +def test_default_mode_invocation_span_parented_to_ambient_span(monkeypatch): + plugin, exporter = _create_default_mode_plugin(monkeypatch) # Simulate the ambient Lambda invocation span from the ADOT layer. ambient = plugin._provider.get_tracer("ambient").start_span("lambda-invocation") diff --git a/packages/aws-durable-execution-sdk-python-otel/tests/test_execution_plugin_integration.py b/packages/aws-durable-execution-sdk-python-otel/tests/test_execution_plugin_integration.py index 3d2f6820..98f450c5 100644 --- a/packages/aws-durable-execution-sdk-python-otel/tests/test_execution_plugin_integration.py +++ b/packages/aws-durable-execution-sdk-python-otel/tests/test_execution_plugin_integration.py @@ -4,9 +4,9 @@ InMemorySpanExporter for the two deployment shapes: * Community collector layer: the plugin owns its provider - (``use_default_tracer_provider=False``); the Workflow span is the trace root. + (``provider_source=AUTO_OTLP`` / ``EXPLICIT``); the Workflow span is the trace root. * ADOT layer: the ADOT Lambda layer supplies the global provider and the ambient - Lambda invocation span (``use_default_tracer_provider=True``); the plugin's + Lambda invocation span (``provider_source=GLOBAL``); the plugin's Invocation span parents to that ambient span. """ @@ -42,7 +42,10 @@ operation_id_to_span_id, ) from aws_durable_execution_sdk_python_otel.execution_plugin import ExecutionOtelPlugin -from aws_durable_execution_sdk_python_otel.otel_plugin_config import OtelPluginConfig +from aws_durable_execution_sdk_python_otel.otel_plugin_config import ( + OtelPluginConfig, + ProviderSource, +) START_TIME = datetime(2024, 1, 2, 3, 4, 5, tzinfo=UTC) @@ -162,8 +165,8 @@ def test_community_layer_full_lifecycle_is_workflow_rooted(): provider, exporter = _provider() plugin = ExecutionOtelPlugin( OtelPluginConfig( + provider_source=ProviderSource.EXPLICIT, tracer_provider=provider, - use_default_tracer_provider=False, context_extractor=lambda _: Context(), enrich_logger=False, ) @@ -209,12 +212,13 @@ def test_community_layer_full_lifecycle_is_workflow_rooted(): # --------------------------------------------------------------------------- # ADOT layer (default provider; ambient invocation span) # --------------------------------------------------------------------------- -def test_adot_layer_full_lifecycle_parents_to_ambient_span(): +def test_adot_layer_full_lifecycle_parents_to_ambient_span(monkeypatch): provider, exporter = _provider() + # Simulate the ADOT layer having configured the global TracerProvider. + monkeypatch.setattr(trace, "get_tracer_provider", lambda: provider) plugin = ExecutionOtelPlugin( OtelPluginConfig( - tracer_provider=provider, - use_default_tracer_provider=True, + provider_source=ProviderSource.GLOBAL, context_extractor=lambda _: Context(), enrich_logger=False, ) @@ -255,9 +259,9 @@ def test_adot_layer_full_lifecycle_parents_to_ambient_span(): def test_second_plugin_configures_cached_tracer_generator(monkeypatch): """A second handler's Workflow span uses its deterministic trace ID.""" provider, exporter = _provider() + monkeypatch.setattr(trace, "get_tracer_provider", lambda: provider) config = OtelPluginConfig( - tracer_provider=provider, - use_default_tracer_provider=True, + provider_source=ProviderSource.GLOBAL, context_extractor=lambda _: Context(), enrich_logger=False, ) diff --git a/packages/aws-durable-execution-sdk-python-otel/tests/test_invocation_plugin.py b/packages/aws-durable-execution-sdk-python-otel/tests/test_invocation_plugin.py index 2e1b5a6b..d6cf063a 100644 --- a/packages/aws-durable-execution-sdk-python-otel/tests/test_invocation_plugin.py +++ b/packages/aws-durable-execution-sdk-python-otel/tests/test_invocation_plugin.py @@ -36,7 +36,10 @@ operation_id_to_span_id, ) from aws_durable_execution_sdk_python_otel.invocation_plugin import InvocationOtelPlugin -from aws_durable_execution_sdk_python_otel.otel_plugin_config import OtelPluginConfig +from aws_durable_execution_sdk_python_otel.otel_plugin_config import ( + OtelPluginConfig, + ProviderSource, +) START_TIME = datetime(2024, 1, 2, 3, 4, 5, tzinfo=UTC) @@ -65,6 +68,7 @@ def _create_plugin() -> tuple[InvocationOtelPlugin, InMemorySpanExporter]: trace_provider.add_span_processor(SimpleSpanProcessor(exporter)) plugin = InvocationOtelPlugin( OtelPluginConfig( + provider_source=ProviderSource.EXPLICIT, tracer_provider=trace_provider, context_extractor=lambda _: Context(), ) @@ -1087,6 +1091,7 @@ def test_workflow_span_name_is_configurable(): trace_provider.add_span_processor(SimpleSpanProcessor(exporter)) plugin = InvocationOtelPlugin( OtelPluginConfig( + provider_source=ProviderSource.EXPLICIT, tracer_provider=trace_provider, context_extractor=lambda _: Context(), workflow_span_name="MyExecution", diff --git a/packages/aws-durable-execution-sdk-python-otel/tests/test_invocation_plugin_integration.py b/packages/aws-durable-execution-sdk-python-otel/tests/test_invocation_plugin_integration.py index 64aa1437..c1c6d4b1 100644 --- a/packages/aws-durable-execution-sdk-python-otel/tests/test_invocation_plugin_integration.py +++ b/packages/aws-durable-execution-sdk-python-otel/tests/test_invocation_plugin_integration.py @@ -45,7 +45,10 @@ operation_id_to_span_id, ) from aws_durable_execution_sdk_python_otel.invocation_plugin import InvocationOtelPlugin -from aws_durable_execution_sdk_python_otel.otel_plugin_config import OtelPluginConfig +from aws_durable_execution_sdk_python_otel.otel_plugin_config import ( + OtelPluginConfig, + ProviderSource, +) START_TIME = datetime(2024, 1, 2, 3, 4, 5, tzinfo=UTC) @@ -190,6 +193,7 @@ def test_community_layer_full_lifecycle_uses_supplied_provider(): provider, exporter = _provider() plugin = InvocationOtelPlugin( OtelPluginConfig( + provider_source=ProviderSource.EXPLICIT, tracer_provider=provider, context_extractor=lambda _: Context(), enrich_logger=False, @@ -213,7 +217,7 @@ def test_adot_layer_full_lifecycle_uses_global_provider(monkeypatch): plugin = InvocationOtelPlugin( OtelPluginConfig( - use_default_tracer_provider=True, + provider_source=ProviderSource.GLOBAL, context_extractor=lambda _: Context(), enrich_logger=False, ) @@ -232,6 +236,7 @@ def test_second_plugin_configures_cached_tracer_generator(monkeypatch): provider, exporter = _provider() first_plugin = InvocationOtelPlugin( OtelPluginConfig( + provider_source=ProviderSource.EXPLICIT, tracer_provider=provider, context_extractor=lambda _: Context(), enrich_logger=False, @@ -239,6 +244,7 @@ def test_second_plugin_configures_cached_tracer_generator(monkeypatch): ) target_plugin = InvocationOtelPlugin( OtelPluginConfig( + provider_source=ProviderSource.EXPLICIT, tracer_provider=provider, context_extractor=lambda _: Context(), enrich_logger=False, diff --git a/packages/aws-durable-execution-sdk-python-otel/tests/test_log_filter.py b/packages/aws-durable-execution-sdk-python-otel/tests/test_log_filter.py index 29645b85..f419f9ad 100644 --- a/packages/aws-durable-execution-sdk-python-otel/tests/test_log_filter.py +++ b/packages/aws-durable-execution-sdk-python-otel/tests/test_log_filter.py @@ -23,7 +23,10 @@ install_log_filter, ) from aws_durable_execution_sdk_python_otel.invocation_plugin import InvocationOtelPlugin -from aws_durable_execution_sdk_python_otel.otel_plugin_config import OtelPluginConfig +from aws_durable_execution_sdk_python_otel.otel_plugin_config import ( + OtelPluginConfig, + ProviderSource, +) START_TIME = datetime(2024, 1, 2, 3, 4, 5, tzinfo=UTC) @@ -39,6 +42,7 @@ def _create_plugin( trace_provider.add_span_processor(SimpleSpanProcessor(exporter)) plugin = InvocationOtelPlugin( OtelPluginConfig( + provider_source=ProviderSource.EXPLICIT, tracer_provider=trace_provider, context_extractor=lambda _: Context(), enrich_logger=enrich_logger, diff --git a/packages/aws-durable-execution-sdk-python-otel/tests/test_provider.py b/packages/aws-durable-execution-sdk-python-otel/tests/test_provider.py index 03488a15..e1ef8382 100644 --- a/packages/aws-durable-execution-sdk-python-otel/tests/test_provider.py +++ b/packages/aws-durable-execution-sdk-python-otel/tests/test_provider.py @@ -11,7 +11,6 @@ OtelPluginConfig, ExporterConfig, ProviderSource, - resolve_provider_source, ) from aws_durable_execution_sdk_python_otel.provider import ( SAMPLING_RATIO_ENV, @@ -22,71 +21,57 @@ ) -def test_explicit_provider_is_used_and_not_owned(): +def test_explicit_provider_is_used(): provider = TracerProvider() - result = create_tracer_provider(OtelPluginConfig(tracer_provider=provider)) + result = create_tracer_provider( + OtelPluginConfig( + provider_source=ProviderSource.EXPLICIT, tracer_provider=provider + ) + ) assert result.tracer_provider is provider assert result.source is ProviderSource.EXPLICIT -def test_use_default_provider_returns_global_and_not_owned(): - result = create_tracer_provider(OtelPluginConfig(use_default_tracer_provider=True)) +def test_global_source_returns_global_provider(): + result = create_tracer_provider( + OtelPluginConfig(provider_source=ProviderSource.GLOBAL) + ) assert result.tracer_provider is trace.get_tracer_provider() assert result.source is ProviderSource.GLOBAL -def test_unset_config_defaults_to_owned_auto_otlp_provider(): - # Both plugins share this default: no explicit provider and - # use_default_tracer_provider left False -> plugin builds/owns an OTLP provider. +def test_unset_config_defaults_to_auto_otlp_provider(): + # The shared default: no provider_source given -> plugin builds its own + # OTLP provider. result = create_tracer_provider(OtelPluginConfig()) assert result.source is ProviderSource.AUTO_OTLP -def test_auto_configured_provider_is_owned_sdk_provider(): +def test_auto_configured_provider_is_sdk_provider(): result = create_tracer_provider(OtelPluginConfig()) assert result.source is ProviderSource.AUTO_OTLP assert isinstance(result.tracer_provider, TracerProvider) -def test_explicit_provider_takes_precedence_over_use_default(): - provider = TracerProvider() - result = create_tracer_provider( - OtelPluginConfig(tracer_provider=provider, use_default_tracer_provider=True) - ) - assert result.tracer_provider is provider - assert result.source is ProviderSource.EXPLICIT - - # --------------------------------------------------------------------------- -# Provider-source resolution (precedence encoded in one place) +# Config validation (each source has the fields it needs) # --------------------------------------------------------------------------- -def test_resolve_provider_source_explicit(): - assert ( - resolve_provider_source(OtelPluginConfig(tracer_provider=TracerProvider())) - is ProviderSource.EXPLICIT - ) +def test_explicit_source_requires_tracer_provider(): + with pytest.raises(ValueError, match="requires a tracer_provider"): + OtelPluginConfig(provider_source=ProviderSource.EXPLICIT) -def test_resolve_provider_source_global(): - assert ( - resolve_provider_source(OtelPluginConfig(use_default_tracer_provider=True)) - is ProviderSource.GLOBAL - ) - +def test_tracer_provider_without_explicit_source_raises(): + # Default source is AUTO_OTLP; a stray tracer_provider would be ignored. + with pytest.raises(ValueError, match="only valid with provider_source=EXPLICIT"): + OtelPluginConfig(tracer_provider=TracerProvider()) -def test_resolve_provider_source_defaults_to_auto_otlp(): - assert resolve_provider_source(OtelPluginConfig()) is ProviderSource.AUTO_OTLP - -def test_resolve_provider_source_explicit_wins_over_default(): - assert ( - resolve_provider_source( - OtelPluginConfig( - tracer_provider=TracerProvider(), use_default_tracer_provider=True - ) +def test_global_source_rejects_tracer_provider(): + with pytest.raises(ValueError, match="only valid with provider_source=EXPLICIT"): + OtelPluginConfig( + provider_source=ProviderSource.GLOBAL, tracer_provider=TracerProvider() ) - is ProviderSource.EXPLICIT - ) # --------------------------------------------------------------------------- From 327b360b7ac5202cc7259fbf16c18fdd25ca0bb6 Mon Sep 17 00:00:00 2001 From: silanhe Date: Wed, 5 Aug 2026 23:19:54 +0000 Subject: [PATCH 06/10] style(otel): apply ruff format --- .../src/otel/otel_logger_example.py | 4 +++- .../src/plugin/execution_with_otel.py | 4 +++- .../otel_plugin_config.py | 4 +--- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/aws-durable-execution-sdk-python-examples/src/otel/otel_logger_example.py b/packages/aws-durable-execution-sdk-python-examples/src/otel/otel_logger_example.py index 44f3755d..fe74d7e3 100644 --- a/packages/aws-durable-execution-sdk-python-examples/src/otel/otel_logger_example.py +++ b/packages/aws-durable-execution-sdk-python-examples/src/otel/otel_logger_example.py @@ -49,7 +49,9 @@ def greet_in_child(child_context: DurableContext, name: str) -> str: @durable_execution( - plugins=[InvocationOtelPlugin(OtelPluginConfig(provider_source=ProviderSource.GLOBAL))] + plugins=[ + InvocationOtelPlugin(OtelPluginConfig(provider_source=ProviderSource.GLOBAL)) + ] ) def handler(_event: Any, context: DurableContext) -> str: # Logged at the top level: enriched with the invocation span_id. diff --git a/packages/aws-durable-execution-sdk-python-examples/src/plugin/execution_with_otel.py b/packages/aws-durable-execution-sdk-python-examples/src/plugin/execution_with_otel.py index 26cc1b64..b1cff3c5 100644 --- a/packages/aws-durable-execution-sdk-python-examples/src/plugin/execution_with_otel.py +++ b/packages/aws-durable-execution-sdk-python-examples/src/plugin/execution_with_otel.py @@ -37,7 +37,9 @@ def add_numbers_in_child(child_context: DurableContext, a: int, b: int): @durable_execution( - plugins=[InvocationOtelPlugin(OtelPluginConfig(provider_source=ProviderSource.GLOBAL))] + plugins=[ + InvocationOtelPlugin(OtelPluginConfig(provider_source=ProviderSource.GLOBAL)) + ] ) def handler(_event: Any, context: DurableContext) -> int: result = 0 diff --git a/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/otel_plugin_config.py b/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/otel_plugin_config.py index 0816e54d..b4d428b9 100644 --- a/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/otel_plugin_config.py +++ b/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/otel_plugin_config.py @@ -97,9 +97,7 @@ def __post_init__(self) -> None: """ if self.provider_source is ProviderSource.EXPLICIT: if self.tracer_provider is None: - raise ValueError( - "provider_source=EXPLICIT requires a tracer_provider." - ) + raise ValueError("provider_source=EXPLICIT requires a tracer_provider.") elif self.tracer_provider is not None: raise ValueError( "tracer_provider is only valid with provider_source=EXPLICIT; " From 665fcca21f3d3b5c832208b70c8967b2c4bda827 Mon Sep 17 00:00:00 2001 From: silanhe Date: Wed, 5 Aug 2026 23:47:19 +0000 Subject: [PATCH 07/10] refactor(otel): default provider_source to GLOBAL Default OtelPluginConfig.provider_source to ProviderSource.GLOBAL (use the globally configured provider, e.g. the ADOT Lambda layer) instead of AUTO_OTLP. --- .../otel_plugin_config.py | 14 +++++++------- .../tests/test_provider.py | 16 +++++++++------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/otel_plugin_config.py b/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/otel_plugin_config.py index b4d428b9..cfbb0181 100644 --- a/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/otel_plugin_config.py +++ b/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/otel_plugin_config.py @@ -36,8 +36,8 @@ class ProviderSource(Enum): """ EXPLICIT = "explicit" # use config.tracer_provider as-is - GLOBAL = "global" # use the global provider (trace.get_tracer_provider()) - AUTO_OTLP = "auto_otlp" # default: plugin builds and owns an OTLP provider + GLOBAL = "global" # default: use the global provider (trace.get_tracer_provider()) + AUTO_OTLP = "auto_otlp" # plugin builds and owns an OTLP provider @dataclass @@ -57,10 +57,10 @@ class OtelPluginConfig: Attributes: provider_source: Selects how the tracer provider is obtained - (:class:`ProviderSource`). Defaults to ``AUTO_OTLP`` (the plugin - builds and owns an OTLP provider). ``GLOBAL`` uses the globally - configured provider (e.g. the ADOT Lambda layer) via - ``trace.get_tracer_provider()``. ``EXPLICIT`` uses ``tracer_provider`` + (:class:`ProviderSource`). Defaults to ``GLOBAL`` (uses the globally + configured provider, e.g. the ADOT Lambda layer, via + ``trace.get_tracer_provider()``). ``AUTO_OTLP`` makes the plugin + build and own an OTLP provider. ``EXPLICIT`` uses ``tracer_provider`` as-is and skips instrumentation registration. tracer_provider: The provider used when ``provider_source`` is ``EXPLICIT``. Required in that case and must be left unset for @@ -77,7 +77,7 @@ class OtelPluginConfig: enrich_logger: Install the root-logger OTel context filter. """ - provider_source: ProviderSource = ProviderSource.AUTO_OTLP + provider_source: ProviderSource = ProviderSource.GLOBAL tracer_provider: SdkTracerProvider | None = None context_extractor: ContextExtractor | None = None instrument_name: str = DEFAULT_INSTRUMENT_NAME diff --git a/packages/aws-durable-execution-sdk-python-otel/tests/test_provider.py b/packages/aws-durable-execution-sdk-python-otel/tests/test_provider.py index e1ef8382..e792c2ff 100644 --- a/packages/aws-durable-execution-sdk-python-otel/tests/test_provider.py +++ b/packages/aws-durable-execution-sdk-python-otel/tests/test_provider.py @@ -40,15 +40,17 @@ def test_global_source_returns_global_provider(): assert result.source is ProviderSource.GLOBAL -def test_unset_config_defaults_to_auto_otlp_provider(): - # The shared default: no provider_source given -> plugin builds its own - # OTLP provider. +def test_unset_config_defaults_to_global_provider(): + # The default: no provider_source given -> use the global provider. result = create_tracer_provider(OtelPluginConfig()) - assert result.source is ProviderSource.AUTO_OTLP + assert result.source is ProviderSource.GLOBAL + assert result.tracer_provider is trace.get_tracer_provider() -def test_auto_configured_provider_is_sdk_provider(): - result = create_tracer_provider(OtelPluginConfig()) +def test_auto_otlp_source_builds_sdk_provider(): + result = create_tracer_provider( + OtelPluginConfig(provider_source=ProviderSource.AUTO_OTLP) + ) assert result.source is ProviderSource.AUTO_OTLP assert isinstance(result.tracer_provider, TracerProvider) @@ -62,7 +64,7 @@ def test_explicit_source_requires_tracer_provider(): def test_tracer_provider_without_explicit_source_raises(): - # Default source is AUTO_OTLP; a stray tracer_provider would be ignored. + # Default source is GLOBAL; a stray tracer_provider would be ignored. with pytest.raises(ValueError, match="only valid with provider_source=EXPLICIT"): OtelPluginConfig(tracer_provider=TracerProvider()) From b942a5ec391194219e7a5318e92236dbeb1f7604 Mon Sep 17 00:00:00 2001 From: silanhe Date: Wed, 5 Aug 2026 23:59:54 +0000 Subject: [PATCH 08/10] refactor(otel): make AUTO_OTLP branch explicit Switch create_tracer_provider on each ProviderSource explicitly with an exhaustive guard, so no branch reads as an implicit default (the default GLOBAL lives on OtelPluginConfig.provider_source). --- .../src/aws_durable_execution_sdk_python_otel/provider.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/provider.py b/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/provider.py index 216d9528..e310c438 100644 --- a/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/provider.py +++ b/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/provider.py @@ -185,7 +185,9 @@ def create_tracer_provider( provider: TracerProvider = config.tracer_provider elif source is ProviderSource.GLOBAL: provider = trace.get_tracer_provider() - else: # AUTO_OTLP + elif source is ProviderSource.AUTO_OTLP: provider = _create_auto_provider(config, id_generator) + else: # pragma: no cover - exhaustive over ProviderSource + raise ValueError(f"unknown provider_source: {source!r}") return ProviderResult(provider, source) From c8b06200be15fc0e97f1d054672e447b981dbcc7 Mon Sep 17 00:00:00 2001 From: silanhe Date: Fri, 7 Aug 2026 18:24:16 +0000 Subject: [PATCH 09/10] ci: re-trigger ci From f77a6c5198db1fee536801af1a6f0258db093d9f Mon Sep 17 00:00:00 2001 From: silanhe Date: Fri, 7 Aug 2026 20:47:48 +0000 Subject: [PATCH 10/10] docs(otel): correct provider_source default in comments Docstrings and an inline comment described the old AUTO_OTLP default, stating the plugin auto-configures an OTLP provider when nothing is supplied. The default is now GLOBAL (uses the globally configured, e.g. ADOT, provider); AUTO_OTLP is the opt-in for a plugin-owned provider. Comment/docstring-only; no behavior change. --- .../execution_plugin.py | 3 ++- .../invocation_plugin.py | 27 ++++++++++--------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/execution_plugin.py b/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/execution_plugin.py index 32fb0872..bc829db6 100644 --- a/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/execution_plugin.py +++ b/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/execution_plugin.py @@ -93,7 +93,8 @@ class ExecutionOtelPlugin(DurableInstrumentationPlugin): Args: config: Shared plugin configuration. When omitted, defaults are used - (auto-configured provider, X-Ray extractor, "Workflow" root span). + (globally configured provider, X-Ray extractor, "Workflow" root + span). """ def __init__(self, config: OtelPluginConfig | None = None) -> None: diff --git a/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/invocation_plugin.py b/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/invocation_plugin.py index 266fca16..4f42ca32 100644 --- a/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/invocation_plugin.py +++ b/packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/invocation_plugin.py @@ -86,12 +86,12 @@ class InvocationOtelPlugin(DurableInstrumentationPlugin): Args: config: Shared plugin configuration (the same OtelPluginConfig accepted by ExecutionOtelPlugin). When omitted, defaults are used (X-Ray - extractor, "Workflow" span name, log enrichment on). When no provider - is configured, an OTLP provider is auto-configured (matching - ExecutionOtelPlugin and the JS SDK plugins); set - ``provider_source=ProviderSource.GLOBAL`` on the config to use the - globally configured tracer provider instead (e.g. the ADOT Lambda - layer). + extractor, "Workflow" span name, log enrichment on). Like + ExecutionOtelPlugin and the JS SDK plugins, the default + ``provider_source`` is ``GLOBAL``: the plugin uses the globally + configured tracer provider (e.g. the ADOT Lambda layer). Set + ``provider_source=ProviderSource.AUTO_OTLP`` on the config to have + the plugin build and own an auto-configured OTLP provider instead. """ DEFAULT_INSTRUMENT_NAME = "aws-durable-execution-sdk-python" @@ -102,10 +102,11 @@ def __init__(self, config: OtelPluginConfig | None = None) -> None: Accepts the same OtelPluginConfig as ExecutionOtelPlugin so both plugins share one configuration surface (context extractor, instrumentation name, provider selection, exporter/propagator settings, log - enrichment). Like ExecutionOtelPlugin and the JS SDK plugins, it - auto-configures an OTLP provider when nothing is supplied; pass - ``provider_source=ProviderSource.GLOBAL`` to use the globally configured - (e.g. ADOT) provider instead. + enrichment). Like ExecutionOtelPlugin and the JS SDK plugins, the + default ``provider_source`` is ``GLOBAL``: it uses the globally + configured (e.g. ADOT) provider. Pass + ``provider_source=ProviderSource.AUTO_OTLP`` to have the plugin build + and own an auto-configured OTLP provider instead. The tracer provider is configured with this plugin's deterministic ID generator so spans for a durable execution share stable trace and @@ -123,9 +124,9 @@ def __init__(self, config: OtelPluginConfig | None = None) -> None: self._enrich_logger = self._config.enrich_logger # Like ExecutionOtelPlugin (and the JS SDK plugins), InvocationOtelPlugin - # auto-configures an OTLP provider when nothing is supplied; set - # provider_source=ProviderSource.GLOBAL on the config for the global - # (ADOT) provider. + # defaults to provider_source=GLOBAL (the globally configured, e.g. ADOT, + # provider); set provider_source=ProviderSource.AUTO_OTLP on the config + # to build and own an auto-configured OTLP provider instead. self._id_generator = DeterministicIdGenerator() result = create_tracer_provider( self._config,