55import static software .amazon .lambda .durable .otel .SpanAttributes .*;
66
77import io .opentelemetry .api .common .AttributeKey ;
8+ import io .opentelemetry .api .common .Attributes ;
89import io .opentelemetry .api .trace .Span ;
910import io .opentelemetry .api .trace .SpanContext ;
11+ import io .opentelemetry .api .trace .SpanKind ;
1012import io .opentelemetry .api .trace .StatusCode ;
1113import io .opentelemetry .api .trace .TraceFlags ;
1214import io .opentelemetry .api .trace .TraceState ;
1315import io .opentelemetry .api .trace .Tracer ;
1416import io .opentelemetry .context .Context ;
1517import io .opentelemetry .context .Scope ;
18+ import io .opentelemetry .sdk .resources .Resource ;
1619import io .opentelemetry .sdk .trace .SdkTracerProvider ;
1720import io .opentelemetry .sdk .trace .SdkTracerProviderBuilder ;
1821import 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 ) {
0 commit comments