Skip to content

Commit 9c3b760

Browse files
Map time units to UCUM format for Dynatrace (#5589)
Closes gh-5588 Co-authored-by: Georg P <[email protected]>
1 parent d7daaef commit 9c3b760

File tree

3 files changed

+87
-17
lines changed

3 files changed

+87
-17
lines changed

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

+39-4
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,11 @@
1515
*/
1616
package io.micrometer.dynatrace.v2;
1717

18-
import com.dynatrace.metric.util.*;
18+
import com.dynatrace.metric.util.DynatraceMetricApiConstants;
19+
import com.dynatrace.metric.util.MetricException;
20+
import com.dynatrace.metric.util.MetricLineBuilder;
1921
import com.dynatrace.metric.util.MetricLineBuilder.MetadataStep;
22+
import com.dynatrace.metric.util.MetricLinePreConfiguration;
2023
import io.micrometer.common.lang.NonNull;
2124
import io.micrometer.common.util.StringUtils;
2225
import io.micrometer.common.util.internal.logging.InternalLogger;
@@ -60,9 +63,11 @@ public final class DynatraceExporterV2 extends AbstractDynatraceExporter {
6063

6164
private static final Pattern IS_NULL_ERROR_RESPONSE = Pattern.compile("\"error\":\\s?null");
6265

63-
private static final Map<String, String> staticDimensions = Collections.singletonMap("dt.metrics.source",
66+
private static final Map<String, String> STATIC_DIMENSIONS = Collections.singletonMap("dt.metrics.source",
6467
"micrometer");
6568

69+
private static final Map<String, String> UCUM_TIME_UNIT_MAP = ucumTimeUnitMap();
70+
6671
// Loggers must be non-static for MockLoggerFactory.injectLogger() in tests.
6772
private final InternalLogger logger = InternalLoggerFactory.getInstance(DynatraceExporterV2.class);
6873

@@ -128,7 +133,7 @@ private boolean shouldIgnoreToken(DynatraceConfig config) {
128133

129134
private Map<String, String> enrichWithMetricsSourceDimensions(Map<String, String> defaultDimensions) {
130135
LinkedHashMap<String, String> orderedDimensions = new LinkedHashMap<>(defaultDimensions);
131-
orderedDimensions.putAll(staticDimensions);
136+
orderedDimensions.putAll(STATIC_DIMENSIONS);
132137
return orderedDimensions;
133138
}
134139

@@ -479,7 +484,8 @@ private boolean shouldExportMetadata(Meter.Id id) {
479484
}
480485

481486
private MetricLineBuilder.MetadataStep enrichMetadata(MetricLineBuilder.MetadataStep metadataStep, Meter meter) {
482-
return metadataStep.description(meter.getId().getDescription()).unit(meter.getId().getBaseUnit());
487+
return metadataStep.description(meter.getId().getDescription())
488+
.unit(mapUnitIfNeeded(meter.getId().getBaseUnit()));
483489
}
484490

485491
/**
@@ -547,4 +553,33 @@ private String extractMetricKey(String metadataLine) {
547553
return metricKey.toString();
548554
}
549555

556+
/**
557+
* Maps a unit string to a UCUM-compliant string, if the mapping is known, see:
558+
* {@link #ucumTimeUnitMap()}.
559+
* @param unit the unit that might be mapped
560+
* @return The UCUM-compliant string if known, otherwise returns the original unit
561+
*/
562+
private static String mapUnitIfNeeded(String unit) {
563+
return unit != null && UCUM_TIME_UNIT_MAP.containsKey(unit) ? UCUM_TIME_UNIT_MAP.get(unit) : unit;
564+
}
565+
566+
/**
567+
* Mapping from OpenJDK's {@link TimeUnit#toString()} and other common time unit
568+
* formats to UCUM-compliant format, see: <a href="https://ucum.org/">ucum.org</a>.
569+
* @return Time unit mapping to UCUM-compliant format
570+
*/
571+
private static Map<String, String> ucumTimeUnitMap() {
572+
Map<String, String> mapping = new HashMap<>();
573+
mapping.put("nanoseconds", "ns");
574+
mapping.put("nanosecond", "ns");
575+
mapping.put("microseconds", "us");
576+
mapping.put("microsecond", "us");
577+
mapping.put("milliseconds", "ms");
578+
mapping.put("millisecond", "ms");
579+
mapping.put("seconds", "s");
580+
mapping.put("second", "s");
581+
582+
return Collections.unmodifiableMap(mapping);
583+
}
584+
550585
}

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

+12-12
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ void shouldSendProperRequest() throws Throwable {
9090
.containsExactly("my.counter,dt.metrics.source=micrometer count,delta=12 " + clock.wallTime(),
9191
"my.timer,dt.metrics.source=micrometer gauge,min=12,max=42,sum=108,count=4 " + clock.wallTime(),
9292
"my.gauge,dt.metrics.source=micrometer gauge," + formatDouble(gauge) + " " + clock.wallTime(),
93-
"#my.timer gauge dt.meta.unit=milliseconds");
93+
"#my.timer gauge dt.meta.unit=ms");
9494
})));
9595
}
9696

@@ -115,7 +115,7 @@ void shouldResetBetweenRequests() throws Throwable {
115115
assertThat(request.getEntity()).asString()
116116
.hasLineCount(2)
117117
.contains("my.timer,dt.metrics.source=micrometer gauge,min=22,max=50,sum=72,count=2 " + clock.wallTime(),
118-
"#my.timer gauge dt.meta.unit=milliseconds");
118+
"#my.timer gauge dt.meta.unit=ms");
119119

120120
// both are bigger than the previous min and smaller than the previous max. They
121121
// will only show up if the
@@ -133,7 +133,7 @@ void shouldResetBetweenRequests() throws Throwable {
133133
assertThat(request2.getEntity()).asString()
134134
.hasLineCount(2)
135135
.contains("my.timer,dt.metrics.source=micrometer gauge,min=33,max=44,sum=77,count=2 " + clock.wallTime(),
136-
"#my.timer gauge dt.meta.unit=milliseconds");
136+
"#my.timer gauge dt.meta.unit=ms");
137137
}
138138

139139
@Test
@@ -150,7 +150,7 @@ void shouldNotTrackPercentilesWithDynatraceSummary() throws Throwable {
150150
verify(httpClient).send(assertArg((request -> assertThat(request.getEntity()).asString()
151151
.hasLineCount(2)
152152
.contains("my.timer,dt.metrics.source=micrometer gauge,min=22,max=55,sum=77,count=2 " + clock.wallTime(),
153-
"#my.timer gauge dt.meta.unit=milliseconds"))));
153+
"#my.timer gauge dt.meta.unit=ms"))));
154154
}
155155

156156
@Test
@@ -204,13 +204,13 @@ void shouldTrackPercentilesWhenDynatraceSummaryInstrumentsNotUsed() throws Throw
204204
// Timer lines
205205
"my.timer,dt.metrics.source=micrometer gauge,min=100,max=100,sum=100,count=1 "
206206
+ clock.wallTime(),
207-
"#my.timer gauge dt.meta.unit=milliseconds",
207+
"#my.timer gauge dt.meta.unit=ms",
208208
// Timer percentile lines. Percentiles are 0 because the step
209209
// rolled over.
210210
"my.timer.percentile,dt.metrics.source=micrometer,phi=0.5 gauge,0 " + clock.wallTime(),
211211
"my.timer.percentile,dt.metrics.source=micrometer,phi=0.7 gauge,0 " + clock.wallTime(),
212212
"my.timer.percentile,dt.metrics.source=micrometer,phi=0.99 gauge,0 " + clock.wallTime(),
213-
"#my.timer.percentile gauge dt.meta.unit=milliseconds",
213+
"#my.timer.percentile gauge dt.meta.unit=ms",
214214

215215
// DistributionSummary lines
216216
"my.ds,dt.metrics.source=micrometer gauge,min=100,max=100,sum=100,count=1 "
@@ -224,15 +224,15 @@ void shouldTrackPercentilesWhenDynatraceSummaryInstrumentsNotUsed() throws Throw
224224
// LongTaskTimer lines
225225
"my.ltt,dt.metrics.source=micrometer gauge,min=100,max=100,sum=100,count=1 "
226226
+ clock.wallTime(),
227-
"#my.ltt gauge dt.meta.unit=milliseconds",
227+
"#my.ltt gauge dt.meta.unit=ms",
228228
// LongTaskTimer percentile lines
229229
// 0th percentile is missing because it doesn't clear the
230230
// "interpolatable line" threshold defined in
231231
// DefaultLongTaskTimer#takeSnapshot().
232232
"my.ltt.percentile,dt.metrics.source=micrometer,phi=0.5 gauge,100 " + clock.wallTime(),
233233
"my.ltt.percentile,dt.metrics.source=micrometer,phi=0.7 gauge,100 " + clock.wallTime(),
234234
"my.ltt.percentile,dt.metrics.source=micrometer,phi=0.99 gauge,100 " + clock.wallTime(),
235-
"#my.ltt.percentile gauge dt.meta.unit=milliseconds"))));
235+
"#my.ltt.percentile gauge dt.meta.unit=ms"))));
236236
}
237237

238238
@Test
@@ -272,13 +272,13 @@ void shouldTrackPercentilesWhenDynatraceSummaryInstrumentsNotUsed_shouldExport0P
272272
// Timer lines
273273
"my.timer,dt.metrics.source=micrometer gauge,min=100,max=100,sum=100,count=1 "
274274
+ clock.wallTime(),
275-
"#my.timer gauge dt.meta.unit=milliseconds",
275+
"#my.timer gauge dt.meta.unit=ms",
276276
// Timer percentile lines. Percentiles are 0 because the step
277277
// rolled over.
278278
"my.timer.percentile,dt.metrics.source=micrometer,phi=0 gauge,0 " + clock.wallTime(),
279279
"my.timer.percentile,dt.metrics.source=micrometer,phi=0.5 gauge,0 " + clock.wallTime(),
280280
"my.timer.percentile,dt.metrics.source=micrometer,phi=0.99 gauge,0 " + clock.wallTime(),
281-
"#my.timer.percentile gauge dt.meta.unit=milliseconds",
281+
"#my.timer.percentile gauge dt.meta.unit=ms",
282282

283283
// DistributionSummary lines
284284
"my.ds,dt.metrics.source=micrometer gauge,min=100,max=100,sum=100,count=1 " + clock.wallTime(),
@@ -303,7 +303,7 @@ void shouldNotExportLinesWithZeroCount() throws Throwable {
303303
verify(httpClient).send(assertArg(request -> assertThat(request.getEntity()).asString()
304304
.hasLineCount(2)
305305
.contains("my.timer,dt.metrics.source=micrometer gauge,min=44,max=44,sum=44,count=1 " + clock.wallTime(),
306-
"#my.timer gauge dt.meta.unit=milliseconds")));
306+
"#my.timer gauge dt.meta.unit=ms")));
307307

308308
// reset for next export interval
309309
reset(httpClient);
@@ -328,7 +328,7 @@ void shouldNotExportLinesWithZeroCount() throws Throwable {
328328
verify(httpClient).send(assertArg(request -> assertThat(request.getEntity()).asString()
329329
.hasLineCount(2)
330330
.contains("my.timer,dt.metrics.source=micrometer gauge,min=33,max=33,sum=33,count=1 " + clock.wallTime(),
331-
"#my.timer gauge dt.meta.unit=milliseconds")));
331+
"#my.timer gauge dt.meta.unit=ms")));
332332
}
333333

334334
private DynatraceConfig createDefaultDynatraceConfig() {

implementations/micrometer-registry-dynatrace/src/test/java/io/micrometer/dynatrace/v2/DynatraceExporterV2Test.java

+36-1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import io.micrometer.common.util.internal.logging.MockLogger;
2222
import io.micrometer.common.util.internal.logging.MockLoggerFactory;
2323
import io.micrometer.core.Issue;
24+
import io.micrometer.core.instrument.LongTaskTimer.Sample;
2425
import io.micrometer.core.instrument.Timer;
2526
import io.micrometer.core.instrument.*;
2627
import io.micrometer.core.ipc.http.HttpSender;
@@ -661,7 +662,7 @@ void shouldSendHeadersAndBody() throws Throwable {
661662
.containsSubsequence("my.counter,dt.metrics.source=micrometer count,delta=12 " + clock.wallTime(),
662663
"my.gauge,dt.metrics.source=micrometer gauge,42 " + clock.wallTime(),
663664
"my.timer,dt.metrics.source=micrometer gauge,min=22,max=22,sum=22,count=1 " + clock.wallTime(),
664-
"#my.timer gauge dt.meta.unit=milliseconds");
665+
"#my.timer gauge dt.meta.unit=ms");
665666
}));
666667
}
667668

@@ -866,6 +867,40 @@ void shouldAddMetadataOnlyWhenUnitOrDescriptionIsPresent() {
866867
"#gauge.du gauge dt.meta.description=temperature,dt.meta.unit=kelvin")));
867868
}
868869

870+
@Test
871+
void shouldHaveUcumCompliantUnits() {
872+
HttpSender.Request.Builder builder = spy(HttpSender.Request.build(config.uri(), httpClient));
873+
when(httpClient.post(anyString())).thenReturn(builder);
874+
875+
meterRegistry.timer("test.timer").record(Duration.ofMillis(12));
876+
meterRegistry.more().timeGauge("test.tg", Tags.empty(), this, TimeUnit.MICROSECONDS, x -> 1_000);
877+
FunctionTimer.builder("test.ft", this, x -> 1, x -> 100, MILLISECONDS).register(meterRegistry);
878+
Counter.builder("test.second").baseUnit("second").register(meterRegistry).increment(100);
879+
Counter.builder("test.seconds").baseUnit("seconds").register(meterRegistry).increment(10);
880+
FunctionCounter.builder("process.cpu.time", this, x -> 1_000_000).baseUnit("ns").register(meterRegistry);
881+
882+
Sample sample = meterRegistry.more().longTaskTimer("test.ltt").start();
883+
clock.add(config.step().plus(Duration.ofSeconds(2)));
884+
885+
exporter.export(meterRegistry.getMeters());
886+
sample.stop();
887+
888+
verify(builder).withPlainText(assertArg(body -> assertThat(body.split("\n")).containsExactlyInAnyOrder(
889+
"test.timer,dt.metrics.source=micrometer gauge,min=12,max=12,sum=12,count=1 " + clock.wallTime(),
890+
"#test.timer gauge dt.meta.unit=ms", "test.tg,dt.metrics.source=micrometer gauge,1 " + clock.wallTime(),
891+
"#test.tg gauge dt.meta.unit=ms",
892+
"test.ft,dt.metrics.source=micrometer gauge,min=100,max=100,sum=100,count=1 " + clock.wallTime(),
893+
"#test.ft gauge dt.meta.unit=ms",
894+
"test.second,dt.metrics.source=micrometer count,delta=100 " + clock.wallTime(),
895+
"#test.second count dt.meta.unit=s",
896+
"test.seconds,dt.metrics.source=micrometer count,delta=10 " + clock.wallTime(),
897+
"#test.seconds count dt.meta.unit=s",
898+
"process.cpu.time,dt.metrics.source=micrometer count,delta=1000000 " + clock.wallTime(),
899+
"#process.cpu.time count dt.meta.unit=ns",
900+
"test.ltt,dt.metrics.source=micrometer gauge,min=62000,max=62000,sum=62000,count=1 " + clock.wallTime(),
901+
"#test.ltt gauge dt.meta.unit=ms")));
902+
}
903+
869904
@Test
870905
void sendsTwoRequestsWhenSizeLimitIsReachedWithMetadata() {
871906
HttpSender.Request.Builder firstReq = spy(HttpSender.Request.build(config.uri(), httpClient));

0 commit comments

Comments
 (0)