Skip to content

Commit f7cc397

Browse files
SilanHehsilan
andauthored
feat(otel): fully enum-driven OtelPluginConfig (#612)
* 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=...)). * 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. * 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). * 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. * 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. * style(otel): apply ruff format * 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. * 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). * ci: re-trigger ci * 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. --------- Co-authored-by: silanhe <hsilan@amazon.com>
1 parent 7a24c81 commit f7cc397

15 files changed

Lines changed: 326 additions & 173 deletions

File tree

packages/aws-durable-execution-sdk-python-examples/src/otel/otel_logger_example.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,11 @@
1616

1717
from typing import Any
1818

19-
from aws_durable_execution_sdk_python_otel import InvocationOtelPlugin
19+
from aws_durable_execution_sdk_python_otel import (
20+
InvocationOtelPlugin,
21+
OtelPluginConfig,
22+
ProviderSource,
23+
)
2024

2125
from aws_durable_execution_sdk_python import StepContext
2226
from aws_durable_execution_sdk_python.context import (
@@ -44,7 +48,11 @@ def greet_in_child(child_context: DurableContext, name: str) -> str:
4448
return result
4549

4650

47-
@durable_execution(plugins=[InvocationOtelPlugin()])
51+
@durable_execution(
52+
plugins=[
53+
InvocationOtelPlugin(OtelPluginConfig(provider_source=ProviderSource.GLOBAL))
54+
]
55+
)
4856
def handler(_event: Any, context: DurableContext) -> str:
4957
# Logged at the top level: enriched with the invocation span_id.
5058
context.logger.info("Workflow started")

packages/aws-durable-execution-sdk-python-examples/src/plugin/execution_with_otel.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@
22

33
from typing import Any
44

5-
from aws_durable_execution_sdk_python_otel import InvocationOtelPlugin
5+
from aws_durable_execution_sdk_python_otel import (
6+
InvocationOtelPlugin,
7+
OtelPluginConfig,
8+
ProviderSource,
9+
)
610

711
from aws_durable_execution_sdk_python import StepContext
812
from aws_durable_execution_sdk_python.config import Duration
@@ -32,7 +36,11 @@ def add_numbers_in_child(child_context: DurableContext, a: int, b: int):
3236
return result
3337

3438

35-
@durable_execution(plugins=[InvocationOtelPlugin()])
39+
@durable_execution(
40+
plugins=[
41+
InvocationOtelPlugin(OtelPluginConfig(provider_source=ProviderSource.GLOBAL))
42+
]
43+
)
3644
def handler(_event: Any, context: DurableContext) -> int:
3745
result = 0
3846
for i in range(3):

packages/aws-durable-execution-sdk-python-otel/README.md

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -168,21 +168,25 @@ See the [ADOT sampling configuration](https://aws-otel.github.io/docs/getting-st
168168
```python
169169
from aws_durable_execution_sdk_python_otel import (
170170
InvocationOtelPlugin,
171+
OtelPluginConfig,
171172
xray_context_extractor,
172173
)
173174

174175
plugin = InvocationOtelPlugin(
175-
# Provide your own TracerProvider if you already have one configured.
176-
# Defaults to the globally configured tracer provider.
177-
trace_provider=None,
178-
# Use a custom context extractor (default: xray_context_extractor).
179-
context_extractor=xray_context_extractor,
180-
# Custom instrumentation scope name
181-
# (default: "aws-durable-execution-sdk-python").
182-
instrument_name="my-service",
183-
# Install a root-logger filter that stamps trace context onto every
184-
# log record (default: True).
185-
enrich_logger=True,
176+
OtelPluginConfig(
177+
# Provide your own TracerProvider if you already have one configured.
178+
# When omitted, an OTLP provider is auto-configured (like ExecutionOtelPlugin);
179+
# set use_default_tracer_provider=True to use the global (e.g. ADOT) provider.
180+
tracer_provider=None,
181+
# Use a custom context extractor (default: xray_context_extractor).
182+
context_extractor=xray_context_extractor,
183+
# Custom instrumentation scope name
184+
# (default: "aws-durable-execution-sdk-python").
185+
instrument_name="my-service",
186+
# Install a root-logger filter that stamps trace context onto every
187+
# log record (default: True).
188+
enrich_logger=True,
189+
)
186190
)
187191
```
188192

@@ -193,15 +197,16 @@ The plugin supports multiple strategies for extracting upstream trace context:
193197
```python
194198
from aws_durable_execution_sdk_python_otel import (
195199
InvocationOtelPlugin,
200+
OtelPluginConfig,
196201
w3c_client_context_extractor,
197202
xray_context_extractor,
198203
)
199204

200205
# Default: X-Ray trace header (recommended for most Lambda deployments)
201-
InvocationOtelPlugin(context_extractor=xray_context_extractor)
206+
InvocationOtelPlugin(OtelPluginConfig(context_extractor=xray_context_extractor))
202207

203208
# W3C Trace Context via clientContext (requires backend propagation support)
204-
InvocationOtelPlugin(context_extractor=w3c_client_context_extractor)
209+
InvocationOtelPlugin(OtelPluginConfig(context_extractor=w3c_client_context_extractor))
205210
```
206211

207212
### Log Correlation
@@ -251,10 +256,15 @@ The main plugin class. Implements `DurableInstrumentationPlugin` from `aws_durab
251256

252257
```python
253258
InvocationOtelPlugin(
254-
trace_provider=None,
255-
context_extractor=None,
256-
instrument_name="aws-durable-execution-sdk-python",
257-
enrich_logger=True,
259+
OtelPluginConfig(
260+
tracer_provider=None,
261+
context_extractor=None,
262+
instrument_name="aws-durable-execution-sdk-python",
263+
enrich_logger=True,
264+
workflow_span_name="Workflow",
265+
# ...and the rest of OtelPluginConfig (use_default_tracer_provider,
266+
# enable_http_instrumentation, exporter_config, propagators).
267+
)
258268
)
259269
```
260270

packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from aws_durable_execution_sdk_python_otel.otel_plugin_config import (
1818
OtelPluginConfig,
1919
ExporterConfig,
20+
ProviderSource,
2021
)
2122
from aws_durable_execution_sdk_python_otel.instrumentations import (
2223
register_standalone_instrumentations,
@@ -44,6 +45,7 @@
4445
"InvocationOtelPlugin",
4546
"OtelContextLogFilter",
4647
"ProviderResult",
48+
"ProviderSource",
4749
"create_tracer_provider",
4850
"derive_workflow_span_id",
4951
"install_log_filter",

packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/execution_plugin.py

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
)
6363
from aws_durable_execution_sdk_python_otel.otel_plugin_config import (
6464
OtelPluginConfig,
65+
ProviderSource,
6566
)
6667
from aws_durable_execution_sdk_python_otel.instrumentations import (
6768
register_standalone_instrumentations,
@@ -92,7 +93,8 @@ class ExecutionOtelPlugin(DurableInstrumentationPlugin):
9293
9394
Args:
9495
config: Shared plugin configuration. When omitted, defaults are used
95-
(auto-configured provider, X-Ray extractor, "Workflow" root span).
96+
(globally configured provider, X-Ray extractor, "Workflow" root
97+
span).
9698
"""
9799

98100
def __init__(self, config: OtelPluginConfig | None = None) -> None:
@@ -101,16 +103,17 @@ def __init__(self, config: OtelPluginConfig | None = None) -> None:
101103
self._config.context_extractor or xray_context_extractor
102104
)
103105
self._workflow_span_name = self._config.workflow_span_name
104-
self._use_default = bool(self._config.use_default_tracer_provider)
105106

106107
self._id_generator = DeterministicIdGenerator()
107108
result = create_tracer_provider(
108109
self._config,
109110
id_generator=self._id_generator,
110-
default_use_global=False,
111111
)
112112
self._provider = result.tracer_provider
113-
self._owns_provider = result.owns_provider
113+
# GLOBAL (ADOT) mode parents the Invocation span to the ambient Lambda
114+
# invocation span instead of the Workflow span (see
115+
# _start_invocation_span).
116+
self._provider_source = result.source
114117

115118
# Deterministic stitching requires an SDK provider exposing id_generator.
116119
from opentelemetry.sdk.trace import TracerProvider as SdkTracerProvider
@@ -129,12 +132,7 @@ def __init__(self, config: OtelPluginConfig | None = None) -> None:
129132
self._tracer: Tracer = self._provider.get_tracer(self._config.instrument_name)
130133

131134
try:
132-
register_standalone_instrumentations(
133-
self._config,
134-
self._provider if self._owns_provider else None,
135-
owns_provider=self._owns_provider,
136-
use_default_tracer_provider=self._use_default,
137-
)
135+
register_standalone_instrumentations(self._config, result)
138136
except Exception:
139137
logger.exception("Failed to register standalone instrumentations")
140138

@@ -244,7 +242,7 @@ def _start_workflow_span(self, info: InvocationStartInfo) -> None:
244242
def _start_invocation_span(self, info: InvocationStartInfo) -> None:
245243
self._id_generator.set_next_span_id(None)
246244
attributes: dict[str, Any]
247-
if self._use_default:
245+
if self._provider_source is ProviderSource.GLOBAL:
248246
# Default-provider mode: parent the Invocation span to the ambient
249247
# Lambda invocation span (from the ADOT layer or other
250248
# auto-instrumentation), which is still the active context here (the

packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/instrumentations.py

Lines changed: 14 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
Mirrors the JS ``registerStandaloneInstrumentations``:
44
55
* A custom (explicit) provider skips ALL instrumentation registration.
6-
* When the global provider is in use (``use_default_tracer_provider``), only the
6+
* When the global provider is in use (``ProviderSource.GLOBAL``), only the
77
AWS SDK instrumentation is registered (not HTTP).
88
* When the plugin owns an auto-configured provider, both AWS SDK and (optionally)
99
HTTP instrumentation are registered against that provider.
@@ -22,13 +22,14 @@
2222
import os
2323
from typing import TYPE_CHECKING, Any
2424

25+
from aws_durable_execution_sdk_python_otel.otel_plugin_config import ProviderSource
2526

26-
if TYPE_CHECKING:
27-
from opentelemetry.trace import TracerProvider
2827

28+
if TYPE_CHECKING:
2929
from aws_durable_execution_sdk_python_otel.otel_plugin_config import (
3030
OtelPluginConfig,
3131
)
32+
from aws_durable_execution_sdk_python_otel.provider import ProviderResult
3233

3334

3435
logger = logging.getLogger(__name__)
@@ -98,31 +99,25 @@ def request_hook(span, pool, request_info) -> None: # noqa: ANN001
9899

99100
def register_standalone_instrumentations(
100101
config: OtelPluginConfig,
101-
tracer_provider: TracerProvider | None,
102-
*,
103-
owns_provider: bool,
104-
use_default_tracer_provider: bool,
102+
result: ProviderResult,
105103
) -> None:
106-
"""Register AWS SDK and HTTP instrumentations per the shared policy.
104+
"""Register AWS SDK and HTTP instrumentations per the resolved source.
107105
108106
Args:
109107
config: Shared plugin configuration.
110-
tracer_provider: The resolved provider (may be the global provider).
111-
owns_provider: True when the plugin created/owns the provider.
112-
use_default_tracer_provider: True when the global provider is in use.
108+
result: The resolved provider and its :class:`ProviderSource`.
113109
"""
114-
# A custom, explicitly-supplied provider means the caller manages their own
115-
# instrumentation: skip everything.
116-
if config.tracer_provider is not None:
110+
if result.source is ProviderSource.EXPLICIT:
111+
# Caller manages their own instrumentation: skip everything.
117112
return
118113

119-
if use_default_tracer_provider:
114+
if result.source is ProviderSource.GLOBAL:
120115
# Global provider: register AWS instrumentation only.
121116
_register_aws_instrumentation(None)
122117
return
123118

124-
# Auto-configured, plugin-owned provider: AWS SDK always; HTTP unless
125-
# explicitly disabled.
126-
_register_aws_instrumentation(tracer_provider)
119+
# AUTO_OTLP: auto-configured, plugin-owned provider -> AWS SDK always; HTTP
120+
# unless explicitly disabled.
121+
_register_aws_instrumentation(result.tracer_provider)
127122
if config.enable_http_instrumentation:
128-
_register_http_instrumentation(tracer_provider)
123+
_register_http_instrumentation(result.tracer_provider)

packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/invocation_plugin.py

Lines changed: 50 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,11 @@
4545
)
4646
from aws_durable_execution_sdk_python_otel.log_filter import install_log_filter
4747
from aws_durable_execution_sdk_python_otel.otel_plugin_config import (
48-
DEFAULT_WORKFLOW_SPAN_NAME,
48+
OtelPluginConfig,
49+
)
50+
from aws_durable_execution_sdk_python_otel.provider import create_tracer_provider
51+
from aws_durable_execution_sdk_python_otel.instrumentations import (
52+
register_standalone_instrumentations,
4953
)
5054

5155

@@ -80,42 +84,56 @@ class InvocationOtelPlugin(DurableInstrumentationPlugin):
8084
original logical operation.
8185
8286
Args:
83-
trace_provider: OpenTelemetry tracer provider used to create spans.
84-
Optional; when omitted, the globally configured tracer provider
85-
(``opentelemetry.trace.get_tracer_provider()``) is used.
86-
context_extractor: Optional extractor for upstream context. Defaults to
87-
AWS X-Ray header extraction.
88-
instrument_name: Instrumentation scope name registered with the tracer.
87+
config: Shared plugin configuration (the same OtelPluginConfig accepted
88+
by ExecutionOtelPlugin). When omitted, defaults are used (X-Ray
89+
extractor, "Workflow" span name, log enrichment on). Like
90+
ExecutionOtelPlugin and the JS SDK plugins, the default
91+
``provider_source`` is ``GLOBAL``: the plugin uses the globally
92+
configured tracer provider (e.g. the ADOT Lambda layer). Set
93+
``provider_source=ProviderSource.AUTO_OTLP`` on the config to have
94+
the plugin build and own an auto-configured OTLP provider instead.
8995
"""
9096

9197
DEFAULT_INSTRUMENT_NAME = "aws-durable-execution-sdk-python"
9298

93-
def __init__(
94-
self,
95-
trace_provider: SdkTracerProvider | None = None,
96-
context_extractor: ContextExtractor | None = None,
97-
instrument_name: str = DEFAULT_INSTRUMENT_NAME,
98-
enrich_logger: bool = True,
99-
workflow_span_name: str = DEFAULT_WORKFLOW_SPAN_NAME,
100-
) -> None:
101-
"""Initialize the plugin with an OpenTelemetry tracer provider.
99+
def __init__(self, config: OtelPluginConfig | None = None) -> None:
100+
"""Initialize the plugin from a shared OtelPluginConfig.
101+
102+
Accepts the same OtelPluginConfig as ExecutionOtelPlugin so both plugins
103+
share one configuration surface (context extractor, instrumentation
104+
name, provider selection, exporter/propagator settings, log
105+
enrichment). Like ExecutionOtelPlugin and the JS SDK plugins, the
106+
default ``provider_source`` is ``GLOBAL``: it uses the globally
107+
configured (e.g. ADOT) provider. Pass
108+
``provider_source=ProviderSource.AUTO_OTLP`` to have the plugin build
109+
and own an auto-configured OTLP provider instead.
102110
103111
The tracer provider is configured with this plugin's deterministic ID
104112
generator so spans for a durable execution share stable trace and
105-
logical operation identifiers. When no provider is supplied, the
106-
globally configured tracer provider is used.
113+
logical operation identifiers.
107114
108-
When enrich_logger is enabled (default), the plugin installs a logging
109-
filter on the root logger at invocation start that stamps the active
110-
OTel trace context onto every emitted log record.
115+
When ``enrich_logger`` is enabled (default), the plugin installs a
116+
logging filter that stamps the active OTel trace context onto every
117+
emitted log record.
111118
"""
112-
self._enrich_logger = enrich_logger
113-
self._workflow_span_name = workflow_span_name
119+
self._config = config or OtelPluginConfig()
114120
self._context_extractor: ContextExtractor = (
115-
context_extractor or xray_context_extractor
121+
self._config.context_extractor or xray_context_extractor
116122
)
123+
self._workflow_span_name = self._config.workflow_span_name
124+
self._enrich_logger = self._config.enrich_logger
125+
126+
# Like ExecutionOtelPlugin (and the JS SDK plugins), InvocationOtelPlugin
127+
# defaults to provider_source=GLOBAL (the globally configured, e.g. ADOT,
128+
# provider); set provider_source=ProviderSource.AUTO_OTLP on the config
129+
# to build and own an auto-configured OTLP provider instead.
130+
self._id_generator = DeterministicIdGenerator()
131+
result = create_tracer_provider(
132+
self._config,
133+
id_generator=self._id_generator,
134+
)
135+
self._provider = result.tracer_provider
117136

118-
self._provider = trace_provider or trace.get_tracer_provider()
119137
# Deterministic trace stitching requires the SDK TracerProvider, which
120138
# exposes id_generator/sampler. The API's default ProxyTracerProvider
121139
# (returned before an SDK provider is configured) does not. Rather than
@@ -128,16 +146,20 @@ def __init__(
128146
self._provider
129147
)
130148
else:
131-
self._id_generator = DeterministicIdGenerator()
132149
logger.warning(
133150
"InvocationOtelPlugin expected an SDK TracerProvider "
134151
"(opentelemetry.sdk.trace.TracerProvider) but got %s. Spans will "
135152
"not use deterministic IDs. "
136153
"Ensure the OpenTelemetry SDK is configured (e.g. via the ADOT "
137-
"Lambda layer) or pass an explicit trace_provider.",
154+
"Lambda layer) or pass an explicit tracer_provider.",
138155
type(self._provider).__name__,
139156
)
140-
self._tracer: Tracer = self._provider.get_tracer(instrument_name)
157+
self._tracer: Tracer = self._provider.get_tracer(self._config.instrument_name)
158+
159+
try:
160+
register_standalone_instrumentations(self._config, result)
161+
except Exception:
162+
logger.exception("Failed to register standalone instrumentations")
141163

142164
# per invocation status:
143165
self._execution_arn = ""

0 commit comments

Comments
 (0)