From 474ce229699be6a6c69ca1c28554f90587347058 Mon Sep 17 00:00:00 2001 From: Jonatan Ivanov Date: Fri, 11 Oct 2024 17:07:10 -0700 Subject: [PATCH] Map time units to UCUM format for Dynatrace Closes gh-5588 Co-authored-by: Georg P Co-authored-by: Jonatan Ivanov --- .../dynatrace/v2/DynatraceExporterV2.java | 43 +++++++++++++++++-- .../dynatrace/DynatraceMeterRegistryTest.java | 24 +++++------ .../dynatrace/v2/DynatraceExporterV2Test.java | 37 +++++++++++++++- 3 files changed, 87 insertions(+), 17 deletions(-) diff --git a/implementations/micrometer-registry-dynatrace/src/main/java/io/micrometer/dynatrace/v2/DynatraceExporterV2.java b/implementations/micrometer-registry-dynatrace/src/main/java/io/micrometer/dynatrace/v2/DynatraceExporterV2.java index a9c889dc60..a76caecfdc 100644 --- a/implementations/micrometer-registry-dynatrace/src/main/java/io/micrometer/dynatrace/v2/DynatraceExporterV2.java +++ b/implementations/micrometer-registry-dynatrace/src/main/java/io/micrometer/dynatrace/v2/DynatraceExporterV2.java @@ -15,8 +15,11 @@ */ package io.micrometer.dynatrace.v2; -import com.dynatrace.metric.util.*; +import com.dynatrace.metric.util.DynatraceMetricApiConstants; +import com.dynatrace.metric.util.MetricException; +import com.dynatrace.metric.util.MetricLineBuilder; import com.dynatrace.metric.util.MetricLineBuilder.MetadataStep; +import com.dynatrace.metric.util.MetricLinePreConfiguration; import io.micrometer.common.lang.NonNull; import io.micrometer.common.util.StringUtils; import io.micrometer.common.util.internal.logging.InternalLogger; @@ -60,9 +63,11 @@ public final class DynatraceExporterV2 extends AbstractDynatraceExporter { private static final Pattern IS_NULL_ERROR_RESPONSE = Pattern.compile("\"error\":\\s?null"); - private static final Map staticDimensions = Collections.singletonMap("dt.metrics.source", + private static final Map STATIC_DIMENSIONS = Collections.singletonMap("dt.metrics.source", "micrometer"); + private static final Map UCUM_TIME_UNIT_MAP = ucumTimeUnitMap(); + // Loggers must be non-static for MockLoggerFactory.injectLogger() in tests. private final InternalLogger logger = InternalLoggerFactory.getInstance(DynatraceExporterV2.class); @@ -128,7 +133,7 @@ private boolean shouldIgnoreToken(DynatraceConfig config) { private Map enrichWithMetricsSourceDimensions(Map defaultDimensions) { LinkedHashMap orderedDimensions = new LinkedHashMap<>(defaultDimensions); - orderedDimensions.putAll(staticDimensions); + orderedDimensions.putAll(STATIC_DIMENSIONS); return orderedDimensions; } @@ -479,7 +484,8 @@ private boolean shouldExportMetadata(Meter.Id id) { } private MetricLineBuilder.MetadataStep enrichMetadata(MetricLineBuilder.MetadataStep metadataStep, Meter meter) { - return metadataStep.description(meter.getId().getDescription()).unit(meter.getId().getBaseUnit()); + return metadataStep.description(meter.getId().getDescription()) + .unit(mapUnitIfNeeded(meter.getId().getBaseUnit())); } /** @@ -547,4 +553,33 @@ private String extractMetricKey(String metadataLine) { return metricKey.toString(); } + /** + * Maps a unit string to a UCUM-compliant string, if the mapping is known, see: + * {@link #ucumTimeUnitMap()}. + * @param unit the unit that might be mapped + * @return The UCUM-compliant string if known, otherwise returns the original unit + */ + private static String mapUnitIfNeeded(String unit) { + return unit != null && UCUM_TIME_UNIT_MAP.containsKey(unit) ? UCUM_TIME_UNIT_MAP.get(unit) : unit; + } + + /** + * Mapping from OpenJDK's {@link TimeUnit#toString()} and other common time unit + * formats to UCUM-compliant format, see: ucum.org. + * @return Time unit mapping to UCUM-compliant format + */ + private static Map ucumTimeUnitMap() { + Map mapping = new HashMap<>(); + mapping.put("nanoseconds", "ns"); + mapping.put("nanosecond", "ns"); + mapping.put("microseconds", "us"); + mapping.put("microsecond", "us"); + mapping.put("milliseconds", "ms"); + mapping.put("millisecond", "ms"); + mapping.put("seconds", "s"); + mapping.put("second", "s"); + + return Collections.unmodifiableMap(mapping); + } + } diff --git a/implementations/micrometer-registry-dynatrace/src/test/java/io/micrometer/dynatrace/DynatraceMeterRegistryTest.java b/implementations/micrometer-registry-dynatrace/src/test/java/io/micrometer/dynatrace/DynatraceMeterRegistryTest.java index 3837885cf9..a6c8d09cb0 100644 --- a/implementations/micrometer-registry-dynatrace/src/test/java/io/micrometer/dynatrace/DynatraceMeterRegistryTest.java +++ b/implementations/micrometer-registry-dynatrace/src/test/java/io/micrometer/dynatrace/DynatraceMeterRegistryTest.java @@ -90,7 +90,7 @@ void shouldSendProperRequest() throws Throwable { .containsExactly("my.counter,dt.metrics.source=micrometer count,delta=12 " + clock.wallTime(), "my.timer,dt.metrics.source=micrometer gauge,min=12,max=42,sum=108,count=4 " + clock.wallTime(), "my.gauge,dt.metrics.source=micrometer gauge," + formatDouble(gauge) + " " + clock.wallTime(), - "#my.timer gauge dt.meta.unit=milliseconds"); + "#my.timer gauge dt.meta.unit=ms"); }))); } @@ -115,7 +115,7 @@ void shouldResetBetweenRequests() throws Throwable { assertThat(request.getEntity()).asString() .hasLineCount(2) .contains("my.timer,dt.metrics.source=micrometer gauge,min=22,max=50,sum=72,count=2 " + clock.wallTime(), - "#my.timer gauge dt.meta.unit=milliseconds"); + "#my.timer gauge dt.meta.unit=ms"); // both are bigger than the previous min and smaller than the previous max. They // will only show up if the @@ -133,7 +133,7 @@ void shouldResetBetweenRequests() throws Throwable { assertThat(request2.getEntity()).asString() .hasLineCount(2) .contains("my.timer,dt.metrics.source=micrometer gauge,min=33,max=44,sum=77,count=2 " + clock.wallTime(), - "#my.timer gauge dt.meta.unit=milliseconds"); + "#my.timer gauge dt.meta.unit=ms"); } @Test @@ -150,7 +150,7 @@ void shouldNotTrackPercentilesWithDynatraceSummary() throws Throwable { verify(httpClient).send(assertArg((request -> assertThat(request.getEntity()).asString() .hasLineCount(2) .contains("my.timer,dt.metrics.source=micrometer gauge,min=22,max=55,sum=77,count=2 " + clock.wallTime(), - "#my.timer gauge dt.meta.unit=milliseconds")))); + "#my.timer gauge dt.meta.unit=ms")))); } @Test @@ -204,13 +204,13 @@ void shouldTrackPercentilesWhenDynatraceSummaryInstrumentsNotUsed() throws Throw // Timer lines "my.timer,dt.metrics.source=micrometer gauge,min=100,max=100,sum=100,count=1 " + clock.wallTime(), - "#my.timer gauge dt.meta.unit=milliseconds", + "#my.timer gauge dt.meta.unit=ms", // Timer percentile lines. Percentiles are 0 because the step // rolled over. "my.timer.percentile,dt.metrics.source=micrometer,phi=0.5 gauge,0 " + clock.wallTime(), "my.timer.percentile,dt.metrics.source=micrometer,phi=0.7 gauge,0 " + clock.wallTime(), "my.timer.percentile,dt.metrics.source=micrometer,phi=0.99 gauge,0 " + clock.wallTime(), - "#my.timer.percentile gauge dt.meta.unit=milliseconds", + "#my.timer.percentile gauge dt.meta.unit=ms", // DistributionSummary lines "my.ds,dt.metrics.source=micrometer gauge,min=100,max=100,sum=100,count=1 " @@ -224,7 +224,7 @@ void shouldTrackPercentilesWhenDynatraceSummaryInstrumentsNotUsed() throws Throw // LongTaskTimer lines "my.ltt,dt.metrics.source=micrometer gauge,min=100,max=100,sum=100,count=1 " + clock.wallTime(), - "#my.ltt gauge dt.meta.unit=milliseconds", + "#my.ltt gauge dt.meta.unit=ms", // LongTaskTimer percentile lines // 0th percentile is missing because it doesn't clear the // "interpolatable line" threshold defined in @@ -232,7 +232,7 @@ void shouldTrackPercentilesWhenDynatraceSummaryInstrumentsNotUsed() throws Throw "my.ltt.percentile,dt.metrics.source=micrometer,phi=0.5 gauge,100 " + clock.wallTime(), "my.ltt.percentile,dt.metrics.source=micrometer,phi=0.7 gauge,100 " + clock.wallTime(), "my.ltt.percentile,dt.metrics.source=micrometer,phi=0.99 gauge,100 " + clock.wallTime(), - "#my.ltt.percentile gauge dt.meta.unit=milliseconds")))); + "#my.ltt.percentile gauge dt.meta.unit=ms")))); } @Test @@ -272,13 +272,13 @@ void shouldTrackPercentilesWhenDynatraceSummaryInstrumentsNotUsed_shouldExport0P // Timer lines "my.timer,dt.metrics.source=micrometer gauge,min=100,max=100,sum=100,count=1 " + clock.wallTime(), - "#my.timer gauge dt.meta.unit=milliseconds", + "#my.timer gauge dt.meta.unit=ms", // Timer percentile lines. Percentiles are 0 because the step // rolled over. "my.timer.percentile,dt.metrics.source=micrometer,phi=0 gauge,0 " + clock.wallTime(), "my.timer.percentile,dt.metrics.source=micrometer,phi=0.5 gauge,0 " + clock.wallTime(), "my.timer.percentile,dt.metrics.source=micrometer,phi=0.99 gauge,0 " + clock.wallTime(), - "#my.timer.percentile gauge dt.meta.unit=milliseconds", + "#my.timer.percentile gauge dt.meta.unit=ms", // DistributionSummary lines "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 { verify(httpClient).send(assertArg(request -> assertThat(request.getEntity()).asString() .hasLineCount(2) .contains("my.timer,dt.metrics.source=micrometer gauge,min=44,max=44,sum=44,count=1 " + clock.wallTime(), - "#my.timer gauge dt.meta.unit=milliseconds"))); + "#my.timer gauge dt.meta.unit=ms"))); // reset for next export interval reset(httpClient); @@ -328,7 +328,7 @@ void shouldNotExportLinesWithZeroCount() throws Throwable { verify(httpClient).send(assertArg(request -> assertThat(request.getEntity()).asString() .hasLineCount(2) .contains("my.timer,dt.metrics.source=micrometer gauge,min=33,max=33,sum=33,count=1 " + clock.wallTime(), - "#my.timer gauge dt.meta.unit=milliseconds"))); + "#my.timer gauge dt.meta.unit=ms"))); } private DynatraceConfig createDefaultDynatraceConfig() { diff --git a/implementations/micrometer-registry-dynatrace/src/test/java/io/micrometer/dynatrace/v2/DynatraceExporterV2Test.java b/implementations/micrometer-registry-dynatrace/src/test/java/io/micrometer/dynatrace/v2/DynatraceExporterV2Test.java index d51cd14c71..bde0e2ea11 100644 --- a/implementations/micrometer-registry-dynatrace/src/test/java/io/micrometer/dynatrace/v2/DynatraceExporterV2Test.java +++ b/implementations/micrometer-registry-dynatrace/src/test/java/io/micrometer/dynatrace/v2/DynatraceExporterV2Test.java @@ -21,6 +21,7 @@ import io.micrometer.common.util.internal.logging.MockLogger; import io.micrometer.common.util.internal.logging.MockLoggerFactory; import io.micrometer.core.Issue; +import io.micrometer.core.instrument.LongTaskTimer.Sample; import io.micrometer.core.instrument.Timer; import io.micrometer.core.instrument.*; import io.micrometer.core.ipc.http.HttpSender; @@ -661,7 +662,7 @@ void shouldSendHeadersAndBody() throws Throwable { .containsSubsequence("my.counter,dt.metrics.source=micrometer count,delta=12 " + clock.wallTime(), "my.gauge,dt.metrics.source=micrometer gauge,42 " + clock.wallTime(), "my.timer,dt.metrics.source=micrometer gauge,min=22,max=22,sum=22,count=1 " + clock.wallTime(), - "#my.timer gauge dt.meta.unit=milliseconds"); + "#my.timer gauge dt.meta.unit=ms"); })); } @@ -866,6 +867,40 @@ void shouldAddMetadataOnlyWhenUnitOrDescriptionIsPresent() { "#gauge.du gauge dt.meta.description=temperature,dt.meta.unit=kelvin"))); } + @Test + void shouldHaveUcumCompliantUnits() { + HttpSender.Request.Builder builder = spy(HttpSender.Request.build(config.uri(), httpClient)); + when(httpClient.post(anyString())).thenReturn(builder); + + meterRegistry.timer("test.timer").record(Duration.ofMillis(12)); + meterRegistry.more().timeGauge("test.tg", Tags.empty(), this, TimeUnit.MICROSECONDS, x -> 1_000); + FunctionTimer.builder("test.ft", this, x -> 1, x -> 100, MILLISECONDS).register(meterRegistry); + Counter.builder("test.second").baseUnit("second").register(meterRegistry).increment(100); + Counter.builder("test.seconds").baseUnit("seconds").register(meterRegistry).increment(10); + FunctionCounter.builder("process.cpu.time", this, x -> 1_000_000).baseUnit("ns").register(meterRegistry); + + Sample sample = meterRegistry.more().longTaskTimer("test.ltt").start(); + clock.add(config.step().plus(Duration.ofSeconds(2))); + + exporter.export(meterRegistry.getMeters()); + sample.stop(); + + verify(builder).withPlainText(assertArg(body -> assertThat(body.split("\n")).containsExactlyInAnyOrder( + "test.timer,dt.metrics.source=micrometer gauge,min=12,max=12,sum=12,count=1 " + clock.wallTime(), + "#test.timer gauge dt.meta.unit=ms", "test.tg,dt.metrics.source=micrometer gauge,1 " + clock.wallTime(), + "#test.tg gauge dt.meta.unit=ms", + "test.ft,dt.metrics.source=micrometer gauge,min=100,max=100,sum=100,count=1 " + clock.wallTime(), + "#test.ft gauge dt.meta.unit=ms", + "test.second,dt.metrics.source=micrometer count,delta=100 " + clock.wallTime(), + "#test.second count dt.meta.unit=s", + "test.seconds,dt.metrics.source=micrometer count,delta=10 " + clock.wallTime(), + "#test.seconds count dt.meta.unit=s", + "process.cpu.time,dt.metrics.source=micrometer count,delta=1000000 " + clock.wallTime(), + "#process.cpu.time count dt.meta.unit=ns", + "test.ltt,dt.metrics.source=micrometer gauge,min=62000,max=62000,sum=62000,count=1 " + clock.wallTime(), + "#test.ltt gauge dt.meta.unit=ms"))); + } + @Test void sendsTwoRequestsWhenSizeLimitIsReachedWithMetadata() { HttpSender.Request.Builder firstReq = spy(HttpSender.Request.build(config.uri(), httpClient));