Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,19 @@
<scope>test</scope>
</dependency>

<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-sdk</artifactId>
<version>${opentelemetry.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-sdk-testing</artifactId>
<version>${opentelemetry.version}</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk18on</artifactId>
Expand Down
28 changes: 28 additions & 0 deletions core/src/main/java/tech/ydb/core/metrics/Attr.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package tech.ydb.core.metrics;

import java.util.Objects;

/**
* Single immutable attribute (key + value) attached to a metric measurement.
*/
public final class Attr {
private final String key;
private final String value;

private Attr(String key, String value) {
this.key = Objects.requireNonNull(key, "key is null");
this.value = Objects.requireNonNull(value, "value is null");
}

public static Attr of(String key, String value) {
return new Attr(key, value);
}

public String getKey() {
return key;
}

public String getValue() {
return value;
}
}
19 changes: 19 additions & 0 deletions core/src/main/java/tech/ydb/core/metrics/DoubleHistogram.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package tech.ydb.core.metrics;

import io.grpc.ExperimentalApi;

/**
* Histogram of {@code double} values (typically used for durations in seconds).
*/
@ExperimentalApi("YDB Meter is experimental and API may change without notice")
public interface DoubleHistogram {
DoubleHistogram NOOP = (value, attrs) -> { };

/**
* Records the given value with optional attributes.
*
* @param value value to record
* @param attrs measurement attributes
*/
void record(double value, Attr... attrs);
}
19 changes: 19 additions & 0 deletions core/src/main/java/tech/ydb/core/metrics/LongCounter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package tech.ydb.core.metrics;

import io.grpc.ExperimentalApi;

/**
* A monotonic counter that records non-negative {@code long} deltas.
*/
@ExperimentalApi("YDB Meter is experimental and API may change without notice")
public interface LongCounter {
LongCounter NOOP = (value, attrs) -> { };

/**
* Adds the given value with optional attributes.
*
* @param value non-negative delta
* @param attrs measurement attributes
*/
void add(long value, Attr... attrs);
}
20 changes: 20 additions & 0 deletions core/src/main/java/tech/ydb/core/metrics/LongMeasurement.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package tech.ydb.core.metrics;

import io.grpc.ExperimentalApi;

/**
* Per-observation handle passed to {@link Meter#createLongGauge} callbacks.
*
* <p>A single callback invocation may call {@link #record} multiple times with different
* attributes to emit several measurements per collection cycle.
*/
@ExperimentalApi("YDB Meter is experimental and API may change without notice")
public interface LongMeasurement {
/**
* Records the current value of the gauge for the given attribute set.
*
* @param value observed value
* @param attrs measurement attributes
*/
void record(long value, Attr... attrs);
}
26 changes: 26 additions & 0 deletions core/src/main/java/tech/ydb/core/metrics/Meter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package tech.ydb.core.metrics;

import java.util.function.Consumer;

import io.grpc.ExperimentalApi;

/**
* Entry point to create metric instruments. The interface is dependency-free, so the SDK core does
* not require an OpenTelemetry runtime to compile or run. Implementations must be thread-safe.
*/
@ExperimentalApi("YDB Meter is experimental and API may change without notice")
public interface Meter {
Meter NOOP = new Meter() { };

default LongCounter createCounter(String name, String unit, String description) {
return LongCounter.NOOP;
}

default DoubleHistogram createHistogram(String name, String unit, String description) {
return DoubleHistogram.NOOP;
}

default void createLongGauge(String name, String unit, String description, Consumer<LongMeasurement> callback) {
// noop: the backend never queries the callback
}
}
86 changes: 86 additions & 0 deletions core/src/main/java/tech/ydb/core/metrics/OpenTelemetryMeter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package tech.ydb.core.metrics;

import java.util.Objects;
import java.util.function.Consumer;

import io.grpc.ExperimentalApi;
import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.common.AttributesBuilder;
import io.opentelemetry.api.metrics.DoubleHistogramBuilder;
import io.opentelemetry.api.metrics.LongCounterBuilder;
import io.opentelemetry.api.metrics.LongGaugeBuilder;

/**
* OpenTelemetry-backed implementation of {@link Meter}.
*/
@ExperimentalApi("YDB Meter is experimental and API may change without notice")
public final class OpenTelemetryMeter implements Meter {
private static final String DEFAULT_SCOPE = "tech.ydb.sdk";

private final io.opentelemetry.api.metrics.Meter meter;

private OpenTelemetryMeter(io.opentelemetry.api.metrics.Meter meter) {
this.meter = Objects.requireNonNull(meter, "meter is null");
}

public static OpenTelemetryMeter fromOpenTelemetry(OpenTelemetry openTelemetry) {
Objects.requireNonNull(openTelemetry, "openTelemetry is null");
return new OpenTelemetryMeter(openTelemetry.getMeter(DEFAULT_SCOPE));
}

public static OpenTelemetryMeter createGlobal() {
return fromOpenTelemetry(GlobalOpenTelemetry.get());
}

@Override
public LongCounter createCounter(String name, String unit, String description) {
LongCounterBuilder builder = meter.counterBuilder(name);
if (unit != null) {
builder.setUnit(unit);
}
if (description != null) {
builder.setDescription(description);
}
io.opentelemetry.api.metrics.LongCounter counter = builder.build();
return (value, attrs) -> counter.add(value, attributesOf(attrs));
}

@Override
public DoubleHistogram createHistogram(String name, String unit, String description) {
DoubleHistogramBuilder builder = meter.histogramBuilder(name);
if (unit != null) {
builder.setUnit(unit);
}
if (description != null) {
builder.setDescription(description);
}
io.opentelemetry.api.metrics.DoubleHistogram histogram = builder.build();
return (value, attrs) -> histogram.record(value, attributesOf(attrs));
}

@Override
public void createLongGauge(String name, String unit, String description, Consumer<LongMeasurement> callback) {
LongGaugeBuilder builder = meter.gaugeBuilder(name).ofLongs();
if (unit != null) {
builder.setUnit(unit);
}
if (description != null) {
builder.setDescription(description);
}
builder.buildWithCallback(otelMeasurement ->
callback.accept((value, attrs) -> otelMeasurement.record(value, attributesOf(attrs))));
}

private static Attributes attributesOf(Attr[] attrs) {
if (attrs == null || attrs.length == 0) {
return Attributes.empty();
}
AttributesBuilder builder = Attributes.builder();
for (Attr attr : attrs) {
builder.put(attr.getKey(), attr.getValue());
}
return builder.build();
}
}
120 changes: 120 additions & 0 deletions core/src/test/java/tech/ydb/core/metrics/OpenTelemetryMeterTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package tech.ydb.core.metrics;

import java.io.IOException;
import java.util.Collection;
import java.util.concurrent.atomic.AtomicLong;

import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.sdk.OpenTelemetrySdk;
import io.opentelemetry.sdk.metrics.SdkMeterProvider;
import io.opentelemetry.sdk.metrics.data.HistogramPointData;
import io.opentelemetry.sdk.metrics.data.LongPointData;
import io.opentelemetry.sdk.metrics.data.MetricData;
import io.opentelemetry.sdk.testing.exporter.InMemoryMetricReader;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;

public class OpenTelemetryMeterTest {
private static final AttributeKey<String> POOL = AttributeKey.stringKey("pool.name");
private static final AttributeKey<String> STATE = AttributeKey.stringKey("state");

private InMemoryMetricReader reader;
private SdkMeterProvider provider;
private OpenTelemetryMeter meter;

@Before
public void setup() {
reader = InMemoryMetricReader.create();
provider = SdkMeterProvider.builder().registerMetricReader(reader).build();
OpenTelemetry openTelemetry = OpenTelemetrySdk.builder().setMeterProvider(provider).build();
meter = OpenTelemetryMeter.fromOpenTelemetry(openTelemetry);
}

@After
public void tearDown() throws IOException {
provider.close();
reader.close();
}

@Test
public void counterReportsValueAndAttributes() {
LongCounter counter = meter.createCounter("ydb.test.counter", "{session}", "test counter");
counter.add(3L, Attr.of("pool.name", "my-pool"));
counter.add(2L, Attr.of("pool.name", "my-pool"));

MetricData metric = single("ydb.test.counter");
Assert.assertEquals("{session}", metric.getUnit());
Assert.assertEquals("test counter", metric.getDescription());

LongPointData point = singleLongPoint(metric.getLongSumData().getPoints());
Assert.assertEquals(5L, point.getValue());
Assert.assertEquals("my-pool", point.getAttributes().get(POOL));
}

@Test
public void histogramReportsValueAndAttributes() {
DoubleHistogram histogram = meter.createHistogram("ydb.test.histogram", "s", "test histogram");
histogram.record(0.5d, Attr.of("pool.name", "my-pool"));

MetricData metric = single("ydb.test.histogram");
Assert.assertEquals("s", metric.getUnit());
Assert.assertEquals("test histogram", metric.getDescription());

Collection<HistogramPointData> points = metric.getHistogramData().getPoints();
Assert.assertEquals(1, points.size());
HistogramPointData point = points.iterator().next();
Assert.assertEquals(1L, point.getCount());
Assert.assertEquals(0.5d, point.getSum(), 0.0001d);
Assert.assertEquals("my-pool", point.getAttributes().get(POOL));
}

@Test
public void gaugeInvokesCallbackOnCollect() {
AtomicLong value = new AtomicLong(7L);
meter.createLongGauge("ydb.test.gauge", "{session}", "test gauge",
m -> m.record(value.get(), Attr.of("pool.name", "my-pool"), Attr.of("state", "idle")));

MetricData metric = single("ydb.test.gauge");
Assert.assertEquals("{session}", metric.getUnit());
Assert.assertEquals("test gauge", metric.getDescription());

LongPointData point = singleLongPoint(metric.getLongGaugeData().getPoints());
Assert.assertEquals(7L, point.getValue());
Assert.assertEquals("my-pool", point.getAttributes().get(POOL));
Assert.assertEquals("idle", point.getAttributes().get(STATE));

value.set(11L);
LongPointData updated = singleLongPoint(single("ydb.test.gauge").getLongGaugeData().getPoints());
Assert.assertEquals(11L, updated.getValue());
}

@Test
public void emptyAttributesAreSupported() {
LongCounter counter = meter.createCounter("ydb.test.noattrs", null, null);
counter.add(1L);

LongPointData point = singleLongPoint(single("ydb.test.noattrs").getLongSumData().getPoints());
Assert.assertEquals(1L, point.getValue());
Assert.assertEquals(Attributes.empty(), point.getAttributes());
}

private MetricData single(String name) {
MetricData found = null;
for (MetricData metric : reader.collectAllMetrics()) {
if (name.equals(metric.getName())) {
found = metric;
}
}
Assert.assertNotNull(name + " metric not found", found);
return found;
}

private static LongPointData singleLongPoint(Collection<LongPointData> points) {
Assert.assertEquals(1, points.size());
return points.iterator().next();
}
}
29 changes: 25 additions & 4 deletions query/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,9 @@
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<!-- Parent BOM uses Mockito 5.x (Java 11+); query tests compile on JDK 8 in CI -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>4.11.0</version>
<scope>test</scope>
</dependency>
<dependency>
Expand Down Expand Up @@ -86,7 +84,9 @@
<environmentVariables>
<TESTCONTAINERS_REUSE_ENABLE>true</TESTCONTAINERS_REUSE_ENABLE>
<YDB_DOCKER_IMAGE>ydbplatform/local-ydb:trunk</YDB_DOCKER_IMAGE>
<YDB_DOCKER_FEATURE_FLAGS>enable_columnshard_bool,enable_resource_pools,enable_arrow_result_set_format</YDB_DOCKER_FEATURE_FLAGS>
<YDB_DOCKER_FEATURE_FLAGS>
enable_columnshard_bool,enable_resource_pools,enable_arrow_result_set_format
</YDB_DOCKER_FEATURE_FLAGS>
</environmentVariables>
</configuration>
</plugin>
Expand All @@ -99,9 +99,30 @@
<jdk>[9,)</jdk>
</activation>
<properties>
<!-- Hide warnings on JDK later 8 -->
<!-- Hide warnings on JDK later 8; allow Mockito agent self-attach on JDK 21+ -->
<!-- Required for Apache Arrow result set format: opens java.nio for direct memory access on JDK 9+ -->
<argLine>--add-opens=java.base/java.nio=ALL-UNNAMED</argLine>
</properties>
</profile>
<profile>
<id>jdk8-build</id>
<activation>
<jdk>1.8</jdk>
</activation>

<properties>
<!-- Downgrade Mockito to 4 version -->
<mockito.version>4.11.0</mockito.version>
</properties>

<dependencies>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<version>${mockito.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</profile>
</profiles>
</project>
Loading