Skip to content

Commit 07d0754

Browse files
committed
Merge branch '1.12.x' into 1.13.x
2 parents 3603789 + b8b4b0d commit 07d0754

File tree

3 files changed

+276
-93
lines changed

3 files changed

+276
-93
lines changed

implementations/micrometer-registry-dynatrace/src/main/java/io/micrometer/dynatrace/DynatraceMeterRegistry.java

Lines changed: 76 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ public class DynatraceMeterRegistry extends StepMeterRegistry {
6060

6161
private final boolean useDynatraceSummaryInstruments;
6262

63+
private final boolean shouldAddZeroPercentile;
64+
65+
private final DenyZeroPercentileMeterFilter zeroPercentileMeterFilter = new DenyZeroPercentileMeterFilter();
66+
6367
private final AbstractDynatraceExporter exporter;
6468

6569
@SuppressWarnings("deprecation")
@@ -73,19 +77,24 @@ private DynatraceMeterRegistry(DynatraceConfig config, Clock clock, ThreadFactor
7377
super(config, clock);
7478

7579
useDynatraceSummaryInstruments = config.useDynatraceSummaryInstruments();
80+
// zero percentile is needed only when using the V2 exporter and not using
81+
// Dynatrace summary instruments.
82+
shouldAddZeroPercentile = config.apiVersion() == DynatraceApiVersion.V2 && !useDynatraceSummaryInstruments;
7683

7784
if (config.apiVersion() == DynatraceApiVersion.V2) {
7885
logger.info("Exporting to Dynatrace metrics API v2");
7986
this.exporter = new DynatraceExporterV2(config, clock, httpClient);
80-
// Not used for Timer and DistributionSummary in V2 anymore, but still used
81-
// for the other timer types.
82-
registerMinPercentile();
8387
}
8488
else {
8589
logger.info("Exporting to Dynatrace metrics API v1");
8690
this.exporter = new DynatraceExporterV1(config, clock, httpClient);
8791
}
8892

93+
if (shouldAddZeroPercentile) {
94+
// zero percentiles automatically added should not be exported.
95+
this.config().meterFilter(zeroPercentileMeterFilter);
96+
}
97+
8998
start(threadFactory);
9099
}
91100

@@ -109,7 +118,12 @@ protected DistributionSummary newDistributionSummary(Meter.Id id,
109118
if (useDynatraceSummaryInstruments) {
110119
return new DynatraceDistributionSummary(id, clock, distributionStatisticConfig, scale);
111120
}
112-
return super.newDistributionSummary(id, distributionStatisticConfig, scale);
121+
122+
DistributionStatisticConfig config = distributionStatisticConfig;
123+
if (shouldAddZeroPercentile) {
124+
config = addZeroPercentileIfMissing(id, distributionStatisticConfig);
125+
}
126+
return super.newDistributionSummary(id, config, scale);
113127
}
114128

115129
@Override
@@ -119,7 +133,12 @@ protected Timer newTimer(Meter.Id id, DistributionStatisticConfig distributionSt
119133
return new DynatraceTimer(id, clock, distributionStatisticConfig, pauseDetector,
120134
exporter.getBaseTimeUnit());
121135
}
122-
return super.newTimer(id, distributionStatisticConfig, pauseDetector);
136+
137+
DistributionStatisticConfig config = distributionStatisticConfig;
138+
if (shouldAddZeroPercentile) {
139+
config = addZeroPercentileIfMissing(id, distributionStatisticConfig);
140+
}
141+
return super.newTimer(id, config, pauseDetector);
123142
}
124143

125144
@Override
@@ -128,78 +147,63 @@ protected LongTaskTimer newLongTaskTimer(Meter.Id id, DistributionStatisticConfi
128147
return new DynatraceLongTaskTimer(id, clock, exporter.getBaseTimeUnit(), distributionStatisticConfig,
129148
false);
130149
}
131-
return super.newLongTaskTimer(id, distributionStatisticConfig);
150+
151+
DistributionStatisticConfig config = distributionStatisticConfig;
152+
if (shouldAddZeroPercentile) {
153+
config = addZeroPercentileIfMissing(id, distributionStatisticConfig);
154+
}
155+
return super.newLongTaskTimer(id, config);
132156
}
133157

134-
/**
135-
* As the micrometer summary statistics (DistributionSummary, and a number of timer
136-
* meter types) do not provide the minimum values that are required by Dynatrace to
137-
* ingest summary metrics, we add the 0th percentile to each summary statistic and use
138-
* that as the minimum value.
139-
*/
140-
private void registerMinPercentile() {
141-
config().meterFilter(new MeterFilter() {
142-
private final Set<String> metersWithArtificialZeroPercentile = ConcurrentHashMap.newKeySet();
143-
144-
/**
145-
* Adds 0th percentile if the user hasn't already added and tracks those meter
146-
* names where the 0th percentile was artificially added.
147-
*/
148-
@Override
149-
public DistributionStatisticConfig configure(Meter.Id id, DistributionStatisticConfig config) {
150-
if (useDynatraceSummaryInstruments && dynatraceInstrumentTypeExists(id)) {
151-
// do not add artificial 0 percentile when using Dynatrace instruments
152-
return config;
153-
}
154-
155-
double[] percentiles;
156-
157-
double[] configPercentiles = config.getPercentiles();
158-
if (configPercentiles == null) {
159-
percentiles = new double[] { 0 };
160-
metersWithArtificialZeroPercentile.add(id.getName() + ".percentile");
161-
}
162-
else if (!containsZeroPercentile(config)) {
163-
percentiles = new double[configPercentiles.length + 1];
164-
System.arraycopy(configPercentiles, 0, percentiles, 0, configPercentiles.length);
165-
percentiles[configPercentiles.length] = 0; // theoretically this is
166-
// already zero
167-
metersWithArtificialZeroPercentile.add(id.getName() + ".percentile");
168-
}
169-
else {
170-
percentiles = configPercentiles;
171-
}
172-
173-
return DistributionStatisticConfig.builder().percentiles(percentiles).build().merge(config);
174-
}
175-
176-
/**
177-
* Denies artificially added 0th percentile meters.
178-
*/
179-
@Override
180-
public MeterFilterReply accept(Meter.Id id) {
181-
return hasArtificialZerothPercentile(id) ? DENY : NEUTRAL;
182-
}
183-
184-
private boolean containsZeroPercentile(DistributionStatisticConfig config) {
185-
return Arrays.stream(config.getPercentiles()).anyMatch(percentile -> percentile == 0);
186-
}
187-
188-
private boolean hasArtificialZerothPercentile(Meter.Id id) {
189-
return metersWithArtificialZeroPercentile.contains(id.getName()) && "0".equals(id.getTag("phi"));
190-
}
191-
});
158+
private static class DenyZeroPercentileMeterFilter implements MeterFilter {
159+
160+
private final Set<String> metersWithArtificialZeroPercentile = ConcurrentHashMap.newKeySet();
161+
162+
private boolean hasArtificialZeroPercentile(Meter.Id id) {
163+
return metersWithArtificialZeroPercentile.contains(id.getName()) && "0".equals(id.getTag("phi"));
164+
}
165+
166+
@Override
167+
public MeterFilterReply accept(Meter.Id id) {
168+
return hasArtificialZeroPercentile(id) ? DENY : NEUTRAL;
169+
}
170+
171+
public void addMeterId(Meter.Id id) {
172+
metersWithArtificialZeroPercentile.add(id.getName() + ".percentile");
173+
}
174+
175+
}
176+
177+
private boolean containsZero(double[] percentiles) {
178+
return Arrays.stream(percentiles).anyMatch(percentile -> percentile == 0);
192179
}
193180

194-
private boolean dynatraceInstrumentTypeExists(Meter.Id id) {
195-
switch (id.getType()) {
196-
case DISTRIBUTION_SUMMARY:
197-
case TIMER:
198-
case LONG_TASK_TIMER:
199-
return true;
200-
default:
201-
return false;
181+
private DistributionStatisticConfig addZeroPercentileIfMissing(Meter.Id id,
182+
DistributionStatisticConfig distributionStatisticConfig) {
183+
double[] percentiles;
184+
185+
double[] configPercentiles = distributionStatisticConfig.getPercentiles();
186+
if (configPercentiles == null) {
187+
percentiles = new double[] { 0. };
188+
zeroPercentileMeterFilter.addMeterId(id);
189+
}
190+
else if (!containsZero(configPercentiles)) {
191+
percentiles = new double[configPercentiles.length + 1];
192+
System.arraycopy(configPercentiles, 0, percentiles, 0, configPercentiles.length);
193+
// theoretically this is already zero
194+
percentiles[configPercentiles.length] = 0;
195+
zeroPercentileMeterFilter.addMeterId(id);
202196
}
197+
else {
198+
// Zero percentile is explicitly added to the config, no need to add it to
199+
// drop list.
200+
return distributionStatisticConfig;
201+
}
202+
203+
return DistributionStatisticConfig.builder()
204+
.percentiles(percentiles)
205+
.build()
206+
.merge(distributionStatisticConfig);
203207
}
204208

205209
public static class Builder {

implementations/micrometer-registry-dynatrace/src/test/java/io/micrometer/dynatrace/DynatraceMeterRegistryTest.java

Lines changed: 170 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,17 @@
1515
*/
1616
package io.micrometer.dynatrace;
1717

18-
import io.micrometer.core.instrument.Counter;
19-
import io.micrometer.core.instrument.MockClock;
20-
import io.micrometer.core.instrument.Timer;
18+
import io.micrometer.core.instrument.*;
2119
import io.micrometer.core.ipc.http.HttpSender;
2220
import org.junit.jupiter.api.BeforeEach;
2321
import org.junit.jupiter.api.Test;
2422
import org.mockito.ArgumentCaptor;
2523

2624
import java.nio.charset.StandardCharsets;
25+
import java.time.Duration;
26+
import java.util.concurrent.CountDownLatch;
27+
import java.util.concurrent.ExecutorService;
28+
import java.util.concurrent.Executors;
2729

2830
import static java.util.concurrent.TimeUnit.MILLISECONDS;
2931
import static org.assertj.core.api.Assertions.assertThat;
@@ -151,6 +153,142 @@ void shouldNotTrackPercentilesWithDynatraceSummary() throws Throwable {
151153
"#my.timer gauge dt.meta.unit=milliseconds"))));
152154
}
153155

156+
@Test
157+
void shouldTrackPercentilesWhenDynatraceSummaryInstrumentsNotUsed() throws Throwable {
158+
DynatraceConfig dynatraceConfig = getNonSummaryInstrumentsConfig();
159+
160+
DynatraceMeterRegistry registry = DynatraceMeterRegistry.builder(dynatraceConfig)
161+
.httpClient(httpClient)
162+
.clock(clock)
163+
.build();
164+
165+
HttpSender.Request.Builder builder = HttpSender.Request.build(config.uri(), httpClient);
166+
when(httpClient.post(config.uri())).thenReturn(builder);
167+
168+
double[] trackedPercentiles = new double[] { 0.5, 0.7, 0.99 };
169+
170+
Timer timer = Timer.builder("my.timer").publishPercentiles(trackedPercentiles).register(registry);
171+
DistributionSummary distributionSummary = DistributionSummary.builder("my.ds")
172+
.publishPercentiles(trackedPercentiles)
173+
.register(registry);
174+
CountDownLatch lttCountDownLatch = new CountDownLatch(1);
175+
LongTaskTimer longTaskTimer = LongTaskTimer.builder("my.ltt")
176+
.publishPercentiles(trackedPercentiles)
177+
.register(registry);
178+
179+
timer.record(Duration.ofMillis(100));
180+
distributionSummary.record(100);
181+
182+
ExecutorService executorService = Executors.newSingleThreadExecutor();
183+
executorService.submit(() -> longTaskTimer.record(() -> {
184+
clock.add(Duration.ofMillis(100));
185+
186+
try {
187+
assertThat(lttCountDownLatch.await(300, MILLISECONDS)).isTrue();
188+
}
189+
catch (InterruptedException e) {
190+
throw new RuntimeException(e);
191+
}
192+
}));
193+
194+
clock.add(dynatraceConfig.step());
195+
196+
registry.publish();
197+
// release long task timer
198+
lttCountDownLatch.countDown();
199+
200+
verify(httpClient).send(
201+
assertArg((request -> assertThat(request.getEntity()).asString()
202+
.hasLineCount(16)
203+
.contains(
204+
// Timer lines
205+
"my.timer,dt.metrics.source=micrometer gauge,min=100,max=100,sum=100,count=1 "
206+
+ clock.wallTime(),
207+
"#my.timer gauge dt.meta.unit=milliseconds",
208+
// Timer percentile lines. Percentiles are 0 because the step
209+
// rolled over.
210+
"my.timer.percentile,dt.metrics.source=micrometer,phi=0.5 gauge,0 " + clock.wallTime(),
211+
"my.timer.percentile,dt.metrics.source=micrometer,phi=0.7 gauge,0 " + clock.wallTime(),
212+
"my.timer.percentile,dt.metrics.source=micrometer,phi=0.99 gauge,0 " + clock.wallTime(),
213+
"#my.timer.percentile gauge dt.meta.unit=milliseconds",
214+
215+
// DistributionSummary lines
216+
"my.ds,dt.metrics.source=micrometer gauge,min=100,max=100,sum=100,count=1 "
217+
+ clock.wallTime(),
218+
// DistributionSummary percentile lines. Percentiles are 0
219+
// because the step rolled over.
220+
"my.ds.percentile,dt.metrics.source=micrometer,phi=0.5 gauge,0 " + clock.wallTime(),
221+
"my.ds.percentile,dt.metrics.source=micrometer,phi=0.7 gauge,0 " + clock.wallTime(),
222+
"my.ds.percentile,dt.metrics.source=micrometer,phi=0.99 gauge,0 " + clock.wallTime(),
223+
224+
// LongTaskTimer lines
225+
"my.ltt,dt.metrics.source=micrometer gauge,min=100,max=100,sum=100,count=1 "
226+
+ clock.wallTime(),
227+
"#my.ltt gauge dt.meta.unit=milliseconds",
228+
// LongTaskTimer percentile lines
229+
// 0th percentile is missing because it doesn't clear the
230+
// "interpolatable line" threshold defined in
231+
// DefaultLongTaskTimer#takeSnapshot().
232+
"my.ltt.percentile,dt.metrics.source=micrometer,phi=0.5 gauge,100 " + clock.wallTime(),
233+
"my.ltt.percentile,dt.metrics.source=micrometer,phi=0.7 gauge,100 " + clock.wallTime(),
234+
"my.ltt.percentile,dt.metrics.source=micrometer,phi=0.99 gauge,100 " + clock.wallTime(),
235+
"#my.ltt.percentile gauge dt.meta.unit=milliseconds"))));
236+
}
237+
238+
@Test
239+
void shouldTrackPercentilesWhenDynatraceSummaryInstrumentsNotUsed_shouldExport0PercentileWhenSpecified()
240+
throws Throwable {
241+
DynatraceConfig dynatraceConfig = getNonSummaryInstrumentsConfig();
242+
243+
DynatraceMeterRegistry registry = DynatraceMeterRegistry.builder(dynatraceConfig)
244+
.httpClient(httpClient)
245+
.clock(clock)
246+
.build();
247+
248+
HttpSender.Request.Builder builder = HttpSender.Request.build(config.uri(), httpClient);
249+
when(httpClient.post(config.uri())).thenReturn(builder);
250+
251+
// create instruments with an explicit 0 percentile. This should be exported.
252+
Timer timer = Timer.builder("my.timer").publishPercentiles(0, 0.5, 0.99).register(registry);
253+
DistributionSummary distributionSummary = DistributionSummary.builder("my.ds")
254+
.publishPercentiles(0, 0.5, 0.99)
255+
.register(registry);
256+
// For LongTaskTimer, the 0 percentile is not tracked as it doesn't clear the
257+
// "interpolatable line" threshold defined in DefaultLongTaskTimer#takeSnapshot().
258+
// see shouldTrackPercentilesWhenDynatraceSummaryInstrumentsNotUsed for a test
259+
// that exports LongTaskTimer percentiles
260+
261+
timer.record(Duration.ofMillis(100));
262+
distributionSummary.record(100);
263+
264+
clock.add(dynatraceConfig.step());
265+
266+
registry.publish();
267+
268+
verify(httpClient)
269+
.send(assertArg((request -> assertThat(request.getEntity()).asString()
270+
.hasLineCount(10)
271+
.contains(
272+
// Timer lines
273+
"my.timer,dt.metrics.source=micrometer gauge,min=100,max=100,sum=100,count=1 "
274+
+ clock.wallTime(),
275+
"#my.timer gauge dt.meta.unit=milliseconds",
276+
// Timer percentile lines. Percentiles are 0 because the step
277+
// rolled over.
278+
"my.timer.percentile,dt.metrics.source=micrometer,phi=0 gauge,0 " + clock.wallTime(),
279+
"my.timer.percentile,dt.metrics.source=micrometer,phi=0.5 gauge,0 " + clock.wallTime(),
280+
"my.timer.percentile,dt.metrics.source=micrometer,phi=0.99 gauge,0 " + clock.wallTime(),
281+
"#my.timer.percentile gauge dt.meta.unit=milliseconds",
282+
283+
// DistributionSummary lines
284+
"my.ds,dt.metrics.source=micrometer gauge,min=100,max=100,sum=100,count=1 " + clock.wallTime(),
285+
// DistributionSummary percentile lines. Percentiles are 0 because
286+
// the step rolled over.
287+
"my.ds.percentile,dt.metrics.source=micrometer,phi=0 gauge,0 " + clock.wallTime(),
288+
"my.ds.percentile,dt.metrics.source=micrometer,phi=0.5 gauge,0 " + clock.wallTime(),
289+
"my.ds.percentile,dt.metrics.source=micrometer,phi=0.99 gauge,0 " + clock.wallTime()))));
290+
}
291+
154292
@Test
155293
void shouldNotExportLinesWithZeroCount() throws Throwable {
156294
HttpSender.Request.Builder builder = HttpSender.Request.build(config.uri(), httpClient);
@@ -217,6 +355,35 @@ public DynatraceApiVersion apiVersion() {
217355
};
218356
}
219357

358+
private static DynatraceConfig getNonSummaryInstrumentsConfig() {
359+
return new DynatraceConfig() {
360+
@Override
361+
public String get(String key) {
362+
return null;
363+
}
364+
365+
@Override
366+
public String uri() {
367+
return "http://localhost";
368+
}
369+
370+
@Override
371+
public String apiToken() {
372+
return "apiToken";
373+
}
374+
375+
@Override
376+
public DynatraceApiVersion apiVersion() {
377+
return DynatraceApiVersion.V2;
378+
}
379+
380+
@Override
381+
public boolean useDynatraceSummaryInstruments() {
382+
return false;
383+
}
384+
};
385+
}
386+
220387
private String formatDouble(double value) {
221388
if (value == (long) value) {
222389
return Long.toString((long) value);

0 commit comments

Comments
 (0)