Skip to content

Commit db9dedb

Browse files
committed
[all] make otel synchronous gauge emit last set metrics in every export interval
1 parent fad1963 commit db9dedb

3 files changed

Lines changed: 138 additions & 15 deletions

File tree

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

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
package com.linkedin.venice.stats;
22

33
import static com.linkedin.venice.stats.VeniceOpenTelemetryMetricNamingFormat.SNAKE_CASE;
4+
import static io.opentelemetry.sdk.metrics.InstrumentType.GAUGE;
45

56
import com.linkedin.venice.stats.metrics.MetricEntity;
67
import io.opentelemetry.exporter.otlp.internal.OtlpConfigUtil;
8+
import io.opentelemetry.sdk.metrics.data.AggregationTemporality;
79
import io.opentelemetry.sdk.metrics.export.AggregationTemporalitySelector;
810
import io.opentelemetry.sdk.metrics.export.MetricExporter;
911
import io.opentelemetry.sdk.metrics.export.MetricReader;
@@ -126,6 +128,16 @@ public class VeniceMetricsConfig {
126128
public static final String OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE =
127129
"otel.exporter.otlp.metrics.temporality.preference";
128130

131+
/**
132+
* Make Synchronous Gauge instruments export the last recorded value even if the instrument is not
133+
* recorded in the current collection interval.
134+
* - If true, it will export the last recorded value even if the instrument is not recorded in the current collection interval.
135+
* - If false, it will use the default behavior of the selected {@link #otelAggregationTemporalitySelector}, which is
136+
* {@link AggregationTemporalitySelector#deltaPreferred()} by default.
137+
*/
138+
public static final String OTEL_VENICE_EXPORT_LAST_RECORDED_VALUE_FOR_SYNCHRONOUS_GAUGE =
139+
"otel.venice.export.last.recorded.value.for.synchronous.gauge";
140+
129141
/**
130142
* Default histogram aggregation to be used for all histograms: Select one of the below <br>
131143
* 1. base2_exponential_bucket_histogram <br>
@@ -208,6 +220,11 @@ public class VeniceMetricsConfig {
208220
/** Metric naming conventions for OpenTelemetry metrics */
209221
private final VeniceOpenTelemetryMetricNamingFormat metricNamingFormat;
210222

223+
/**
224+
* Whether to export the last recorded value for synchronous Gauge instruments.
225+
*/
226+
private final boolean exportLastRecordedValueForSynchronousGauge;
227+
211228
/** Aggregation Temporality selector to export only the delta or cumulate or different */
212229
private final AggregationTemporalitySelector otelAggregationTemporalitySelector;
213230

@@ -234,6 +251,7 @@ private VeniceMetricsConfig(Builder builder) {
234251
this.otelHeaders = builder.otelHeaders;
235252
this.exportOtelMetricsToLog = builder.exportOtelMetricsToLog;
236253
this.metricNamingFormat = builder.metricNamingFormat;
254+
this.exportLastRecordedValueForSynchronousGauge = builder.exportLastRecordedValueForSynchronousGauge;
237255
this.otelAggregationTemporalitySelector = builder.otelAggregationTemporalitySelector;
238256
this.useOtelExponentialHistogram = builder.useOtelExponentialHistogram;
239257
this.otelExponentialHistogramMaxScale = builder.otelExponentialHistogramMaxScale;
@@ -257,6 +275,7 @@ public static class Builder {
257275
Map<String, String> otelHeaders = new HashMap<>();
258276
private boolean exportOtelMetricsToLog = false;
259277
private VeniceOpenTelemetryMetricNamingFormat metricNamingFormat = SNAKE_CASE;
278+
private boolean exportLastRecordedValueForSynchronousGauge = true;
260279
private AggregationTemporalitySelector otelAggregationTemporalitySelector =
261280
AggregationTemporalitySelector.deltaPreferred();
262281
private boolean useOtelExponentialHistogram = true;
@@ -331,6 +350,11 @@ public Builder setMetricNamingFormat(String metricNamingFormat) {
331350
return this;
332351
}
333352

353+
public Builder setExportLastRecordedValueForSynchronousGauge(boolean exportLastRecordedValueForSynchronousGauge) {
354+
this.exportLastRecordedValueForSynchronousGauge = exportLastRecordedValueForSynchronousGauge;
355+
return this;
356+
}
357+
334358
public Builder setOtelAggregationTemporalitySelector(
335359
AggregationTemporalitySelector otelAggregationTemporalitySelector) {
336360
this.otelAggregationTemporalitySelector = otelAggregationTemporalitySelector;
@@ -439,6 +463,10 @@ public Builder extractAndSetOtelConfigs(Map<String, String> configs) {
439463
otelHeaders.put(headers[0], headers[1]);
440464
}
441465

466+
if ((configValue = configs.get(OTEL_VENICE_EXPORT_LAST_RECORDED_VALUE_FOR_SYNCHRONOUS_GAUGE)) != null) {
467+
setExportLastRecordedValueForSynchronousGauge(Boolean.parseBoolean(configValue));
468+
}
469+
442470
if ((configValue = configs.get(OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE)) != null) {
443471
switch (configValue.toLowerCase(Locale.ROOT)) {
444472
case "cumulative":
@@ -507,6 +535,12 @@ private void checkAndSetDefaults() {
507535
} else {
508536
LOGGER.warn("OpenTelemetry metrics are disabled");
509537
}
538+
539+
if (exportLastRecordedValueForSynchronousGauge) {
540+
// Override the configured temporality selector to ensure synchronous gauges export last recorded value
541+
otelAggregationTemporalitySelector =
542+
getTemporalitySelector(exportLastRecordedValueForSynchronousGauge, otelAggregationTemporalitySelector);
543+
}
510544
}
511545

512546
public VeniceMetricsConfig build() {
@@ -572,6 +606,10 @@ public VeniceOpenTelemetryMetricNamingFormat getMetricNamingFormat() {
572606
return metricNamingFormat;
573607
}
574608

609+
public boolean exportLastRecordedValueForSynchronousGauge() {
610+
return exportLastRecordedValueForSynchronousGauge;
611+
}
612+
575613
public AggregationTemporalitySelector getOtelAggregationTemporalitySelector() {
576614
return otelAggregationTemporalitySelector;
577615
}
@@ -609,4 +647,23 @@ public String toString() {
609647
+ otelExponentialHistogramMaxScale + ", otelExponentialHistogramMaxBuckets="
610648
+ otelExponentialHistogramMaxBuckets + ", tehutiMetricConfig=" + tehutiMetricConfig + '}';
611649
}
650+
651+
/**
652+
* Custom AggregationTemporalitySelector which enforces that if the instrument type is
653+
* GAUGE and {@link #exportLastRecordedValueForSynchronousGauge} is true,
654+
* it returns CUMULATIVE for GAUGE type instruments.
655+
* This is to support Synchronous GAUGE exporting the last set value even when
656+
* it is not set during the last export interval
657+
* Check https://github.com/open-telemetry/opentelemetry-java/pull/7634 for more details
658+
*/
659+
public static AggregationTemporalitySelector getTemporalitySelector(
660+
boolean exportLastRecordedValueForSynchronousGauge,
661+
AggregationTemporalitySelector configuredTemporalitySelector) {
662+
return instrumentType -> {
663+
if (exportLastRecordedValueForSynchronousGauge && instrumentType == GAUGE) {
664+
return AggregationTemporality.CUMULATIVE;
665+
}
666+
return configuredTemporalitySelector.getAggregationTemporality(instrumentType);
667+
};
668+
}
612669
}

internal/venice-client-common/src/test/java/com/linkedin/venice/stats/VeniceMetricsConfigTest.java

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import static com.linkedin.venice.stats.VeniceMetricsConfig.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT;
77
import static com.linkedin.venice.stats.VeniceMetricsConfig.OTEL_EXPORTER_OTLP_METRICS_PROTOCOL;
88
import static com.linkedin.venice.stats.VeniceMetricsConfig.OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE;
9+
import static com.linkedin.venice.stats.VeniceMetricsConfig.OTEL_VENICE_EXPORT_LAST_RECORDED_VALUE_FOR_SYNCHRONOUS_GAUGE;
910
import static com.linkedin.venice.stats.VeniceMetricsConfig.OTEL_VENICE_METRICS_CUSTOM_DIMENSIONS_MAP;
1011
import static com.linkedin.venice.stats.VeniceMetricsConfig.OTEL_VENICE_METRICS_ENABLED;
1112
import static com.linkedin.venice.stats.VeniceMetricsConfig.OTEL_VENICE_METRICS_EXPORT_TO_ENDPOINT;
@@ -19,7 +20,10 @@
1920
import static org.testng.Assert.assertTrue;
2021

2122
import com.linkedin.venice.stats.VeniceMetricsConfig.Builder;
23+
import com.linkedin.venice.utils.DataProviderUtils;
2224
import io.opentelemetry.exporter.otlp.internal.OtlpConfigUtil;
25+
import io.opentelemetry.sdk.metrics.InstrumentType;
26+
import io.opentelemetry.sdk.metrics.data.AggregationTemporality;
2327
import io.opentelemetry.sdk.metrics.export.AggregationTemporalitySelector;
2428
import io.tehuti.metrics.MetricConfig;
2529
import java.util.HashMap;
@@ -46,7 +50,15 @@ public void testDefaultValuesWithBasicConfig() {
4650
assertTrue(config.getOtelHeaders().isEmpty());
4751
assertFalse(config.exportOtelMetricsToLog());
4852
assertEquals(config.getMetricNamingFormat(), SNAKE_CASE);
49-
assertEquals(config.getOtelAggregationTemporalitySelector(), AggregationTemporalitySelector.deltaPreferred());
53+
assertEquals(config.exportLastRecordedValueForSynchronousGauge(), true);
54+
AggregationTemporalitySelector defaultSelector =
55+
VeniceMetricsConfig.getTemporalitySelector(true, AggregationTemporalitySelector.deltaPreferred());
56+
for (InstrumentType type: InstrumentType.values()) {
57+
assertEquals(
58+
config.getOtelAggregationTemporalitySelector().getAggregationTemporality(type),
59+
defaultSelector.getAggregationTemporality(type),
60+
type.name());
61+
}
5062
assertEquals(config.useOtelExponentialHistogram(), true);
5163
assertEquals(config.getOtelExponentialHistogramMaxScale(), 3);
5264
assertEquals(config.getOtelExponentialHistogramMaxBuckets(), 250);
@@ -132,20 +144,50 @@ public void testEnableHttpGrpcEndpointConfigWithRequiredFields() {
132144
assertEquals(config.getOtelEndpoint(), "http://localhost");
133145
}
134146

135-
@Test
136-
public void testSetAggregationTemporalitySelector() {
147+
@Test(dataProvider = "True-and-False", dataProviderClass = DataProviderUtils.class)
148+
public void testSetAggregationTemporalitySelector(boolean useLastRecordedValueForSynchronousGauge) {
137149
Map<String, String> otelConfigs = new HashMap<>();
138150
otelConfigs.put(OTEL_VENICE_METRICS_ENABLED, "true");
139151
otelConfigs.put(OTEL_VENICE_METRICS_EXPORT_TO_ENDPOINT, "true");
140152
otelConfigs.put(OTEL_EXPORTER_OTLP_METRICS_PROTOCOL, OtlpConfigUtil.PROTOCOL_HTTP_PROTOBUF);
141153
otelConfigs.put(OTEL_EXPORTER_OTLP_METRICS_ENDPOINT, "http://localhost");
154+
otelConfigs.put(
155+
OTEL_VENICE_EXPORT_LAST_RECORDED_VALUE_FOR_SYNCHRONOUS_GAUGE,
156+
Boolean.toString(useLastRecordedValueForSynchronousGauge));
142157
otelConfigs.put(OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE, "delta");
143158

144159
VeniceMetricsConfig config = new Builder().setServiceName("TestService")
145160
.setMetricPrefix("TestPrefix")
146161
.extractAndSetOtelConfigs(otelConfigs)
147162
.build();
148-
assertEquals(config.getOtelAggregationTemporalitySelector(), AggregationTemporalitySelector.deltaPreferred());
163+
AggregationTemporalitySelector defaultSelector = VeniceMetricsConfig.getTemporalitySelector(
164+
useLastRecordedValueForSynchronousGauge,
165+
AggregationTemporalitySelector.deltaPreferred());
166+
for (InstrumentType type: InstrumentType.values()) {
167+
assertEquals(
168+
config.getOtelAggregationTemporalitySelector().getAggregationTemporality(type),
169+
defaultSelector.getAggregationTemporality(type));
170+
}
171+
}
172+
173+
@Test(dataProvider = "True-and-False", dataProviderClass = DataProviderUtils.class)
174+
public void testGetTemporalitySelector(boolean useLastRecordedValueForSynchronousGauge) {
175+
AggregationTemporalitySelector[] selectors =
176+
new AggregationTemporalitySelector[] { AggregationTemporalitySelector.deltaPreferred(),
177+
AggregationTemporalitySelector.alwaysCumulative(), AggregationTemporalitySelector.lowMemory() };
178+
for (AggregationTemporalitySelector baseSelector: selectors) {
179+
AggregationTemporalitySelector selector =
180+
VeniceMetricsConfig.getTemporalitySelector(useLastRecordedValueForSynchronousGauge, baseSelector);
181+
for (InstrumentType type: InstrumentType.values()) {
182+
if (useLastRecordedValueForSynchronousGauge && type == InstrumentType.GAUGE) {
183+
// If exportLastRecordedValue and type is GAUGE, must be CUMULATIVE
184+
assertEquals(selector.getAggregationTemporality(type), AggregationTemporality.CUMULATIVE);
185+
} else {
186+
// Otherwise, should be same as baseSelector
187+
assertEquals(selector.getAggregationTemporality(type), baseSelector.getAggregationTemporality(type));
188+
}
189+
}
190+
}
149191
}
150192

151193
@Test(expectedExceptions = IllegalArgumentException.class)

internal/venice-client-common/src/test/java/com/linkedin/venice/stats/metrics/MetricTypeTest.java

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,12 @@
1818
import com.linkedin.venice.utils.DataProviderUtils;
1919
import io.opentelemetry.api.common.Attributes;
2020
import io.opentelemetry.sdk.metrics.data.MetricData;
21+
import io.opentelemetry.sdk.metrics.export.AggregationTemporalitySelector;
22+
import io.opentelemetry.sdk.metrics.export.DefaultAggregationSelector;
2123
import io.opentelemetry.sdk.testing.exporter.InMemoryMetricReader;
2224
import io.tehuti.metrics.MetricConfig;
23-
import java.util.Arrays;
2425
import java.util.Collection;
26+
import java.util.Collections;
2527
import java.util.HashMap;
2628
import java.util.HashSet;
2729
import java.util.Map;
@@ -63,7 +65,7 @@ private static VeniceOpenTelemetryMetricsRepository createOtelRepo(
6365
VeniceMetricsConfig metricsConfig = new VeniceMetricsConfig.Builder().setEmitOtelMetrics(true)
6466
.setMetricPrefix(METRIC_PREFIX)
6567
.setOtelAdditionalMetricsReader(inMemoryMetricReader)
66-
.setMetricEntities(Arrays.asList(metricEntity))
68+
.setMetricEntities(Collections.singletonList(metricEntity))
6769
.setTehutiMetricConfig(new MetricConfig())
6870
.build();
6971
return new VeniceOpenTelemetryMetricsRepository(metricsConfig);
@@ -169,29 +171,51 @@ public void testOTelRecordHistogram(boolean isExponentialHistogram) {
169171
}
170172
}
171173

172-
@Test
173-
public void testOTelRecordGauge() {
174+
@Test(dataProvider = "True-and-False", dataProviderClass = DataProviderUtils.class)
175+
public void testOTelRecordGauge(boolean exportLastRecordedValueForSynchronousGauge) {
174176
MetricEntity metricEntityGauge = new MetricEntity(
175177
"test_metric_gauge",
176178
MetricType.GAUGE,
177179
MetricUnit.NUMBER,
178180
TEST_DESCRIPTION,
179181
getTestDimensions());
180-
InMemoryMetricReader inMemoryMetricReader = InMemoryMetricReader.create();
182+
InMemoryMetricReader inMemoryMetricReader = InMemoryMetricReader.create(
183+
VeniceMetricsConfig.getTemporalitySelector(
184+
exportLastRecordedValueForSynchronousGauge,
185+
AggregationTemporalitySelector.deltaPreferred()),
186+
DefaultAggregationSelector.getDefault());
181187
VeniceOpenTelemetryMetricsRepository otelMetricsRepository =
182188
createOtelRepo(metricEntityGauge, inMemoryMetricReader);
183189
MetricEntityStateBase metricEntityStateBaseGauge = MetricEntityStateBase
184190
.create(metricEntityGauge, otelMetricsRepository, getBaseDimensionsMap(), getBaseAttributes());
185191
metricEntityStateBaseGauge.record(10L);
186192
metricEntityStateBaseGauge.record(20L);
187-
Collection<MetricData> metrics = inMemoryMetricReader.collectAllMetrics();
188-
assertFalse(metrics.isEmpty(), "Metrics should not be empty");
189-
assertEquals(metrics.size(), 1, "There should be one metric recorded");
193+
// validate the last recorded value is 20L: Note that the validate method calls collectAllMetrics() which is
194+
// equivalent to an export
190195
validateLongPointDataFromGauge(inMemoryMetricReader, 20L, getBaseAttributes(), "test_metric_gauge", METRIC_PREFIX);
191196

192-
// record another value and validate again
193-
metricEntityStateBaseGauge.record(30L);
194-
validateLongPointDataFromGauge(inMemoryMetricReader, 30L, getBaseAttributes(), "test_metric_gauge", METRIC_PREFIX);
197+
if (exportLastRecordedValueForSynchronousGauge) {
198+
// should be able to read the same value again after export
199+
validateLongPointDataFromGauge(
200+
inMemoryMetricReader,
201+
20L,
202+
getBaseAttributes(),
203+
"test_metric_gauge",
204+
METRIC_PREFIX);
205+
} else {
206+
// should not be able to read the same value again after export
207+
try {
208+
validateLongPointDataFromGauge(
209+
inMemoryMetricReader,
210+
20L,
211+
getBaseAttributes(),
212+
"test_metric_gauge",
213+
METRIC_PREFIX);
214+
fail("Should not be able to read the same value again after export");
215+
} catch (AssertionError e) {
216+
// expected
217+
}
218+
}
195219
}
196220

197221
@Test

0 commit comments

Comments
 (0)