Skip to content

Commit ce5770a

Browse files
committed
Allow Otlp*MetricExporter's to publish export stats
Previously there was no way to report stats for otlp metric exporters. Now we can supply a MeterProvider lazily. When export is invoked if the previously provided meter is a noop implementation it will try again, allowing for delayed/lazy initialization.
1 parent f801a15 commit ce5770a

File tree

6 files changed

+433
-4
lines changed

6 files changed

+433
-4
lines changed

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

Lines changed: 6 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,10 @@ private Meter meter() {
9292
.get("io.opentelemetry.exporters." + exporterName + "-" + transportName);
9393
}
9494

95+
private static boolean isNoop(LongCounter counter) {
96+
return counter.getClass().getSimpleName().startsWith("Noop");
97+
}
98+
9599
/**
96100
* Create an instance for recording exporter metrics under the meter {@code
97101
* "io.opentelemetry.exporters." + exporterName + "-grpc}".
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
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+
// Verify the supplied meterProvider is not reused.
61+
exporterMetrics.addSeen(10);
62+
exporterMetrics.addSeen(20);
63+
verify(meterProviderSupplier, times(1)).get();
64+
exporterMetrics.addSuccess(30);
65+
exporterMetrics.addSuccess(40);
66+
verify(meterProviderSupplier, times(2)).get();
67+
exporterMetrics.addFailed(50);
68+
exporterMetrics.addFailed(60);
69+
verify(meterProviderSupplier, times(2)).get();
70+
}
71+
72+
@Test
73+
void createHttpProtobuf_noopMeterProvider() {
74+
when(meterProviderSupplier.get()).thenReturn(MeterProvider.noop());
75+
ExporterMetrics exporterMetrics =
76+
ExporterMetrics.createHttpProtobuf("test", "test", meterProviderSupplier);
77+
verifyNoInteractions(meterProviderSupplier); // Ensure lazy
78+
// Verify the supplied meterProvider is not reused.
79+
exporterMetrics.addSeen(10);
80+
exporterMetrics.addSeen(20);
81+
verify(meterProviderSupplier, times(2)).get();
82+
exporterMetrics.addSuccess(30);
83+
exporterMetrics.addSuccess(40);
84+
verify(meterProviderSupplier, times(4)).get();
85+
exporterMetrics.addFailed(50);
86+
exporterMetrics.addFailed(60);
87+
verify(meterProviderSupplier, times(6)).get();
88+
}
89+
}

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;
@@ -57,7 +58,6 @@ public final class OtlpHttpMetricExporterBuilder {
5758
OtlpHttpMetricExporterBuilder(HttpExporterBuilder<Marshaler> delegate, MemoryMode memoryMode) {
5859
this.delegate = delegate;
5960
this.memoryMode = memoryMode;
60-
delegate.setMeterProvider(MeterProvider::noop);
6161
OtlpUserAgent.addUserAgentHeader(delegate::addConstantHeaders);
6262
}
6363

@@ -235,6 +235,27 @@ public OtlpHttpMetricExporterBuilder setProxyOptions(ProxyOptions proxyOptions)
235235
return this;
236236
}
237237

238+
/**
239+
* Sets the {@link MeterProvider} to use to collect metrics related to export. If not set, uses
240+
* {@link GlobalOpenTelemetry#getMeterProvider()}.
241+
*/
242+
public OtlpHttpMetricExporterBuilder setMeterProvider(MeterProvider meterProvider) {
243+
requireNonNull(meterProvider, "meterProvider");
244+
delegate.setMeterProvider(() -> meterProvider);
245+
return this;
246+
}
247+
248+
/**
249+
* Sets the {@link MeterProvider} supplier to use to collect metrics related to export. If not
250+
* set, uses {@link GlobalOpenTelemetry#getMeterProvider()}.
251+
*/
252+
public OtlpHttpMetricExporterBuilder setMeterProvider(
253+
Supplier<MeterProvider> meterProviderSupplier) {
254+
requireNonNull(meterProviderSupplier, "meterProvider");
255+
delegate.setMeterProvider(meterProviderSupplier);
256+
return this;
257+
}
258+
238259
/**
239260
* Set the {@link MemoryMode}. If unset, defaults to {@link #DEFAULT_MEMORY_MODE}.
240261
*

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;
@@ -66,7 +67,6 @@ public final class OtlpGrpcMetricExporterBuilder {
6667
OtlpGrpcMetricExporterBuilder(GrpcExporterBuilder<Marshaler> delegate, MemoryMode memoryMode) {
6768
this.delegate = delegate;
6869
this.memoryMode = memoryMode;
69-
delegate.setMeterProvider(MeterProvider::noop);
7070
OtlpUserAgent.addUserAgentHeader(delegate::addConstantHeader);
7171
}
7272

@@ -268,6 +268,27 @@ public OtlpGrpcMetricExporterBuilder setRetryPolicy(@Nullable RetryPolicy retryP
268268
return this;
269269
}
270270

271+
/**
272+
* Sets the {@link MeterProvider} to use to collect metrics related to export. If not set, uses
273+
* {@link GlobalOpenTelemetry#getMeterProvider()}.
274+
*/
275+
public OtlpGrpcMetricExporterBuilder setMeterProvider(MeterProvider meterProvider) {
276+
requireNonNull(meterProvider, "meterProvider");
277+
delegate.setMeterProvider(() -> meterProvider);
278+
return this;
279+
}
280+
281+
/**
282+
* Sets the {@link MeterProvider} supplier to use to collect metrics related to export. If not
283+
* set, uses {@link GlobalOpenTelemetry#getMeterProvider()}.
284+
*/
285+
public OtlpGrpcMetricExporterBuilder setMeterProvider(
286+
Supplier<MeterProvider> meterProviderSupplier) {
287+
requireNonNull(meterProviderSupplier, "meterProvider");
288+
delegate.setMeterProvider(meterProviderSupplier);
289+
return this;
290+
}
291+
271292
/**
272293
* Set the {@link MemoryMode}. If unset, defaults to {@link #DEFAULT_MEMORY_MODE}.
273294
*
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.exporter.otlp;
7+
8+
import static io.opentelemetry.sdk.metrics.internal.data.ImmutableMetricData.createDoubleGauge;
9+
import static java.util.Collections.singleton;
10+
import static org.junit.jupiter.api.Assertions.assertThrows;
11+
import static org.mockito.ArgumentMatchers.any;
12+
import static org.mockito.ArgumentMatchers.eq;
13+
import static org.mockito.Mockito.mock;
14+
import static org.mockito.Mockito.verify;
15+
import static org.mockito.Mockito.verifyNoInteractions;
16+
import static org.mockito.Mockito.verifyNoMoreInteractions;
17+
import static org.mockito.Mockito.when;
18+
19+
import io.opentelemetry.api.GlobalOpenTelemetry;
20+
import io.opentelemetry.api.OpenTelemetry;
21+
import io.opentelemetry.api.metrics.LongCounter;
22+
import io.opentelemetry.api.metrics.LongCounterBuilder;
23+
import io.opentelemetry.api.metrics.Meter;
24+
import io.opentelemetry.api.metrics.MeterProvider;
25+
import io.opentelemetry.api.trace.TracerProvider;
26+
import io.opentelemetry.context.propagation.ContextPropagators;
27+
import io.opentelemetry.exporter.otlp.metrics.OtlpGrpcMetricExporter;
28+
import io.opentelemetry.exporter.otlp.metrics.OtlpGrpcMetricExporterBuilder;
29+
import io.opentelemetry.sdk.common.InstrumentationScopeInfo;
30+
import io.opentelemetry.sdk.metrics.SdkMeterProvider;
31+
import io.opentelemetry.sdk.metrics.SdkMeterProviderBuilder;
32+
import io.opentelemetry.sdk.metrics.data.MetricData;
33+
import io.opentelemetry.sdk.metrics.export.PeriodicMetricReader;
34+
import io.opentelemetry.sdk.metrics.internal.data.ImmutableGaugeData;
35+
import io.opentelemetry.sdk.resources.Resource;
36+
import java.util.Collection;
37+
import java.util.concurrent.atomic.AtomicReference;
38+
import java.util.function.Supplier;
39+
import org.junit.jupiter.api.Test;
40+
41+
class OtlpGrpcMetricExporterBuilderTest {
42+
43+
private static final Collection<MetricData> DATA_SET =
44+
singleton(
45+
createDoubleGauge(
46+
Resource.empty(),
47+
InstrumentationScopeInfo.create("test"),
48+
"test",
49+
"test",
50+
"test",
51+
ImmutableGaugeData.empty()));
52+
53+
private final MeterProvider meterProvider = mock(MeterProvider.class);
54+
private final Meter meter = mock(Meter.class);
55+
private final LongCounterBuilder counterBuilder = mock(LongCounterBuilder.class);
56+
private final LongCounter counter = mock(LongCounter.class);
57+
58+
@Test
59+
void setMeterProvider_null() {
60+
OtlpGrpcMetricExporterBuilder builder = OtlpGrpcMetricExporter.builder();
61+
assertThrows(
62+
NullPointerException.class,
63+
() -> builder.setMeterProvider((MeterProvider) null),
64+
"MeterProvider must not be null");
65+
assertThrows(
66+
NullPointerException.class,
67+
() -> builder.setMeterProvider((Supplier<MeterProvider>) null),
68+
"MeterProvider must not be null");
69+
}
70+
71+
@Test
72+
void setMeterProvider() {
73+
when(meterProvider.get(any())).thenReturn(meter);
74+
when(meter.counterBuilder(eq("otlp.exporter.seen"))).thenReturn(counterBuilder);
75+
when(counterBuilder.build()).thenReturn(counter);
76+
77+
@SuppressWarnings("unchecked")
78+
Supplier<MeterProvider> provider = mock(Supplier.class);
79+
try (OtlpGrpcMetricExporter exporter =
80+
OtlpGrpcMetricExporter.builder().setMeterProvider(provider).build()) {
81+
verifyNoInteractions(provider, meterProvider, meter, counterBuilder, counter);
82+
83+
// Collection before MeterProvider is initialized.
84+
when(provider.get()).thenReturn(MeterProvider.noop());
85+
exporter.export(DATA_SET);
86+
87+
verifyNoInteractions(meterProvider, meter, counterBuilder, counter);
88+
89+
// Collection after MeterProvider is initialized.
90+
when(provider.get()).thenReturn(meterProvider);
91+
exporter.export(DATA_SET);
92+
93+
verify(meter).counterBuilder(eq("otlp.exporter.seen"));
94+
verify(counter).add(eq(1L), any());
95+
verifyNoMoreInteractions(meter, counter);
96+
}
97+
}
98+
99+
@Test
100+
void setMeterProvider_defaultGlobal() {
101+
GlobalOpenTelemetry.set(
102+
new OpenTelemetry() {
103+
@Override
104+
public MeterProvider getMeterProvider() {
105+
return meterProvider;
106+
}
107+
108+
@Override
109+
public TracerProvider getTracerProvider() {
110+
return TracerProvider.noop();
111+
}
112+
113+
@Override
114+
public ContextPropagators getPropagators() {
115+
return ContextPropagators.noop();
116+
}
117+
});
118+
when(meterProvider.get(any())).thenReturn(meter);
119+
when(meter.counterBuilder(eq("otlp.exporter.seen"))).thenReturn(counterBuilder);
120+
when(counterBuilder.build()).thenReturn(counter);
121+
122+
try (OtlpGrpcMetricExporter exporter = OtlpGrpcMetricExporter.builder().build()) {
123+
verifyNoInteractions(meterProvider, meter, counterBuilder, counter);
124+
125+
exporter.export(DATA_SET);
126+
127+
verify(meter).counterBuilder(eq("otlp.exporter.seen"));
128+
verify(counter).add(eq(1L), any());
129+
verifyNoMoreInteractions(meter, counter);
130+
} finally {
131+
GlobalOpenTelemetry.resetForTest();
132+
}
133+
}
134+
135+
@Test
136+
void setMeterProvider_noMocks() {
137+
AtomicReference<SdkMeterProvider> meterProviderAtomicReference = new AtomicReference<>();
138+
SdkMeterProviderBuilder builder =
139+
SdkMeterProvider.builder()
140+
.registerMetricReader(
141+
PeriodicMetricReader.create(
142+
OtlpGrpcMetricExporter.builder()
143+
.setMeterProvider(meterProviderAtomicReference::get)
144+
.build()));
145+
meterProviderAtomicReference.set(builder.build());
146+
meterProviderAtomicReference.get().close();
147+
}
148+
}

0 commit comments

Comments
 (0)