Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@

from typing import Any

from aws_durable_execution_sdk_python_otel import InvocationOtelPlugin
from aws_durable_execution_sdk_python_otel import (
InvocationOtelPlugin,
OtelPluginConfig,
ProviderSource,
)

from aws_durable_execution_sdk_python import StepContext
from aws_durable_execution_sdk_python.context import (
Expand Down Expand Up @@ -44,7 +48,11 @@ def greet_in_child(child_context: DurableContext, name: str) -> str:
return result


@durable_execution(plugins=[InvocationOtelPlugin()])
@durable_execution(
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.
context.logger.info("Workflow started")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@

from typing import Any

from aws_durable_execution_sdk_python_otel import InvocationOtelPlugin
from aws_durable_execution_sdk_python_otel import (
InvocationOtelPlugin,
OtelPluginConfig,
ProviderSource,
)

from aws_durable_execution_sdk_python import StepContext
from aws_durable_execution_sdk_python.config import Duration
Expand Down Expand Up @@ -32,7 +36,11 @@ def add_numbers_in_child(child_context: DurableContext, a: int, b: int):
return result


@durable_execution(plugins=[InvocationOtelPlugin()])
@durable_execution(
plugins=[
InvocationOtelPlugin(OtelPluginConfig(provider_source=ProviderSource.GLOBAL))
]
)
def handler(_event: Any, context: DurableContext) -> int:
result = 0
for i in range(3):
Expand Down
44 changes: 27 additions & 17 deletions packages/aws-durable-execution-sdk-python-otel/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment on lines +182 to +183

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These new doc comments are stale and contradict the API this PR ships:

  • They reference use_default_tracer_provider, which this PR removes from OtelPluginConfig. Passing it now raises TypeError (unknown field). The same dead reference appears again in the API-reference block below (line 264: # ...and the rest of OtelPluginConfig (use_default_tracer_provider, ...)).
  • "When omitted, an OTLP provider is auto-configured" is incorrect: the new default is provider_source=GLOBAL (uses trace.get_tracer_provider()), not AUTO_OTLP. Auto-configuration now requires provider_source=ProviderSource.AUTO_OTLP.
  • The tracer_provider=None example with the comment "Provide your own TracerProvider if you already have one configured" is misleading — supplying a provider now requires provider_source=ProviderSource.EXPLICIT, otherwise OtelPluginConfig.__post_init__ raises ValueError.

Suggest rewording these comments to describe provider_source (GLOBAL default; AUTO_OTLP to build/own an OTLP provider; EXPLICIT + tracer_provider to supply your own) and dropping the use_default_tracer_provider mentions here and at line 264.

# (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,
)
)
```

Expand All @@ -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
Expand Down Expand Up @@ -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).
)
)
```

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from aws_durable_execution_sdk_python_otel.otel_plugin_config import (
OtelPluginConfig,
ExporterConfig,
ProviderSource,
)
from aws_durable_execution_sdk_python_otel.instrumentations import (
register_standalone_instrumentations,
Expand Down Expand Up @@ -44,6 +45,7 @@
"InvocationOtelPlugin",
"OtelContextLogFilter",
"ProviderResult",
"ProviderSource",
"create_tracer_provider",
"derive_workflow_span_id",
"install_log_filter",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -92,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:
Expand All @@ -101,16 +103,17 @@ 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 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
Expand All @@ -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")

Expand Down Expand Up @@ -244,7 +242,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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -22,13 +22,14 @@
import os
from typing import TYPE_CHECKING, Any

from aws_durable_execution_sdk_python_otel.otel_plugin_config 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__)
Expand Down Expand Up @@ -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)
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)


Expand Down Expand Up @@ -80,42 +84,56 @@ 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). 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"

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, 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
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
# 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,
id_generator=self._id_generator,
)
self._provider = result.tracer_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
Expand All @@ -128,16 +146,20 @@ 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, result)
except Exception:
logger.exception("Failed to register standalone instrumentations")

# per invocation status:
self._execution_arn = ""
Expand Down
Loading
Loading