Skip to content

Commit

Permalink
Map time units to UCUM format for Dynatrace (#5589)
Browse files Browse the repository at this point in the history
Closes gh-5588

Co-authored-by: Georg P <georg.pirklbauer@dynatrace.com>
  • Loading branch information
jonatan-ivanov and pirgeo authored Oct 14, 2024
1 parent d7daaef commit 9c3b760
Show file tree
Hide file tree
Showing 3 changed files with 87 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<String, String> staticDimensions = Collections.singletonMap("dt.metrics.source",
private static final Map<String, String> STATIC_DIMENSIONS = Collections.singletonMap("dt.metrics.source",
"micrometer");

private static final Map<String, String> UCUM_TIME_UNIT_MAP = ucumTimeUnitMap();

// Loggers must be non-static for MockLoggerFactory.injectLogger() in tests.
private final InternalLogger logger = InternalLoggerFactory.getInstance(DynatraceExporterV2.class);

Expand Down Expand Up @@ -128,7 +133,7 @@ private boolean shouldIgnoreToken(DynatraceConfig config) {

private Map<String, String> enrichWithMetricsSourceDimensions(Map<String, String> defaultDimensions) {
LinkedHashMap<String, String> orderedDimensions = new LinkedHashMap<>(defaultDimensions);
orderedDimensions.putAll(staticDimensions);
orderedDimensions.putAll(STATIC_DIMENSIONS);
return orderedDimensions;
}

Expand Down Expand Up @@ -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()));
}

/**
Expand Down Expand Up @@ -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: <a href="https://ucum.org/">ucum.org</a>.
* @return Time unit mapping to UCUM-compliant format
*/
private static Map<String, String> ucumTimeUnitMap() {
Map<String, String> 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);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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");
})));
}

Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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 "
Expand All @@ -224,15 +224,15 @@ 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
// DefaultLongTaskTimer#takeSnapshot().
"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
Expand Down Expand Up @@ -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(),
Expand All @@ -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);
Expand All @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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");
}));
}

Expand Down Expand Up @@ -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));
Expand Down

0 comments on commit 9c3b760

Please sign in to comment.