Skip to content

Commit e4e5e89

Browse files
feat(observability): introduce minimal tracing implementation (#4105)
## Summary This PR introduces a new tracing mechanism in GAX that allows recording traces using OpenTelemetry. It provides a way of recording spans and attributes, following the existing `ApiTracer` class pattern with a few tracing-specific additions. The implementation is meant to be extensible to support other implementations. ## New Classes - **`TraceManager`**: An interface for managing spans and attributes; can be implemented by observability frameworks. - **`OpenTelemetryTraceManager`**: An implementation of `TraceManager` that uses the OpenTelemetry API. - **`AppCentricTracer`**: An `ApiTracer` implementation that delegates span management to a `TraceManager`. - **`AppCentricTracerFactory`**: A factory for creating `AppCentricTracer` instances. - **`ApiTracerContext`**: A context object that carries information (like `EndpointContext`'s server address property) used to infer common attributes for all tracers. - **`Span`**: A handle returned by `TraceManager` to manage the lifecycle of a specific span (ending it, recording errors, or setting attributes). ## Approach ### Connecting Tracer with Manager The implementation aims to decouple `AppCentricTracer` from `TraceManager`. When a tracer starts an operation or an attempt, it requests a `Span` from the recorder. This handle allows the tracer to update the span (e.g., adding attributes or recording errors) to keep `AppCentricTracer` separated from specific recorder implementations (like OpenTelemetry's `Span` object). ### Attribute Inference via `ApiTracerContext` To provide a source of Span Attributes that are common to all operations, we introduced `ApiTracerContext`. This context is passed to `ApiTracerFactory` and contains information such as serverAddress (provided by `EndpointContext`). It is operated by `ClientContext`. Initially, only `serverAddress` is contained in this class and it's meant to obtain the `server.address` attribute. The class is ultimately operated by `AppCentricTracer` to extract the necessary attributes. ### Integration Tests A new integration test, `ITOtelTracing`, was added to the `java-showcase` module: - It validates that the expected spans (operation and attempt spans) are recorded with the correct names, parent-child relationships, and attributes (including `server.address` and `gcp.client.language`). #### Note on java-bigtable downstream check Since [SkipTrailersTest](https://github.com/googleapis/java-bigtable/blob/49fe7692c55747714ada4296ff0f856b1109eba5/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/SkipTrailersTest.java#L97) mocks the tracer factory, the `EndpointContext` call to `apiTracerFactory.withContext()` returns a null factory, causing a null pointer exception when building the client context. We expect the test to be adjusted with this change with the next release. ### Confirmation in Cloud Trace <img width="1801" height="843" alt="image" src="https://github.com/user-attachments/assets/dfccf278-7bfc-43fe-bf64-51cae21494ea" /> --------- Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
1 parent c6555f5 commit e4e5e89

File tree

17 files changed

+983
-2
lines changed

17 files changed

+983
-2
lines changed

gax-java/dependencies.properties

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ maven.com_google_api_grpc_grpc_google_common_protos=com.google.api.grpc:grpc-goo
4040
maven.com_google_auth_google_auth_library_oauth2_http=com.google.auth:google-auth-library-oauth2-http:1.42.1
4141
maven.com_google_auth_google_auth_library_credentials=com.google.auth:google-auth-library-credentials:1.42.1
4242
maven.io_opentelemetry_opentelemetry_api=io.opentelemetry:opentelemetry-api:1.47.0
43+
maven.io_opentelemetry_opentelemetry_context=io.opentelemetry:opentelemetry-context:1.47.0
4344
maven.io_opencensus_opencensus_api=io.opencensus:opencensus-api:0.31.1
4445
maven.io_opencensus_opencensus_contrib_grpc_metrics=io.opencensus:opencensus-contrib-grpc-metrics:0.31.1
4546
maven.io_opencensus_opencensus_contrib_http_util=io.opencensus:opencensus-contrib-http-util:0.31.1

gax-java/gax/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ _COMPILE_DEPS = [
1919
"@com_google_errorprone_error_prone_annotations//jar",
2020
"@com_google_guava_guava//jar",
2121
"@io_opentelemetry_opentelemetry_api//jar",
22+
"@io_opentelemetry_opentelemetry_context//jar",
2223
"@io_opencensus_opencensus_api//jar",
2324
"@io_opencensus_opencensus_contrib_http_util//jar",
2425
"@io_grpc_grpc_java//context:context",

gax-java/gax/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,11 @@
7373
<artifactId>opentelemetry-api</artifactId>
7474
<optional>true</optional>
7575
</dependency>
76+
<dependency>
77+
<groupId>io.opentelemetry</groupId>
78+
<artifactId>opentelemetry-context</artifactId>
79+
<optional>true</optional>
80+
</dependency>
7681
<!-- Logging dependency -->
7782
<dependency>
7883
<groupId>org.slf4j</groupId>

gax-java/gax/src/main/java/com/google/api/gax/rpc/ClientContext.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
import com.google.api.gax.core.ExecutorAsBackgroundResource;
4242
import com.google.api.gax.core.ExecutorProvider;
4343
import com.google.api.gax.rpc.internal.QuotaProjectIdHidingCredentials;
44+
import com.google.api.gax.tracing.ApiTracerContext;
4445
import com.google.api.gax.tracing.ApiTracerFactory;
4546
import com.google.api.gax.tracing.BaseApiTracerFactory;
4647
import com.google.auth.ApiKeyCredentials;
@@ -269,6 +270,11 @@ public static ClientContext create(StubSettings settings) throws IOException {
269270
if (watchdogProvider != null && watchdogProvider.shouldAutoClose()) {
270271
backgroundResources.add(watchdog);
271272
}
273+
ApiTracerContext apiTracerContext =
274+
ApiTracerContext.newBuilder()
275+
.setServerAddress(endpointContext.resolvedServerAddress())
276+
.build();
277+
ApiTracerFactory apiTracerFactory = settings.getTracerFactory().withContext(apiTracerContext);
272278

273279
return newBuilder()
274280
.setBackgroundResources(backgroundResources.build())
@@ -284,7 +290,7 @@ public static ClientContext create(StubSettings settings) throws IOException {
284290
.setQuotaProjectId(settings.getQuotaProjectId())
285291
.setStreamWatchdog(watchdog)
286292
.setStreamWatchdogCheckIntervalDuration(settings.getStreamWatchdogCheckIntervalDuration())
287-
.setTracerFactory(settings.getTracerFactory())
293+
.setTracerFactory(apiTracerFactory)
288294
.setEndpointContext(endpointContext)
289295
.build();
290296
}

gax-java/gax/src/main/java/com/google/api/gax/rpc/EndpointContext.java

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
import com.google.auto.value.AutoValue;
4141
import com.google.common.annotations.VisibleForTesting;
4242
import com.google.common.base.Strings;
43+
import com.google.common.net.HostAndPort;
4344
import java.io.IOException;
4445
import java.util.logging.Level;
4546
import java.util.logging.Logger;
@@ -133,6 +134,8 @@ public static EndpointContext getDefaultInstance() {
133134

134135
public abstract String resolvedEndpoint();
135136

137+
public abstract String resolvedServerAddress();
138+
136139
public abstract Builder toBuilder();
137140

138141
public static Builder newBuilder() {
@@ -228,6 +231,8 @@ public abstract static class Builder {
228231

229232
public abstract Builder setResolvedEndpoint(String resolvedEndpoint);
230233

234+
public abstract Builder setResolvedServerAddress(String serverAddress);
235+
231236
public abstract Builder setResolvedUniverseDomain(String resolvedUniverseDomain);
232237

233238
abstract Builder setUseS2A(boolean useS2A);
@@ -382,6 +387,23 @@ boolean shouldUseS2A() {
382387
return mtlsEndpoint().contains(Credentials.GOOGLE_DEFAULT_UNIVERSE);
383388
}
384389

390+
private String parseServerAddress(String endpoint) {
391+
if (Strings.isNullOrEmpty(endpoint)) {
392+
return endpoint;
393+
}
394+
String hostPort = endpoint;
395+
if (hostPort.contains("://")) {
396+
// Strip the scheme if present. HostAndPort doesn't support schemes.
397+
hostPort = hostPort.substring(hostPort.indexOf("://") + 3);
398+
}
399+
try {
400+
return HostAndPort.fromString(hostPort).getHost();
401+
} catch (IllegalArgumentException e) {
402+
// Fallback for cases HostAndPort can't handle.
403+
return hostPort;
404+
}
405+
}
406+
385407
// Default to port 443 for HTTPS. Using HTTP requires explicitly setting the endpoint
386408
private String buildEndpointTemplate(String serviceName, String resolvedUniverseDomain) {
387409
return serviceName + "." + resolvedUniverseDomain + ":443";
@@ -416,7 +438,9 @@ String mtlsEndpointResolver(
416438
public EndpointContext build() throws IOException {
417439
// The Universe Domain is used to resolve the Endpoint. It should be resolved first
418440
setResolvedUniverseDomain(determineUniverseDomain());
419-
setResolvedEndpoint(determineEndpoint());
441+
String endpoint = determineEndpoint();
442+
setResolvedEndpoint(endpoint);
443+
setResolvedServerAddress(parseServerAddress(endpoint));
420444
setUseS2A(shouldUseS2A());
421445
return autoBuild();
422446
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*
2+
* Copyright 2026 Google LLC
3+
*
4+
* Redistribution and use in source and binary forms, with or without
5+
* modification, are permitted provided that the following conditions are
6+
* met:
7+
*
8+
* * Redistributions of source code must retain the above copyright
9+
* notice, this list of conditions and the following disclaimer.
10+
* * Redistributions in binary form must reproduce the above
11+
* copyright notice, this list of conditions and the following disclaimer
12+
* in the documentation and/or other materials provided with the
13+
* distribution.
14+
* * Neither the name of Google LLC nor the names of its
15+
* contributors may be used to endorse or promote products derived from
16+
* this software without specific prior written permission.
17+
*
18+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19+
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20+
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21+
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22+
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23+
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24+
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25+
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26+
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27+
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29+
*/
30+
31+
package com.google.api.gax.tracing;
32+
33+
import com.google.api.core.InternalApi;
34+
import com.google.auto.value.AutoValue;
35+
import java.util.HashMap;
36+
import java.util.Map;
37+
import javax.annotation.Nullable;
38+
39+
/**
40+
* A context object that contains information used to infer attributes that are common for all
41+
* {@link ApiTracer}s.
42+
*
43+
* <p>For internal use only.
44+
*/
45+
@InternalApi
46+
@AutoValue
47+
public abstract class ApiTracerContext {
48+
49+
/**
50+
* @return a map of attributes to be included in attempt-level spans
51+
*/
52+
public Map<String, String> getAttemptAttributes() {
53+
Map<String, String> attributes = new HashMap<>();
54+
if (getServerAddress() != null) {
55+
attributes.put(AppCentricAttributes.SERVER_ADDRESS_ATTRIBUTE, getServerAddress());
56+
}
57+
return attributes;
58+
}
59+
60+
@Nullable
61+
public abstract String getServerAddress();
62+
63+
public static Builder newBuilder() {
64+
return new AutoValue_ApiTracerContext.Builder();
65+
}
66+
67+
@AutoValue.Builder
68+
public abstract static class Builder {
69+
public abstract Builder setServerAddress(String serverAddress);
70+
71+
public abstract ApiTracerContext build();
72+
}
73+
}

gax-java/gax/src/main/java/com/google/api/gax/tracing/ApiTracerFactory.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,4 +61,15 @@ enum OperationType {
6161
* @param operationType the type of operation that the tracer will trace
6262
*/
6363
ApiTracer newTracer(ApiTracer parent, SpanName spanName, OperationType operationType);
64+
65+
/**
66+
* Returns a new {@link ApiTracerFactory} that will use the provided context to infer attributes
67+
* for all tracers created by the factory.
68+
*
69+
* @param context an {@link ApiTracerContext} object containing information to construct
70+
* attributes
71+
*/
72+
default ApiTracerFactory withContext(ApiTracerContext context) {
73+
return this;
74+
}
6475
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*
2+
* Copyright 2026 Google LLC
3+
*
4+
* Redistribution and use in source and binary forms, with or without
5+
* modification, are permitted provided that the following conditions are
6+
* met:
7+
*
8+
* * Redistributions of source code must retain the above copyright
9+
* notice, this list of conditions and the following disclaimer.
10+
* * Redistributions in binary form must reproduce the above
11+
* copyright notice, this list of conditions and the following disclaimer
12+
* in the documentation and/or other materials provided with the
13+
* distribution.
14+
* * Neither the name of Google LLC nor the names of its
15+
* contributors may be used to endorse or promote products derived from
16+
* this software without specific prior written permission.
17+
*
18+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19+
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20+
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21+
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22+
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23+
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24+
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25+
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26+
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27+
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29+
*/
30+
31+
package com.google.api.gax.tracing;
32+
33+
import com.google.api.core.BetaApi;
34+
import com.google.api.core.InternalApi;
35+
36+
/**
37+
* Utility class with common attribute names in app-centric observability.
38+
*
39+
* <p>For internal use only.
40+
*/
41+
@InternalApi
42+
@BetaApi
43+
public class AppCentricAttributes {
44+
/** The address of the server being called (e.g., "pubsub.googleapis.com"). */
45+
public static final String SERVER_ADDRESS_ATTRIBUTE = "server.address";
46+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/*
2+
* Copyright 2026 Google LLC
3+
*
4+
* Redistribution and use in source and binary forms, with or without
5+
* modification, are permitted provided that the following conditions are
6+
* met:
7+
*
8+
* * Redistributions of source code must retain the above copyright
9+
* notice, this list of conditions and the following disclaimer.
10+
* * Redistributions in binary form must reproduce the above
11+
* copyright notice, this list of conditions and the following disclaimer
12+
* in the documentation and/or other materials provided with the
13+
* distribution.
14+
* * Neither the name of Google LLC nor the names of its
15+
* contributors may be used to endorse or promote products derived from
16+
* this software without specific prior written permission.
17+
*
18+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19+
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20+
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21+
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22+
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23+
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24+
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25+
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26+
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27+
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29+
*/
30+
31+
package com.google.api.gax.tracing;
32+
33+
import com.google.api.core.BetaApi;
34+
import com.google.api.core.InternalApi;
35+
import java.util.HashMap;
36+
import java.util.Map;
37+
38+
/**
39+
* An implementation of {@link ApiTracer} that uses a {@link TraceManager} to record traces. This
40+
* implementation is agnostic to the specific {@link TraceManager} in order to allow extensions that
41+
* interact with other backends.
42+
*/
43+
@BetaApi
44+
@InternalApi
45+
public class AppCentricTracer implements ApiTracer {
46+
public static final String LANGUAGE_ATTRIBUTE = "gcp.client.language";
47+
48+
public static final String DEFAULT_LANGUAGE = "Java";
49+
50+
private final TraceManager traceManager;
51+
private final Map<String, String> attemptAttributes;
52+
private final String attemptSpanName;
53+
private final ApiTracerContext apiTracerContext;
54+
private TraceManager.Span attemptHandle;
55+
56+
/**
57+
* Creates a new instance of {@code AppCentricTracer}.
58+
*
59+
* @param traceManager the {@link TraceManager} to use for recording spans
60+
* @param attemptSpanName the name of the individual attempt spans
61+
*/
62+
public AppCentricTracer(
63+
TraceManager traceManager, ApiTracerContext apiTracerContext, String attemptSpanName) {
64+
this.traceManager = traceManager;
65+
this.attemptSpanName = attemptSpanName;
66+
this.apiTracerContext = apiTracerContext;
67+
this.attemptAttributes = new HashMap<>();
68+
buildAttributes();
69+
}
70+
71+
private void buildAttributes() {
72+
this.attemptAttributes.put(LANGUAGE_ATTRIBUTE, DEFAULT_LANGUAGE);
73+
this.attemptAttributes.putAll(this.apiTracerContext.getAttemptAttributes());
74+
}
75+
76+
@Override
77+
public void attemptStarted(Object request, int attemptNumber) {
78+
Map<String, String> attemptAttributes = new HashMap<>(this.attemptAttributes);
79+
// Start the specific attempt span with the operation span as parent
80+
this.attemptHandle = traceManager.createSpan(attemptSpanName, attemptAttributes);
81+
}
82+
83+
@Override
84+
public void attemptSucceeded() {
85+
endAttempt();
86+
}
87+
88+
private void endAttempt() {
89+
if (attemptHandle != null) {
90+
attemptHandle.end();
91+
attemptHandle = null;
92+
}
93+
}
94+
}

0 commit comments

Comments
 (0)