Skip to content

Commit 0e841b6

Browse files
Merge branch '1.12.x'
2 parents 13e6ab0 + a7720f4 commit 0e841b6

File tree

5 files changed

+286
-4
lines changed

5 files changed

+286
-4
lines changed

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

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,7 @@
1717

1818
import io.micrometer.common.util.internal.logging.InternalLogger;
1919
import io.micrometer.common.util.internal.logging.InternalLoggerFactory;
20-
import io.micrometer.core.instrument.Clock;
21-
import io.micrometer.core.instrument.DistributionSummary;
22-
import io.micrometer.core.instrument.Meter;
23-
import io.micrometer.core.instrument.Timer;
20+
import io.micrometer.core.instrument.*;
2421
import io.micrometer.core.instrument.config.MeterFilter;
2522
import io.micrometer.core.instrument.config.MeterFilterReply;
2623
import io.micrometer.core.instrument.distribution.DistributionStatisticConfig;
@@ -30,6 +27,7 @@
3027
import io.micrometer.core.ipc.http.HttpSender;
3128
import io.micrometer.core.ipc.http.HttpUrlConnectionSender;
3229
import io.micrometer.dynatrace.types.DynatraceDistributionSummary;
30+
import io.micrometer.dynatrace.types.DynatraceLongTaskTimer;
3331
import io.micrometer.dynatrace.types.DynatraceTimer;
3432
import io.micrometer.dynatrace.v1.DynatraceExporterV1;
3533
import io.micrometer.dynatrace.v2.DynatraceExporterV2;
@@ -124,6 +122,15 @@ protected Timer newTimer(Meter.Id id, DistributionStatisticConfig distributionSt
124122
return super.newTimer(id, distributionStatisticConfig, pauseDetector);
125123
}
126124

125+
@Override
126+
protected LongTaskTimer newLongTaskTimer(Meter.Id id, DistributionStatisticConfig distributionStatisticConfig) {
127+
if (useDynatraceSummaryInstruments) {
128+
return new DynatraceLongTaskTimer(id, clock, exporter.getBaseTimeUnit(), distributionStatisticConfig,
129+
false);
130+
}
131+
return super.newLongTaskTimer(id, distributionStatisticConfig);
132+
}
133+
127134
/**
128135
* As the micrometer summary statistics (DistributionSummary, and a number of timer
129136
* meter types) do not provide the minimum values that are required by Dynatrace to
@@ -188,6 +195,7 @@ private boolean dynatraceInstrumentTypeExists(Meter.Id id) {
188195
switch (id.getType()) {
189196
case DISTRIBUTION_SUMMARY:
190197
case TIMER:
198+
case LONG_TASK_TIMER:
191199
return true;
192200
default:
193201
return false;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/*
2+
* Copyright 2024 VMware, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.micrometer.dynatrace.types;
17+
18+
import io.micrometer.core.instrument.Clock;
19+
import io.micrometer.core.instrument.distribution.DistributionStatisticConfig;
20+
import io.micrometer.core.instrument.internal.DefaultLongTaskTimer;
21+
22+
import java.util.concurrent.TimeUnit;
23+
24+
/**
25+
* {@code LongTaskTimer} implementation that ensures produced data is consistent for
26+
* exporting to Dynatrace.
27+
*
28+
* @author Georg Pirklbauer
29+
* @since 1.9.18
30+
*/
31+
public final class DynatraceLongTaskTimer extends DefaultLongTaskTimer implements DynatraceSummarySnapshotSupport {
32+
33+
public DynatraceLongTaskTimer(Id id, Clock clock, TimeUnit baseTimeUnit,
34+
DistributionStatisticConfig distributionStatisticConfig, boolean supportsAggregablePercentiles) {
35+
super(id, clock, baseTimeUnit, distributionStatisticConfig, supportsAggregablePercentiles);
36+
}
37+
38+
/**
39+
* @deprecated see {@link DynatraceSummarySnapshotSupport#hasValues()}.
40+
*/
41+
@Override
42+
@Deprecated
43+
public boolean hasValues() {
44+
return activeTasks() > 0;
45+
}
46+
47+
@Override
48+
public DynatraceSummarySnapshot takeSummarySnapshot() {
49+
return takeSummarySnapshot(baseTimeUnit());
50+
}
51+
52+
@Override
53+
public DynatraceSummarySnapshot takeSummarySnapshot(TimeUnit unit) {
54+
if (activeTasks() < 1) {
55+
return DynatraceSummarySnapshot.EMPTY;
56+
}
57+
58+
DynatraceSummary summary = new DynatraceSummary();
59+
// sample.duration(...) will return -1 if the task is already finished
60+
// (only currently active tasks are measured).
61+
// -1 will be ignored in recordNonNegative.
62+
super.forEachActive(sample -> summary.recordNonNegative(sample.duration(unit)));
63+
64+
return summary.takeSummarySnapshot();
65+
}
66+
67+
@Override
68+
public DynatraceSummarySnapshot takeSummarySnapshotAndReset() {
69+
return takeSummarySnapshotAndReset(baseTimeUnit());
70+
}
71+
72+
@Override
73+
public DynatraceSummarySnapshot takeSummarySnapshotAndReset(TimeUnit unit) {
74+
// LongTaskTimer records a snapshot of in-flight operations, e.g.: the number of
75+
// active requests.
76+
// Therefore, the Snapshot needs to be created from scratch during the export.
77+
// In takeSummarySnapshot(TimeUnit) above, the Summary object is deleted at the
78+
// end of the method, therefore effectively resetting the snapshot.
79+
return takeSummarySnapshot(unit);
80+
}
81+
82+
}

implementations/micrometer-registry-dynatrace/src/main/java/io/micrometer/dynatrace/types/DynatraceSummarySnapshot.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
@Immutable
2727
public final class DynatraceSummarySnapshot {
2828

29+
public static final DynatraceSummarySnapshot EMPTY = new DynatraceSummarySnapshot(0, 0, 0, 0);
30+
2931
private final double min;
3032

3133
private final double max;
@@ -57,4 +59,10 @@ public long getCount() {
5759
return count;
5860
}
5961

62+
@Override
63+
public String toString() {
64+
return "DynatraceSummarySnapshot{" + "min=" + min + ", max=" + max + ", total=" + total + ", count=" + count
65+
+ '}';
66+
}
67+
6068
}

implementations/micrometer-registry-dynatrace/src/main/java/io/micrometer/dynatrace/v2/DynatraceExporterV2.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,18 @@ Stream<String> toDistributionSummaryLine(DistributionSummary meter, Map<String,
329329
}
330330

331331
Stream<String> toLongTaskTimerLine(LongTaskTimer meter, Map<String, String> seenMetadata) {
332+
// use Dynatrace Snapshotting to ensure consistent data
333+
if (meter instanceof DynatraceSummarySnapshotSupport) {
334+
DynatraceSummarySnapshot snapshot = ((DynatraceSummarySnapshotSupport) meter)
335+
.takeSummarySnapshot(getBaseTimeUnit());
336+
if (snapshot.getCount() == 0) {
337+
return Stream.empty();
338+
}
339+
return createSummaryLine(meter, seenMetadata, snapshot.getMin(), snapshot.getMax(), snapshot.getTotal(),
340+
snapshot.getCount());
341+
}
342+
343+
// fall back to default implementation if the meter is not DynatraceLongTaskTimer
332344
HistogramSnapshot snapshot = meter.takeSnapshot();
333345

334346
long count = snapshot.count();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
/*
2+
* Copyright 2024 VMware, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.micrometer.dynatrace.types;
17+
18+
import io.micrometer.core.instrument.Meter;
19+
import io.micrometer.core.instrument.MockClock;
20+
import io.micrometer.core.instrument.Tags;
21+
import io.micrometer.core.instrument.distribution.DistributionStatisticConfig;
22+
import org.assertj.core.data.Offset;
23+
import org.junit.jupiter.api.Test;
24+
25+
import java.time.Duration;
26+
import java.util.concurrent.CountDownLatch;
27+
import java.util.concurrent.ExecutorService;
28+
import java.util.concurrent.Executors;
29+
import java.util.concurrent.TimeUnit;
30+
31+
import static org.assertj.core.api.Assertions.assertThat;
32+
33+
/**
34+
* Tests for {@link DynatraceLongTaskTimer}.
35+
*/
36+
class DynatraceLongTaskTimerTest {
37+
38+
private static final Offset<Double> OFFSET = Offset.offset(0.0001);
39+
40+
private static final Meter.Id ID = new Meter.Id("test.id", Tags.empty(), "1", "desc",
41+
Meter.Type.DISTRIBUTION_SUMMARY);
42+
43+
private static final DistributionStatisticConfig DISTRIBUTION_STATISTIC_CONFIG = DistributionStatisticConfig.NONE;
44+
45+
private static final MockClock CLOCK = new MockClock();
46+
47+
private static final Offset<Double> TOLERANCE = Offset.offset(0.000001);
48+
49+
@Test
50+
void singleTaskValuesAreRecorded() throws InterruptedException {
51+
DynatraceLongTaskTimer ltt = new DynatraceLongTaskTimer(ID, CLOCK, TimeUnit.MILLISECONDS,
52+
DISTRIBUTION_STATISTIC_CONFIG, false);
53+
ExecutorService executorService = Executors.newSingleThreadExecutor();
54+
55+
CountDownLatch taskHasBeenRunningLatch = new CountDownLatch(1);
56+
CountDownLatch stopLatch = new CountDownLatch(1);
57+
58+
executorService.submit(() -> ltt.record(() -> {
59+
CLOCK.add(Duration.ofMillis(100));
60+
taskHasBeenRunningLatch.countDown();
61+
62+
try {
63+
// wait until the snapshot has been taken
64+
assertThat(stopLatch.await(300, TimeUnit.MILLISECONDS)).isTrue();
65+
}
66+
catch (InterruptedException e) {
67+
throw new RuntimeException(e);
68+
}
69+
}));
70+
71+
assertThat(taskHasBeenRunningLatch.await(300, TimeUnit.MILLISECONDS)).isTrue();
72+
73+
DynatraceSummarySnapshot snapshot = ltt.takeSummarySnapshot();
74+
// can release the background task
75+
stopLatch.countDown();
76+
77+
assertThat(snapshot.getMin()).isCloseTo(100, TOLERANCE);
78+
assertThat(snapshot.getMax()).isCloseTo(100, TOLERANCE);
79+
assertThat(snapshot.getCount()).isEqualTo(1);
80+
// in the case of count == 1, the total has to be equal to min and max
81+
assertThat(snapshot.getTotal()).isGreaterThan(0)
82+
.isCloseTo(snapshot.getMin(), TOLERANCE)
83+
.isCloseTo(snapshot.getMax(), TOLERANCE);
84+
}
85+
86+
@Test
87+
void parallelTasksValuesAreRecorded() throws InterruptedException {
88+
DynatraceLongTaskTimer ltt = new DynatraceLongTaskTimer(ID, CLOCK, TimeUnit.MILLISECONDS,
89+
DISTRIBUTION_STATISTIC_CONFIG, false);
90+
ExecutorService executorService = Executors.newFixedThreadPool(2);
91+
92+
CountDownLatch firstTaskHasBeenRunning = new CountDownLatch(1);
93+
// both tasks need to be running for a while before we take the snapshot
94+
CountDownLatch bothTasksHaveBeenRunningLatch = new CountDownLatch(2);
95+
CountDownLatch stopLatch = new CountDownLatch(1);
96+
97+
// Task 1
98+
executorService.submit(() -> ltt.record(() -> {
99+
try {
100+
CLOCK.add(Duration.ofMillis(40));
101+
102+
// task 1 starts first, runs for 40ms (see CLOCK.add(Duration) above),
103+
// then the second task starts. The second task can start after this
104+
// latch has counted down (unblocked) the other thread.
105+
firstTaskHasBeenRunning.countDown();
106+
bothTasksHaveBeenRunningLatch.countDown();
107+
108+
// wait until the snapshot has been taken
109+
assertThat(stopLatch.await(300, TimeUnit.MILLISECONDS)).isTrue();
110+
}
111+
catch (InterruptedException e) {
112+
throw new RuntimeException(e);
113+
}
114+
}));
115+
116+
// Task 1 (see above) has been running for 40ms, and is still running now (until
117+
// stopLatch is unblocked).
118+
assertThat(firstTaskHasBeenRunning.await(300, TimeUnit.MILLISECONDS)).isTrue();
119+
120+
// Task 2
121+
executorService.submit(() -> ltt.record(() -> {
122+
try {
123+
// At this point both tasks are running.
124+
// Adding to the clock here means that both tasks are running at the
125+
// same time and adding 30ms adds 30ms to both running task.
126+
CLOCK.add(Duration.ofMillis(30));
127+
128+
// Release the latch: this means that both tasks
129+
// have been running and the time has been added to the clock. Both
130+
// tasks will (conceptually) continue to run until the stopLatch is
131+
// unblocked.
132+
bothTasksHaveBeenRunningLatch.countDown();
133+
134+
// wait until the snapshot has been taken
135+
assertThat(stopLatch.await(300, TimeUnit.MILLISECONDS)).isTrue();
136+
}
137+
catch (InterruptedException e) {
138+
throw new RuntimeException(e);
139+
}
140+
}));
141+
142+
// both tasks have been running for different lengths of time (task 1 for a total
143+
// of 70ms (40+30ms), and task 2 for a total of 30ms).
144+
assertThat(bothTasksHaveBeenRunningLatch.await(300, TimeUnit.MILLISECONDS)).isTrue();
145+
146+
// take a snapshot of the state where both tasks are running for different lengths
147+
// of time.
148+
DynatraceSummarySnapshot snapshot = ltt.takeSummarySnapshot();
149+
150+
// the two running tasks are now allowed to exit.
151+
stopLatch.countDown();
152+
153+
// Task 1 has been "running" for 70ms at the time of recording and will
154+
// supply the max
155+
assertThat(snapshot.getMax()).isCloseTo(70, OFFSET);
156+
// Task 2 has been "running" for only 30ms at the time of recording and
157+
// will supply the min
158+
assertThat(snapshot.getMin()).isCloseTo(30, OFFSET);
159+
// Both tasks have been running in parallel.
160+
// After the second CLOCK.add(Duration) is called, the first task has been running
161+
// for 70ms, and the second task has been running for 30ms
162+
// together, they have been running for 100ms in total.
163+
assertThat(snapshot.getTotal()).isCloseTo(100, OFFSET);
164+
// Two tasks were running in parallel.
165+
assertThat(snapshot.getCount()).isEqualTo(2);
166+
// On the clock, 70ms have passed. MockClock starts at 1, that's why the result
167+
// here
168+
// is 71 instead of 70.
169+
assertThat(CLOCK.wallTime()).isEqualTo(71);
170+
}
171+
172+
}

0 commit comments

Comments
 (0)