diff --git a/core/src/main/java/tech/ydb/core/grpc/Observability.java b/core/src/main/java/tech/ydb/core/grpc/Observability.java
new file mode 100644
index 000000000..c0d539da2
--- /dev/null
+++ b/core/src/main/java/tech/ydb/core/grpc/Observability.java
@@ -0,0 +1,29 @@
+package tech.ydb.core.grpc;
+
+public final class Observability {
+ public static final String TRACING_CHAIN = "ydb-sdk-tracing";
+ public static final String METRICS_CHAIN = "ydb-sdk-metrics";
+
+ public static final String TRACING_CHAIN_VERSION = "0.1.0";
+ public static final String METRICS_CHAIN_VERSION = "0.1.0";
+
+ public static final String TRACING_CHAIN_TOKEN = TRACING_CHAIN + "/" + TRACING_CHAIN_VERSION;
+ public static final String METRICS_CHAIN_TOKEN = METRICS_CHAIN + "/" + METRICS_CHAIN_VERSION;
+
+ private Observability() {
+ }
+
+ public static String appendChains(String base, boolean tracing, boolean metrics) {
+ if (!tracing && !metrics) {
+ return base;
+ }
+ StringBuilder sb = new StringBuilder(base);
+ if (tracing) {
+ sb.append(';').append(TRACING_CHAIN_TOKEN);
+ }
+ if (metrics) {
+ sb.append(';').append(METRICS_CHAIN_TOKEN);
+ }
+ return sb.toString();
+ }
+}
diff --git a/core/src/main/java/tech/ydb/core/grpc/YdbHeaders.java b/core/src/main/java/tech/ydb/core/grpc/YdbHeaders.java
index 4479b93c2..6e1208a96 100644
--- a/core/src/main/java/tech/ydb/core/grpc/YdbHeaders.java
+++ b/core/src/main/java/tech/ydb/core/grpc/YdbHeaders.java
@@ -41,7 +41,6 @@ private YdbHeaders() { }
public static ClientInterceptor createMetadataInterceptor(GrpcTransportBuilder builder) {
Metadata extraHeaders = new Metadata();
extraHeaders.put(YdbHeaders.DATABASE, builder.getDatabase());
- extraHeaders.put(YdbHeaders.BUILD_INFO, builder.getBuildInfo());
String appName = builder.getApplicationName();
if (appName != null) {
extraHeaders.put(YdbHeaders.APPLICATION_NAME, appName);
diff --git a/core/src/main/java/tech/ydb/core/impl/BaseGrpcTransport.java b/core/src/main/java/tech/ydb/core/impl/BaseGrpcTransport.java
index eaf2ff069..8d9700f9b 100644
--- a/core/src/main/java/tech/ydb/core/impl/BaseGrpcTransport.java
+++ b/core/src/main/java/tech/ydb/core/impl/BaseGrpcTransport.java
@@ -69,6 +69,10 @@ protected BaseGrpcTransport(EndpointRecord serverEndpoint) {
protected abstract GrpcChannel getChannel(GrpcRequestSettings settings);
+ protected String getBuildInfo() {
+ return null;
+ }
+
protected void pessimizeEndpoint(EndpointRecord endpoint, String reason) {
// nothing to pessimize
}
@@ -244,6 +248,10 @@ private static Status deadlineExpiredStatus(MethodDescriptor, ?> method, GrpcR
private Metadata makeMetadataFromSettings(GrpcRequestSettings settings, EndpointRecord endpoint) {
Metadata metadata = new Metadata();
+ String buildInfo = getBuildInfo();
+ if (buildInfo != null) {
+ metadata.put(YdbHeaders.BUILD_INFO, buildInfo);
+ }
String token = getAuthCallOptions().getToken();
if (token != null) {
metadata.put(YdbHeaders.AUTH_TICKET, token);
diff --git a/core/src/main/java/tech/ydb/core/impl/BuildInfoChainSupport.java b/core/src/main/java/tech/ydb/core/impl/BuildInfoChainSupport.java
new file mode 100644
index 000000000..f69e46bda
--- /dev/null
+++ b/core/src/main/java/tech/ydb/core/impl/BuildInfoChainSupport.java
@@ -0,0 +1,22 @@
+package tech.ydb.core.impl;
+
+import tech.ydb.core.grpc.GrpcTransport;
+
+/**
+ * Internal bridge for enabling SDK observability adoption chains on the transport after it has been built.
+ *
+ * Unlike tracing (configured on the builder before {@code build()}), metrics adoption becomes known only
+ * once a higher-level client is wired with a real {@code Meter}, which happens after the transport exists.
+ * This class avoids adding a public transport hook by unwrapping the default SDK implementation and calling
+ * a package-private extension point on it; non-SDK transports silently no-op.
+ */
+public final class BuildInfoChainSupport {
+ private BuildInfoChainSupport() {
+ }
+
+ public static void enableMetricsChain(GrpcTransport transport) {
+ if (transport instanceof YdbTransportImpl) {
+ ((YdbTransportImpl) transport).enableMetricsChain();
+ }
+ }
+}
diff --git a/core/src/main/java/tech/ydb/core/impl/FixedCallOptionsTransport.java b/core/src/main/java/tech/ydb/core/impl/FixedCallOptionsTransport.java
index c93f1576a..05a4da80a 100644
--- a/core/src/main/java/tech/ydb/core/impl/FixedCallOptionsTransport.java
+++ b/core/src/main/java/tech/ydb/core/impl/FixedCallOptionsTransport.java
@@ -1,6 +1,7 @@
package tech.ydb.core.impl;
import java.util.concurrent.ScheduledExecutorService;
+import java.util.function.Supplier;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -22,18 +23,21 @@ public class FixedCallOptionsTransport extends BaseGrpcTransport {
private final AuthCallOptions callOptions;
private final String database;
private final GrpcChannel channel;
+ private final Supplier buildInfoSupplier;
public FixedCallOptionsTransport(
ScheduledExecutorService scheduler,
AuthCallOptions callOptions,
String database,
EndpointRecord endpoint,
- ManagedChannelFactory channelFactory) {
+ ManagedChannelFactory channelFactory,
+ Supplier buildInfoSupplier) {
super(endpoint);
this.scheduler = scheduler;
this.callOptions = callOptions;
this.database = database;
this.channel = new GrpcChannel(endpoint, channelFactory);
+ this.buildInfoSupplier = buildInfoSupplier;
}
@Override
@@ -46,6 +50,11 @@ public String getDatabase() {
return database;
}
+ @Override
+ protected String getBuildInfo() {
+ return buildInfoSupplier.get();
+ }
+
@Override
protected void shutdown() {
channel.shutdown();
diff --git a/core/src/main/java/tech/ydb/core/impl/YdbTransportImpl.java b/core/src/main/java/tech/ydb/core/impl/YdbTransportImpl.java
index 52d4ba018..c25d755da 100644
--- a/core/src/main/java/tech/ydb/core/impl/YdbTransportImpl.java
+++ b/core/src/main/java/tech/ydb/core/impl/YdbTransportImpl.java
@@ -15,12 +15,14 @@
import tech.ydb.core.grpc.GrpcRequestSettings;
import tech.ydb.core.grpc.GrpcTransport;
import tech.ydb.core.grpc.GrpcTransportBuilder;
+import tech.ydb.core.grpc.Observability;
import tech.ydb.core.impl.auth.AuthCallOptions;
import tech.ydb.core.impl.pool.EndpointPool;
import tech.ydb.core.impl.pool.EndpointRecord;
import tech.ydb.core.impl.pool.GrpcChannel;
import tech.ydb.core.impl.pool.GrpcChannelPool;
import tech.ydb.core.impl.pool.ManagedChannelFactory;
+import tech.ydb.core.tracing.NoopTracer;
import tech.ydb.core.tracing.Tracer;
/**
@@ -38,6 +40,10 @@ public class YdbTransportImpl extends BaseGrpcTransport {
private final YdbDiscovery discovery;
private final Tracer tracer;
+ private final String baseBuildInfo;
+ private final boolean tracingChainEnabled;
+ private volatile boolean metricsChainEnabled;
+
public YdbTransportImpl(GrpcTransportBuilder builder) {
super(builder);
BalancingSettings balancingSettings = getBalancingSettings(builder);
@@ -45,6 +51,8 @@ public YdbTransportImpl(GrpcTransportBuilder builder) {
this.database = Strings.nullToEmpty(builder.getDatabase());
this.tracer = builder.getTracer();
+ this.baseBuildInfo = builder.getBuildInfo();
+ this.tracingChainEnabled = tracer != NoopTracer.getInstance();
logger.info("Create YDB transport with endpoint {} and {}", serverEndpoint, balancingSettings);
@@ -124,6 +132,19 @@ public Tracer getTracer() {
return tracer;
}
+ void enableMetricsChain() {
+ this.metricsChainEnabled = true;
+ }
+
+ @Override
+ protected String getBuildInfo() {
+ return baseBuildInfo;
+ }
+
+ private String discoveryBuildInfo() {
+ return Observability.appendChains(baseBuildInfo, tracingChainEnabled, metricsChainEnabled);
+ }
+
@Override
public AuthCallOptions getAuthCallOptions() {
return callOptions;
@@ -167,7 +188,14 @@ public CompletableFuture handleEndpoints(List endpoints
@Override
public GrpcTransport createDiscoveryTransport() {
- return new FixedCallOptionsTransport(scheduler, callOptions, database, serverEndpoint, channelFactory);
+ return new FixedCallOptionsTransport(
+ scheduler,
+ callOptions,
+ database,
+ serverEndpoint,
+ channelFactory,
+ YdbTransportImpl.this::discoveryBuildInfo
+ );
}
}
}
diff --git a/core/src/main/java/tech/ydb/core/impl/auth/AuthCallOptions.java b/core/src/main/java/tech/ydb/core/impl/auth/AuthCallOptions.java
index 925a36be3..712fff5ad 100644
--- a/core/src/main/java/tech/ydb/core/impl/auth/AuthCallOptions.java
+++ b/core/src/main/java/tech/ydb/core/impl/auth/AuthCallOptions.java
@@ -38,7 +38,8 @@ public AuthCallOptions(
AuthRpcProvider super GrpcAuthRpc> authProvider = builder.getAuthProvider();
if (authProvider != null) {
- GrpcAuthRpc rpc = new GrpcAuthRpc(endpoints, scheduler, builder.getDatabase(), channelFactory);
+ GrpcAuthRpc rpc = new GrpcAuthRpc(
+ endpoints, scheduler, builder.getDatabase(), channelFactory, builder.getBuildInfo());
authIdentity = builder.getAuthProvider().createAuthIdentity(rpc);
} else {
authIdentity = null;
diff --git a/core/src/main/java/tech/ydb/core/impl/auth/GrpcAuthRpc.java b/core/src/main/java/tech/ydb/core/impl/auth/GrpcAuthRpc.java
index dd0b1a1df..6f2608d9a 100644
--- a/core/src/main/java/tech/ydb/core/impl/auth/GrpcAuthRpc.java
+++ b/core/src/main/java/tech/ydb/core/impl/auth/GrpcAuthRpc.java
@@ -19,13 +19,15 @@ public class GrpcAuthRpc {
private final ScheduledExecutorService scheduler;
private final String database;
private final ManagedChannelFactory channelFactory;
+ private final String buildInfo;
private final AtomicInteger endpointIdx = new AtomicInteger();
public GrpcAuthRpc(
List endpoints,
ScheduledExecutorService scheduler,
String database,
- ManagedChannelFactory channelFactory) {
+ ManagedChannelFactory channelFactory,
+ String buildInfo) {
if (endpoints == null || endpoints.isEmpty()) {
throw new IllegalStateException("Empty endpoints list for auth rpc");
}
@@ -33,6 +35,7 @@ public GrpcAuthRpc(
this.scheduler = scheduler;
this.database = database;
this.channelFactory = channelFactory;
+ this.buildInfo = buildInfo;
}
public ExecutorService getExecutor() {
@@ -55,13 +58,15 @@ public void changeEndpoint() {
}
public GrpcTransport createTransport() {
- // For auth provider we use transport without auth (with default CallOptions)
+ // For auth provider we use transport without auth (with default CallOptions).
+ // Auth requests report only the base build-info chain, matching regular (non-discovery) requests.
return new FixedCallOptionsTransport(
scheduler,
new AuthCallOptions(),
database,
endpoints.get(endpointIdx.get()),
- channelFactory
+ channelFactory,
+ () -> buildInfo
);
}
diff --git a/core/src/test/java/tech/ydb/core/grpc/ObservabilityTest.java b/core/src/test/java/tech/ydb/core/grpc/ObservabilityTest.java
new file mode 100644
index 000000000..7e3598bf6
--- /dev/null
+++ b/core/src/test/java/tech/ydb/core/grpc/ObservabilityTest.java
@@ -0,0 +1,41 @@
+package tech.ydb.core.grpc;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class ObservabilityTest {
+
+ private static final String BASE = "ydb-java-sdk/1.2.3";
+
+ @Test
+ public void disabledObservabilityLeavesBaseUntouched() {
+ Assert.assertEquals(BASE, Observability.appendChains(BASE, false, false));
+ }
+
+ @Test
+ public void tracingOnlyAppendsTracingChain() {
+ Assert.assertEquals(
+ BASE + ";" + Observability.TRACING_CHAIN_TOKEN,
+ Observability.appendChains(BASE, true, false));
+ }
+
+ @Test
+ public void metricsOnlyAppendsMetricsChain() {
+ Assert.assertEquals(
+ BASE + ";" + Observability.METRICS_CHAIN_TOKEN,
+ Observability.appendChains(BASE, false, true));
+ }
+
+ @Test
+ public void bothChainsKeepTracingBeforeMetrics() {
+ Assert.assertEquals(
+ BASE + ";" + Observability.TRACING_CHAIN_TOKEN + ";" + Observability.METRICS_CHAIN_TOKEN,
+ Observability.appendChains(BASE, true, true));
+ }
+
+ @Test
+ public void chainTokensFollowNameSlashVersionFormat() {
+ Assert.assertEquals("ydb-sdk-tracing/0.1.0", Observability.TRACING_CHAIN_TOKEN);
+ Assert.assertEquals("ydb-sdk-metrics/0.1.0", Observability.METRICS_CHAIN_TOKEN);
+ }
+}
diff --git a/core/src/test/java/tech/ydb/core/impl/MockedCall.java b/core/src/test/java/tech/ydb/core/impl/MockedCall.java
index 9b4bcf8e4..9bbada958 100644
--- a/core/src/test/java/tech/ydb/core/impl/MockedCall.java
+++ b/core/src/test/java/tech/ydb/core/impl/MockedCall.java
@@ -15,6 +15,7 @@
public abstract class MockedCall extends ClientCall {
private final Executor executor;
+ private volatile Metadata lastHeaders;
protected MockedCall(Executor executor) {
this.executor = executor;
@@ -22,8 +23,17 @@ protected MockedCall(Executor executor) {
protected abstract void complete(Listener listener);
+ /**
+ * Returns the request headers captured on the most recent {@link #start} call, or {@code null} if the
+ * call has not been started yet. Handy for asserting per-request metadata such as x-ydb-sdk-build-info.
+ */
+ public Metadata getLastHeaders() {
+ return lastHeaders;
+ }
+
@Override
public void start(Listener listener, Metadata headers) {
+ this.lastHeaders = headers;
executor.execute(() -> complete(listener));
}
diff --git a/core/src/test/java/tech/ydb/core/impl/YdbDiscoveryTest.java b/core/src/test/java/tech/ydb/core/impl/YdbDiscoveryTest.java
index cde86d055..16a04cdd5 100644
--- a/core/src/test/java/tech/ydb/core/impl/YdbDiscoveryTest.java
+++ b/core/src/test/java/tech/ydb/core/impl/YdbDiscoveryTest.java
@@ -255,7 +255,9 @@ public Instant instant() {
@Override
public GrpcTransport createDiscoveryTransport() {
EndpointRecord discovery = new EndpointRecord("unknown", 1234);
- return new FixedCallOptionsTransport(scheduler, new AuthCallOptions(), "/test", discovery, channelFactory);
+ return new FixedCallOptionsTransport(
+ scheduler, new AuthCallOptions(), "/test", discovery, channelFactory, () -> null
+ );
}
@Override
diff --git a/core/src/test/java/tech/ydb/core/impl/YdbTransportImplTest.java b/core/src/test/java/tech/ydb/core/impl/YdbTransportImplTest.java
index b3e3566b2..613321ee3 100644
--- a/core/src/test/java/tech/ydb/core/impl/YdbTransportImplTest.java
+++ b/core/src/test/java/tech/ydb/core/impl/YdbTransportImplTest.java
@@ -28,6 +28,7 @@
import tech.ydb.core.UnexpectedResultException;
import tech.ydb.core.grpc.GrpcRequestSettings;
import tech.ydb.core.grpc.GrpcTransport;
+import tech.ydb.core.grpc.Observability;
import tech.ydb.core.grpc.YdbHeaders;
import tech.ydb.core.impl.pool.EndpointRecord;
import tech.ydb.core.impl.pool.ManagedChannelFactory;
@@ -35,6 +36,7 @@
import tech.ydb.core.tracing.NoopTracer;
import tech.ydb.core.tracing.Span;
import tech.ydb.core.tracing.Tracer;
+import tech.ydb.core.utils.Version;
import tech.ydb.proto.discovery.DiscoveryProtos;
import tech.ydb.proto.discovery.v1.DiscoveryServiceGrpc;
@@ -275,6 +277,62 @@ public void customTracerTest() {
Assert.assertSame(customTracer, transport.getTracer());
}
+ @Test
+ public void tracingChainRidesDiscoveryRequestOnly() {
+ MockedCall.DiscoveryCall discoveryCall =
+ MockedCall.discovery(testScheduler, "self", new EndpointRecord("node", 2136));
+ Mockito.when(discoveryChannel.newCall(Mockito.eq(DiscoveryServiceGrpc.getListEndpointsMethod()), Mockito.any()))
+ .thenReturn(discoveryCall);
+
+ MockedCall.WhoAmICall whoAmICall = MockedCall.whoAmICall(testScheduler, "i am node");
+ Mockito.when(transportChannel.newCall(Mockito.eq(DiscoveryServiceGrpc.getWhoAmIMethod()), Mockito.any()))
+ .thenReturn(whoAmICall);
+
+ GrpcTransport transport = GrpcTransport.forConnectionString("grpc://mocked:2136/local")
+ .withChannelFactoryBuilder(builder -> channelFactory)
+ .withTracer(Mockito.mock(Tracer.class))
+ .build();
+
+ Assert.assertTrue(whoAmI(transport).join().isSuccess());
+
+ String base = "ydb-java-sdk/" + Version.getVersion().get();
+ // The discovery request reports the tracing adoption chain ...
+ Assert.assertEquals(base + ";" + Observability.TRACING_CHAIN_TOKEN,
+ discoveryCall.getLastHeaders().get(YdbHeaders.BUILD_INFO));
+ // ... while a regular request reports only the base chain.
+ Assert.assertEquals(base, whoAmICall.getLastHeaders().get(YdbHeaders.BUILD_INFO));
+
+ transport.close();
+ }
+
+ @Test
+ public void metricsChainAppearsOnDiscoveryAfterMeterEnabled() {
+ MockedScheduler scheduler = new MockedScheduler(MockedClock.create(ZoneId.of("UTC")));
+
+ MockedCall.DiscoveryCall discoveryCall =
+ MockedCall.discovery("self", new EndpointRecord("node", 2136));
+ Mockito.when(discoveryChannel.newCall(Mockito.eq(DiscoveryServiceGrpc.getListEndpointsMethod()), Mockito.any()))
+ .thenReturn(discoveryCall);
+
+ @SuppressWarnings("deprecation")
+ GrpcTransport transport = GrpcTransport.forConnectionString("grpc://mocked:2136/local")
+ .withSchedulerFactory(() -> scheduler)
+ .withChannelFactoryBuilder(builder -> channelFactory)
+ .buildAsync(null);
+
+ // Metrics adoption becomes known only after a client is wired with a real Meter, i.e. after build().
+ BuildInfoChainSupport.enableMetricsChain(transport);
+
+ // The next discovery round must pick up the metrics adoption chain.
+ scheduler.runNextTask();
+
+ String base = "ydb-java-sdk/" + Version.getVersion().get();
+ Assert.assertEquals(base + ";" + Observability.METRICS_CHAIN_TOKEN,
+ discoveryCall.getLastHeaders().get(YdbHeaders.BUILD_INFO));
+
+ transport.close();
+ }
+
@Test
public void spanAttributesAndTraceparentAreSetInMetadata() {
EndpointRecord endpoint = new EndpointRecord("node", 2136, 42, "dc-a", null);
diff --git a/core/src/test/java/tech/ydb/core/impl/pool/DefaultChannelFactoryTest.java b/core/src/test/java/tech/ydb/core/impl/pool/DefaultChannelFactoryTest.java
index dd92b391c..7289635f9 100644
--- a/core/src/test/java/tech/ydb/core/impl/pool/DefaultChannelFactoryTest.java
+++ b/core/src/test/java/tech/ydb/core/impl/pool/DefaultChannelFactoryTest.java
@@ -29,7 +29,9 @@
import tech.ydb.core.grpc.GrpcTransport;
import tech.ydb.core.grpc.GrpcTransportBuilder;
+import tech.ydb.core.grpc.Observability;
import tech.ydb.core.grpc.YdbHeaders;
+import tech.ydb.core.tracing.Tracer;
import tech.ydb.core.utils.Version;
/**
@@ -107,7 +109,8 @@ public void defaultParams() {
Metadata metadata = metadataCapture.getValue();
Assert.assertEquals("/Root", metadata.get(YdbHeaders.DATABASE));
- Assert.assertEquals("ydb-java-sdk/" + Version.getVersion().get(), metadata.get(YdbHeaders.BUILD_INFO));
+ // BUILD_INFO is discovery-only; the channel-level interceptor must not attach it on the request path.
+ Assert.assertNull(metadata.get(YdbHeaders.BUILD_INFO));
Assert.assertNull(metadata.get(YdbHeaders.APPLICATION_NAME));
Assert.assertNull(metadata.get(YdbHeaders.CLIENT_PROCESS_ID));
}
@@ -149,7 +152,7 @@ public void customHeadersTest() {
Metadata metadata = metadataCapture.getValue();
Assert.assertEquals("/Root", metadata.get(YdbHeaders.DATABASE));
- Assert.assertEquals("ydb-java-sdk/" + Version.getVersion().get(), metadata.get(YdbHeaders.BUILD_INFO));
+ Assert.assertNull(metadata.get(YdbHeaders.BUILD_INFO));
Assert.assertEquals("test-application", metadata.get(YdbHeaders.APPLICATION_NAME));
Assert.assertEquals("client-hostname", metadata.get(YdbHeaders.CLIENT_PROCESS_ID));
}
@@ -166,7 +169,27 @@ public void customBuildInfoTest() {
String version = "ydb-java-sdk/" + Version.getVersion().get();
Metadata metadata = metadataCapture.getValue();
- Assert.assertEquals(version + ";driver/1.0.0;test-app/1.0.0", metadata.get(YdbHeaders.BUILD_INFO));
+ Assert.assertNull(metadata.get(YdbHeaders.BUILD_INFO));
+ Assert.assertEquals(version + ";driver/1.0.0;test-app/1.0.0", builder.getBuildInfo());
+ }
+
+ @Test
+ public void tracerDoesNotLeakObservabilityChainIntoBaseBuildInfo() {
+ // Observability chains must ride the discovery request only, never the base build-info that is
+ // reported on every request. Configuring a tracer must therefore leave the base build-info intact.
+ Tracer tracer = Mockito.mock(Tracer.class);
+ GrpcTransportBuilder builder = GrpcTransport.forHost(MOCKED_HOST, MOCKED_PORT, "/Root")
+ .withTracer(tracer);
+ ManagedChannelFactory factory = ChannelFactoryLoader.load().buildFactory(builder);
+
+ Assert.assertSame(channelMock, factory.newManagedChannel(MOCKED_HOST, MOCKED_PORT, null));
+ channelStaticMock.verify(FOR_ADDRESS, Mockito.times(1));
+
+ String version = "ydb-java-sdk/" + Version.getVersion().get();
+ Metadata metadata = metadataCapture.getValue();
+ Assert.assertNull(metadata.get(YdbHeaders.BUILD_INFO));
+ Assert.assertEquals(version, builder.getBuildInfo());
+ Assert.assertFalse(builder.getBuildInfo().contains(Observability.TRACING_CHAIN_TOKEN));
}
@Test
diff --git a/query/src/main/java/tech/ydb/query/impl/QueryClientImpl.java b/query/src/main/java/tech/ydb/query/impl/QueryClientImpl.java
index 0994f07cd..d701e340d 100644
--- a/query/src/main/java/tech/ydb/query/impl/QueryClientImpl.java
+++ b/query/src/main/java/tech/ydb/query/impl/QueryClientImpl.java
@@ -9,6 +9,7 @@
import tech.ydb.core.Result;
import tech.ydb.core.grpc.GrpcTransport;
+import tech.ydb.core.impl.BuildInfoChainSupport;
import tech.ydb.core.metrics.Meter;
import tech.ydb.core.tracing.Tracer;
import tech.ydb.query.QueryClient;
@@ -139,6 +140,9 @@ public Builder withMeter(Meter meter, String poolName) {
"poolName must be a non-empty string when a Meter is provided");
this.meter = meter;
this.sessionPoolName = poolName;
+ if (meter != Meter.NOOP) {
+ BuildInfoChainSupport.enableMetricsChain(transport);
+ }
return this;
}
diff --git a/query/src/test/java/tech/ydb/query/impl/QueryClientBuilderObservabilityTest.java b/query/src/test/java/tech/ydb/query/impl/QueryClientBuilderObservabilityTest.java
new file mode 100644
index 000000000..611231586
--- /dev/null
+++ b/query/src/test/java/tech/ydb/query/impl/QueryClientBuilderObservabilityTest.java
@@ -0,0 +1,44 @@
+package tech.ydb.query.impl;
+
+import java.util.concurrent.ScheduledExecutorService;
+
+import org.junit.Test;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
+
+import tech.ydb.core.grpc.GrpcTransport;
+import tech.ydb.core.impl.BuildInfoChainSupport;
+import tech.ydb.core.metrics.Meter;
+
+public class QueryClientBuilderObservabilityTest {
+
+ @Test
+ public void noopMeterDoesNotEnableMetricsBuildInfoChain() {
+ GrpcTransport transport = Mockito.mock(GrpcTransport.class);
+ ScheduledExecutorService scheduler = Mockito.mock(ScheduledExecutorService.class);
+ Mockito.when(transport.getScheduler()).thenReturn(scheduler);
+ Mockito.when(transport.getDatabase()).thenReturn("/local");
+
+ try (MockedStatic helper = Mockito.mockStatic(BuildInfoChainSupport.class)) {
+ QueryClientImpl.Builder builder = new QueryClientImpl.Builder(transport);
+ builder.withMeter(Meter.NOOP, "default");
+
+ helper.verifyNoInteractions();
+ }
+ }
+
+ @Test
+ public void openTelemetryMeterEnablesMetricsBuildInfoChain() {
+ GrpcTransport transport = Mockito.mock(GrpcTransport.class);
+ ScheduledExecutorService scheduler = Mockito.mock(ScheduledExecutorService.class);
+ Mockito.when(transport.getScheduler()).thenReturn(scheduler);
+ Mockito.when(transport.getDatabase()).thenReturn("/local");
+
+ try (MockedStatic helper = Mockito.mockStatic(BuildInfoChainSupport.class)) {
+ QueryClientImpl.Builder builder = new QueryClientImpl.Builder(transport);
+ builder.withMeter(Mockito.mock(Meter.class), "default");
+
+ helper.verify(() -> BuildInfoChainSupport.enableMetricsChain(transport), Mockito.times(1));
+ }
+ }
+}
diff --git a/table/src/main/java/tech/ydb/table/impl/PooledTableClient.java b/table/src/main/java/tech/ydb/table/impl/PooledTableClient.java
index 38834344b..36b524362 100644
--- a/table/src/main/java/tech/ydb/table/impl/PooledTableClient.java
+++ b/table/src/main/java/tech/ydb/table/impl/PooledTableClient.java
@@ -15,6 +15,7 @@
import tech.ydb.table.impl.pool.SessionPool;
import tech.ydb.table.impl.pool.SessionPoolOptions;
import tech.ydb.table.rpc.TableRpc;
+import tech.ydb.table.rpc.grpc.GrpcTableRpc;
/**
* @author Aleksandr Gorshenin
@@ -142,6 +143,9 @@ public Builder withMeter(Meter meter, String poolName) {
"poolName must be a non-empty string when a Meter is provided");
this.meter = meter;
this.poolName = poolName;
+ if (meter != Meter.NOOP) {
+ GrpcTableRpc.enableMetricsChain(tableRpc);
+ }
return this;
}
diff --git a/table/src/main/java/tech/ydb/table/rpc/grpc/GrpcTableRpc.java b/table/src/main/java/tech/ydb/table/rpc/grpc/GrpcTableRpc.java
index f1c597646..ab2e44f71 100644
--- a/table/src/main/java/tech/ydb/table/rpc/grpc/GrpcTableRpc.java
+++ b/table/src/main/java/tech/ydb/table/rpc/grpc/GrpcTableRpc.java
@@ -12,6 +12,7 @@
import tech.ydb.core.grpc.GrpcReadStream;
import tech.ydb.core.grpc.GrpcRequestSettings;
import tech.ydb.core.grpc.GrpcTransport;
+import tech.ydb.core.impl.BuildInfoChainSupport;
import tech.ydb.core.operation.OperationBinder;
import tech.ydb.core.operation.StatusExtractor;
import tech.ydb.core.tracing.Tracer;
@@ -252,6 +253,13 @@ public Tracer getTracer() {
return transport.getTracer();
}
+ public static void enableMetricsChain(TableRpc tableRpc) {
+ if (tableRpc instanceof GrpcTableRpc) {
+ GrpcTableRpc grpc = (GrpcTableRpc) tableRpc;
+ BuildInfoChainSupport.enableMetricsChain(grpc.transport);
+ }
+ }
+
@Override
public void close() {
if (transportOwned) {
diff --git a/table/src/test/java/tech/ydb/table/impl/PooledTableClientObservabilityTest.java b/table/src/test/java/tech/ydb/table/impl/PooledTableClientObservabilityTest.java
new file mode 100644
index 000000000..dbf62f50a
--- /dev/null
+++ b/table/src/test/java/tech/ydb/table/impl/PooledTableClientObservabilityTest.java
@@ -0,0 +1,35 @@
+package tech.ydb.table.impl;
+
+import org.junit.Test;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
+
+import tech.ydb.core.metrics.Meter;
+import tech.ydb.table.rpc.TableRpc;
+import tech.ydb.table.rpc.grpc.GrpcTableRpc;
+
+public class PooledTableClientObservabilityTest {
+
+ @Test
+ public void noopMeterDoesNotEnableMetricsBuildInfoChain() {
+ TableRpc rpc = Mockito.mock(TableRpc.class);
+
+ try (MockedStatic helper = Mockito.mockStatic(GrpcTableRpc.class)) {
+ PooledTableClient.newClient(rpc).withMeter(Meter.NOOP, "default");
+
+ helper.verifyNoInteractions();
+ }
+ }
+
+ @Test
+ public void realMeterEnablesMetricsBuildInfoChain() {
+ TableRpc rpc = Mockito.mock(TableRpc.class);
+ Meter meter = Mockito.mock(Meter.class);
+
+ try (MockedStatic helper = Mockito.mockStatic(GrpcTableRpc.class)) {
+ PooledTableClient.newClient(rpc).withMeter(meter, "default");
+
+ helper.verify(() -> GrpcTableRpc.enableMetricsChain(rpc), Mockito.times(1));
+ }
+ }
+}