diff --git a/gax-java/dependencies.properties b/gax-java/dependencies.properties
index 3b46b82fff..2fb4fb1316 100644
--- a/gax-java/dependencies.properties
+++ b/gax-java/dependencies.properties
@@ -40,6 +40,7 @@ maven.com_google_api_grpc_grpc_google_common_protos=com.google.api.grpc:grpc-goo
maven.com_google_auth_google_auth_library_oauth2_http=com.google.auth:google-auth-library-oauth2-http:1.42.1
maven.com_google_auth_google_auth_library_credentials=com.google.auth:google-auth-library-credentials:1.42.1
maven.io_opentelemetry_opentelemetry_api=io.opentelemetry:opentelemetry-api:1.47.0
+maven.io_opentelemetry_opentelemetry_context=io.opentelemetry:opentelemetry-context:1.47.0
maven.io_opencensus_opencensus_api=io.opencensus:opencensus-api:0.31.1
maven.io_opencensus_opencensus_contrib_grpc_metrics=io.opencensus:opencensus-contrib-grpc-metrics:0.31.1
maven.io_opencensus_opencensus_contrib_http_util=io.opencensus:opencensus-contrib-http-util:0.31.1
diff --git a/gax-java/gax/BUILD.bazel b/gax-java/gax/BUILD.bazel
index 80b26ad785..15ed36bcbd 100644
--- a/gax-java/gax/BUILD.bazel
+++ b/gax-java/gax/BUILD.bazel
@@ -19,6 +19,7 @@ _COMPILE_DEPS = [
"@com_google_errorprone_error_prone_annotations//jar",
"@com_google_guava_guava//jar",
"@io_opentelemetry_opentelemetry_api//jar",
+ "@io_opentelemetry_opentelemetry_context//jar",
"@io_opencensus_opencensus_api//jar",
"@io_opencensus_opencensus_contrib_http_util//jar",
"@io_grpc_grpc_java//context:context",
diff --git a/gax-java/gax/src/main/java/com/google/api/gax/rpc/ClientContext.java b/gax-java/gax/src/main/java/com/google/api/gax/rpc/ClientContext.java
index 72d54356b0..e782fdd926 100644
--- a/gax-java/gax/src/main/java/com/google/api/gax/rpc/ClientContext.java
+++ b/gax-java/gax/src/main/java/com/google/api/gax/rpc/ClientContext.java
@@ -41,6 +41,7 @@
import com.google.api.gax.core.ExecutorAsBackgroundResource;
import com.google.api.gax.core.ExecutorProvider;
import com.google.api.gax.rpc.internal.QuotaProjectIdHidingCredentials;
+import com.google.api.gax.tracing.ApiTracerContext;
import com.google.api.gax.tracing.ApiTracerFactory;
import com.google.api.gax.tracing.BaseApiTracerFactory;
import com.google.auth.ApiKeyCredentials;
@@ -269,6 +270,11 @@ public static ClientContext create(StubSettings settings) throws IOException {
if (watchdogProvider != null && watchdogProvider.shouldAutoClose()) {
backgroundResources.add(watchdog);
}
+ ApiTracerContext apiTracerContext =
+ ApiTracerContext.newBuilder()
+ .setServerAddress(endpointContext.resolvedServerAddress())
+ .build();
+ ApiTracerFactory apiTracerFactory = settings.getTracerFactory().withContext(apiTracerContext);
return newBuilder()
.setBackgroundResources(backgroundResources.build())
@@ -284,7 +290,7 @@ public static ClientContext create(StubSettings settings) throws IOException {
.setQuotaProjectId(settings.getQuotaProjectId())
.setStreamWatchdog(watchdog)
.setStreamWatchdogCheckIntervalDuration(settings.getStreamWatchdogCheckIntervalDuration())
- .setTracerFactory(settings.getTracerFactory())
+ .setTracerFactory(apiTracerFactory)
.setEndpointContext(endpointContext)
.build();
}
diff --git a/gax-java/gax/src/main/java/com/google/api/gax/rpc/EndpointContext.java b/gax-java/gax/src/main/java/com/google/api/gax/rpc/EndpointContext.java
index a2e44d8a8b..84111dd620 100644
--- a/gax-java/gax/src/main/java/com/google/api/gax/rpc/EndpointContext.java
+++ b/gax-java/gax/src/main/java/com/google/api/gax/rpc/EndpointContext.java
@@ -40,6 +40,7 @@
import com.google.auto.value.AutoValue;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings;
+import com.google.common.net.HostAndPort;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
@@ -133,6 +134,8 @@ public static EndpointContext getDefaultInstance() {
public abstract String resolvedEndpoint();
+ public abstract String resolvedServerAddress();
+
public abstract Builder toBuilder();
public static Builder newBuilder() {
@@ -228,6 +231,8 @@ public abstract static class Builder {
public abstract Builder setResolvedEndpoint(String resolvedEndpoint);
+ public abstract Builder setResolvedServerAddress(String serverAddress);
+
public abstract Builder setResolvedUniverseDomain(String resolvedUniverseDomain);
abstract Builder setUseS2A(boolean useS2A);
@@ -382,6 +387,23 @@ boolean shouldUseS2A() {
return mtlsEndpoint().contains(Credentials.GOOGLE_DEFAULT_UNIVERSE);
}
+ private String parseServerAddress(String endpoint) {
+ if (Strings.isNullOrEmpty(endpoint)) {
+ return endpoint;
+ }
+ String hostPort = endpoint;
+ if (hostPort.contains("://")) {
+ // Strip the scheme if present. HostAndPort doesn't support schemes.
+ hostPort = hostPort.substring(hostPort.indexOf("://") + 3);
+ }
+ try {
+ return HostAndPort.fromString(hostPort).getHost();
+ } catch (IllegalArgumentException e) {
+ // Fallback for cases HostAndPort can't handle.
+ return hostPort;
+ }
+ }
+
// Default to port 443 for HTTPS. Using HTTP requires explicitly setting the endpoint
private String buildEndpointTemplate(String serviceName, String resolvedUniverseDomain) {
return serviceName + "." + resolvedUniverseDomain + ":443";
@@ -416,7 +438,9 @@ String mtlsEndpointResolver(
public EndpointContext build() throws IOException {
// The Universe Domain is used to resolve the Endpoint. It should be resolved first
setResolvedUniverseDomain(determineUniverseDomain());
- setResolvedEndpoint(determineEndpoint());
+ String endpoint = determineEndpoint();
+ setResolvedEndpoint(endpoint);
+ setResolvedServerAddress(parseServerAddress(endpoint));
setUseS2A(shouldUseS2A());
return autoBuild();
}
diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/ApiTracerContext.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/ApiTracerContext.java
new file mode 100644
index 0000000000..1c92bd8db3
--- /dev/null
+++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/ApiTracerContext.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2026 Google LLC
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * * Neither the name of Google LLC nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.google.api.gax.tracing;
+
+import com.google.api.core.InternalApi;
+import com.google.auto.value.AutoValue;
+import javax.annotation.Nullable;
+
+/**
+ * A context object that contains information used to infer attributes that are common for all
+ * {@link ApiTracer}s.
+ *
+ *
For internal use only.
+ */
+@InternalApi
+@AutoValue
+public abstract class ApiTracerContext {
+
+ @Nullable
+ public abstract String getServerAddress();
+
+ public static Builder newBuilder() {
+ return new AutoValue_ApiTracerContext.Builder();
+ }
+
+ @AutoValue.Builder
+ public abstract static class Builder {
+ public abstract Builder setServerAddress(String serverAddress);
+
+ public abstract ApiTracerContext build();
+ }
+}
diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/ApiTracerFactory.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/ApiTracerFactory.java
index bb8345b88c..07a0fcf12d 100644
--- a/gax-java/gax/src/main/java/com/google/api/gax/tracing/ApiTracerFactory.java
+++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/ApiTracerFactory.java
@@ -61,4 +61,15 @@ enum OperationType {
* @param operationType the type of operation that the tracer will trace
*/
ApiTracer newTracer(ApiTracer parent, SpanName spanName, OperationType operationType);
+
+ /**
+ * Returns a new {@link ApiTracerFactory} that will use the provided context to infer attributes
+ * for all tracers created by the factory.
+ *
+ * @param context an {@link ApiTracerContext} object containing information to construct
+ * attributes
+ */
+ default ApiTracerFactory withContext(ApiTracerContext context) {
+ return this;
+ }
}
diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/AppCentricAttributes.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/AppCentricAttributes.java
new file mode 100644
index 0000000000..a060ba4a88
--- /dev/null
+++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/AppCentricAttributes.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2026 Google LLC
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * * Neither the name of Google LLC nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.google.api.gax.tracing;
+
+import com.google.api.core.InternalApi;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Utility class for providing common attributes used in app-centric observability.
+ *
+ *
This class extracts information from {@link ApiTracerContext} and maps it to standardized
+ * attribute keys that are expected by {@link ApiTracerFactory} implementations that conform to
+ * app-centric observability
+ *
+ *
For internal use only.
+ */
+@InternalApi
+public class AppCentricAttributes {
+ /** The address of the server being called (e.g., "pubsub.googleapis.com"). */
+ public static final String SERVER_ADDRESS_ATTRIBUTE = "server.address";
+
+ /**
+ * Extracts attempt-level attributes from the provided {@link ApiTracerContext}.
+ *
+ * @param context the context containing information about the current API call
+ * @return a map of attributes to be included in attempt-level spans
+ */
+ public static Map getAttemptAttributes(ApiTracerContext context) {
+ Map attributes = new HashMap<>();
+ if (context.getServerAddress() != null) {
+ attributes.put(SERVER_ADDRESS_ATTRIBUTE, context.getServerAddress());
+ }
+ return attributes;
+ }
+}
diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/AppCentricTracer.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/AppCentricTracer.java
new file mode 100644
index 0000000000..c951e71415
--- /dev/null
+++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/AppCentricTracer.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2026 Google LLC
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * * Neither the name of Google LLC nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.google.api.gax.tracing;
+
+import com.google.api.core.BetaApi;
+import com.google.api.core.InternalApi;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * An implementation of {@link ApiTracer} that uses a {@link TraceRecorder} to record traces. This
+ * implementation is agnostic to the specific {@link TraceRecorder} in order to allow extensions
+ * that interact with other backends.
+ */
+@BetaApi
+@InternalApi
+public class AppCentricTracer implements ApiTracer {
+ public static final String LANGUAGE_ATTRIBUTE = "gcp.client.language";
+
+ public static final String DEFAULT_LANGUAGE = "Java";
+
+ private final TraceRecorder recorder;
+ private final Map attemptAttributes;
+ private final String attemptSpanName;
+ private TraceRecorder.TraceSpan attemptHandle;
+
+ /**
+ * Creates a new instance of {@code AppCentricTracer}.
+ *
+ * @param recorder the {@link TraceRecorder} to use for recording spans
+ * @param attemptSpanName the name of the individual attempt spans
+ * @param attemptAttributes attributes to be added to each attempt span
+ */
+ public AppCentricTracer(
+ TraceRecorder recorder, String attemptSpanName, Map attemptAttributes) {
+ this.recorder = recorder;
+ this.attemptSpanName = attemptSpanName;
+ this.attemptAttributes = new HashMap<>(attemptAttributes);
+ this.attemptAttributes.put(LANGUAGE_ATTRIBUTE, DEFAULT_LANGUAGE);
+
+ // Start the long-lived operation span.
+ }
+
+ @Override
+ public void attemptStarted(Object request, int attemptNumber) {
+ Map attemptAttributes = new HashMap<>(this.attemptAttributes);
+ // Start the specific attempt span with the operation span as parent
+ this.attemptHandle = recorder.createSpan(attemptSpanName, attemptAttributes);
+ }
+
+ @Override
+ public void attemptSucceeded() {
+ endAttempt();
+ }
+
+ private void endAttempt() {
+ if (attemptHandle != null) {
+ attemptHandle.end();
+ attemptHandle = null;
+ }
+ }
+}
diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/AppCentricTracerFactory.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/AppCentricTracerFactory.java
new file mode 100644
index 0000000000..c227095179
--- /dev/null
+++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/AppCentricTracerFactory.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2026 Google LLC
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * * Neither the name of Google LLC nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.google.api.gax.tracing;
+
+import com.google.api.core.BetaApi;
+import com.google.api.core.InternalApi;
+import com.google.common.annotations.VisibleForTesting;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A {@link ApiTracerFactory} to build instances of {@link AppCentricTracer}.
+ *
+ * This class wraps the {@link TraceRecorder} and pass it to {@link AppCentricTracer}. It will be
+ * used to record traces in {@link AppCentricTracer}.
+ *
+ *
This class is expected to be initialized once during client initialization.
+ */
+@BetaApi
+@InternalApi
+public class AppCentricTracerFactory implements ApiTracerFactory {
+ private final TraceRecorder traceRecorder;
+
+ /** Mapping of client attributes that are set for every AppCentricTracer at operation level */
+ private final Map operationAttributes;
+
+ /** Mapping of client attributes that are set for every AppCentricTracer at attempt level */
+ private final Map attemptAttributes;
+
+ /** Creates a AppCentricTracerFactory */
+ public AppCentricTracerFactory(TraceRecorder traceRecorder) {
+ this(traceRecorder, new HashMap<>(), new HashMap<>());
+ }
+
+ /**
+ * Pass in a Map of client level attributes which will be added to every single AppCentricTracer
+ * created from the ApiTracerFactory. This is package private since span attributes are determined
+ * internally.
+ */
+ @VisibleForTesting
+ AppCentricTracerFactory(
+ TraceRecorder traceRecorder,
+ Map operationAttributes,
+ Map attemptAttributes) {
+ this.traceRecorder = traceRecorder;
+
+ this.operationAttributes = new HashMap<>(operationAttributes);
+ this.attemptAttributes = new HashMap<>(attemptAttributes);
+ }
+
+ @Override
+ public ApiTracer newTracer(ApiTracer parent, SpanName spanName, OperationType operationType) {
+ // 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";
+
+ AppCentricTracer appCentricTracer =
+ new AppCentricTracer(traceRecorder, attemptSpanName, this.attemptAttributes);
+ return appCentricTracer;
+ }
+
+ @Override
+ public ApiTracerFactory withContext(ApiTracerContext context) {
+ Map newAttemptAttributes = new HashMap<>(this.attemptAttributes);
+ newAttemptAttributes.putAll(AppCentricAttributes.getAttemptAttributes(context));
+ return new AppCentricTracerFactory(traceRecorder, operationAttributes, newAttemptAttributes);
+ }
+}
diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/OpenTelemetryTraceRecorder.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/OpenTelemetryTraceRecorder.java
new file mode 100644
index 0000000000..8e5202e3d9
--- /dev/null
+++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/OpenTelemetryTraceRecorder.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2026 Google LLC
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * * Neither the name of Google LLC nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.google.api.gax.tracing;
+
+import com.google.api.core.BetaApi;
+import com.google.api.core.InternalApi;
+import io.opentelemetry.api.OpenTelemetry;
+import io.opentelemetry.api.trace.Span;
+import io.opentelemetry.api.trace.SpanBuilder;
+import io.opentelemetry.api.trace.SpanKind;
+import io.opentelemetry.api.trace.Tracer;
+import java.util.Map;
+
+/**
+ * OpenTelemetry implementation of recording traces. This implementation collects the measurements
+ * related to the lifecyle of an RPC.
+ */
+@BetaApi
+@InternalApi
+public class OpenTelemetryTraceRecorder implements TraceRecorder {
+ private final Tracer tracer;
+
+ public OpenTelemetryTraceRecorder(OpenTelemetry openTelemetry) {
+ this.tracer = openTelemetry.getTracer("gax-java");
+ }
+
+ @Override
+ public TraceSpan createSpan(String name, Map attributes) {
+ SpanBuilder spanBuilder = tracer.spanBuilder(name);
+
+ // Attempt spans are of the CLIENT kind
+ spanBuilder.setSpanKind(SpanKind.CLIENT);
+
+ if (attributes != null) {
+ attributes.forEach((k, v) -> spanBuilder.setAttribute(k, v));
+ }
+
+ Span span = spanBuilder.startSpan();
+
+ return new OtelTraceSpan(span);
+ }
+
+ private static class OtelTraceSpan implements TraceSpan {
+ private final Span span;
+
+ private OtelTraceSpan(Span span) {
+ this.span = span;
+ }
+
+ @Override
+ public void end() {
+ span.end();
+ }
+ }
+}
diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/TraceRecorder.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/TraceRecorder.java
new file mode 100644
index 0000000000..64f063fe62
--- /dev/null
+++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/TraceRecorder.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2026 Google LLC
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * * Neither the name of Google LLC nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.google.api.gax.tracing;
+
+import com.google.api.core.BetaApi;
+import com.google.api.core.InternalApi;
+import java.util.Map;
+
+/**
+ * Provides an interface for tracing recording. The implementer is expected to use an observability
+ * framework, e.g. OpenTelemetry. There should be only one instance of TraceRecorder per client.
+ */
+@BetaApi
+@InternalApi
+public interface TraceRecorder {
+ /** Starts a span and returns a handle to manage its lifecycle. */
+ TraceSpan createSpan(String name, Map attributes);
+
+ interface TraceSpan {
+ void end();
+ }
+}
diff --git a/gax-java/gax/src/test/java/com/google/api/gax/rpc/EndpointContextTest.java b/gax-java/gax/src/test/java/com/google/api/gax/rpc/EndpointContextTest.java
index ef64ccd726..c1bcc50512 100644
--- a/gax-java/gax/src/test/java/com/google/api/gax/rpc/EndpointContextTest.java
+++ b/gax-java/gax/src/test/java/com/google/api/gax/rpc/EndpointContextTest.java
@@ -593,4 +593,49 @@ void shouldUseS2A_success() throws IOException {
.setUsingGDCH(false);
Truth.assertThat(defaultEndpointContextBuilder.shouldUseS2A()).isTrue();
}
+
+ @Test
+ void endpointContextBuild_resolvesPortAndServerAddress() throws IOException {
+ String endpoint = "http://localhost:7469";
+ EndpointContext endpointContext =
+ defaultEndpointContextBuilder
+ .setClientSettingsEndpoint(endpoint)
+ .setTransportChannelProviderEndpoint(null)
+ .build();
+ Truth.assertThat(endpointContext.resolvedServerAddress()).isEqualTo("localhost");
+
+ endpoint = "localhost:7469";
+ endpointContext =
+ defaultEndpointContextBuilder
+ .setClientSettingsEndpoint(endpoint)
+ .setTransportChannelProviderEndpoint(null)
+ .build();
+ Truth.assertThat(endpointContext.resolvedServerAddress()).isEqualTo("localhost");
+
+ endpoint = "test.googleapis.com:443";
+ endpointContext =
+ defaultEndpointContextBuilder
+ .setClientSettingsEndpoint(endpoint)
+ .setTransportChannelProviderEndpoint(null)
+ .build();
+ Truth.assertThat(endpointContext.resolvedServerAddress()).isEqualTo("test.googleapis.com");
+
+ // IPv6 literal with port
+ endpoint = "[2001:db8::1]:443";
+ endpointContext =
+ defaultEndpointContextBuilder
+ .setClientSettingsEndpoint(endpoint)
+ .setTransportChannelProviderEndpoint(null)
+ .build();
+ Truth.assertThat(endpointContext.resolvedServerAddress()).isEqualTo("2001:db8::1");
+
+ // Bare IPv6 literal (no port)
+ endpoint = "2001:db8::1";
+ endpointContext =
+ defaultEndpointContextBuilder
+ .setClientSettingsEndpoint(endpoint)
+ .setTransportChannelProviderEndpoint(null)
+ .build();
+ Truth.assertThat(endpointContext.resolvedServerAddress()).isEqualTo("2001:db8::1");
+ }
}
diff --git a/gax-java/gax/src/test/java/com/google/api/gax/tracing/AppCentricAttributesTest.java b/gax-java/gax/src/test/java/com/google/api/gax/tracing/AppCentricAttributesTest.java
new file mode 100644
index 0000000000..8256101b57
--- /dev/null
+++ b/gax-java/gax/src/test/java/com/google/api/gax/tracing/AppCentricAttributesTest.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2026 Google LLC
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * * Neither the name of Google LLC nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.google.api.gax.tracing;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import java.util.Map;
+import org.junit.jupiter.api.Test;
+
+class AppCentricAttributesTest {
+
+ @Test
+ void testGetAttemptAttributes_serverAddress() {
+ ApiTracerContext context =
+ ApiTracerContext.newBuilder().setServerAddress("test-address").build();
+
+ Map attributes = AppCentricAttributes.getAttemptAttributes(context);
+
+ assertThat(attributes).hasSize(1);
+ assertThat(attributes)
+ .containsEntry(AppCentricAttributes.SERVER_ADDRESS_ATTRIBUTE, "test-address");
+ }
+
+ @Test
+ void testGetAttemptAttributes_nonePresent() {
+ ApiTracerContext context = ApiTracerContext.newBuilder().build();
+
+ Map attributes = AppCentricAttributes.getAttemptAttributes(context);
+
+ assertThat(attributes).isEmpty();
+ }
+}
diff --git a/gax-java/gax/src/test/java/com/google/api/gax/tracing/AppCentricTracerFactoryTest.java b/gax-java/gax/src/test/java/com/google/api/gax/tracing/AppCentricTracerFactoryTest.java
new file mode 100644
index 0000000000..9cf81b457c
--- /dev/null
+++ b/gax-java/gax/src/test/java/com/google/api/gax/tracing/AppCentricTracerFactoryTest.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright 2026 Google LLC
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * * Neither the name of Google LLC nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.google.api.gax.tracing;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.ArgumentMatchers.anyMap;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.google.common.collect.ImmutableMap;
+import java.util.Map;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+
+class AppCentricTracerFactoryTest {
+
+ @Test
+ void testNewTracer_createsOpenTelemetryTracingTracer() {
+ TraceRecorder recorder = mock(TraceRecorder.class);
+ when(recorder.createSpan(anyString(), anyMap()))
+ .thenReturn(mock(TraceRecorder.TraceSpan.class));
+
+ AppCentricTracerFactory factory = new AppCentricTracerFactory(recorder);
+ ApiTracer tracer =
+ factory.newTracer(
+ null, SpanName.of("service", "method"), ApiTracerFactory.OperationType.Unary);
+ assertThat(tracer).isInstanceOf(AppCentricTracer.class);
+ }
+
+ @Test
+ void testNewTracer_addsAttributes() {
+ TraceRecorder recorder = mock(TraceRecorder.class);
+ TraceRecorder.TraceSpan attemptHandle = mock(TraceRecorder.TraceSpan.class);
+ when(recorder.createSpan(anyString(), anyMap())).thenReturn(attemptHandle);
+
+ AppCentricTracerFactory factory =
+ new AppCentricTracerFactory(
+ recorder, ImmutableMap.of(), ImmutableMap.of("server.port", "443"));
+ ApiTracer tracer =
+ factory.newTracer(
+ null, SpanName.of("service", "method"), ApiTracerFactory.OperationType.Unary);
+
+ tracer.attemptStarted(null, 1);
+
+ ArgumentCaptor