Skip to content

Commit 331c6af

Browse files
authored
Experimental metric reader and view cardinality limits (#5494)
1 parent 4d034b0 commit 331c6af

19 files changed

+499
-89
lines changed

sdk/all/src/test/java/io/opentelemetry/sdk/OpenTelemetrySdkTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -421,7 +421,7 @@ void stringRepresentation() {
421421
+ "clock=SystemClock{}, "
422422
+ "resource=Resource{schemaUrl=null, attributes={service.name=\"otel-test\"}}, "
423423
+ "metricReaders=[PeriodicMetricReader{exporter=MockMetricExporter{}, intervalNanos=60000000000}], "
424-
+ "views=[RegisteredView{instrumentSelector=InstrumentSelector{instrumentName=instrument}, view=View{name=new-instrument, aggregation=DefaultAggregation, attributesProcessor=NoopAttributesProcessor{}}}]"
424+
+ "views=[RegisteredView{instrumentSelector=InstrumentSelector{instrumentName=instrument}, view=View{name=new-instrument, aggregation=DefaultAggregation, attributesProcessor=NoopAttributesProcessor{}, cardinalityLimit=2000}}]"
425425
+ "}, "
426426
+ "loggerProvider=SdkLoggerProvider{"
427427
+ "clock=SystemClock{}, "

sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/SdkMeterProvider.java

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import io.opentelemetry.sdk.metrics.export.MetricReader;
1717
import io.opentelemetry.sdk.metrics.internal.SdkMeterProviderUtil;
1818
import io.opentelemetry.sdk.metrics.internal.exemplar.ExemplarFilter;
19+
import io.opentelemetry.sdk.metrics.internal.export.CardinalityLimitSelector;
1920
import io.opentelemetry.sdk.metrics.internal.export.MetricProducer;
2021
import io.opentelemetry.sdk.metrics.internal.export.RegisteredReader;
2122
import io.opentelemetry.sdk.metrics.internal.state.MeterProviderSharedState;
@@ -26,6 +27,7 @@
2627
import java.util.ArrayList;
2728
import java.util.Collection;
2829
import java.util.Collections;
30+
import java.util.IdentityHashMap;
2931
import java.util.List;
3032
import java.util.concurrent.TimeUnit;
3133
import java.util.concurrent.atomic.AtomicBoolean;
@@ -54,17 +56,19 @@ public static SdkMeterProviderBuilder builder() {
5456

5557
SdkMeterProvider(
5658
List<RegisteredView> registeredViews,
57-
List<MetricReader> metricReaders,
59+
IdentityHashMap<MetricReader, CardinalityLimitSelector> metricReaders,
5860
Clock clock,
5961
Resource resource,
6062
ExemplarFilter exemplarFilter) {
6163
long startEpochNanos = clock.now();
6264
this.registeredViews = registeredViews;
6365
this.registeredReaders =
64-
metricReaders.stream()
66+
metricReaders.entrySet().stream()
6567
.map(
66-
reader ->
67-
RegisteredReader.create(reader, ViewRegistry.create(reader, registeredViews)))
68+
entry ->
69+
RegisteredReader.create(
70+
entry.getKey(),
71+
ViewRegistry.create(entry.getKey(), entry.getValue(), registeredViews)))
6872
.collect(toList());
6973
this.sharedState =
7074
MeterProviderSharedState.create(clock, resource, exemplarFilter, startEpochNanos);

sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/SdkMeterProviderBuilder.java

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@
1010
import io.opentelemetry.sdk.metrics.internal.SdkMeterProviderUtil;
1111
import io.opentelemetry.sdk.metrics.internal.debug.SourceInfo;
1212
import io.opentelemetry.sdk.metrics.internal.exemplar.ExemplarFilter;
13+
import io.opentelemetry.sdk.metrics.internal.export.CardinalityLimitSelector;
1314
import io.opentelemetry.sdk.metrics.internal.view.RegisteredView;
1415
import io.opentelemetry.sdk.resources.Resource;
1516
import java.util.ArrayList;
17+
import java.util.IdentityHashMap;
1618
import java.util.List;
1719
import java.util.Objects;
1820

@@ -32,7 +34,8 @@ public final class SdkMeterProviderBuilder {
3234

3335
private Clock clock = Clock.getDefault();
3436
private Resource resource = Resource.getDefault();
35-
private final List<MetricReader> metricReaders = new ArrayList<>();
37+
private final IdentityHashMap<MetricReader, CardinalityLimitSelector> metricReaders =
38+
new IdentityHashMap<>();
3639
private final List<RegisteredView> registeredViews = new ArrayList<>();
3740
private ExemplarFilter exemplarFilter = DEFAULT_EXEMPLAR_FILTER;
3841

@@ -96,7 +99,11 @@ public SdkMeterProviderBuilder registerView(InstrumentSelector selector, View vi
9699
Objects.requireNonNull(view, "view");
97100
registeredViews.add(
98101
RegisteredView.create(
99-
selector, view, view.getAttributesProcessor(), SourceInfo.fromCurrentStack()));
102+
selector,
103+
view,
104+
view.getAttributesProcessor(),
105+
view.getCardinalityLimit(),
106+
SourceInfo.fromCurrentStack()));
100107
return this;
101108
}
102109

@@ -106,7 +113,20 @@ public SdkMeterProviderBuilder registerView(InstrumentSelector selector, View vi
106113
* <p>Note: custom implementations of {@link MetricReader} are not currently supported.
107114
*/
108115
public SdkMeterProviderBuilder registerMetricReader(MetricReader reader) {
109-
metricReaders.add(reader);
116+
metricReaders.put(reader, CardinalityLimitSelector.defaultCardinalityLimitSelector());
117+
return this;
118+
}
119+
120+
/**
121+
* Registers a {@link MetricReader} with a {@link CardinalityLimitSelector}.
122+
*
123+
* <p>Note: not currently stable but available for experimental use via {@link
124+
* SdkMeterProviderUtil#registerMetricReaderWithCardinalitySelector(SdkMeterProviderBuilder,
125+
* MetricReader, CardinalityLimitSelector)}.
126+
*/
127+
SdkMeterProviderBuilder registerMetricReader(
128+
MetricReader reader, CardinalityLimitSelector cardinalityLimitSelector) {
129+
metricReaders.put(reader, cardinalityLimitSelector);
110130
return this;
111131
}
112132

sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/View.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,10 @@ static View create(
3232
@Nullable String name,
3333
@Nullable String description,
3434
Aggregation aggregation,
35-
AttributesProcessor attributesProcessor) {
36-
return new AutoValue_View(name, description, aggregation, attributesProcessor);
35+
AttributesProcessor attributesProcessor,
36+
int cardinalityLimit) {
37+
return new AutoValue_View(
38+
name, description, aggregation, attributesProcessor, cardinalityLimit);
3739
}
3840

3941
View() {}
@@ -58,6 +60,9 @@ static View create(
5860
/** Returns the attribute processor used for this view. */
5961
abstract AttributesProcessor getAttributesProcessor();
6062

63+
/** Returns the cardinality limit for this view. */
64+
abstract int getCardinalityLimit();
65+
6166
@Override
6267
public final String toString() {
6368
StringJoiner joiner = new StringJoiner(", ", "View{", "}");
@@ -69,6 +74,7 @@ public final String toString() {
6974
}
7075
joiner.add("aggregation=" + getAggregation());
7176
joiner.add("attributesProcessor=" + getAttributesProcessor());
77+
joiner.add("cardinalityLimit=" + getCardinalityLimit());
7278
return joiner.toString();
7379
}
7480
}

sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/ViewBuilder.java

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import io.opentelemetry.sdk.metrics.internal.SdkMeterProviderUtil;
99
import io.opentelemetry.sdk.metrics.internal.aggregator.AggregatorFactory;
10+
import io.opentelemetry.sdk.metrics.internal.state.MetricStorage;
1011
import io.opentelemetry.sdk.metrics.internal.view.AttributesProcessor;
1112
import java.util.Objects;
1213
import java.util.function.Predicate;
@@ -23,6 +24,7 @@ public final class ViewBuilder {
2324
@Nullable private String description;
2425
private Aggregation aggregation = Aggregation.defaultAggregation();
2526
private AttributesProcessor processor = AttributesProcessor.noop();
27+
private int cardinalityLimit = MetricStorage.DEFAULT_MAX_CARDINALITY;
2628

2729
ViewBuilder() {}
2830

@@ -85,8 +87,24 @@ ViewBuilder addAttributesProcessor(AttributesProcessor attributesProcessor) {
8587
return this;
8688
}
8789

90+
/**
91+
* Set the cardinality limit.
92+
*
93+
* <p>Note: not currently stable but cardinality limit can be configured via
94+
* SdkMeterProviderUtil#setCardinalityLimit(ViewBuilder, int)}.
95+
*
96+
* @param cardinalityLimit the maximum number of series for a metric
97+
*/
98+
ViewBuilder setCardinalityLimit(int cardinalityLimit) {
99+
if (cardinalityLimit <= 0) {
100+
throw new IllegalArgumentException("cardinalityLimit must be > 0");
101+
}
102+
this.cardinalityLimit = cardinalityLimit;
103+
return this;
104+
}
105+
88106
/** Returns a {@link View} with the configuration of this builder. */
89107
public View build() {
90-
return View.create(name, description, aggregation, processor);
108+
return View.create(name, description, aggregation, processor, cardinalityLimit);
91109
}
92110
}

sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/SdkMeterProviderUtil.java

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88
import io.opentelemetry.sdk.metrics.SdkMeterProvider;
99
import io.opentelemetry.sdk.metrics.SdkMeterProviderBuilder;
1010
import io.opentelemetry.sdk.metrics.ViewBuilder;
11+
import io.opentelemetry.sdk.metrics.export.MetricReader;
1112
import io.opentelemetry.sdk.metrics.internal.exemplar.ExemplarFilter;
13+
import io.opentelemetry.sdk.metrics.internal.export.CardinalityLimitSelector;
1214
import io.opentelemetry.sdk.metrics.internal.view.AttributesProcessor;
1315
import io.opentelemetry.sdk.metrics.internal.view.StringPredicates;
1416
import java.lang.reflect.InvocationTargetException;
@@ -26,7 +28,7 @@ private SdkMeterProviderUtil() {}
2628
/**
2729
* Reflectively assign the {@link ExemplarFilter} to the {@link SdkMeterProviderBuilder}.
2830
*
29-
* @param sdkMeterProviderBuilder the
31+
* @param sdkMeterProviderBuilder the builder
3032
*/
3133
public static void setExemplarFilter(
3234
SdkMeterProviderBuilder sdkMeterProviderBuilder, ExemplarFilter exemplarFilter) {
@@ -42,6 +44,28 @@ public static void setExemplarFilter(
4244
}
4345
}
4446

47+
/**
48+
* Reflectively add a {@link MetricReader} with the {@link CardinalityLimitSelector} to the {@link
49+
* SdkMeterProviderBuilder}.
50+
*
51+
* @param sdkMeterProviderBuilder the builder
52+
*/
53+
public static void registerMetricReaderWithCardinalitySelector(
54+
SdkMeterProviderBuilder sdkMeterProviderBuilder,
55+
MetricReader metricReader,
56+
CardinalityLimitSelector cardinalityLimitSelector) {
57+
try {
58+
Method method =
59+
SdkMeterProviderBuilder.class.getDeclaredMethod(
60+
"registerMetricReader", MetricReader.class, CardinalityLimitSelector.class);
61+
method.setAccessible(true);
62+
method.invoke(sdkMeterProviderBuilder, metricReader, cardinalityLimitSelector);
63+
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
64+
throw new IllegalStateException(
65+
"Error calling addMetricReader on SdkMeterProviderBuilder", e);
66+
}
67+
}
68+
4569
/**
4670
* Reflectively add an {@link AttributesProcessor} to the {@link ViewBuilder} which appends
4771
* key-values from baggage to all measurements.
@@ -81,6 +105,21 @@ private static void addAttributesProcessor(
81105
}
82106
}
83107

108+
/**
109+
* Reflectively set the {@code cardinalityLimit} on the {@link ViewBuilder}.
110+
*
111+
* @param viewBuilder the builder
112+
*/
113+
public static void setCardinalityLimit(ViewBuilder viewBuilder, int cardinalityLimit) {
114+
try {
115+
Method method = ViewBuilder.class.getDeclaredMethod("setCardinalityLimit", int.class);
116+
method.setAccessible(true);
117+
method.invoke(viewBuilder, cardinalityLimit);
118+
} catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
119+
throw new IllegalStateException("Error setting cardinalityLimit on ViewBuilder", e);
120+
}
121+
}
122+
84123
/** Reflectively reset the {@link SdkMeterProvider}, clearing all registered instruments. */
85124
public static void resetForTest(SdkMeterProvider sdkMeterProvider) {
86125
try {
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.sdk.metrics.internal.export;
7+
8+
import io.opentelemetry.sdk.metrics.InstrumentType;
9+
import io.opentelemetry.sdk.metrics.SdkMeterProviderBuilder;
10+
import io.opentelemetry.sdk.metrics.export.MetricReader;
11+
import io.opentelemetry.sdk.metrics.internal.SdkMeterProviderUtil;
12+
import io.opentelemetry.sdk.metrics.internal.state.MetricStorage;
13+
14+
/**
15+
* Customize the {@link io.opentelemetry.sdk.metrics.export.MetricReader} cardinality limit as a
16+
* function of {@link InstrumentType}. Register via {@link
17+
* SdkMeterProviderUtil#registerMetricReaderWithCardinalitySelector(SdkMeterProviderBuilder,
18+
* MetricReader, CardinalityLimitSelector)}.
19+
*
20+
* <p>This class is internal and is hence not for public use. Its APIs are unstable and can change
21+
* at any time.
22+
*/
23+
@FunctionalInterface
24+
public interface CardinalityLimitSelector {
25+
26+
/**
27+
* The default {@link CardinalityLimitSelector}, allowing each metric to have {@code 2000} points.
28+
*/
29+
static CardinalityLimitSelector defaultCardinalityLimitSelector() {
30+
return unused -> MetricStorage.DEFAULT_MAX_CARDINALITY;
31+
}
32+
33+
/**
34+
* Return the default cardinality limit for metrics from instruments of type {@code
35+
* instrumentType}. The cardinality limit dictates the maximum number of distinct points (or time
36+
* series) for the metric.
37+
*/
38+
int getCardinalityLimit(InstrumentType instrumentType);
39+
}

sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/state/AsynchronousMetricStorage.java

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ final class AsynchronousMetricStorage<T extends PointData, U extends ExemplarDat
4646
private final AggregationTemporality aggregationTemporality;
4747
private final Aggregator<T, U> aggregator;
4848
private final AttributesProcessor attributesProcessor;
49+
private final int maxCardinality;
4950
private Map<Attributes, T> points = new HashMap<>();
5051
private Map<Attributes, T> lastPoints =
5152
new HashMap<>(); // Only populated if aggregationTemporality == DELTA
@@ -54,7 +55,8 @@ private AsynchronousMetricStorage(
5455
RegisteredReader registeredReader,
5556
MetricDescriptor metricDescriptor,
5657
Aggregator<T, U> aggregator,
57-
AttributesProcessor attributesProcessor) {
58+
AttributesProcessor attributesProcessor,
59+
int maxCardinality) {
5860
this.registeredReader = registeredReader;
5961
this.metricDescriptor = metricDescriptor;
6062
this.aggregationTemporality =
@@ -63,6 +65,7 @@ private AsynchronousMetricStorage(
6365
.getAggregationTemporality(metricDescriptor.getSourceInstrument().getType());
6466
this.aggregator = aggregator;
6567
this.attributesProcessor = attributesProcessor;
68+
this.maxCardinality = maxCardinality;
6669
}
6770

6871
/**
@@ -83,7 +86,8 @@ static <T extends PointData, U extends ExemplarData> AsynchronousMetricStorage<T
8386
registeredReader,
8487
metricDescriptor,
8588
aggregator,
86-
registeredView.getViewAttributesProcessor());
89+
registeredView.getViewAttributesProcessor(),
90+
registeredView.getCardinalityLimit());
8791
}
8892

8993
/**
@@ -109,13 +113,13 @@ void record(Measurement measurement) {
109113
private void recordPoint(T point) {
110114
Attributes attributes = point.getAttributes();
111115

112-
if (points.size() >= MetricStorage.MAX_CARDINALITY) {
116+
if (points.size() >= maxCardinality) {
113117
throttlingLogger.log(
114118
Level.WARNING,
115119
"Instrument "
116120
+ metricDescriptor.getSourceInstrument().getName()
117121
+ " has exceeded the maximum allowed cardinality ("
118-
+ MetricStorage.MAX_CARDINALITY
122+
+ maxCardinality
119123
+ ").");
120124
return;
121125
}

sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/state/DefaultSynchronousMetricStorage.java

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,14 +50,16 @@ public final class DefaultSynchronousMetricStorage<T extends PointData, U extend
5050
private final ConcurrentHashMap<Attributes, AggregatorHandle<T, U>> aggregatorHandles =
5151
new ConcurrentHashMap<>();
5252
private final AttributesProcessor attributesProcessor;
53+
private final int maxCardinality;
5354
private final ConcurrentLinkedQueue<AggregatorHandle<T, U>> aggregatorHandlePool =
5455
new ConcurrentLinkedQueue<>();
5556

5657
DefaultSynchronousMetricStorage(
5758
RegisteredReader registeredReader,
5859
MetricDescriptor metricDescriptor,
5960
Aggregator<T, U> aggregator,
60-
AttributesProcessor attributesProcessor) {
61+
AttributesProcessor attributesProcessor,
62+
int maxCardinality) {
6163
this.registeredReader = registeredReader;
6264
this.metricDescriptor = metricDescriptor;
6365
this.aggregationTemporality =
@@ -66,6 +68,7 @@ public final class DefaultSynchronousMetricStorage<T extends PointData, U extend
6668
.getAggregationTemporality(metricDescriptor.getSourceInstrument().getType());
6769
this.aggregator = aggregator;
6870
this.attributesProcessor = attributesProcessor;
71+
this.maxCardinality = maxCardinality;
6972
}
7073

7174
// Visible for testing
@@ -97,13 +100,13 @@ private AggregatorHandle<T, U> getAggregatorHandle(Attributes attributes, Contex
97100
if (handle != null) {
98101
return handle;
99102
}
100-
if (aggregatorHandles.size() >= MAX_CARDINALITY) {
103+
if (aggregatorHandles.size() >= maxCardinality) {
101104
logger.log(
102105
Level.WARNING,
103106
"Instrument "
104107
+ metricDescriptor.getSourceInstrument().getName()
105108
+ " has exceeded the maximum allowed cardinality ("
106-
+ MAX_CARDINALITY
109+
+ maxCardinality
107110
+ ").");
108111
return null;
109112
}
@@ -143,9 +146,9 @@ public MetricData collect(
143146
}
144147
});
145148

146-
// Trim pool down if needed. pool.size() will only exceed MAX_CARDINALITY if new handles are
149+
// Trim pool down if needed. pool.size() will only exceed maxCardinality if new handles are
147150
// created during collection.
148-
int toDelete = aggregatorHandlePool.size() - MAX_CARDINALITY;
151+
int toDelete = aggregatorHandlePool.size() - maxCardinality;
149152
for (int i = 0; i < toDelete; i++) {
150153
aggregatorHandlePool.poll();
151154
}

0 commit comments

Comments
 (0)