org.slf4j
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..2d833c6261
--- /dev/null
+++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/ApiTracerContext.java
@@ -0,0 +1,73 @@
+/*
+ * 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 java.util.HashMap;
+import java.util.Map;
+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 {
+
+ /**
+ * @return a map of attributes to be included in attempt-level spans
+ */
+ public Map getAttemptAttributes() {
+ Map attributes = new HashMap<>();
+ if (getServerAddress() != null) {
+ attributes.put(AppCentricAttributes.SERVER_ADDRESS_ATTRIBUTE, getServerAddress());
+ }
+ return attributes;
+ }
+
+ @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..162fe16d8e
--- /dev/null
+++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/AppCentricAttributes.java
@@ -0,0 +1,46 @@
+/*
+ * 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;
+
+/**
+ * Utility class with common attribute names in app-centric observability.
+ *
+ * For internal use only.
+ */
+@InternalApi
+@BetaApi
+public class AppCentricAttributes {
+ /** The address of the server being called (e.g., "pubsub.googleapis.com"). */
+ public static final String SERVER_ADDRESS_ATTRIBUTE = "server.address";
+}
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..b26e3ddc55
--- /dev/null
+++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/AppCentricTracer.java
@@ -0,0 +1,94 @@
+/*
+ * 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 TraceManager} to record traces. This
+ * implementation is agnostic to the specific {@link TraceManager} 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 TraceManager traceManager;
+ private final Map attemptAttributes;
+ private final String attemptSpanName;
+ private final ApiTracerContext apiTracerContext;
+ private TraceManager.Span attemptHandle;
+
+ /**
+ * Creates a new instance of {@code AppCentricTracer}.
+ *
+ * @param traceManager the {@link TraceManager} to use for recording spans
+ * @param attemptSpanName the name of the individual attempt spans
+ */
+ public AppCentricTracer(
+ TraceManager traceManager, ApiTracerContext apiTracerContext, String attemptSpanName) {
+ this.traceManager = traceManager;
+ this.attemptSpanName = attemptSpanName;
+ this.apiTracerContext = apiTracerContext;
+ this.attemptAttributes = new HashMap<>();
+ buildAttributes();
+ }
+
+ private void buildAttributes() {
+ this.attemptAttributes.put(LANGUAGE_ATTRIBUTE, DEFAULT_LANGUAGE);
+ this.attemptAttributes.putAll(this.apiTracerContext.getAttemptAttributes());
+ }
+
+ @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 = traceManager.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..06fd14ad0b
--- /dev/null
+++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/AppCentricTracerFactory.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 com.google.common.annotations.VisibleForTesting;
+
+/**
+ * A {@link ApiTracerFactory} to build instances of {@link AppCentricTracer}.
+ *
+ * This class wraps the {@link TraceManager} 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 TraceManager traceManager;
+
+ private final ApiTracerContext apiTracerContext;
+
+ /** Creates a AppCentricTracerFactory */
+ public AppCentricTracerFactory(TraceManager traceManager) {
+ this(traceManager, ApiTracerContext.newBuilder().build());
+ }
+
+ /**
+ * 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(TraceManager traceManager, ApiTracerContext apiTracerContext) {
+ this.traceManager = traceManager;
+ this.apiTracerContext = apiTracerContext;
+ }
+
+ @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(traceManager, this.apiTracerContext, attemptSpanName);
+ return appCentricTracer;
+ }
+
+ @Override
+ public ApiTracerFactory withContext(ApiTracerContext context) {
+ return new AppCentricTracerFactory(traceManager, context);
+ }
+}
diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/OpenTelemetryTraceManager.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/OpenTelemetryTraceManager.java
new file mode 100644
index 0000000000..fbbdb292b0
--- /dev/null
+++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/OpenTelemetryTraceManager.java
@@ -0,0 +1,81 @@
+/*
+ * 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.SpanBuilder;
+import io.opentelemetry.api.trace.SpanKind;
+import java.util.Map;
+
+/**
+ * OpenTelemetry implementation of managing traces. This implementation collects the measurements
+ * related to the lifecyle of an RPC.
+ */
+@BetaApi
+@InternalApi
+public class OpenTelemetryTraceManager implements TraceManager {
+ private final io.opentelemetry.api.trace.Tracer tracer;
+
+ public OpenTelemetryTraceManager(OpenTelemetry openTelemetry) {
+ this.tracer = openTelemetry.getTracer("gax-java");
+ }
+
+ @Override
+ public Span 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));
+ }
+
+ io.opentelemetry.api.trace.Span span = spanBuilder.startSpan();
+
+ return new OtelSpan(span);
+ }
+
+ private static class OtelSpan implements Span {
+ private final io.opentelemetry.api.trace.Span span;
+
+ private OtelSpan(io.opentelemetry.api.trace.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/TraceManager.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/TraceManager.java
new file mode 100644
index 0000000000..edafd1dfb2
--- /dev/null
+++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/TraceManager.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 management. The implementer is expected to use an observability
+ * framework, e.g. OpenTelemetry. There should be only one instance of TraceManager per client.
+ */
+@BetaApi
+@InternalApi
+public interface TraceManager {
+ /** Starts a span and returns a handle to manage its lifecycle. */
+ Span createSpan(String name, Map attributes);
+
+ interface Span {
+ 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/AppCentricTracerFactoryTest.java b/gax-java/gax/src/test/java/com/google/api/gax/tracing/AppCentricTracerFactoryTest.java
new file mode 100644
index 0000000000..918d787da9
--- /dev/null
+++ b/gax-java/gax/src/test/java/com/google/api/gax/tracing/AppCentricTracerFactoryTest.java
@@ -0,0 +1,131 @@
+/*
+ * 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 java.util.Map;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+
+class AppCentricTracerFactoryTest {
+
+ @Test
+ void testNewTracer_createsOpenTelemetryTracingTracer() {
+ TraceManager recorder = mock(TraceManager.class);
+ when(recorder.createSpan(anyString(), anyMap())).thenReturn(mock(TraceManager.Span.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() {
+ TraceManager recorder = mock(TraceManager.class);
+ TraceManager.Span attemptHandle = mock(TraceManager.Span.class);
+ when(recorder.createSpan(anyString(), anyMap())).thenReturn(attemptHandle);
+
+ ApiTracerFactory factory =
+ new AppCentricTracerFactory(recorder, ApiTracerContext.newBuilder().build());
+ factory =
+ factory.withContext(ApiTracerContext.newBuilder().setServerAddress("test-address").build());
+ ApiTracer tracer =
+ factory.newTracer(
+ null, SpanName.of("service", "method"), ApiTracerFactory.OperationType.Unary);
+
+ tracer.attemptStarted(null, 1);
+
+ ArgumentCaptor