Compare commits

...
Author SHA1 Message Date
robin.bygrave 72ce587629 Add dbName() to MetaQueryPlan - easier to support multi-db query plan capture handling 2026-08-14 18:03:02 +12:00
robin.bygrave 77e98a3f54 Metrics - support both CUMULATIVE and DELTA metrics collection concurrently
ebean insight [and StatsD] work off DELTA metrics, where as OTEL
and Prometheus want CUMULATIVE. With this change we can have a metrics
collection for ebean insight using DELTA mode and have a second collection
use CUMULATIVE for reporting to OTEL - for the case of fan-out metrics
going to 2 places.

This isn't strictly needed when only one collection mode is used.
2026-08-14 16:31:28 +12:00
14 changed files with 113 additions and 20 deletions
@@ -9,6 +9,11 @@ import java.time.Instant;
*/
public interface MetaQueryPlan {
/**
* Return the name of the database for the query.
*/
String dbName();
/**
* Return the bean type for the query.
*/
@@ -80,6 +80,10 @@ public final class DtoMappingRequest {
return name;
}
public String dbName() {
return server.name();
}
public String hash() {
return hash;
}
@@ -15,6 +15,7 @@ abstract class DtoQueryPlanBase implements DtoQueryPlan, SpiQueryPlan {
private final QueryPlanMetric planMetric;
private final TimedMetric metric;
private final Class<?> beanType;
private final String dbName;
private final String name;
private final String hash;
private final String sql;
@@ -26,6 +27,7 @@ abstract class DtoQueryPlanBase implements DtoQueryPlan, SpiQueryPlan {
this.planMetric = request.createMetric();
this.metric = planMetric.metric();
this.beanType = request.type();
this.dbName = request.dbName();
this.name = request.name();
this.hash = request.hash();
this.sql = request.sql();
@@ -91,6 +93,6 @@ abstract class DtoQueryPlanBase implements DtoQueryPlan, SpiQueryPlan {
@Override
public SpiDbQueryPlan createMeta(String bind, String planString) {
return new DQueryPlanOutput(beanType, name, hash, sql, profileLocation, bind, planString);
return new DQueryPlanOutput(beanType, dbName, name, hash, sql, profileLocation, bind, planString);
}
}
@@ -4,15 +4,13 @@ import io.ebean.meta.MetricVisitor;
import io.ebean.metric.CountMetric;
import io.ebean.metric.CountMetricStats;
import java.util.concurrent.atomic.LongAdder;
/**
* Used to collect counter metrics.
*/
final class DCountMetric implements CountMetric {
private final String name;
private final LongAdder count = new LongAdder();
private final ValueAdder count = new ValueAdder();
private String reportName;
DCountMetric(String name) {
@@ -29,12 +27,12 @@ final class DCountMetric implements CountMetric {
@Override
public void increment() {
count.increment();
count.add(1);
}
@Override
public boolean isEmpty() {
return count.sum() == 0;
return count.currentValue() == 0;
}
@Override
@@ -44,12 +42,12 @@ final class DCountMetric implements CountMetric {
@Override
public long get(boolean reset) {
return reset ? count.sumThenReset() : count.sum();
return count.get(reset);
}
@Override
public void visit(MetricVisitor visitor) {
long val = visitor.reset() ? count.sumThenReset() : count.sum();
long val = count.get(visitor.reset());
if (val > 0) {
final String name = reportName != null ? reportName : reportName(visitor);
visitor.visitCount(new DCountMetricStats(name, val));
@@ -4,7 +4,6 @@ import io.ebean.meta.MetricVisitor;
import io.ebean.metric.TimedMetric;
import java.util.concurrent.atomic.LongAccumulator;
import java.util.concurrent.atomic.LongAdder;
/**
* Used to collect timed execution statistics.
@@ -15,8 +14,8 @@ import java.util.concurrent.atomic.LongAdder;
final class DTimedMetric implements TimedMetric {
private final String name;
private final LongAdder count = new LongAdder();
private final LongAdder total = new LongAdder();
private final ValueAdder count = new ValueAdder();
private final ValueAdder total = new ValueAdder();
private final LongAccumulator max = new LongAccumulator(Math::max, 0);
private boolean collected;
private String reportName;
@@ -43,14 +42,14 @@ final class DTimedMetric implements TimedMetric {
@Override
public void add(long value) {
count.increment();
count.add(1);
total.add(value);
max.accumulate(value);
}
@Override
public boolean isEmpty() {
return count.sum() == 0;
return count.currentValue() == 0;
}
@Override
@@ -62,16 +61,17 @@ final class DTimedMetric implements TimedMetric {
@Override
public void visit(MetricVisitor visitor) {
final long countSum = visitor.reset() ? count.sumThenReset() : count.sum();
final boolean reset = visitor.reset();
final long countSum = count.get(reset);
if (countSum > 0) {
final String name = reportName != null ? reportName : reportName(visitor);
visitor.visitTimed(stats(visitor.reset(), name, countSum));
visitor.visitTimed(stats(reset, name, countSum));
}
}
@Override
public DTimeMetricStats collect(boolean reset) {
final long countSum = reset ? count.sumThenReset() : count.sum();
final long countSum = count.get(reset);
if (countSum == 0) {
return null;
} else {
@@ -84,7 +84,7 @@ final class DTimedMetric implements TimedMetric {
*/
private DTimeMetricStats stats(boolean reset, String name, long countSum) {
try {
final long totalSum = reset ? total.sumThenReset() : total.sum();
final long totalSum = total.get(reset);
return new DTimeMetricStats(name, collected, countSum, totalSum, max.getThenReset());
} finally {
collected = true;
@@ -0,0 +1,36 @@
package io.ebeaninternal.server.profile;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.LongAdder;
/**
* Accumulates a value while supporting cumulative and reset-based delta reads.
*/
final class ValueAdder {
private final LongAdder value = new LongAdder();
private final AtomicLong previousValue = new AtomicLong();
void add(long amount) {
value.add(amount);
}
long get(boolean reset) {
long currentValue = value.sum();
if (!reset) {
return currentValue;
}
long previous = previousValue.getAndSet(currentValue);
return currentValue >= previous ? currentValue - previous : currentValue;
}
void reset() {
value.reset();
previousValue.set(0);
}
long currentValue() {
return value.sum();
}
}
@@ -274,7 +274,7 @@ public class CQueryPlan implements SpiQueryPlan {
@Override
public final DQueryPlanOutput createMeta(String bind, String planString) {
return new DQueryPlanOutput(beanType(), name, hash, sql, profileLocation, bind, planString);
return new DQueryPlanOutput(beanType(), server.name(), name, hash, sql, profileLocation, bind, planString);
}
public DataReader createDataReader(boolean unmodifiable, ResultSet rset) {
@@ -12,6 +12,7 @@ import java.time.Instant;
public final class DQueryPlanOutput implements MetaQueryPlan, SpiDbQueryPlan {
private final Class<?> beanType;
private final String dbName;
private final String label;
private final ProfileLocation profileLocation;
@@ -25,8 +26,9 @@ public final class DQueryPlanOutput implements MetaQueryPlan, SpiDbQueryPlan {
private Instant whenCaptured;
private Object tenantId;
public DQueryPlanOutput(Class<?> beanType, String label, String hash, String sql, ProfileLocation profileLocation, String bind, String plan) {
public DQueryPlanOutput(Class<?> beanType, String dbName, String label, String hash, String sql, ProfileLocation profileLocation, String bind, String plan) {
this.beanType = beanType;
this.dbName = dbName;
this.label = label;
this.hash = hash;
this.sql = sql;
@@ -35,6 +37,11 @@ public final class DQueryPlanOutput implements MetaQueryPlan, SpiDbQueryPlan {
this.plan = plan;
}
@Override
public String dbName() {
return dbName;
}
@Override
public String hash() {
return hash;
@@ -21,11 +21,13 @@ public final class SqlQueryPlan implements SpiQueryPlan {
private final String name;
private final String hash;
private final String sql;
private final String dbName;
private final SpiQueryBindCapture bindCapture;
SqlQueryPlan(SpiEbeanServer server, String name, String sql) {
this.name = name;
this.sql = sql;
this.dbName = server.name();
this.hash = Md5.hash(sql, name);
this.bindCapture = server.createQueryBindCapture(this);
}
@@ -77,6 +79,6 @@ public final class SqlQueryPlan implements SpiQueryPlan {
@Override
public SpiDbQueryPlan createMeta(String bind, String planString) {
return new DQueryPlanOutput(null, name, hash, sql, null, bind, planString);
return new DQueryPlanOutput(null, dbName, name, hash, sql, null, bind, planString);
}
}
@@ -40,4 +40,19 @@ class DCountMetricTest {
assertThat(result2.get(0).count()).isEqualTo(12);
}
}
@Test
void cumulativeAndDeltaAreIndependent() {
DCountMetric counter = new DCountMetric("org.hello");
counter.add(7);
assertThat(counter.get(false)).isEqualTo(7);
assertThat(counter.get(false)).isEqualTo(7);
assertThat(counter.get(true)).isEqualTo(7);
counter.add(5);
assertThat(counter.get(false)).isEqualTo(12);
assertThat(counter.get(true)).isEqualTo(5);
assertThat(counter.get(true)).isEqualTo(0);
}
}
@@ -116,4 +116,25 @@ public class DTimedMetricTest {
assertThat(stats.total()).isEqualTo(1470);
assertThat(stats.max()).isEqualTo(160);
}
@Test
void cumulativeAndDeltaAreIndependent() {
DTimedMetric metric = new DTimedMetric("org.timed");
metric.add(560);
metric.add(500);
DTimeMetricStats cumulative = metric.collect(false);
assertThat(cumulative.count()).isEqualTo(2);
assertThat(cumulative.total()).isEqualTo(1060);
metric.add(160);
DTimeMetricStats delta = metric.collect(true);
assertThat(delta.count()).isEqualTo(3);
assertThat(delta.total()).isEqualTo(1220);
cumulative = metric.collect(false);
assertThat(cumulative.count()).isEqualTo(3);
assertThat(cumulative.total()).isEqualTo(1220);
}
}
@@ -125,6 +125,7 @@ class DtoQueryPlanCaptureTest extends BaseTestCase {
.orElse(null);
assertThat(dtoPlan).as("captured a native DTO query plan").isNotNull();
assertThat(dtoPlan.dbName()).isEqualTo(DB.getDefault().name());
assertThat(dtoPlan.sql()).contains("from o_customer where id > ?");
assertThat(dtoPlan.plan()).isNotEmpty();
}
@@ -71,6 +71,7 @@ class SqlQueryPlanCaptureTest extends BaseTestCase {
.orElse(null);
assertThat(sqlPlan).as("captured a SqlQuery query plan").isNotNull();
assertThat(sqlPlan.dbName()).isEqualTo(DB.getDefault().name());
assertThat(sqlPlan.sql()).contains("from o_customer where id > ?");
assertThat(sqlPlan.plan()).isNotEmpty();
}
@@ -191,6 +191,7 @@ public class TestCustomerFinder extends BaseTestCase {
request.maxTimeMillis(10_000);
List<MetaQueryPlan> plans0 = server().metaInfo().queryPlanCollectNow(request);
assertThat(plans0).isNotEmpty();
assertThat(plans0).extracting(MetaQueryPlan::dbName).containsOnly(server().name());
for (MetaQueryPlan plan : plans0) {
logger.info("queryPlan label:{}, queryTimeMicros:{} captureMicros:{} whenCaptured:{} captureCount:{} loc:{} sql:{} bind:{} plan:{}",