diff --git a/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracerFactory.java b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracerFactory.java index f891795564f9..cfa074dd6e4a 100644 --- a/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracerFactory.java +++ b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracerFactory.java @@ -30,8 +30,10 @@ package com.google.api.gax.tracing; +import com.google.api.client.util.Strings; import com.google.api.core.BetaApi; import com.google.api.core.InternalApi; +import com.google.api.gax.rpc.LibraryMetadata; import com.google.common.annotations.VisibleForTesting; import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.api.trace.Tracer; @@ -48,12 +50,12 @@ @InternalApi public class SpanTracerFactory implements ApiTracerFactory { private final Tracer tracer; - + private final OpenTelemetry openTelemetry; private final ApiTracerContext apiTracerContext; /** Creates a SpanTracerFactory */ public SpanTracerFactory(OpenTelemetry openTelemetry) { - this(openTelemetry.getTracer("gax-java"), ApiTracerContext.empty()); + this(openTelemetry, null, ApiTracerContext.empty()); } /** @@ -62,13 +64,18 @@ public SpanTracerFactory(OpenTelemetry openTelemetry) { * internally. */ @VisibleForTesting - SpanTracerFactory(Tracer tracer, ApiTracerContext apiTracerContext) { + SpanTracerFactory(OpenTelemetry openTelemetry, Tracer tracer, ApiTracerContext apiTracerContext) { + this.openTelemetry = openTelemetry; this.tracer = tracer; this.apiTracerContext = apiTracerContext; } @Override public ApiTracer newTracer(ApiTracer parent, SpanName spanName, OperationType operationType) { + if (tracer == null) { + // Return a no-op tracer if withContext hasn't been called to initialize the tracer properly + return new BaseApiTracer(); + } // TODO(diegomarquezp): this is a placeholder for span names and will be adjusted as the // feature is developed. String attemptSpanName = spanName.getClientName() + "/" + spanName.getMethodName() + "/attempt"; @@ -78,6 +85,10 @@ public ApiTracer newTracer(ApiTracer parent, SpanName spanName, OperationType op @Override public ApiTracer newTracer(ApiTracer parent, ApiTracerContext apiTracerContext) { + if (tracer == null) { + // Return a no-op tracer if withContext hasn't been called to initialize the tracer properly + return new BaseApiTracer(); + } ApiTracerContext mergedContext = this.apiTracerContext.merge(apiTracerContext); return new SpanTracer(tracer, mergedContext); } @@ -87,8 +98,21 @@ public ApiTracerContext getApiTracerContext() { return apiTracerContext; } + /** + * Returns a new SpanTracerFactory with the provided context. The Tracer is re-initialized using + * the artifact name and version from the library metadata. + */ @Override public ApiTracerFactory withContext(ApiTracerContext context) { - return new SpanTracerFactory(tracer, apiTracerContext.merge(context)); + if (context == null) { + return new BaseApiTracerFactory(); + } + LibraryMetadata metadata = context.libraryMetadata(); + if (metadata == null || metadata.isEmpty() || Strings.isNullOrEmpty(metadata.artifactName())) { + return new BaseApiTracerFactory(); + } + Tracer newTracer = openTelemetry.getTracer(metadata.artifactName(), metadata.version()); + ApiTracerContext mergedContext = this.apiTracerContext.merge(context); + return new SpanTracerFactory(openTelemetry, newTracer, mergedContext); } } diff --git a/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/tracing/SpanTracerFactoryTest.java b/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/tracing/SpanTracerFactoryTest.java index 6054c67ca733..64bba80e33e5 100644 --- a/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/tracing/SpanTracerFactoryTest.java +++ b/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/tracing/SpanTracerFactoryTest.java @@ -33,6 +33,7 @@ import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -41,6 +42,7 @@ import com.google.api.gax.rpc.LibraryMetadata; import com.google.api.gax.tracing.ApiTracerContext.Transport; import com.google.api.gax.tracing.ApiTracerFactory.OperationType; +import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.trace.Span; @@ -54,25 +56,36 @@ import org.mockito.ArgumentCaptor; class SpanTracerFactoryTest { + private OpenTelemetry openTelemetry; private Tracer tracer; private SpanBuilder spanBuilder; private Span span; + private LibraryMetadata validMetadata; + @BeforeEach void setUp() { + openTelemetry = mock(OpenTelemetry.class); tracer = mock(Tracer.class); spanBuilder = mock(SpanBuilder.class); span = mock(Span.class); + when(openTelemetry.getTracer(nullable(String.class), nullable(String.class))) + .thenReturn(tracer); when(tracer.spanBuilder(anyString())).thenReturn(spanBuilder); when(spanBuilder.setSpanKind(any())).thenReturn(spanBuilder); when(spanBuilder.setAllAttributes(any(Attributes.class))).thenReturn(spanBuilder); when(spanBuilder.startSpan()).thenReturn(span); + + validMetadata = mock(LibraryMetadata.class); + when(validMetadata.artifactName()).thenReturn("gax-java"); + when(validMetadata.version()).thenReturn("2.1.0"); } @ParameterizedTest @ValueSource(booleans = {false, true}) void testNewTracer_createsSpanTracer(boolean useContext) { - SpanTracerFactory factory = new SpanTracerFactory(tracer, ApiTracerContext.empty()); + SpanTracerFactory factory = + new SpanTracerFactory(openTelemetry, tracer, ApiTracerContext.empty()); ApiTracer tracerInstance; if (useContext) { ApiTracerContext context = @@ -92,11 +105,12 @@ void testNewTracer_createsSpanTracer(boolean useContext) { @ParameterizedTest @ValueSource(booleans = {false, true}) void testNewTracer_addsAttributes(boolean useContext) { - ApiTracerFactory factory = new SpanTracerFactory(tracer, ApiTracerContext.empty()); + ApiTracerFactory factory = + new SpanTracerFactory(openTelemetry, tracer, ApiTracerContext.empty()); factory = factory.withContext( ApiTracerContext.newBuilder() - .setLibraryMetadata(LibraryMetadata.empty()) + .setLibraryMetadata(validMetadata) .setServerAddress("test-address") .build()); ApiTracer tracerInstance; @@ -105,7 +119,7 @@ void testNewTracer_addsAttributes(boolean useContext) { ApiTracerContext.newBuilder() .setFullMethodName("service/method") .setTransport(Transport.GRPC) - .setLibraryMetadata(LibraryMetadata.empty()) + .setLibraryMetadata(validMetadata) .build(); tracerInstance = factory.newTracer(null, context); } else { @@ -128,11 +142,12 @@ void testNewTracer_addsAttributes(boolean useContext) { void testWithContext_addsInferredAttributes(boolean useContext) { ApiTracerContext context = ApiTracerContext.newBuilder() - .setLibraryMetadata(LibraryMetadata.empty()) + .setLibraryMetadata(validMetadata) .setServerAddress("example.com") .build(); - SpanTracerFactory factory = new SpanTracerFactory(tracer, ApiTracerContext.empty()); + SpanTracerFactory factory = + new SpanTracerFactory(openTelemetry, tracer, ApiTracerContext.empty()); ApiTracerFactory factoryWithContext = factory.withContext(context); ApiTracer tracerInstance; @@ -141,7 +156,7 @@ void testWithContext_addsInferredAttributes(boolean useContext) { ApiTracerContext.newBuilder() .setFullMethodName("service/method") .setTransport(Transport.GRPC) - .setLibraryMetadata(LibraryMetadata.empty()) + .setLibraryMetadata(validMetadata) .build(); tracerInstance = factoryWithContext.newTracer(null, callContext); } else { @@ -164,9 +179,11 @@ void testWithContext_addsInferredAttributes(boolean useContext) { @ParameterizedTest @ValueSource(booleans = {false, true}) void testWithContext_noEndpointContext_doesNotAddServerAddressAttribute(boolean useContext) { - ApiTracerContext context = ApiTracerContext.empty(); + ApiTracerContext context = + ApiTracerContext.newBuilder().setLibraryMetadata(validMetadata).build(); - SpanTracerFactory factory = new SpanTracerFactory(tracer, ApiTracerContext.empty()); + SpanTracerFactory factory = + new SpanTracerFactory(openTelemetry, tracer, ApiTracerContext.empty()); ApiTracerFactory factoryWithContext = factory.withContext(context); ApiTracer tracerInstance; @@ -175,7 +192,7 @@ void testWithContext_noEndpointContext_doesNotAddServerAddressAttribute(boolean ApiTracerContext.newBuilder() .setFullMethodName("service/method") .setTransport(Transport.GRPC) - .setLibraryMetadata(LibraryMetadata.empty()) + .setLibraryMetadata(validMetadata) .build(); tracerInstance = factoryWithContext.newTracer(null, callContext); } else { @@ -203,7 +220,8 @@ void testNewTracer_withContext_grpc_usesFullMethodName() { .setLibraryMetadata(LibraryMetadata.empty()) .build(); - SpanTracerFactory factory = new SpanTracerFactory(tracer, ApiTracerContext.empty()); + SpanTracerFactory factory = + new SpanTracerFactory(openTelemetry, tracer, ApiTracerContext.empty()); ApiTracer tracerInstance = factory.newTracer(null, context); tracerInstance.attemptStarted(null, 1); @@ -229,7 +247,8 @@ void testNewTracer_withContext_http_usesHttpMethodAndPathTemplate( .setLibraryMetadata(LibraryMetadata.empty()) .build(); - SpanTracerFactory factory = new SpanTracerFactory(tracer, ApiTracerContext.empty()); + SpanTracerFactory factory = + new SpanTracerFactory(openTelemetry, tracer, ApiTracerContext.empty()); ApiTracer tracerInstance = factory.newTracer(null, context); tracerInstance.attemptStarted(null, 1); @@ -246,7 +265,8 @@ void testNewTracer_withContext_http_noHttpMethodOrPathTemplate_usesFullMethodNam .setLibraryMetadata(LibraryMetadata.empty()) .build(); - SpanTracerFactory factory = new SpanTracerFactory(tracer, ApiTracerContext.empty()); + SpanTracerFactory factory = + new SpanTracerFactory(openTelemetry, tracer, ApiTracerContext.empty()); ApiTracer tracerInstance = factory.newTracer(null, context); tracerInstance.attemptStarted(null, 1); @@ -256,7 +276,8 @@ void testNewTracer_withContext_http_noHttpMethodOrPathTemplate_usesFullMethodNam @Test void testNewTracer_withSpanName_usesPlaceholder() { - SpanTracerFactory factory = new SpanTracerFactory(tracer, ApiTracerContext.empty()); + SpanTracerFactory factory = + new SpanTracerFactory(openTelemetry, tracer, ApiTracerContext.empty()); ApiTracer tracerInstance = factory.newTracer(null, SpanName.of("Service", "Method"), OperationType.Unary); @@ -272,7 +293,7 @@ void testNewTracer_mergesFactoryContext() { .setServerAddress("factory-address") .setLibraryMetadata(LibraryMetadata.empty()) .build(); - SpanTracerFactory factory = new SpanTracerFactory(tracer, apiTracerContext); + SpanTracerFactory factory = new SpanTracerFactory(openTelemetry, tracer, apiTracerContext); ApiTracerContext callContext = ApiTracerContext.newBuilder() @@ -293,4 +314,51 @@ void testNewTracer_mergesFactoryContext() { assertThat(attributes.asMap()) .containsEntry(AttributeKey.stringKey("rpc.method"), "Service/Method"); } + + @Test + void testNoOpWhenTracerNull() { + SpanTracerFactory factory = + new SpanTracerFactory(openTelemetry, null, ApiTracerContext.empty()); + + ApiTracer tracerInstance = + factory.newTracer(null, SpanName.of("Service", "Method"), OperationType.Unary); + + assertThat(tracerInstance).isInstanceOf(BaseApiTracer.class); + + ApiTracer tracerInstance2 = factory.newTracer(null, ApiTracerContext.empty()); + + assertThat(tracerInstance2).isInstanceOf(BaseApiTracer.class); + } + + @Test + void testWithContext_nullContext_returnsBaseApiTracerFactory() { + SpanTracerFactory factory = + new SpanTracerFactory(openTelemetry, tracer, ApiTracerContext.empty()); + ApiTracerFactory factoryWithContext = factory.withContext(null); + assertThat(factoryWithContext).isInstanceOf(BaseApiTracerFactory.class); + } + + @Test + void testWithContext_nullMetadata_returnsBaseApiTracerFactory() { + SpanTracerFactory factory = + new SpanTracerFactory(openTelemetry, tracer, ApiTracerContext.empty()); + // Assuming ApiTracerContext.empty() has null libraryMetadata + ApiTracerFactory factoryWithContext = factory.withContext(ApiTracerContext.empty()); + assertThat(factoryWithContext).isInstanceOf(BaseApiTracerFactory.class); + } + + @Test + void testWithContext_nullTracer_returnsBaseApiTracerFactory() { + OpenTelemetry mockOpenTelemetry = mock(OpenTelemetry.class); + when(mockOpenTelemetry.getTracer(nullable(String.class), nullable(String.class))) + .thenReturn(null); + + SpanTracerFactory factory = + new SpanTracerFactory(mockOpenTelemetry, tracer, ApiTracerContext.empty()); + ApiTracerContext context = + ApiTracerContext.newBuilder().setLibraryMetadata(LibraryMetadata.empty()).build(); + + ApiTracerFactory factoryWithContext = factory.withContext(context); + assertThat(factoryWithContext).isInstanceOf(BaseApiTracerFactory.class); + } } diff --git a/sdk-platform-java/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITOtelTracing.java b/sdk-platform-java/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITOtelTracing.java index 6aba8412a1ab..ff4731c99d36 100644 --- a/sdk-platform-java/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITOtelTracing.java +++ b/sdk-platform-java/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITOtelTracing.java @@ -47,9 +47,6 @@ import com.google.showcase.v1beta1.EchoClient; import com.google.showcase.v1beta1.EchoRequest; import com.google.showcase.v1beta1.EchoSettings; -import com.google.showcase.v1beta1.GetUserRequest; -import com.google.showcase.v1beta1.IdentityClient; -import com.google.showcase.v1beta1.User; import com.google.showcase.v1beta1.it.util.TestClientInitializer; import com.google.showcase.v1beta1.stub.EchoStub; import com.google.showcase.v1beta1.stub.EchoStubSettings; @@ -98,25 +95,20 @@ void tearDown() { } @Test - void testTracing_successfulIdentityGetUser_grpc() throws Exception { + void testTracing_successfulEcho_grpc() throws Exception { SpanTracerFactory tracingFactory = new SpanTracerFactory(openTelemetrySdk); - try (IdentityClient client = - TestClientInitializer.createGrpcIdentityClientOpentelemetry(tracingFactory)) { + try (EchoClient client = + TestClientInitializer.createGrpcEchoClientOpentelemetry(tracingFactory)) { - try { - client.getUser(GetUserRequest.newBuilder().setName("users/test-user").build()); - } catch (Exception e) { - // Ignored, the showcase server may not have this user, but trace is still - // generated. - } + client.echo(EchoRequest.newBuilder().setContent("tracing-test").build()); List spans = spanExporter.getFinishedSpanItems(); assertThat(spans).isNotEmpty(); SpanData attemptSpan = spans.stream() - .filter(span -> span.getName().equals("google.showcase.v1beta1.Identity/GetUser")) + .filter(span -> span.getName().equals("google.showcase.v1beta1.Echo/Echo")) .findFirst() .orElseThrow(() -> new AssertionError("Incorrect span name")); assertThat(attemptSpan.getKind()).isEqualTo(SpanKind.CLIENT); @@ -154,47 +146,30 @@ void testTracing_successfulIdentityGetUser_grpc() throws Exception { attemptSpan .getAttributes() .get(AttributeKey.stringKey(ObservabilityAttributes.GRPC_RPC_METHOD_ATTRIBUTE))) - .isEqualTo("google.showcase.v1beta1.Identity/GetUser"); - // {x-version-update-start:gapic-showcase:current} - assertThat( - attemptSpan - .getAttributes() - .get(AttributeKey.stringKey(ObservabilityAttributes.VERSION_ATTRIBUTE))) - .isEqualTo("0.0.0-SNAPSHOT"); + .isEqualTo("google.showcase.v1beta1.Echo/Echo"); + assertThat(attemptSpan.getInstrumentationScopeInfo().getName()).isEqualTo(SHOWCASE_ARTIFACT); // {x-version-update-end} - assertThat( - attemptSpan - .getAttributes() - .get( - AttributeKey.stringKey( - ObservabilityAttributes.DESTINATION_RESOURCE_ID_ATTRIBUTE))) - .isEqualTo("users/test-user"); } } @Test - void testTracing_successfulIdentityGetUser_httpjson() throws Exception { + void testTracing_successfulEcho_httpjson() throws Exception { SpanTracerFactory tracingFactory = new SpanTracerFactory(openTelemetrySdk); - try (IdentityClient client = - TestClientInitializer.createHttpJsonIdentityClientOpentelemetry(tracingFactory)) { + try (EchoClient client = + TestClientInitializer.createHttpJsonEchoClientOpentelemetry(tracingFactory)) { - try { - client.getUser(GetUserRequest.newBuilder().setName("users/test-user").build()); - } catch (Exception e) { - // Ignored, the showcase server may not have this user, but trace is still - // generated. - } + client.echo(EchoRequest.newBuilder().setContent("tracing-test").build()); List spans = spanExporter.getFinishedSpanItems(); assertThat(spans).isNotEmpty(); SpanData attemptSpan = spans.stream() - .filter(span -> span.getName().equals("GET v1beta1/{name=users/*}")) + .filter(span -> span.getName().equals("POST v1beta1/echo:echo")) .findFirst() .orElseThrow( - () -> new AssertionError("Attempt span 'GET v1beta1/{name=users/*}' not found")); + () -> new AssertionError("Attempt span 'POST v1beta1/echo:echo' not found")); assertThat(attemptSpan.getKind()).isEqualTo(SpanKind.CLIENT); assertThat( attemptSpan @@ -225,28 +200,13 @@ void testTracing_successfulIdentityGetUser_httpjson() throws Exception { attemptSpan .getAttributes() .get(AttributeKey.stringKey(ObservabilityAttributes.HTTP_METHOD_ATTRIBUTE))) - .isEqualTo("GET"); + .isEqualTo("POST"); assertThat( attemptSpan .getAttributes() .get(AttributeKey.stringKey(ObservabilityAttributes.HTTP_URL_TEMPLATE_ATTRIBUTE))) - .isEqualTo("v1beta1/{name=users/*}"); - assertThat( - attemptSpan - .getAttributes() - .get( - AttributeKey.stringKey( - ObservabilityAttributes.DESTINATION_RESOURCE_ID_ATTRIBUTE))) - .isEqualTo("users/test-user"); - - User fetchedUser = User.newBuilder().setName("users/test-user").build(); - long expectedMagnitude = computeExpectedHttpJsonResponseSize(fetchedUser); - Long observedMagnitude = - attemptSpan - .getAttributes() - .get(AttributeKey.longKey(ObservabilityAttributes.HTTP_RESPONSE_BODY_SIZE)); - assertThat(observedMagnitude).isNotNull(); - assertThat(observedMagnitude).isAtLeast((long) (expectedMagnitude * (1 - 0.15))); + .isEqualTo("v1beta1/echo:echo"); + assertThat(attemptSpan.getInstrumentationScopeInfo().getName()).isEqualTo(SHOWCASE_ARTIFACT); } }