6868import io .grpc .MethodDescriptor ;
6969import io .opentelemetry .api .GlobalOpenTelemetry ;
7070import io .opentelemetry .api .common .AttributeKey ;
71+ import io .opentelemetry .api .trace .Span ;
7172import io .opentelemetry .api .trace .SpanKind ;
73+ import io .opentelemetry .api .trace .Tracer ;
74+ import io .opentelemetry .context .Scope ;
7275import io .opentelemetry .sdk .OpenTelemetrySdk ;
7376import io .opentelemetry .sdk .testing .exporter .InMemorySpanExporter ;
7477import io .opentelemetry .sdk .trace .SdkTracerProvider ;
@@ -129,6 +132,7 @@ class ITOtelTracing {
129132
130133 private InMemorySpanExporter spanExporter ;
131134 private OpenTelemetrySdk openTelemetrySdk ;
135+ private Tracer tracer ;
132136
133137 @ BeforeEach
134138 void setup () {
@@ -141,6 +145,7 @@ void setup() {
141145
142146 openTelemetrySdk =
143147 OpenTelemetrySdk .builder ().setTracerProvider (tracerProvider ).buildAndRegisterGlobal ();
148+ tracer = openTelemetrySdk .getTracer ("it-otel-tracing-test" );
144149 }
145150
146151 @ AfterEach
@@ -456,6 +461,69 @@ void testTracing_retry_grpc() throws Exception {
456461 assertThat (resendCounts ).containsExactlyElementsIn (expectedCounts ).inOrder ();
457462 }
458463
464+ /**
465+ * Verifies that when an RPC is invoked within an active OpenTelemetry trace context, all attempt
466+ * spans (including the initial call and retries scheduled on background threads) properly link to
467+ * the parent span context using gRPC transport.
468+ */
469+ @ Test
470+ void testTracing_retry_grpc_linkedToParentContext () throws Exception {
471+ final int attempts = 5 ;
472+ final StatusCode .Code statusCode = StatusCode .Code .UNAVAILABLE ;
473+ RetrySettings retrySettings =
474+ RetrySettings .newBuilder ()
475+ .setTotalTimeout (org .threeten .bp .Duration .ofMillis (5000L ))
476+ .setMaxAttempts (attempts )
477+ .build ();
478+
479+ OpenTelemetryTracingFactory tracingFactory = new OpenTelemetryTracingFactory (openTelemetrySdk );
480+
481+ EchoSettings grpcEchoSettings = createEchoSettings (false );
482+ EchoStubSettings .Builder grpcEchoSettingsBuilder =
483+ (EchoStubSettings .Builder ) grpcEchoSettings .getStubSettings ().toBuilder ();
484+ grpcEchoSettingsBuilder
485+ .echoSettings ()
486+ .setRetrySettings (retrySettings )
487+ .setRetryableCodes (statusCode );
488+ grpcEchoSettingsBuilder .setTracerFactory (tracingFactory );
489+ EchoStub stub = new ExtendedEchoStubSettings (grpcEchoSettingsBuilder ).createStub ();
490+ EchoClient grpcClient = EchoClient .create (stub );
491+
492+ EchoRequest echoRequest =
493+ EchoRequest .newBuilder ()
494+ .setError (Status .newBuilder ().setCode (statusCode .ordinal ()).build ())
495+ .build ();
496+
497+ Span parentSpan = tracer .spanBuilder ("application-parent-operation" ).startSpan ();
498+ try (Scope scope = parentSpan .makeCurrent ()) {
499+ assertThrows (UnavailableException .class , () -> grpcClient .echo (echoRequest ));
500+ } finally {
501+ parentSpan .end ();
502+ }
503+
504+ List <SpanData > spans = spanExporter .getFinishedSpanItems ();
505+ assertThat (spans ).hasSize (attempts + 1 );
506+
507+ SpanData rootSpan =
508+ spans .stream ()
509+ .filter (span -> span .getName ().equals ("application-parent-operation" ))
510+ .findFirst ()
511+ .orElseThrow (() -> new AssertionError ("Parent span not found" ));
512+
513+ List <SpanData > attemptSpans =
514+ spans .stream ()
515+ .filter (span -> span .getName ().equals (SPAN_NAME_ECHO_GRPC ))
516+ .collect (java .util .stream .Collectors .toList ());
517+ assertThat (attemptSpans ).hasSize (attempts );
518+
519+ for (SpanData attemptSpan : attemptSpans ) {
520+ assertThat (attemptSpan .getKind ()).isEqualTo (SpanKind .CLIENT );
521+ assertThat (attemptSpan .getParentSpanId ()).isEqualTo (rootSpan .getSpanContext ().getSpanId ());
522+ assertThat (attemptSpan .getSpanContext ().getTraceId ())
523+ .isEqualTo (rootSpan .getSpanContext ().getTraceId ());
524+ }
525+ }
526+
459527 @ Test
460528 void testTracing_retry_httpjson () throws Exception {
461529 final int attempts = 5 ;
@@ -534,6 +602,69 @@ void testTracing_retry_httpjson() throws Exception {
534602 assertThat (resendCounts ).containsExactlyElementsIn (expectedCounts ).inOrder ();
535603 }
536604
605+ /**
606+ * Verifies that when an RPC is invoked within an active OpenTelemetry trace context, all attempt
607+ * spans (including the initial call and retries scheduled on background threads) properly link to
608+ * the parent span context using HTTP/JSON transport.
609+ */
610+ @ Test
611+ void testTracing_retry_httpjson_linkedToParentContext () throws Exception {
612+ final int attempts = 5 ;
613+ final StatusCode .Code statusCode = StatusCode .Code .INVALID_ARGUMENT ;
614+ RetrySettings retrySettings =
615+ RetrySettings .newBuilder ()
616+ .setTotalTimeout (org .threeten .bp .Duration .ofMillis (5000L ))
617+ .setMaxAttempts (attempts )
618+ .build ();
619+
620+ OpenTelemetryTracingFactory tracingFactory = new OpenTelemetryTracingFactory (openTelemetrySdk );
621+
622+ EchoSettings httpJsonEchoSettings = createEchoSettings (true );
623+ EchoStubSettings .Builder httpJsonEchoSettingsBuilder =
624+ (EchoStubSettings .Builder ) httpJsonEchoSettings .getStubSettings ().toBuilder ();
625+ httpJsonEchoSettingsBuilder
626+ .echoSettings ()
627+ .setRetrySettings (retrySettings )
628+ .setRetryableCodes (statusCode );
629+ httpJsonEchoSettingsBuilder .setTracerFactory (tracingFactory );
630+ EchoStub stub = new ExtendedEchoStubSettings (httpJsonEchoSettingsBuilder ).createStub ();
631+ EchoClient httpClient = EchoClient .create (stub );
632+
633+ EchoRequest echoRequest =
634+ EchoRequest .newBuilder ()
635+ .setError (Status .newBuilder ().setCode (statusCode .ordinal ()).build ())
636+ .build ();
637+
638+ Span parentSpan = tracer .spanBuilder ("application-parent-operation" ).startSpan ();
639+ try (Scope scope = parentSpan .makeCurrent ()) {
640+ assertThrows (InvalidArgumentException .class , () -> httpClient .echo (echoRequest ));
641+ } finally {
642+ parentSpan .end ();
643+ }
644+
645+ List <SpanData > spans = spanExporter .getFinishedSpanItems ();
646+ assertThat (spans ).hasSize (attempts + 1 );
647+
648+ SpanData rootSpan =
649+ spans .stream ()
650+ .filter (span -> span .getName ().equals ("application-parent-operation" ))
651+ .findFirst ()
652+ .orElseThrow (() -> new AssertionError ("Parent span not found" ));
653+
654+ List <SpanData > attemptSpans =
655+ spans .stream ()
656+ .filter (span -> span .getName ().equals (SPAN_NAME_ECHO_HTTP ))
657+ .collect (java .util .stream .Collectors .toList ());
658+ assertThat (attemptSpans ).hasSize (attempts );
659+
660+ for (SpanData attemptSpan : attemptSpans ) {
661+ assertThat (attemptSpan .getKind ()).isEqualTo (SpanKind .CLIENT );
662+ assertThat (attemptSpan .getParentSpanId ()).isEqualTo (rootSpan .getSpanContext ().getSpanId ());
663+ assertThat (attemptSpan .getSpanContext ().getTraceId ())
664+ .isEqualTo (rootSpan .getSpanContext ().getTraceId ());
665+ }
666+ }
667+
537668 private SpanData getErrorSpan () {
538669 Awaitility .await ()
539670 .atMost (Duration .ofSeconds (5 ))
0 commit comments