Skip to content

Commit 5082230

Browse files
committed
fix(otel): Fix X-Ray trace integration and cross-invocation retry spans
1 parent 0097f4a commit 5082230

8 files changed

Lines changed: 246 additions & 39 deletions

File tree

examples/template.yaml

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -370,9 +370,6 @@ Resources:
370370
- !Sub
371371
- arn:aws:lambda:${AWS::Region}:901920570463:layer:aws-otel-java-agent-${AdotArch}-ver-1-32-0:6
372372
- AdotArch: amd64
373-
Environment:
374-
Variables:
375-
AWS_LAMBDA_EXEC_WRAPPER: /opt/otel-handler
376373

377374
OtelXRayWaitExampleFunction:
378375
Type: AWS::Serverless::Function
@@ -389,9 +386,6 @@ Resources:
389386
- !Sub
390387
- arn:aws:lambda:${AWS::Region}:901920570463:layer:aws-otel-java-agent-${AdotArch}-ver-1-32-0:6
391388
- AdotArch: amd64
392-
Environment:
393-
Variables:
394-
AWS_LAMBDA_EXEC_WRAPPER: /opt/otel-handler
395389

396390
RetryInvokeExampleFunction:
397391
Type: AWS::Serverless::Function

otel-plugin/src/main/java/software/amazon/lambda/durable/otel/OtelPlugin.java

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,17 @@
55
import static software.amazon.lambda.durable.otel.SpanAttributes.*;
66

77
import io.opentelemetry.api.common.AttributeKey;
8+
import io.opentelemetry.api.common.Attributes;
89
import io.opentelemetry.api.trace.Span;
910
import io.opentelemetry.api.trace.SpanContext;
11+
import io.opentelemetry.api.trace.SpanKind;
1012
import io.opentelemetry.api.trace.StatusCode;
1113
import io.opentelemetry.api.trace.TraceFlags;
1214
import io.opentelemetry.api.trace.TraceState;
1315
import io.opentelemetry.api.trace.Tracer;
1416
import io.opentelemetry.context.Context;
1517
import io.opentelemetry.context.Scope;
18+
import io.opentelemetry.sdk.resources.Resource;
1619
import io.opentelemetry.sdk.trace.SdkTracerProvider;
1720
import io.opentelemetry.sdk.trace.SdkTracerProviderBuilder;
1821
import java.time.Instant;
@@ -128,6 +131,12 @@ public OtelPlugin(SdkTracerProviderBuilder tracerProviderBuilder, ContextExtract
128131
public OtelPlugin(
129132
SdkTracerProviderBuilder tracerProviderBuilder, ContextExtractor contextExtractor, boolean enableMdc) {
130133
this.idGenerator = new DeterministicIdGenerator();
134+
135+
// Set service.name to "invocation" — X-Ray uses this as the display name for SERVER spans,
136+
// creating a separate service node in the trace map labeled "invocation".
137+
var resource = Resource.create(Attributes.of(AttributeKey.stringKey("service.name"), "invocation"));
138+
tracerProviderBuilder.setResource(resource);
139+
131140
this.tracerProvider = tracerProviderBuilder.setIdGenerator(idGenerator).build();
132141
this.tracer = tracerProvider.get(INSTRUMENTATION_NAME);
133142
this.contextExtractor = contextExtractor;
@@ -152,10 +161,17 @@ public void onInvocationStart(InvocationInfo info) {
152161
}
153162
// If no extracted context, idGenerator falls back to ARN-derived trace ID
154163

155-
// Determine parent context for the invocation span
164+
// Determine parent context for the invocation span.
165+
// When the X-Ray header has a Parent field, create the invocation span as a
166+
// child of that segment. This connects our OTLP-exported spans to the Lambda
167+
// service's X-Ray segments.
156168
Context parentContext;
157-
if (extractedContext != null && extractedContext.parentSpanId() != null) {
158-
// Create a remote parent span context from the X-Ray Parent field
169+
var activeSpan = Span.fromContext(Context.current());
170+
if (activeSpan.getSpanContext().isValid()) {
171+
// An auto-instrumenter (e.g., ADOT agent) is active — parent to its span
172+
parentContext = Context.current();
173+
} else if (extractedContext != null && extractedContext.parentSpanId() != null) {
174+
// No active instrumenter, but X-Ray header has parent — create remote parent
159175
var parentSpanContext = SpanContext.createFromRemoteParent(
160176
extractedContext.traceId(),
161177
extractedContext.parentSpanId(),
@@ -166,8 +182,10 @@ public void onInvocationStart(InvocationInfo info) {
166182
parentContext = Context.root();
167183
}
168184

169-
// Create invocation span as child of Lambda's X-Ray segment (via Parent field)
170-
var spanBuilder = tracer.spanBuilder("durable.invocation")
185+
// Create a SERVER span to establish a separate X-Ray service node.
186+
// X-Ray uses service.name for the segment display name.
187+
var spanBuilder = tracer.spanBuilder("invocation")
188+
.setSpanKind(SpanKind.SERVER)
171189
.setParent(parentContext)
172190
.setAttribute(DURABLE_EXECUTION_ARN, info.durableExecutionArn())
173191
.setAttribute(DURABLE_FIRST_INVOCATION, info.isFirstInvocation());
@@ -227,8 +245,6 @@ public void onInvocationEnd(InvocationEndInfo info) {
227245
public void onOperationStart(OperationInfo info) {
228246
if (info.id() == null) return;
229247

230-
idGenerator.setNextSpanOperationId(info.id());
231-
232248
var parentContext = resolveParentContext(info.parentId());
233249

234250
var spanBuilder = tracer.spanBuilder(spanName(info.type(), info.subType(), info.name()))
@@ -237,6 +253,19 @@ public void onOperationStart(OperationInfo info) {
237253
.setAttribute(DURABLE_OPERATION_ID, info.id())
238254
.setAttribute(DURABLE_OPERATION_TYPE, info.type());
239255

256+
if (info.isContinuation()) {
257+
// Operation was already started in a prior invocation — use a random span ID
258+
// and add a Link to the deterministic span from the original invocation for correlation.
259+
var deterministicSpanId = idGenerator.generateSpanIdForOperation(info.id());
260+
var traceId = idGenerator.generateTraceId();
261+
var linkedSpanContext =
262+
SpanContext.create(traceId, deterministicSpanId, TraceFlags.getSampled(), TraceState.getDefault());
263+
spanBuilder.addLink(linkedSpanContext);
264+
} else {
265+
// First execution — use deterministic span ID so continuations can link back
266+
idGenerator.setNextSpanOperationId(info.id());
267+
}
268+
240269
if (info.name() != null) {
241270
spanBuilder.setAttribute(DURABLE_OPERATION_NAME, info.name());
242271
}
@@ -400,17 +429,17 @@ private Context resolveParentContext(String parentId) {
400429

401430
private static String spanName(String type, String subType, String name) {
402431
if (name != null) {
403-
return "durable." + (subType != null ? subType.toLowerCase() : type.toLowerCase()) + ":" + name;
432+
return name;
404433
}
405-
return "durable." + (subType != null ? subType.toLowerCase() : type.toLowerCase());
434+
return subType != null ? subType.toLowerCase() : type.toLowerCase();
406435
}
407436

408437
private static String attemptSpanName(String type, String subType, String name, Integer attempt) {
409438
var base = spanName(type, subType, name);
410439
if (attempt != null) {
411-
return base + " [attempt " + attempt + "]";
440+
return base + " attempt " + attempt;
412441
}
413-
return base + " [fn]";
442+
return base;
414443
}
415444

416445
private static String attemptKey(String operationId, Integer attempt) {

otel-plugin/src/main/java/software/amazon/lambda/durable/otel/XRayContextExtractor.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,21 @@ public class XRayContextExtractor implements ContextExtractor {
2525

2626
private static final Logger logger = LoggerFactory.getLogger(XRayContextExtractor.class);
2727
private static final String XRAY_ENV_VAR = "_X_AMZN_TRACE_ID";
28+
private static final String XRAY_SYSTEM_PROPERTY = "com.amazonaws.xray.traceHeader";
2829
private static final Pattern HEX_32 = Pattern.compile("[0-9a-f]{32}");
2930
private static final Pattern HEX_16 = Pattern.compile("[0-9a-f]{16}");
3031

3132
@Override
3233
public ExtractedContext extract() {
33-
var traceHeader = System.getenv(XRAY_ENV_VAR);
34+
// Try system property first — Lambda runtime updates this per invocation
35+
// and it avoids JVM environment variable caching issues.
36+
var traceHeader = System.getProperty(XRAY_SYSTEM_PROPERTY);
3437
if (traceHeader == null || traceHeader.isEmpty()) {
35-
logger.debug("No X-Ray trace header found in environment");
38+
// Fallback to environment variable
39+
traceHeader = System.getenv(XRAY_ENV_VAR);
40+
}
41+
if (traceHeader == null || traceHeader.isEmpty()) {
42+
logger.debug("No X-Ray trace header found in environment or system properties");
3643
return null;
3744
}
3845

0 commit comments

Comments
 (0)