org.bouncycastle
bcprov-jdk18on
diff --git a/core/src/main/java/tech/ydb/core/metrics/Attr.java b/core/src/main/java/tech/ydb/core/metrics/Attr.java
new file mode 100644
index 000000000..7c8fa9c8c
--- /dev/null
+++ b/core/src/main/java/tech/ydb/core/metrics/Attr.java
@@ -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;
+ }
+}
diff --git a/core/src/main/java/tech/ydb/core/metrics/DoubleHistogram.java b/core/src/main/java/tech/ydb/core/metrics/DoubleHistogram.java
new file mode 100644
index 000000000..012a3444b
--- /dev/null
+++ b/core/src/main/java/tech/ydb/core/metrics/DoubleHistogram.java
@@ -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);
+}
diff --git a/core/src/main/java/tech/ydb/core/metrics/LongCounter.java b/core/src/main/java/tech/ydb/core/metrics/LongCounter.java
new file mode 100644
index 000000000..003b0ff8f
--- /dev/null
+++ b/core/src/main/java/tech/ydb/core/metrics/LongCounter.java
@@ -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);
+}
diff --git a/core/src/main/java/tech/ydb/core/metrics/LongMeasurement.java b/core/src/main/java/tech/ydb/core/metrics/LongMeasurement.java
new file mode 100644
index 000000000..378e901bb
--- /dev/null
+++ b/core/src/main/java/tech/ydb/core/metrics/LongMeasurement.java
@@ -0,0 +1,20 @@
+package tech.ydb.core.metrics;
+
+import io.grpc.ExperimentalApi;
+
+/**
+ * Per-observation handle passed to {@link Meter#createLongGauge} callbacks.
+ *
+ * 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);
+}
diff --git a/core/src/main/java/tech/ydb/core/metrics/Meter.java b/core/src/main/java/tech/ydb/core/metrics/Meter.java
new file mode 100644
index 000000000..15ca5c096
--- /dev/null
+++ b/core/src/main/java/tech/ydb/core/metrics/Meter.java
@@ -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 callback) {
+ // noop: the backend never queries the callback
+ }
+}
diff --git a/core/src/main/java/tech/ydb/core/metrics/OpenTelemetryMeter.java b/core/src/main/java/tech/ydb/core/metrics/OpenTelemetryMeter.java
new file mode 100644
index 000000000..f00980ace
--- /dev/null
+++ b/core/src/main/java/tech/ydb/core/metrics/OpenTelemetryMeter.java
@@ -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 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();
+ }
+}
diff --git a/core/src/test/java/tech/ydb/core/metrics/OpenTelemetryMeterTest.java b/core/src/test/java/tech/ydb/core/metrics/OpenTelemetryMeterTest.java
new file mode 100644
index 000000000..35e4cc706
--- /dev/null
+++ b/core/src/test/java/tech/ydb/core/metrics/OpenTelemetryMeterTest.java
@@ -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 POOL = AttributeKey.stringKey("pool.name");
+ private static final AttributeKey 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 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 points) {
+ Assert.assertEquals(1, points.size());
+ return points.iterator().next();
+ }
+}
diff --git a/query/pom.xml b/query/pom.xml
index 14628a48d..5db83c8aa 100644
--- a/query/pom.xml
+++ b/query/pom.xml
@@ -53,11 +53,9 @@
junit
test
-