diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 4a4bd63fb..dcc1eae27 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -71,6 +71,34 @@ jobs: - name: Generate SAM template run: python3 generate-template.py working-directory: ./examples + - name: Resolve latest ADOT Java layer ARN + env: + GH_TOKEN: ${{ github.token }} + run: | + # The ADOT Java layer is regional: account ID and version vary per region. + # Resolve the current ARN for AWS_REGION from the release notes table (region | ARN). + ADOT_LAYER_ARN=$( + gh api repos/aws-observability/aws-otel-java-instrumentation/releases/latest \ + --jq .body | + awk -F '|' -v region="$AWS_REGION" ' + $2 ~ "^[[:space:]]*" region "[[:space:]]*$" { + gsub(/^[[:space:]]+|[[:space:]]+$/, "", $3) + print $3 + exit + } + ' + ) + if [ -z "$ADOT_LAYER_ARN" ]; then + echo "Could not resolve the latest ADOT Java layer for $AWS_REGION" + exit 1 + fi + aws lambda get-layer-version-by-arn \ + --arn "$ADOT_LAYER_ARN" \ + --region "$AWS_REGION" \ + --query LayerVersionArn \ + --output text > /dev/null + echo "Using ADOT Java layer $ADOT_LAYER_ARN" + echo "ADOT_LAYER_ARN=$ADOT_LAYER_ARN" >> "$GITHUB_ENV" - name: sam build env: MAVEN_OPTS: -DskipTests=true -Dmaven.test.skip=true @@ -87,7 +115,7 @@ jobs: run: | sam deploy --stack-name Java${{ matrix.java }}-JavaSDKCloudBasedIntegrationTestStack \ --resolve-image-repos --resolve-s3 --parameter-overrides \ - 'ParameterKey=Architecture,ParameterValue=x86_64 ParameterKey=JavaVersion,ParameterValue=java${{ matrix.java }} ParameterKey=FunctionNamePrefix,ParameterValue=Java${{ matrix.java }}- ParameterKey=RoleArn,ParameterValue=${{ secrets.TEST_LAMBDA_EXECUTION_ROLE_ARN }}' + 'ParameterKey=Architecture,ParameterValue=x86_64 ParameterKey=JavaVersion,ParameterValue=java${{ matrix.java }} ParameterKey=FunctionNamePrefix,ParameterValue=Java${{ matrix.java }}- ParameterKey=RoleArn,ParameterValue=${{ secrets.TEST_LAMBDA_EXECUTION_ROLE_ARN }} ParameterKey=AdotLayerArn,ParameterValue='"$ADOT_LAYER_ARN" working-directory: ./examples - name: Cloud Based Integration Tests run: | diff --git a/examples/README.md b/examples/README.md index f10eac11d..fa24c9cc3 100644 --- a/examples/README.md +++ b/examples/README.md @@ -48,9 +48,26 @@ The SAM template configures: - `DurableConfig` with `ExecutionTimeout` and `RetentionPeriodInDays` - CloudWatch log groups for Lambda functions with 7 days of retention - IAM permissions for `lambda:CheckpointDurableExecutions` and `lambda:GetDurableExecutionState` +- ADOT tracing examples with active X-Ray tracing and the ADOT Lambda layer +- OTel examples that use `new OtelPlugin()` with `AWS_LAMBDA_EXEC_WRAPPER` and `OTEL_JAVAAGENT_EXTENSIONS` so the OTel plugin SPI is loaded by the Java agent `template.yaml` is generated from the Java example handlers and is intentionally not checked in. Re-run `python3 generate-template.py` after adding or removing a deployable example handler. +The examples package copies the OTel plugin jar into `lib/` so the ADOT Java agent can load it as an extension for examples that enable `OTEL_JAVAAGENT_EXTENSIONS`. + +### ADOT layer region + +The ADOT layer is regional — its account ID and version vary by region — so the template exposes it as the `AdotLayerArn` parameter. The default targets `us-west-2` (the region used by the e2e tests), and the e2e workflow resolves the current ARN for its region automatically. + +If you deploy the tracing examples to any other region, override the parameter with that region's ARN, otherwise the deploy fails CloudFormation's `ResourceExistenceCheck` on a cross-region layer: + +```bash +sam deploy --parameter-overrides \ + AdotLayerArn=arn:aws:lambda:us-east-1:615299751070:layer:AWSOpenTelemetryDistroJava:16 +``` + +Find the current ARN for your region in the [ADOT Java instrumentation releases](https://github.com/aws-observability/aws-otel-java-instrumentation/releases/latest). You can also persist the override in your (git-ignored) `samconfig.toml` under `parameter_overrides`. + ## Invoke Deployed Functions ```bash @@ -96,6 +113,7 @@ mvn test -Dtest=CloudBasedIntegrationTest \ | [CustomShouldCompleteMapExample](src/main/java/software/amazon/lambda/durable/examples/map/CustomShouldCompleteMapExample.java) | Custom map completion with `shouldComplete` decisions | | [WaitForConditionExample](src/main/java/software/amazon/lambda/durable/examples/wait/WaitForConditionExample.java) | Poll a condition until met with `waitForCondition()` | | [OtelExample](src/main/java/software/amazon/lambda/durable/examples/general/OtelExample.java) | OpenTelemetry instrumentation with logging span export | +| [OtelXRayDefaultConstructorExample](src/main/java/software/amazon/lambda/durable/examples/otel/OtelXRayDefaultConstructorExample.java) | Export spans to X-Ray with `new OtelPlugin()` and no handler-side OpenTelemetry initialization | | [OtelXRayStepExample](src/main/java/software/amazon/lambda/durable/examples/otel/OtelXRayStepExample.java) | Export step spans to X-Ray through the ADOT Lambda Layer | | [OtelXRayWaitExample](src/main/java/software/amazon/lambda/durable/examples/otel/OtelXRayWaitExample.java) | Trace a step-wait-step workflow across Lambda invocations | | [OtelXRayMapExample](src/main/java/software/amazon/lambda/durable/examples/otel/OtelXRayMapExample.java) | Trace concurrent map operations and item steps in X-Ray | diff --git a/examples/generate-template.py b/examples/generate-template.py index 75e6cce09..6b1607fe6 100755 --- a/examples/generate-template.py +++ b/examples/generate-template.py @@ -4,6 +4,7 @@ import argparse import re +import xml.etree.ElementTree as ET from dataclasses import dataclass from pathlib import Path @@ -13,6 +14,17 @@ EXAMPLE_PACKAGE_ROOT = SOURCE_ROOT / "software/amazon/lambda/durable/examples" DEFAULT_OUTPUT = EXAMPLES_DIR / "template.yaml" TEMPLATE_ANNOTATION = "ExampleTemplate" +POM_NAMESPACE = {"m": "http://maven.apache.org/POM/4.0.0"} + + +def read_otel_plugin_jar_path() -> str: + root = ET.parse(EXAMPLES_DIR / "pom.xml").getroot() + version = root.findtext("m:version", namespaces=POM_NAMESPACE) + if version is None: + version = root.findtext("m:parent/m:version", namespaces=POM_NAMESPACE) + if version is None: + raise ValueError("Unable to read examples version from pom.xml") + return f"/var/task/lib/aws-durable-execution-sdk-java-plugin-otel-{version}.jar" @dataclass(frozen=True) @@ -22,6 +34,7 @@ class ExampleFunction: suffix: str condition: str | None tracing: bool + java_agent: bool @property def logical_id(self) -> str: @@ -58,24 +71,26 @@ def is_top_level_durable_handler(source: str, class_name: str) -> bool: return bool(match and "extends DurableHandler" in match.group("header")) -def read_template_annotation(source: str, class_name: str) -> tuple[str | None, bool]: +def read_template_annotation(source: str, class_name: str) -> tuple[str | None, bool, bool]: class_match = re.search(rf"public\s+(?:final\s+)?class\s+{class_name}\b", source) if not class_match: - return None, False + return None, False, False prefix = source[: class_match.start()] matches = list( re.finditer(rf"@(?:[A-Za-z_][\w.]*\.)?{TEMPLATE_ANNOTATION}\s*(?:\((?P
.*?)\))?", prefix, re.DOTALL) ) if not matches: - return None, False + return None, False, False body = matches[-1].group("body") or "" condition_match = re.search(r'condition\s*=\s*"([^"]+)"', body) tracing_match = re.search(r"tracing\s*=\s*(true|false)", body) + java_agent_match = re.search(r"javaAgent\s*=\s*(true|false)", body) condition = condition_match.group(1) if condition_match else None tracing = tracing_match.group(1) == "true" if tracing_match else False - return condition, tracing + java_agent = java_agent_match.group(1) == "true" if java_agent_match else False + return condition, tracing, java_agent def discover_examples() -> list[ExampleFunction]: @@ -86,7 +101,7 @@ def discover_examples() -> list[ExampleFunction]: if not is_top_level_durable_handler(source, class_name): continue - condition, tracing = read_template_annotation(source, class_name) + condition, tracing, java_agent = read_template_annotation(source, class_name) package_name = read_package(source, path) examples.append( ExampleFunction( @@ -95,12 +110,13 @@ def discover_examples() -> list[ExampleFunction]: suffix=kebab_case(class_name), condition=condition, tracing=tracing, + java_agent=java_agent, ) ) return examples -def emit_function(lines: list[str], example: ExampleFunction) -> None: +def emit_function(lines: list[str], example: ExampleFunction, java_agent_extension_path: str) -> None: lines.extend( [ f" {example.logical_id}:", @@ -123,9 +139,17 @@ def emit_function(lines: list[str], example: ExampleFunction) -> None: [ " Tracing: Active", " Layers:", - " - !Sub", - " - arn:aws:lambda:${AWS::Region}:901920570463:layer:aws-otel-java-agent-${AdotArch}-ver-1-32-0:6", - " - AdotArch: amd64", + " - !Ref AdotLayerArn", + ] + ) + if example.java_agent: + lines.extend( + [ + " Environment:", + " Variables:", + " AWS_LAMBDA_EXEC_WRAPPER: /opt/otel-instrument", + f' JAVA_TOOL_OPTIONS: "-Dotel.javaagent.extensions={java_agent_extension_path}"', + f" OTEL_JAVAAGENT_EXTENSIONS: {java_agent_extension_path}", ] ) lines.append("") @@ -151,6 +175,7 @@ def emit_log_group(lines: list[str], example: ExampleFunction) -> None: def render_template(examples: list[ExampleFunction]) -> str: + java_agent_extension_path = read_otel_plugin_jar_path() lines = [ "# This file is generated by examples/generate-template.py. Do not edit it by hand.", 'AWSTemplateFormatVersion: "2010-09-09"', @@ -176,6 +201,14 @@ def render_template(examples: list[ExampleFunction]) -> str: " RoleArn:", " Type: String", " Description: IAM Role ARN for Lambda function execution", + " AdotLayerArn:", + " Type: String", + " Default: arn:aws:lambda:us-west-2:615299751070:layer:AWSOpenTelemetryDistroJava:16", + " Description: >-", + " ARN of the ADOT (AWS Distro for OpenTelemetry) Lambda layer used by the tracing examples.", + " The layer is regional: its account ID and version vary by region, so override this per", + " deployment region. The default targets us-west-2 (the region used by e2e tests); CI", + " resolves the latest ARN for its region.", "", "Conditions:", " IsJava21OrLater:", @@ -202,7 +235,7 @@ def render_template(examples: list[ExampleFunction]) -> str: for example in examples: emit_log_group(lines, example) - emit_function(lines, example) + emit_function(lines, example, java_agent_extension_path) lines.append("Outputs:") for index, example in enumerate(examples): diff --git a/examples/pom.xml b/examples/pom.xml index 7c3530cb2..da063dbd1 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -50,18 +50,6 @@In production, replace {@code LoggingSpanExporter} with {@code OtlpGrpcSpanExporter} to send spans to an OTLP - * collector (X-Ray, Datadog, etc.). + *
In production, replace {@code LoggingSpanExporter} with the exporter for your observability backend. * *
Expected trace structure: * *
- * durable.invocation - * ├── durable.step:create-greeting [attempt 1] - * ├── durable.step:create-greeting (operation, backfilled) - * ├── durable.step:transform [attempt 1] - * └── durable.step:transform (operation, backfilled) + * invocation + * ├── create-greeting + * │ └── create-greeting attempt 1 + * └── transform + * └── transform attempt 1 **/ public class OtelExample extends DurableHandler
{@link OtelPlugin#OtelPlugin()} uses the global provider initialized by the ADOT Java agent with deterministic
+ * span ID generation installed by the plugin's OpenTelemetry autoconfigure SPI.
+ */
+@ExampleTemplate(tracing = true, javaAgent = true)
+public class OtelXRayDefaultConstructorExample extends DurableHandler Exports spans through the ADOT Java agent global OpenTelemetry provider. Requires:
*
* Expected trace structure in X-Ray:
@@ -32,18 +30,12 @@
* └── transform attempt 1
*
*/
-@ExampleTemplate(tracing = true)
+@ExampleTemplate(tracing = true, javaAgent = true)
public class OtelXRayStepExample extends DurableHandler Exports spans via OTLP to the ADOT Lambda Layer collector. Requires:
+ * Exports spans through the ADOT Java agent global OpenTelemetry provider. Requires:
*
* Expected trace structure in X-Ray (all under one trace ID — backend propagates same Root):
*
* These tests deploy Lambda functions configured with:
*
*
*
*
*
*
*
*
* Trace (single trace ID across both invocations)
- * ├── durable.invocation (invocation 1)
- * │ ├── durable.step:before-wait
- * │ │ └── durable.step:before-wait [attempt 1]
- * │ └── durable.wait:pause (ended as PENDING)
- * └── durable.invocation (invocation 2)
- * ├── durable.wait:pause (completed)
- * └── durable.step:after-wait
- * └── durable.step:after-wait [attempt 1]
+ * ├── invocation (invocation 1)
+ * │ ├── before-wait
+ * │ │ └── before-wait attempt 1
+ * │ └── pause (ended as PENDING)
+ * └── invocation (invocation 2)
+ * ├── pause (completed)
+ * └── after-wait
+ * └── after-wait attempt 1
*
*/
-@ExampleTemplate(tracing = true)
+@ExampleTemplate(tracing = true, javaAgent = true)
public class OtelXRayWaitExample extends DurableHandler
- *
*
@@ -121,7 +126,7 @@ void simpleSteps_producesUnifiedTraceInXRay() throws Exception {
var uniqueInput = "XRay-" + System.currentTimeMillis();
var result = runner.run(new GreetingRequest(uniqueInput));
- assertEquals(ExecutionStatus.SUCCEEDED, result.getStatus(), "Execution failed: " + result);
+ assertExecutionSucceeded(result);
assertEquals("HELLO, " + uniqueInput.toUpperCase() + "!", result.getResult());
// 2. Wait for X-Ray ingestion
@@ -190,7 +195,7 @@ void waitAndResume_producesUnifiedTraceAcrossInvocations() throws Exception {
var uniqueInput = "Wait-" + System.currentTimeMillis();
var result = runner.run(new GreetingRequest(uniqueInput));
- assertEquals(ExecutionStatus.SUCCEEDED, result.getStatus(), "Execution failed: " + result);
+ assertExecutionSucceeded(result);
assertTrue(
result.getResult().contains("Resumed and completed"),
"Expected result to contain 'Resumed and completed', got: " + result.getResult());
@@ -251,8 +256,136 @@ void waitAndResume_producesUnifiedTraceAcrossInvocations() throws Exception {
+ invocationCount + " invocations in trace " + durableTrace.id());
}
+ @Test
+ void defaultConstructorWithAdotJavaAgent_producesDurableSpansInXRay() throws Exception {
+ var startTime = Instant.now();
+
+ var runner = CloudDurableTestRunner.create(
+ arn("otel-xray-default-constructor-example"), GreetingRequest.class, String.class, lambdaClient);
+ var uniqueInput = "Default-" + System.currentTimeMillis();
+ var execution = runner.startAsync(new GreetingRequest(uniqueInput));
+ var result = execution.pollUntilComplete();
+
+ assertExecutionSucceeded(result);
+ assertEquals("HELLO, " + uniqueInput.toUpperCase() + "!", result.getResult());
+
+ Thread.sleep(XRAY_INGESTION_DELAY.toMillis());
+
+ var traces = queryTracesWithRetry(startTime, Instant.now(), "otel-xray-default-constructor-example");
+ assertFalse(traces.isEmpty(), "Expected at least one trace in X-Ray after execution");
+
+ var allTraces = getFullTraces(traces);
+ var durableTrace = allTraces.stream()
+ .filter(trace ->
+ trace.segments().stream().anyMatch(seg -> segmentContains(seg, "default-create-greeting")))
+ .findFirst()
+ .orElse(null);
+
+ assertNotNull(
+ durableTrace,
+ "Expected to find a trace with default-create-greeting segment. Segment names: "
+ + allTraces.stream()
+ .flatMap(t -> t.segments().stream())
+ .map(CloudBasedOtelIntegrationTest::getSegmentName)
+ .collect(Collectors.joining(", ")));
+
+ var segmentDocuments =
+ durableTrace.segments().stream().map(Segment::document).toList();
+ var allSegmentText = String.join("\n", segmentDocuments);
+
+ assertTrue(allSegmentText.contains("invocation"), "Expected invocation span in trace");
+ assertTrue(
+ allSegmentText.contains("default-create-greeting"), "Expected default-create-greeting span in trace");
+ assertTrue(allSegmentText.contains("default-transform"), "Expected default-transform span in trace");
+ }
+
// ─── Helpers ─────────────────────────────────────────────────────────
+ private static void assertExecutionSucceeded(TestResult> result) {
+ assertTrue(
+ result.isSucceeded() && result.getError().isEmpty(),
+ () -> "Execution failed:\n" + summarizeExecutionHistory(result));
+ }
+
+ private static String summarizeExecutionHistory(TestResult> result) {
+ var failureSummary = result.getHistoryEvents().stream()
+ .map(CloudBasedOtelIntegrationTest::formatFailureEvent)
+ .filter(summary -> !summary.isBlank())
+ .collect(Collectors.joining("\n"));
+ if (!failureSummary.isBlank()) {
+ return failureSummary;
+ }
+
+ return result.getError()
+ .map(error -> "Execution error: " + formatError(error))
+ .orElseGet(() -> "Events: "
+ + result.getHistoryEvents().stream()
+ .map(event -> event.eventId() + ":" + event.eventType())
+ .collect(Collectors.joining(", ")));
+ }
+
+ private static String formatFailureEvent(Event event) {
+ if (EventType.INVOCATION_COMPLETED.equals(event.eventType())) {
+ var details = event.invocationCompletedDetails();
+ if (details != null && details.error() != null && details.error().payload() != null) {
+ return formatEventError(event, details.error().payload());
+ }
+ }
+
+ if (EventType.EXECUTION_FAILED.equals(event.eventType())) {
+ var details = event.executionFailedDetails();
+ if (details != null && details.error() != null && details.error().payload() != null) {
+ return formatEventError(event, details.error().payload());
+ }
+ }
+
+ if (EventType.EXECUTION_TIMED_OUT.equals(event.eventType())) {
+ var details = event.executionTimedOutDetails();
+ if (details != null && details.error() != null && details.error().payload() != null) {
+ return formatEventError(event, details.error().payload());
+ }
+ }
+
+ if (EventType.EXECUTION_STOPPED.equals(event.eventType())) {
+ var details = event.executionStoppedDetails();
+ if (details != null && details.error() != null && details.error().payload() != null) {
+ return formatEventError(event, details.error().payload());
+ }
+ }
+
+ return "";
+ }
+
+ private static String formatEventError(Event event, ErrorObject error) {
+ return event.eventType() + " event " + event.eventId() + ": " + formatError(error);
+ }
+
+ private static String formatError(ErrorObject error) {
+ var parts = new ArrayList
Requires the ADOT Lambda Layer for trace export. Configure with: * *
Note: Do NOT set {@code AWS_LAMBDA_EXEC_WRAPPER}. The collector extension runs independently. The wrapper would - * attach the auto-instrumentation agent which creates a competing TracerProvider. + *
When using {@link #OtelPlugin()}, the plugin requires {@code OtelPluginAutoConfigurationCustomizerProvider} to + * have been installed by the OpenTelemetry Java agent and uses the global provider directly. * *
Thread-safe: uses {@link ConcurrentHashMap} for span/scope storage since the SDK runs user code on multiple * threads. @@ -73,7 +78,7 @@ public class OtelPlugin implements DurableExecutionPlugin { private static final Logger logger = LoggerFactory.getLogger(OtelPlugin.class); private static final String INSTRUMENTATION_NAME = "aws-durable-execution-sdk-java"; - private final SdkTracerProvider tracerProvider; + private final SdkTracerProvider sdkTracerProvider; private final Tracer tracer; private final DeterministicIdGenerator idGenerator; private final ContextExtractor contextExtractor; @@ -99,12 +104,13 @@ public class OtelPlugin implements DurableExecutionPlugin { *
Uses the provided tracer provider builder. Customers configure exporters and span processors on the builder — * the plugin handles ID generation. * - *
For ADOT layer usage, configure with an OTLP exporter: + *
For ADOT Java agent usage, prefer {@link #OtelPlugin()} with the plugin jar configured through + * {@code OTEL_JAVAAGENT_EXTENSIONS}. Use this builder constructor when you want to own the exporter pipeline: * *
{@code
- * var otlpExporter = OtlpGrpcSpanExporter.getDefault(); // sends to localhost:4317
+ * var exporter = LoggingSpanExporter.create();
* var plugin = new OtelPlugin(
- * SdkTracerProvider.builder().addSpanProcessor(SimpleSpanProcessor.create(otlpExporter)));
+ * SdkTracerProvider.builder().addSpanProcessor(SimpleSpanProcessor.create(exporter)));
* }
*
* @param tracerProviderBuilder the tracer provider builder (ID generator will be overridden)
@@ -113,6 +119,16 @@ public OtelPlugin(SdkTracerProviderBuilder tracerProviderBuilder) {
this(tracerProviderBuilder, new XRayContextExtractor(), true);
}
+ /**
+ * Creates an OTel plugin with default settings: X-Ray context extraction and MDC enabled.
+ *
+ * Uses {@code GlobalOpenTelemetry} directly and assumes deterministic ID generation was installed by
+ * {@code OtelPluginAutoConfigurationCustomizerProvider}.
+ */
+ public OtelPlugin() {
+ this(getDefaultTracerProvider(), createDefaultIdGenerator());
+ }
+
/**
* Creates an OTel plugin with a custom context extractor, MDC enabled.
*
@@ -139,12 +155,22 @@ public OtelPlugin(
var resource = Resource.create(Attributes.of(ServiceAttributes.SERVICE_NAME, "invocation"));
tracerProviderBuilder.addResource(resource);
- this.tracerProvider = tracerProviderBuilder.setIdGenerator(idGenerator).build();
- this.tracer = tracerProvider.get(INSTRUMENTATION_NAME);
+ this.sdkTracerProvider =
+ tracerProviderBuilder.setIdGenerator(idGenerator).build();
+ this.tracer = sdkTracerProvider.get(INSTRUMENTATION_NAME);
this.contextExtractor = contextExtractor;
this.enableMdc = enableMdc;
}
+ private OtelPlugin(TracerProvider tracerProvider, DeterministicIdGenerator idGenerator) {
+ this.idGenerator = idGenerator;
+ this.sdkTracerProvider = getSdkTracerProviderForFlush(tracerProvider);
+ this.tracer = tracerProvider.get(INSTRUMENTATION_NAME);
+
+ this.contextExtractor = new XRayContextExtractor();
+ this.enableMdc = true;
+ }
+
// ─── Invocation hooks ────────────────────────────────────────────────
@Override
@@ -156,10 +182,15 @@ public void onInvocationStart(InvocationInfo info) {
// Extract trace context from environment (X-Ray header)
var extractedContext = contextExtractor.extract();
+ if (extractedContext == null) {
+ extractedContext = extractCurrentSpanContext();
+ }
if (extractedContext != null) {
// Use the X-Ray trace ID — backend propagates same Root across all invocations
idGenerator.setExtractedTraceId(extractedContext.traceId());
+ } else {
+ idGenerator.setExtractedTraceId(null);
}
// If no extracted context, idGenerator falls back to ARN-derived trace ID
@@ -167,7 +198,7 @@ public void onInvocationStart(InvocationInfo info) {
Context parentContext;
if (extractedContext != null && extractedContext.parentSpanId() != null) {
// X-Ray header has parent — create the invocation span as a child of that segment.
- // This connects our OTLP-exported spans to the Lambda service's X-Ray segments.
+ // This connects plugin spans to the Lambda service's X-Ray segments.
var parentSpanContext = SpanContext.createFromRemoteParent(
extractedContext.traceId(),
extractedContext.parentSpanId(),
@@ -228,10 +259,12 @@ public void onInvocationEnd(InvocationEndInfo info) {
invocationSpan.end();
invocationSpan = null;
- // Flush spans before Lambda freezes
- var flushResult = tracerProvider.forceFlush().join(5, java.util.concurrent.TimeUnit.SECONDS);
- if (!flushResult.isSuccess()) {
- logger.warn("OTel span flush failed or timed out — some spans may be lost");
+ if (sdkTracerProvider != null) {
+ // Flush spans before Lambda freezes
+ var flushResult = sdkTracerProvider.forceFlush().join(5, TimeUnit.SECONDS);
+ if (!flushResult.isSuccess()) {
+ logger.warn("OTel span flush failed or timed out — some spans may be lost");
+ }
}
}
@@ -465,4 +498,82 @@ private static String attemptSpanName(String type, String subType, String name,
private static String attemptKey(String operationId, Integer attempt) {
return operationId + "-" + (attempt != null ? attempt : "ctx");
}
+
+ private static ExtractedContext extractCurrentSpanContext() {
+ var spanContext = Span.current().getSpanContext();
+ if (!spanContext.isValid()) {
+ return null;
+ }
+ return new ExtractedContext(spanContext.getTraceId(), spanContext.getSpanId());
+ }
+
+ private static TracerProvider getDefaultTracerProvider() {
+ validateAutoConfigurationCustomizerProviderInstalled();
+
+ var globalTracerProvider = GlobalOpenTelemetry.getTracerProvider();
+ if (globalTracerProvider == TracerProvider.noop()) {
+ throw new IllegalStateException("OtelPlugin() requires GlobalOpenTelemetry to be initialized by "
+ + "OtelPluginAutoConfigurationCustomizerProvider through the OpenTelemetry Java agent.");
+ }
+ logger.info(
+ "OtelPlugin initialized from existing GlobalOpenTelemetry tracer provider {}; assuming deterministic "
+ + "span IDs were installed through AutoConfigurationCustomizerProvider",
+ globalTracerProvider.getClass().getName());
+ return globalTracerProvider;
+ }
+
+ private static DeterministicIdGenerator createDefaultIdGenerator() {
+ // This is intentionally a separate instance from the SPI provider's generator. The Java agent extension and
+ // application may load this plugin in different class loaders, so DeterministicIdGenerator bridges invocation
+ // state through system properties that the SPI-installed generator can read when spans are started.
+ return new DeterministicIdGenerator();
+ }
+
+ private static void validateAutoConfigurationCustomizerProviderInstalled() {
+ if (OtelPluginAutoConfigurationState.isInstalled()) {
+ return;
+ }
+ throw new IllegalStateException(
+ "OtelPlugin() requires OtelPluginAutoConfigurationCustomizerProvider to be installed by the "
+ + "OpenTelemetry Java agent. Package this plugin jar as an agent extension and set "
+ + "OTEL_JAVAAGENT_EXTENSIONS or -Dotel.javaagent.extensions to that jar before constructing "
+ + "OtelPlugin(). "
+ + javaAgentExtensionsDiagnostic());
+ }
+
+ private static String javaAgentExtensionsDiagnostic() {
+ var propertyValue = System.getProperty("otel.javaagent.extensions");
+ var environmentValue = System.getenv("OTEL_JAVAAGENT_EXTENSIONS");
+ var configuredPath = propertyValue != null ? propertyValue : environmentValue;
+ return "otel.javaagent.extensions="
+ + valueOrUnset(propertyValue)
+ + ", OTEL_JAVAAGENT_EXTENSIONS="
+ + valueOrUnset(environmentValue)
+ + ", configured extension path exists="
+ + extensionPathExists(configuredPath);
+ }
+
+ private static String valueOrUnset(String value) {
+ return value != null ? value : "