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 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)); + } + } +}