Skip to content

Commit adf8d50

Browse files
committed
[da-vinci][server] Redesign ServerReadQuotaUsageStats OTel metrics as
counters with outcome dimension Replace 6 OTel metrics (ASYNC_GAUGE + COUNTER) with 3 consolidated metrics: - read.quota.request.count (ASYNC_COUNTER_FOR_HIGH_PERF_CASES) - read.quota.key.count (ASYNC_COUNTER_FOR_HIGH_PERF_CASES) - read.quota.usage_ratio (ASYNC_DOUBLE_GAUGE, unchanged) New QuotaRequestOutcome dimension (ALLOWED/REJECTED/ALLOWED_UNINTENTIONALLY) combined with VersionRole provides per-outcome, per-role breakdown. Also adds ASYNC_DOUBLE_GAUGE MetricType and RATIO MetricUnit to the metric infra framework, with DoubleSupplier support in AsyncMetricEntityState/Base/OneEnum. API change: recordAllowedUnintentionally now takes a version parameter for OTel version role classification.
1 parent b961f65 commit adf8d50

34 files changed

Lines changed: 1500 additions & 199 deletions

File tree

clients/da-vinci-client/src/main/java/com/linkedin/davinci/stats/ServerMetricEntity.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ public static List<Class<? extends ModuleMetricEntityInterface>> getMetricEntity
3838
HeartbeatMonitoringOtelMetricEntity.class,
3939
BlobTransferOtelMetricEntity.class,
4040
KafkaConsumerServiceOtelMetricEntity.class,
41-
RocksDBMemoryOtelMetricEntity.class);
41+
RocksDBMemoryOtelMetricEntity.class,
42+
ServerReadQuotaOtelMetricEntity.class);
4243
}
4344

4445
public static final Collection<MetricEntity> SERVER_METRIC_ENTITIES =
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package com.linkedin.davinci.stats;
2+
3+
import static com.linkedin.venice.stats.dimensions.VeniceMetricsDimensions.VENICE_CLUSTER_NAME;
4+
import static com.linkedin.venice.stats.dimensions.VeniceMetricsDimensions.VENICE_QUOTA_REQUEST_OUTCOME;
5+
import static com.linkedin.venice.stats.dimensions.VeniceMetricsDimensions.VENICE_STORE_NAME;
6+
import static com.linkedin.venice.stats.dimensions.VeniceMetricsDimensions.VENICE_VERSION_ROLE;
7+
import static com.linkedin.venice.utils.Utils.setOf;
8+
9+
import com.linkedin.venice.stats.dimensions.VeniceMetricsDimensions;
10+
import com.linkedin.venice.stats.metrics.MetricEntity;
11+
import com.linkedin.venice.stats.metrics.MetricType;
12+
import com.linkedin.venice.stats.metrics.MetricUnit;
13+
import com.linkedin.venice.stats.metrics.ModuleMetricEntityInterface;
14+
import java.util.Set;
15+
16+
17+
/**
18+
* OTel metric entities for server read quota usage stats.
19+
*
20+
* <p>Consolidates Tehuti sensors into 3 OTel metrics:
21+
* <ul>
22+
* <li>{@link #READ_QUOTA_REQUEST_COUNT} — high-perf counter with outcome and version role dimensions</li>
23+
* <li>{@link #READ_QUOTA_KEY_COUNT} — high-perf counter with outcome and version role dimensions</li>
24+
* <li>{@link #READ_QUOTA_USAGE_RATIO} — async double gauge (unchanged)</li>
25+
* </ul>
26+
*/
27+
public enum ServerReadQuotaOtelMetricEntity implements ModuleMetricEntityInterface {
28+
READ_QUOTA_REQUEST_COUNT(
29+
"read.quota.request.count", MetricType.ASYNC_COUNTER_FOR_HIGH_PERF_CASES, MetricUnit.NUMBER,
30+
"Count of read quota requests per outcome and version role",
31+
setOf(VENICE_STORE_NAME, VENICE_CLUSTER_NAME, VENICE_VERSION_ROLE, VENICE_QUOTA_REQUEST_OUTCOME)
32+
),
33+
READ_QUOTA_KEY_COUNT(
34+
"read.quota.key.count", MetricType.ASYNC_COUNTER_FOR_HIGH_PERF_CASES, MetricUnit.NUMBER,
35+
"Count of read quota keys (RCU) per outcome and version role",
36+
setOf(VENICE_STORE_NAME, VENICE_CLUSTER_NAME, VENICE_VERSION_ROLE, VENICE_QUOTA_REQUEST_OUTCOME)
37+
),
38+
READ_QUOTA_USAGE_RATIO(
39+
"read.quota.usage_ratio", MetricType.ASYNC_DOUBLE_GAUGE, MetricUnit.RATIO,
40+
"Ratio of read quota used, based on requested keys per second relative to the node's quota responsibility",
41+
setOf(VENICE_STORE_NAME, VENICE_CLUSTER_NAME)
42+
);
43+
44+
private final MetricEntity metricEntity;
45+
46+
ServerReadQuotaOtelMetricEntity(
47+
String metricName,
48+
MetricType metricType,
49+
MetricUnit unit,
50+
String description,
51+
Set<VeniceMetricsDimensions> dimensions) {
52+
this.metricEntity = new MetricEntity(metricName, metricType, unit, description, dimensions);
53+
}
54+
55+
@Override
56+
public MetricEntity getMetricEntity() {
57+
return metricEntity;
58+
}
59+
}

clients/da-vinci-client/src/main/java/com/linkedin/davinci/stats/ingestion/IngestionOtelMetricEntity.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@ public enum IngestionOtelMetricEntity implements ModuleMetricEntityInterface {
3737
),
3838

3939
DISK_QUOTA_USED(
40-
"ingestion.disk_quota.used", MetricType.ASYNC_GAUGE, MetricUnit.RATIO, "Disk quota used for the store version",
41-
setOf(VENICE_STORE_NAME, VENICE_CLUSTER_NAME, VENICE_VERSION_ROLE)
40+
"ingestion.disk_quota.used", MetricType.ASYNC_DOUBLE_GAUGE, MetricUnit.RATIO,
41+
"Disk quota usage ratio for the store version", setOf(VENICE_STORE_NAME, VENICE_CLUSTER_NAME, VENICE_VERSION_ROLE)
4242
),
4343

4444
INGESTION_RECORDS_CONSUMED(

clients/da-vinci-client/src/main/java/com/linkedin/davinci/stats/ingestion/IngestionOtelStats.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -441,8 +441,8 @@ private long getPushTimeoutCountForRole(VersionRole role) {
441441
return pushTimeoutByVersion.getOrDefault(version, 0);
442442
}
443443

444-
private long getDiskQuotaUsedForRole(VersionRole role) {
445-
return (long) (IngestionStatsUtils.getStorageQuotaUsed(getTaskForRole(role)) * 100);
444+
private double getDiskQuotaUsedForRole(VersionRole role) {
445+
return IngestionStatsUtils.getStorageQuotaUsed(getTaskForRole(role));
446446
}
447447

448448
private long getIdleTimeForRole(VersionRole role) {

clients/da-vinci-client/src/test/java/com/linkedin/davinci/stats/ServerMetricEntityTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
public class ServerMetricEntityTest {
2323
@Test
2424
public void testServerMetricEntitiesCount() {
25-
assertEquals(SERVER_METRIC_ENTITIES.size(), 132, "Expected 132 unique metric entities");
25+
assertEquals(SERVER_METRIC_ENTITIES.size(), 135, "Expected 135 unique metric entities");
2626
}
2727

2828
/**

clients/da-vinci-client/src/test/java/com/linkedin/davinci/stats/ingestion/IngestionOtelMetricEntityTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,9 +85,9 @@ private static Map<IngestionOtelMetricEntity, MetricEntityExpectation> expectedD
8585
IngestionOtelMetricEntity.DISK_QUOTA_USED,
8686
new MetricEntityExpectation(
8787
"ingestion.disk_quota.used",
88-
MetricType.ASYNC_GAUGE,
88+
MetricType.ASYNC_DOUBLE_GAUGE,
8989
MetricUnit.RATIO,
90-
"Disk quota used for the store version",
90+
"Disk quota usage ratio for the store version",
9191
storeClusterVersion));
9292
map.put(
9393
IngestionOtelMetricEntity.CONSUMER_IDLE_TIME,

internal/venice-client-common/src/main/java/com/linkedin/venice/stats/VeniceOpenTelemetryMetricsRepository.java

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import io.opentelemetry.api.metrics.LongUpDownCounterBuilder;
2727
import io.opentelemetry.api.metrics.Meter;
2828
import io.opentelemetry.api.metrics.MeterProvider;
29+
import io.opentelemetry.api.metrics.ObservableDoubleGauge;
2930
import io.opentelemetry.api.metrics.ObservableLongCounter;
3031
import io.opentelemetry.api.metrics.ObservableLongGauge;
3132
import io.opentelemetry.api.metrics.ObservableLongMeasurement;
@@ -55,6 +56,7 @@
5556
import java.util.Set;
5657
import java.util.concurrent.TimeUnit;
5758
import java.util.function.Consumer;
59+
import java.util.function.DoubleSupplier;
5860
import java.util.function.LongSupplier;
5961
import javax.annotation.Nonnull;
6062
import org.apache.logging.log4j.LogManager;
@@ -423,6 +425,45 @@ public ObservableLongGauge createAsyncLongGauge(
423425
});
424426
}
425427

428+
/**
429+
* Asynchronous double gauge that will call the callback during metrics collection.
430+
* Use this for metrics requiring fractional precision (e.g., ratios in [0.0, 1.0]).
431+
*
432+
* <p>Each call creates a new SDK instrument handle via {@code buildWithCallback} — there is no
433+
* deduplication. Multiple callers can register callbacks for the same metric name; the OTel SDK
434+
* natively aggregates all their data points during collection.
435+
*/
436+
public ObservableDoubleGauge createAsyncDoubleGauge(
437+
MetricEntity metricEntity,
438+
@Nonnull DoubleSupplier asyncCallback,
439+
@Nonnull Attributes attributes) {
440+
if (!emitOpenTelemetryMetrics()) {
441+
return null;
442+
}
443+
return meter.gaugeBuilder(getFullMetricName(metricEntity))
444+
.setUnit(metricEntity.getUnit().name())
445+
.setDescription(getMetricDescription(metricEntity, metricsConfig))
446+
.buildWithCallback(measurement -> {
447+
double v;
448+
try {
449+
v = asyncCallback.getAsDouble();
450+
} catch (Exception e) {
451+
recordFailureMetric(metricEntity, e);
452+
return;
453+
}
454+
measurement.record(v, attributes);
455+
});
456+
}
457+
458+
public Object createInstrument(MetricEntity metricEntity, DoubleSupplier asyncDoubleCallback, Attributes attributes) {
459+
if (metricEntity.getMetricType() != MetricType.ASYNC_DOUBLE_GAUGE) {
460+
throw new IllegalArgumentException(
461+
"DoubleSupplier callback requires ASYNC_DOUBLE_GAUGE metric type, but got: " + metricEntity.getMetricType()
462+
+ " for metric: " + metricEntity.getMetricName());
463+
}
464+
return createAsyncDoubleGauge(metricEntity, asyncDoubleCallback, attributes);
465+
}
466+
426467
public Object createInstrument(MetricEntity metricEntity, LongSupplier asyncCallback, Attributes attributes) {
427468
MetricType metricType = metricEntity.getMetricType();
428469
switch (metricType) {
@@ -442,6 +483,11 @@ public Object createInstrument(MetricEntity metricEntity, LongSupplier asyncCall
442483
case ASYNC_GAUGE:
443484
return createAsyncLongGauge(metricEntity, asyncCallback, attributes);
444485

486+
case ASYNC_DOUBLE_GAUGE:
487+
throw new IllegalArgumentException(
488+
"ASYNC_DOUBLE_GAUGE requires DoubleSupplier callback. Use createInstrument(MetricEntity, DoubleSupplier, Attributes) instead. Metric: "
489+
+ metricEntity.getMetricName());
490+
445491
case ASYNC_COUNTER_FOR_HIGH_PERF_CASES:
446492
case ASYNC_UP_DOWN_COUNTER_FOR_HIGH_PERF_CASES:
447493
/**
@@ -458,7 +504,7 @@ public Object createInstrument(MetricEntity metricEntity, LongSupplier asyncCall
458504

459505
@VisibleForTesting
460506
public Object createInstrument(MetricEntity metricEntity) {
461-
return createInstrument(metricEntity, null, null);
507+
return createInstrument(metricEntity, (LongSupplier) null, null);
462508
}
463509

464510
/**
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.linkedin.venice.stats.dimensions;
2+
3+
/**
4+
* Outcome of a read quota enforcement decision: whether the request was allowed,
5+
* rejected, or allowed without quota check (unintentionally).
6+
*/
7+
public enum QuotaRequestOutcome implements VeniceDimensionInterface {
8+
/** Request passed quota check and was served normally. */
9+
ALLOWED,
10+
11+
/** Request exceeded the store-version quota and was rejected. */
12+
REJECTED,
13+
14+
/**
15+
* Request was allowed without a quota check — either the quota enforcer had not yet
16+
* initialized, or no rate limiter was allocated for the requested resource.
17+
*/
18+
ALLOWED_UNINTENTIONALLY;
19+
20+
@Override
21+
public VeniceMetricsDimensions getDimensionName() {
22+
return VeniceMetricsDimensions.VENICE_QUOTA_REQUEST_OUTCOME;
23+
}
24+
}

internal/venice-client-common/src/main/java/com/linkedin/venice/stats/dimensions/VeniceMetricsDimensions.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,10 @@ public enum VeniceMetricsDimensions {
135135
VENICE_HEARTBEAT_COMPONENT("venice.heartbeat.component"),
136136

137137
/** {@link VeniceConsumerPoolAction} Consumer pool action (subscribe, update_assignment). */
138-
VENICE_CONSUMER_POOL_ACTION("venice.consumer_pool.action");
138+
VENICE_CONSUMER_POOL_ACTION("venice.consumer_pool.action"),
139+
140+
/** {@link QuotaRequestOutcome} Outcome of read quota enforcement (allowed, rejected, allowed_unintentionally). */
141+
VENICE_QUOTA_REQUEST_OUTCOME("venice.quota.request.outcome");
139142

140143
private final String[] dimensionName = new String[VeniceOpenTelemetryMetricNamingFormat.SIZE];
141144

internal/venice-client-common/src/main/java/com/linkedin/venice/stats/metrics/AsyncMetricEntityState.java

Lines changed: 77 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import java.util.Map;
1515
import java.util.Objects;
1616
import java.util.Set;
17+
import java.util.function.DoubleSupplier;
1718
import java.util.function.LongSupplier;
1819

1920

@@ -56,13 +57,59 @@ public AsyncMetricEntityState(
5657
List<MeasurableStat> tehutiMetricStats,
5758
LongSupplier asyncCallback,
5859
Attributes asyncAttributes) {
60+
this(
61+
metricEntity,
62+
otelRepository,
63+
baseDimensionsMap,
64+
registerTehutiSensorFn,
65+
tehutiMetricNameEnum,
66+
tehutiMetricStats);
67+
validateAsyncCallback(asyncCallback);
68+
if (emitOpenTelemetryMetrics()) {
69+
setOtelMetric(otelRepository.createInstrument(this.metricEntity, asyncCallback, asyncAttributes));
70+
}
71+
registerTehutiSensor(registerTehutiSensorFn, tehutiMetricNameEnum, tehutiMetricStats);
72+
}
73+
74+
/** Constructor for {@link MetricType#ASYNC_DOUBLE_GAUGE} metrics that use a {@link DoubleSupplier} callback. */
75+
public AsyncMetricEntityState(
76+
MetricEntity metricEntity,
77+
VeniceOpenTelemetryMetricsRepository otelRepository,
78+
Map<VeniceMetricsDimensions, String> baseDimensionsMap,
79+
TehutiSensorRegistrationFunction registerTehutiSensorFn,
80+
TehutiMetricNameEnum tehutiMetricNameEnum,
81+
List<MeasurableStat> tehutiMetricStats,
82+
DoubleSupplier asyncDoubleCallback,
83+
Attributes asyncAttributes) {
84+
this(
85+
metricEntity,
86+
otelRepository,
87+
baseDimensionsMap,
88+
registerTehutiSensorFn,
89+
tehutiMetricNameEnum,
90+
tehutiMetricStats);
91+
validateAsyncCallback(asyncDoubleCallback != null);
92+
if (emitOpenTelemetryMetrics()) {
93+
setOtelMetric(otelRepository.createInstrument(this.metricEntity, asyncDoubleCallback, asyncAttributes));
94+
}
95+
registerTehutiSensor(registerTehutiSensorFn, tehutiMetricNameEnum, tehutiMetricStats);
96+
}
97+
98+
/** Common field initialization for all constructors. */
99+
private AsyncMetricEntityState(
100+
MetricEntity metricEntity,
101+
VeniceOpenTelemetryMetricsRepository otelRepository,
102+
Map<VeniceMetricsDimensions, String> baseDimensionsMap,
103+
TehutiSensorRegistrationFunction registerTehutiSensorFn,
104+
TehutiMetricNameEnum tehutiMetricNameEnum,
105+
List<MeasurableStat> tehutiMetricStats) {
59106
this.metricEntity = metricEntity;
60107
this.emitOpenTelemetryMetrics = otelRepository != null && otelRepository.emitOpenTelemetryMetrics();
61108
this.emitTehutiMetrics =
62109
shouldEmitTehutiMetrics(otelRepository, registerTehutiSensorFn, tehutiMetricNameEnum, tehutiMetricStats);
63110
this.otelRepository = otelRepository;
64111
this.baseDimensionsMap = baseDimensionsMap;
65-
createMetric(tehutiMetricNameEnum, tehutiMetricStats, registerTehutiSensorFn, asyncCallback, asyncAttributes);
112+
validateTehutiAsyncGaugeConsistency(tehutiMetricStats);
66113
}
67114

68115
/**
@@ -96,60 +143,65 @@ public interface TehutiSensorRegistrationFunction {
96143
}
97144

98145
/**
99-
* Validates
100-
* 1. whether an async callback is provided for an async metric
101-
* 2. only when tehutiMetricStats contains AsyncGauge, the metric type should be ASYNC_GAUGE and tehutiMetricStats
102-
* should contain only one stat.
146+
* Validates that async callback presence is consistent with the metric type.
147+
* Accepts a {@link LongSupplier} directly for the common case.
148+
*/
149+
private void validateAsyncCallback(LongSupplier asyncCallback) {
150+
validateAsyncCallback(asyncCallback != null);
151+
}
152+
153+
/**
154+
* Validates that async callback presence is consistent with the metric type.
103155
*
104-
* @param tehutiMetricStats the tehuti metrics stats for the given metric entity.
105-
* @param asyncCallback the async callback function to be used for async metrics.
156+
* @param hasAsyncCallback whether an async callback (LongSupplier or DoubleSupplier) was provided
106157
*/
107-
private void validateMetric(List<MeasurableStat> tehutiMetricStats, LongSupplier asyncCallback) {
108-
if (asyncCallback != null && !metricEntity.getMetricType().isAsyncMetric()) {
158+
private void validateAsyncCallback(boolean hasAsyncCallback) {
159+
if (hasAsyncCallback && !metricEntity.getMetricType().isAsyncMetric()) {
109160
throw new IllegalArgumentException(
110161
"Async callback is provided, but the metric type is not async for metric: " + metricEntity.getMetricName());
111-
} else if (metricEntity.getMetricType().isAsyncMetric() && asyncCallback == null
162+
} else if (metricEntity.getMetricType().isAsyncMetric() && !hasAsyncCallback
112163
&& !metricEntity.getMetricType().isObservableCounterType()) {
113164
// Observable counter types (ASYNC_COUNTER_FOR_HIGH_PERF_CASES and ASYNC_UP_DOWN_COUNTER_FOR_HIGH_PERF_CASES)
114165
// are async but handle callback registration internally via registerObservableLongCounter/UpDownCounter(),
115166
// so they don't need a callback passed in here.
116167
throw new IllegalArgumentException(
117168
"Async callback is not provided, but the metric type is async for metric: " + metricEntity.getMetricName());
118169
}
170+
}
119171

120-
// ASYNC_GAUGE specific: If both tehuti and Otel are present, validate if all are nothing is async
172+
/**
173+
* Validates that Tehuti AsyncGauge stats are consistent with the OTel metric type.
174+
* Called once during construction after field initialization.
175+
*/
176+
private void validateTehutiAsyncGaugeConsistency(List<MeasurableStat> tehutiMetricStats) {
121177
if (tehutiMetricStats == null || tehutiMetricStats.isEmpty()) {
122178
return;
123179
}
124-
// if tehutiMetricStats has AsyncGauge() then the metric type should be ASYNC_GAUGE
125180
if (tehutiMetricStats.stream().anyMatch(stat -> stat instanceof AsyncGauge)) {
126181
if (tehutiMetricStats.size() > 1) {
127182
throw new IllegalArgumentException(
128183
"Tehuti metric stats contains AsyncGauge, but it should be the only stat for metric: "
129184
+ metricEntity.getMetricName());
130185
}
131-
if (metricEntity.getMetricType() != MetricType.ASYNC_GAUGE) {
186+
MetricType mt = metricEntity.getMetricType();
187+
if (mt != MetricType.ASYNC_GAUGE && mt != MetricType.ASYNC_DOUBLE_GAUGE) {
132188
throw new IllegalArgumentException(
133-
"Tehuti metric stats contains AsyncGauge, but the otel metric type is not ASYNC_GAUGE for metric: "
189+
"Tehuti metric stats contains AsyncGauge, but the otel metric type is not ASYNC_GAUGE/ASYNC_DOUBLE_GAUGE for metric: "
134190
+ metricEntity.getMetricName());
135191
}
136-
} else if (metricEntity.getMetricType() == MetricType.ASYNC_GAUGE) {
192+
} else if (metricEntity.getMetricType() == MetricType.ASYNC_GAUGE
193+
|| metricEntity.getMetricType() == MetricType.ASYNC_DOUBLE_GAUGE) {
137194
throw new IllegalArgumentException(
138-
"Tehuti metric stats does not contain AsyncGauge, but the otel metric type is ASYNC_GAUGE for metric: "
195+
"Tehuti metric stats does not contain AsyncGauge, but the otel metric type is ASYNC_GAUGE/ASYNC_DOUBLE_GAUGE for metric: "
139196
+ metricEntity.getMetricName());
140197
}
141198
}
142199

143-
private void createMetric(
144-
TehutiMetricNameEnum tehutiMetricNameEnum,
145-
List<MeasurableStat> tehutiMetricStats,
200+
/** Registers the Tehuti sensor if Tehuti metrics are enabled. */
201+
private void registerTehutiSensor(
146202
TehutiSensorRegistrationFunction registerTehutiSensorFn,
147-
LongSupplier asyncCallback,
148-
Attributes asyncAttributes) {
149-
validateMetric(tehutiMetricStats, asyncCallback);
150-
if (emitOpenTelemetryMetrics()) {
151-
setOtelMetric(otelRepository.createInstrument(this.metricEntity, asyncCallback, asyncAttributes));
152-
}
203+
TehutiMetricNameEnum tehutiMetricNameEnum,
204+
List<MeasurableStat> tehutiMetricStats) {
153205
if (emitTehutiMetrics()) {
154206
setTehutiSensor(
155207
registerTehutiSensorFn

0 commit comments

Comments
 (0)