Skip to content

Commit b468a2e

Browse files
authored
Allow Otlp*MetricExporter's to publish export stats (#7255)
1 parent 2460c92 commit b468a2e

File tree

7 files changed

+471
-5
lines changed

7 files changed

+471
-5
lines changed
Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,9 @@
11
Comparing source compatibility of opentelemetry-exporter-otlp-1.50.0-SNAPSHOT.jar against opentelemetry-exporter-otlp-1.49.0.jar
2-
No changes.
2+
*** MODIFIED CLASS: PUBLIC FINAL io.opentelemetry.exporter.otlp.http.metrics.OtlpHttpMetricExporterBuilder (not serializable)
3+
=== CLASS FILE FORMAT VERSION: 52.0 <- 52.0
4+
+++ NEW METHOD: PUBLIC(+) io.opentelemetry.exporter.otlp.http.metrics.OtlpHttpMetricExporterBuilder setMeterProvider(io.opentelemetry.api.metrics.MeterProvider)
5+
+++ NEW METHOD: PUBLIC(+) io.opentelemetry.exporter.otlp.http.metrics.OtlpHttpMetricExporterBuilder setMeterProvider(java.util.function.Supplier<io.opentelemetry.api.metrics.MeterProvider>)
6+
*** MODIFIED CLASS: PUBLIC FINAL io.opentelemetry.exporter.otlp.metrics.OtlpGrpcMetricExporterBuilder (not serializable)
7+
=== CLASS FILE FORMAT VERSION: 52.0 <- 52.0
8+
+++ NEW METHOD: PUBLIC(+) io.opentelemetry.exporter.otlp.metrics.OtlpGrpcMetricExporterBuilder setMeterProvider(io.opentelemetry.api.metrics.MeterProvider)
9+
+++ NEW METHOD: PUBLIC(+) io.opentelemetry.exporter.otlp.metrics.OtlpGrpcMetricExporterBuilder setMeterProvider(java.util.function.Supplier<io.opentelemetry.api.metrics.MeterProvider>)

exporters/common/src/main/java/io/opentelemetry/exporter/internal/ExporterMetrics.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ public void addFailed(long value) {
7070

7171
private LongCounter seen() {
7272
LongCounter seen = this.seen;
73-
if (seen == null) {
73+
if (seen == null || isNoop(seen)) {
7474
seen = meter().counterBuilder(exporterName + ".exporter.seen").build();
7575
this.seen = seen;
7676
}
@@ -79,7 +79,7 @@ private LongCounter seen() {
7979

8080
private LongCounter exported() {
8181
LongCounter exported = this.exported;
82-
if (exported == null) {
82+
if (exported == null || isNoop(exported)) {
8383
exported = meter().counterBuilder(exporterName + ".exporter.exported").build();
8484
this.exported = exported;
8585
}
@@ -92,6 +92,12 @@ private Meter meter() {
9292
.get("io.opentelemetry.exporters." + exporterName + "-" + transportName);
9393
}
9494

95+
private static boolean isNoop(LongCounter counter) {
96+
// This is a poor way to identify a Noop implementation, but the API doesn't provide a better
97+
// way. Perhaps we could add a common "Noop" interface to allow for an instanceof check?
98+
return counter.getClass().getSimpleName().startsWith("Noop");
99+
}
100+
95101
/**
96102
* Create an instance for recording exporter metrics under the meter {@code
97103
* "io.opentelemetry.exporters." + exporterName + "-grpc}".
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.exporter.internal;
7+
8+
import static org.mockito.Mockito.mock;
9+
import static org.mockito.Mockito.times;
10+
import static org.mockito.Mockito.verify;
11+
import static org.mockito.Mockito.verifyNoInteractions;
12+
import static org.mockito.Mockito.when;
13+
14+
import io.opentelemetry.api.metrics.MeterProvider;
15+
import io.opentelemetry.sdk.common.CompletableResultCode;
16+
import io.opentelemetry.sdk.metrics.InstrumentType;
17+
import io.opentelemetry.sdk.metrics.SdkMeterProvider;
18+
import io.opentelemetry.sdk.metrics.data.AggregationTemporality;
19+
import io.opentelemetry.sdk.metrics.export.CollectionRegistration;
20+
import io.opentelemetry.sdk.metrics.export.MetricReader;
21+
import java.util.function.Supplier;
22+
import org.junit.jupiter.api.Test;
23+
24+
class ExporterMetricsTest {
25+
26+
@SuppressWarnings("unchecked")
27+
Supplier<MeterProvider> meterProviderSupplier = mock(Supplier.class);
28+
29+
@Test
30+
void createHttpProtobuf_validMeterProvider() {
31+
when(meterProviderSupplier.get())
32+
.thenReturn(
33+
SdkMeterProvider.builder()
34+
// Have to provide a valid reader.
35+
.registerMetricReader(
36+
new MetricReader() {
37+
@Override
38+
public void register(CollectionRegistration registration) {}
39+
40+
@Override
41+
public CompletableResultCode forceFlush() {
42+
return CompletableResultCode.ofSuccess();
43+
}
44+
45+
@Override
46+
public CompletableResultCode shutdown() {
47+
return CompletableResultCode.ofSuccess();
48+
}
49+
50+
@Override
51+
public AggregationTemporality getAggregationTemporality(
52+
InstrumentType instrumentType) {
53+
return AggregationTemporality.CUMULATIVE;
54+
}
55+
})
56+
.build());
57+
ExporterMetrics exporterMetrics =
58+
ExporterMetrics.createHttpProtobuf("test", "test", meterProviderSupplier);
59+
verifyNoInteractions(meterProviderSupplier); // Ensure lazy
60+
61+
// Verify the supplier is only called once per underlying meter.
62+
exporterMetrics.addSeen(10);
63+
exporterMetrics.addSeen(20);
64+
verify(meterProviderSupplier, times(1)).get();
65+
exporterMetrics.addSuccess(30);
66+
exporterMetrics.addSuccess(40);
67+
verify(meterProviderSupplier, times(2)).get();
68+
exporterMetrics.addFailed(50);
69+
exporterMetrics.addFailed(60);
70+
verify(meterProviderSupplier, times(2)).get();
71+
}
72+
73+
@Test
74+
void createHttpProtobuf_noopMeterProvider() {
75+
when(meterProviderSupplier.get()).thenReturn(MeterProvider.noop());
76+
ExporterMetrics exporterMetrics =
77+
ExporterMetrics.createHttpProtobuf("test", "test", meterProviderSupplier);
78+
verifyNoInteractions(meterProviderSupplier); // Ensure lazy
79+
80+
// Verify the supplier is invoked multiple times since it returns a noop meter.
81+
exporterMetrics.addSeen(10);
82+
exporterMetrics.addSeen(20);
83+
verify(meterProviderSupplier, times(2)).get();
84+
exporterMetrics.addSuccess(30);
85+
exporterMetrics.addSuccess(40);
86+
verify(meterProviderSupplier, times(4)).get();
87+
exporterMetrics.addFailed(50);
88+
exporterMetrics.addFailed(60);
89+
verify(meterProviderSupplier, times(6)).get();
90+
}
91+
}

exporters/otlp/all/src/main/java/io/opentelemetry/exporter/otlp/http/metrics/OtlpHttpMetricExporterBuilder.java

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import static io.opentelemetry.api.internal.Utils.checkArgument;
99
import static java.util.Objects.requireNonNull;
1010

11+
import io.opentelemetry.api.GlobalOpenTelemetry;
1112
import io.opentelemetry.api.metrics.MeterProvider;
1213
import io.opentelemetry.exporter.internal.compression.Compressor;
1314
import io.opentelemetry.exporter.internal.compression.CompressorProvider;
@@ -61,7 +62,6 @@ public final class OtlpHttpMetricExporterBuilder {
6162
this.aggregationTemporalitySelector = aggregationTemporalitySelector;
6263
this.defaultAggregationSelector = defaultAggregationSelector;
6364
this.memoryMode = memoryMode;
64-
delegate.setMeterProvider(MeterProvider::noop);
6565
OtlpUserAgent.addUserAgentHeader(delegate::addConstantHeaders);
6666
}
6767

@@ -243,6 +243,27 @@ public OtlpHttpMetricExporterBuilder setProxyOptions(ProxyOptions proxyOptions)
243243
return this;
244244
}
245245

246+
/**
247+
* Sets the {@link MeterProvider} to use to collect metrics related to export. If not set, uses
248+
* {@link GlobalOpenTelemetry#getMeterProvider()}.
249+
*/
250+
public OtlpHttpMetricExporterBuilder setMeterProvider(MeterProvider meterProvider) {
251+
requireNonNull(meterProvider, "meterProvider");
252+
delegate.setMeterProvider(() -> meterProvider);
253+
return this;
254+
}
255+
256+
/**
257+
* Sets the {@link MeterProvider} supplier to use to collect metrics related to export. If not
258+
* set, uses {@link GlobalOpenTelemetry#getMeterProvider()}.
259+
*/
260+
public OtlpHttpMetricExporterBuilder setMeterProvider(
261+
Supplier<MeterProvider> meterProviderSupplier) {
262+
requireNonNull(meterProviderSupplier, "meterProvider");
263+
delegate.setMeterProvider(meterProviderSupplier);
264+
return this;
265+
}
266+
246267
/**
247268
* Set the {@link MemoryMode}. If unset, defaults to {@link #DEFAULT_MEMORY_MODE}.
248269
*

exporters/otlp/all/src/main/java/io/opentelemetry/exporter/otlp/metrics/OtlpGrpcMetricExporterBuilder.java

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import static java.util.Objects.requireNonNull;
1010

1111
import io.grpc.ManagedChannel;
12+
import io.opentelemetry.api.GlobalOpenTelemetry;
1213
import io.opentelemetry.api.metrics.MeterProvider;
1314
import io.opentelemetry.exporter.internal.compression.Compressor;
1415
import io.opentelemetry.exporter.internal.compression.CompressorProvider;
@@ -69,7 +70,6 @@ public final class OtlpGrpcMetricExporterBuilder {
6970
this.aggregationTemporalitySelector = aggregationTemporalitySelector;
7071
this.defaultAggregationSelector = defaultAggregationSelector;
7172
this.memoryMode = memoryMode;
72-
delegate.setMeterProvider(MeterProvider::noop);
7373
OtlpUserAgent.addUserAgentHeader(delegate::addConstantHeader);
7474
}
7575

@@ -273,6 +273,27 @@ public OtlpGrpcMetricExporterBuilder setRetryPolicy(@Nullable RetryPolicy retryP
273273
return this;
274274
}
275275

276+
/**
277+
* Sets the {@link MeterProvider} to use to collect metrics related to export. If not set, uses
278+
* {@link GlobalOpenTelemetry#getMeterProvider()}.
279+
*/
280+
public OtlpGrpcMetricExporterBuilder setMeterProvider(MeterProvider meterProvider) {
281+
requireNonNull(meterProvider, "meterProvider");
282+
delegate.setMeterProvider(() -> meterProvider);
283+
return this;
284+
}
285+
286+
/**
287+
* Sets the {@link MeterProvider} supplier to use to collect metrics related to export. If not
288+
* set, uses {@link GlobalOpenTelemetry#getMeterProvider()}.
289+
*/
290+
public OtlpGrpcMetricExporterBuilder setMeterProvider(
291+
Supplier<MeterProvider> meterProviderSupplier) {
292+
requireNonNull(meterProviderSupplier, "meterProvider");
293+
delegate.setMeterProvider(meterProviderSupplier);
294+
return this;
295+
}
296+
276297
/**
277298
* Set the {@link MemoryMode}. If unset, defaults to {@link #DEFAULT_MEMORY_MODE}.
278299
*

exporters/otlp/all/src/test/java/io/opentelemetry/exporter/otlp/http/metrics/OtlpHttpMetricExporterBuilderTest.java

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,176 @@
55

66
package io.opentelemetry.exporter.otlp.http.metrics;
77

8+
import static io.opentelemetry.sdk.metrics.internal.data.ImmutableMetricData.createDoubleGauge;
9+
import static java.util.Collections.singleton;
810
import static org.assertj.core.api.Assertions.assertThat;
11+
import static org.junit.jupiter.api.Assertions.assertThrows;
12+
import static org.mockito.ArgumentMatchers.any;
13+
import static org.mockito.ArgumentMatchers.eq;
14+
import static org.mockito.Mockito.mock;
15+
import static org.mockito.Mockito.verify;
16+
import static org.mockito.Mockito.verifyNoInteractions;
17+
import static org.mockito.Mockito.verifyNoMoreInteractions;
18+
import static org.mockito.Mockito.when;
919

20+
import io.opentelemetry.api.GlobalOpenTelemetry;
21+
import io.opentelemetry.api.OpenTelemetry;
22+
import io.opentelemetry.api.metrics.LongCounter;
23+
import io.opentelemetry.api.metrics.LongCounterBuilder;
24+
import io.opentelemetry.api.metrics.Meter;
25+
import io.opentelemetry.api.metrics.MeterProvider;
26+
import io.opentelemetry.api.trace.TracerProvider;
27+
import io.opentelemetry.context.propagation.ContextPropagators;
28+
import io.opentelemetry.sdk.common.InstrumentationScopeInfo;
1029
import io.opentelemetry.sdk.common.export.MemoryMode;
30+
import io.opentelemetry.sdk.metrics.SdkMeterProvider;
31+
import io.opentelemetry.sdk.metrics.SdkMeterProviderBuilder;
32+
import io.opentelemetry.sdk.metrics.data.MetricData;
1133
import io.opentelemetry.sdk.metrics.export.AggregationTemporalitySelector;
1234
import io.opentelemetry.sdk.metrics.export.DefaultAggregationSelector;
35+
import io.opentelemetry.sdk.metrics.export.PeriodicMetricReader;
36+
import io.opentelemetry.sdk.metrics.internal.data.ImmutableGaugeData;
37+
import io.opentelemetry.sdk.resources.Resource;
38+
import java.util.Collection;
39+
import java.util.concurrent.atomic.AtomicReference;
40+
import java.util.function.Supplier;
1341
import org.junit.jupiter.api.Test;
1442
import org.mockito.Mockito;
1543

1644
class OtlpHttpMetricExporterBuilderTest {
1745

46+
private static final Collection<MetricData> DATA_SET =
47+
singleton(
48+
createDoubleGauge(
49+
Resource.empty(),
50+
InstrumentationScopeInfo.create("test"),
51+
"test",
52+
"test",
53+
"test",
54+
ImmutableGaugeData.empty()));
55+
56+
private final SdkMeterProvider meterProvider = mock(SdkMeterProvider.class);
57+
private final Meter meter = mock(Meter.class);
58+
private final LongCounterBuilder counterBuilder = mock(LongCounterBuilder.class);
59+
private final LongCounter counter = mock(LongCounter.class);
60+
61+
@Test
62+
void setMeterProvider_null() {
63+
OtlpHttpMetricExporterBuilder builder = OtlpHttpMetricExporter.builder();
64+
assertThrows(
65+
NullPointerException.class,
66+
() -> builder.setMeterProvider((MeterProvider) null),
67+
"MeterProvider must not be null");
68+
assertThrows(
69+
NullPointerException.class,
70+
() -> builder.setMeterProvider((Supplier<MeterProvider>) null),
71+
"MeterProvider must not be null");
72+
}
73+
74+
@Test
75+
void setMeterProvider() {
76+
when(meterProvider.get(any())).thenReturn(meter);
77+
when(meter.counterBuilder(eq("otlp.exporter.seen"))).thenReturn(counterBuilder);
78+
when(counterBuilder.build()).thenReturn(counter);
79+
80+
try (OtlpHttpMetricExporter exporter =
81+
OtlpHttpMetricExporter.builder().setMeterProvider(meterProvider).build()) {
82+
verifyNoInteractions(meterProvider, meter, counterBuilder, counter);
83+
84+
// Collection before MeterProvider is initialized.
85+
when(meterProvider.get(any())).thenReturn(MeterProvider.noop().get("test"));
86+
exporter.export(DATA_SET);
87+
88+
verifyNoInteractions(meter, counterBuilder, counter);
89+
90+
// Collection after MeterProvider is initialized.
91+
when(meterProvider.get(any())).thenReturn(meter);
92+
exporter.export(DATA_SET);
93+
94+
verify(meter).counterBuilder(eq("otlp.exporter.seen"));
95+
verify(counter).add(eq(1L), any());
96+
verifyNoMoreInteractions(meter, counter);
97+
}
98+
}
99+
100+
@Test
101+
void setMeterProvider_supplier() {
102+
when(meterProvider.get(any())).thenReturn(meter);
103+
when(meter.counterBuilder(eq("otlp.exporter.seen"))).thenReturn(counterBuilder);
104+
when(counterBuilder.build()).thenReturn(counter);
105+
106+
@SuppressWarnings("unchecked")
107+
Supplier<MeterProvider> provider = mock(Supplier.class);
108+
try (OtlpHttpMetricExporter exporter =
109+
OtlpHttpMetricExporter.builder().setMeterProvider(provider).build()) {
110+
verifyNoInteractions(provider, meterProvider, meter, counterBuilder, counter);
111+
112+
// Collection before MeterProvider is initialized.
113+
when(provider.get()).thenReturn(MeterProvider.noop());
114+
exporter.export(DATA_SET);
115+
116+
verifyNoInteractions(meterProvider, meter, counterBuilder, counter);
117+
118+
// Collection after MeterProvider is initialized.
119+
when(provider.get()).thenReturn(meterProvider);
120+
exporter.export(DATA_SET);
121+
122+
verify(meter).counterBuilder(eq("otlp.exporter.seen"));
123+
verify(counter).add(eq(1L), any());
124+
verifyNoMoreInteractions(meter, counter);
125+
}
126+
}
127+
128+
@Test
129+
void setMeterProvider_defaultGlobal() {
130+
GlobalOpenTelemetry.set(
131+
new OpenTelemetry() {
132+
@Override
133+
public MeterProvider getMeterProvider() {
134+
return meterProvider;
135+
}
136+
137+
@Override
138+
public TracerProvider getTracerProvider() {
139+
return TracerProvider.noop();
140+
}
141+
142+
@Override
143+
public ContextPropagators getPropagators() {
144+
return ContextPropagators.noop();
145+
}
146+
});
147+
when(meterProvider.get(any())).thenReturn(meter);
148+
when(meter.counterBuilder(eq("otlp.exporter.seen"))).thenReturn(counterBuilder);
149+
when(counterBuilder.build()).thenReturn(counter);
150+
151+
try (OtlpHttpMetricExporter exporter = OtlpHttpMetricExporter.builder().build()) {
152+
verifyNoInteractions(meterProvider, meter, counterBuilder, counter);
153+
154+
exporter.export(DATA_SET);
155+
156+
verify(meter).counterBuilder(eq("otlp.exporter.seen"));
157+
verify(counter).add(eq(1L), any());
158+
verifyNoMoreInteractions(meter, counter);
159+
} finally {
160+
GlobalOpenTelemetry.resetForTest();
161+
}
162+
}
163+
164+
@Test
165+
void setMeterProvider_noMocks() {
166+
AtomicReference<SdkMeterProvider> meterProviderAtomicReference = new AtomicReference<>();
167+
SdkMeterProviderBuilder builder =
168+
SdkMeterProvider.builder()
169+
.registerMetricReader(
170+
PeriodicMetricReader.create(
171+
OtlpHttpMetricExporter.builder()
172+
.setMeterProvider(meterProviderAtomicReference::get)
173+
.build()));
174+
meterProviderAtomicReference.set(builder.build());
175+
meterProviderAtomicReference.get().close();
176+
}
177+
18178
@Test
19179
void verifyToBuilderPreservesSettings() {
20180
AggregationTemporalitySelector temporalitySelector =

0 commit comments

Comments
 (0)