Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Dynatrace v2] Update to utils library v2 #4064

2 changes: 1 addition & 1 deletion dependencies.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ def VERSIONS = [
'ch.qos.logback:logback-classic:1.2.+',
'colt:colt:1.2.0',
'com.amazonaws:aws-java-sdk-cloudwatch:latest.release',
'com.dynatrace.metric.util:dynatrace-metric-utils-java:1.+',
'com.dynatrace.metric.util:dynatrace-metric-utils-java:latest.release',
'com.fasterxml.jackson.core:jackson-databind:latest.release',
'com.github.ben-manes.caffeine:caffeine:2.+',
'com.github.charithe:kafka-junit:latest.release',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@
import java.util.function.BiFunction;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

Expand Down Expand Up @@ -71,22 +70,33 @@ public final class DynatraceExporterV2 extends AbstractDynatraceExporter {
// This should be non-static for MockLoggerFactory.injectLogger() in tests.
private final InternalLogger logger = InternalLoggerFactory.getInstance(DynatraceExporterV2.class);

private final MetricBuilderFactory metricBuilderFactory;
private MetricLinePreConfiguration preConfiguration;

private boolean skipExport = false;

public DynatraceExporterV2(DynatraceConfig config, Clock clock, HttpSender httpClient) {
super(config, clock, httpClient);

logger.info("Exporting to endpoint {}", config.uri());

MetricBuilderFactory.MetricBuilderFactoryBuilder factoryBuilder = MetricBuilderFactory.builder()
.withPrefix(config.metricKeyPrefix())
.withDefaultDimensions(parseDefaultDimensions(config.defaultDimensions()));
try {
MetricLinePreConfiguration.Builder preConfigBuilder = MetricLinePreConfiguration.builder()
.prefix(config.metricKeyPrefix())
.defaultDimensions(enrichWithMetricsSourceDimension(config.defaultDimensions()));

if (config.enrichWithDynatraceMetadata()) {
factoryBuilder.withDynatraceMetadata();
}
if (config.enrichWithDynatraceMetadata()) {
preConfigBuilder.dynatraceMetadataDimensions();
}

metricBuilderFactory = factoryBuilder.build();
preConfiguration = preConfigBuilder.build();
}
catch (MetricException e) {
// if the preconfiguration is invalid, all created metric lines would be
// invalid, and exporting any line becomes useless. Therefore, we log an
// error, and don't export at all.
logger.error(e.getMessage());
skipExport = true;
}
jonatan-ivanov marked this conversation as resolved.
Show resolved Hide resolved
}

private boolean isValidEndpoint(String uri) {
Expand Down Expand Up @@ -114,12 +124,10 @@ private boolean shouldIgnoreToken(DynatraceConfig config) {
return false;
}

private DimensionList parseDefaultDimensions(Map<String, String> defaultDimensions) {
List<Dimension> dimensions = Stream
.concat(defaultDimensions.entrySet().stream(), staticDimensions.entrySet().stream())
.map(entry -> Dimension.create(entry.getKey(), entry.getValue()))
.collect(Collectors.toList());
return DimensionList.fromCollection(dimensions);
private Map<String, String> enrichWithMetricsSourceDimension(Map<String, String> defaultDimensions) {
LinkedHashMap<String, String> orderedDimensions = new LinkedHashMap<>(defaultDimensions);
orderedDimensions.putAll(staticDimensions);
return orderedDimensions;
}

/**
Expand All @@ -133,6 +141,11 @@ private DimensionList parseDefaultDimensions(Map<String, String> defaultDimensio
*/
@Override
public void export(List<Meter> meters) {
if (skipExport) {
logger.warn("Dynatrace configuration is invalid, skipping export.");
return;
}

Map<String, String> seenMetadata = null;
if (config.exportMeterMetadata()) {
seenMetadata = new HashMap<>();
Expand Down Expand Up @@ -209,11 +222,11 @@ private String createGaugeLine(Meter meter, Map<String, String> seenMetadata, Me
meter.getId().getName()));
return null;
}
Metric.Builder metricBuilder = createMetricBuilder(meter).setDoubleGaugeValue(value);
MetricLineBuilder.GaugeStep metricBuilder = createMetricBuilder(meter).gauge();
jonatan-ivanov marked this conversation as resolved.
Show resolved Hide resolved

storeMetadataLine(metricBuilder, seenMetadata);
storeMetadataLine(createMetadataBuilder(metricBuilder, meter), seenMetadata);

return metricBuilder.serializeMetricLine();
return metricBuilder.value(value).timestamp(Instant.ofEpochMilli(clock.wallTime())).build();
}
catch (MetricException e) {
logger.warn(METER_EXCEPTION_LOG_FORMAT, meter.getId(), e.getMessage());
Expand All @@ -229,12 +242,13 @@ Stream<String> toCounterLine(Counter counter, Map<String, String> seenMetadata)

private String createCounterLine(Meter meter, Map<String, String> seenMetadata, Measurement measurement) {
try {
Metric.Builder metricBuilder = createMetricBuilder(meter)
.setDoubleCounterValueDelta(measurement.getValue());
MetricLineBuilder.CounterStep metricBuilder = createMetricBuilder(meter).count();

storeMetadataLine(metricBuilder, seenMetadata);
storeMetadataLine(createMetadataBuilder(metricBuilder, meter), seenMetadata);

return metricBuilder.serializeMetricLine();
return metricBuilder.delta(measurement.getValue())
.timestamp(Instant.ofEpochMilli(clock.wallTime()))
.build();
}
catch (MetricException e) {
logger.warn(METER_EXCEPTION_LOG_FORMAT, meter.getId(), e.getMessage());
Expand Down Expand Up @@ -285,11 +299,13 @@ private double minFromHistogramSnapshot(HistogramSnapshot histogramSnapshot, Tim
private Stream<String> createSummaryLine(Meter meter, Map<String, String> seenMetadata, double min, double max,
double total, long count) {
try {
Metric.Builder builder = createMetricBuilder(meter).setDoubleSummaryValue(min, max, total, count);
MetricLineBuilder.GaugeStep metricBuilder = createMetricBuilder(meter).gauge();

storeMetadataLine(builder, seenMetadata);
storeMetadataLine(createMetadataBuilder(metricBuilder, meter), seenMetadata);

return Stream.of(builder.serializeMetricLine());
return Stream.of(metricBuilder.summary(min, max, total, count)
.timestamp(Instant.ofEpochMilli(clock.wallTime()))
.build());
}
catch (MetricException e) {
logger.warn(METER_EXCEPTION_LOG_FORMAT, meter.getId(), e.getMessage());
Expand Down Expand Up @@ -375,17 +391,14 @@ private Stream<String> toMeterLine(Meter meter, BiFunction<Meter, Measurement, S
.filter(Objects::nonNull);
}

private Metric.Builder createMetricBuilder(Meter meter) {
return metricBuilderFactory.newMetricBuilder(meter.getId().getName())
.setDimensions(fromTags(meter.getId().getTags()))
.setTimestamp(Instant.ofEpochMilli(clock.wallTime()))
.setUnit(meter.getId().getBaseUnit())
.setDescription(meter.getId().getDescription());
}
private MetricLineBuilder.TypeStep createMetricBuilder(Meter meter) throws MetricException {
MetricLineBuilder.TypeStep metricLineBuilder = MetricLineBuilder.create(preConfiguration)
jonatan-ivanov marked this conversation as resolved.
Show resolved Hide resolved
.metricKey(meter.getId().getName());
for (Tag tag : meter.getId().getTags()) {
metricLineBuilder.dimension(tag.getKey(), tag.getValue());
}

private DimensionList fromTags(List<Tag> tags) {
return DimensionList.fromCollection(
tags.stream().map(tag -> Dimension.create(tag.getKey(), tag.getValue())).collect(Collectors.toList()));
return metricLineBuilder;
}

private <T> Stream<T> streamOf(Iterable<T> iterable) {
Expand Down Expand Up @@ -453,37 +466,55 @@ private void handleSuccess(int totalSent, HttpSender.Response response) {
}
}

private void storeMetadataLine(Metric.Builder metricBuilder, Map<String, String> seenMetadata)
private MetricLineBuilder.MetadataStep createMetadataBuilder(MetricLineBuilder.GaugeStep metricBuilder,
jonatan-ivanov marked this conversation as resolved.
Show resolved Hide resolved
Meter meter) {
return enrichMetadataBuilder(metricBuilder.metadata(), meter);
}

private MetricLineBuilder.MetadataStep createMetadataBuilder(MetricLineBuilder.CounterStep metricBuilder,
Meter meter) {
return enrichMetadataBuilder(metricBuilder.metadata(), meter);
}

private MetricLineBuilder.MetadataStep enrichMetadataBuilder(MetricLineBuilder.MetadataStep metadataBuilder,
Meter meter) {
return metadataBuilder.description(meter.getId().getDescription()).unit(meter.getId().getBaseUnit());
}

private void storeMetadataLine(MetricLineBuilder.MetadataStep metadataBuilder, Map<String, String> seenMetadata)
throws MetricException {
// if the config to export metadata is turned off, the seenMetadata map will be
// null.
if (seenMetadata == null) {
return;
}

String key = metricBuilder.getNormalizedMetricKey();
String metadataLine = metadataBuilder.build();
if (metadataBuilder == null) {
jonatan-ivanov marked this conversation as resolved.
Show resolved Hide resolved
return;
}

String key = extractMetricKey(metadataLine);
if (!seenMetadata.containsKey(key)) {
// if there is no metadata associated with the key, add it.
seenMetadata.put(key, metricBuilder.serializeMetadataLine());
seenMetadata.put(key, metadataLine);
}
else {
// get the previously stored metadata line
String previousMetadataLine = seenMetadata.get(key);
// if the previous line is not null, a metadata object had already been set in
// the past and no conflicting metadata lines had been added thereafter.
if (previousMetadataLine != null) {
String newMetadataLine = metricBuilder.serializeMetadataLine();
// if the new metadata line conflicts with the old one, we don't know
// which one is the correct metadata and will not export any.
// the map entry is set to null to ensure other metadata lines cannot be
// set for this metric key.
if (!previousMetadataLine.equals(newMetadataLine)) {
if (!previousMetadataLine.equals(metadataLine)) {
seenMetadata.put(key, null);
logger.warn(
"Metadata discrepancy detected:\n" + "original metadata:\t{}\n" + "tried to set new:\t{}\n"
+ "Metadata for metric key {} will not be sent.",
previousMetadataLine, newMetadataLine, key);
previousMetadataLine, metadataLine, key);
}
}
// else:
Expand All @@ -493,4 +524,23 @@ private void storeMetadataLine(Metric.Builder metricBuilder, Map<String, String>
}
}

private String extractMetricKey(String metadataLine) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't MetadataStep do this for us instead of encoding it into a string and then trying to decode it again?
This also feels quite brittle. Btw should the index start at 1?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Btw should the index start at 1?

Yes that's on purpose, the first index will always be #, I added a clarifying comment in 0d6d7f0

if (metadataLine == null) {
return null;
}

StringBuilder metricKey = new StringBuilder(32);

for (int i = 1; i < metadataLine.length(); i++) {
char c = metadataLine.charAt(i);
if (c == ' ' || c == ',') {
break;
}

metricKey.append(c);
}

return metricKey.toString();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,9 @@ void shouldSendProperRequest() throws Throwable {

String[] lines = new String(request.getEntity(), StandardCharsets.UTF_8).trim().split("\n");
assertThat(lines).hasSize(4)
.containsExactly("my.counter,dt.metrics.source=micrometer count,delta=12.0 " + clock.wallTime(),
"my.timer,dt.metrics.source=micrometer gauge,min=12.0,max=42.0,sum=108.0,count=4 "
+ clock.wallTime(),
"my.gauge,dt.metrics.source=micrometer gauge," + gauge + " " + clock.wallTime(),
.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");
})));
}
Expand All @@ -113,8 +112,8 @@ void shouldResetBetweenRequests() throws Throwable {

assertThat(request.getEntity()).asString()
.hasLineCount(2)
.contains("my.timer,dt.metrics.source=micrometer gauge,min=22.0,max=50.0,sum=72.0,count=2 "
+ clock.wallTime(), "#my.timer gauge dt.meta.unit=milliseconds");
.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");

// both are bigger than the previous min and smaller than the previous max. They
// will only show up if the
Expand All @@ -131,8 +130,9 @@ void shouldResetBetweenRequests() throws Throwable {

assertThat(request2.getEntity()).asString()
.hasLineCount(2)
.containsIgnoringNewLines("my.timer,dt.metrics.source=micrometer gauge,min=33.0,max=44.0,sum=77.0,count=2 "
+ clock.wallTime(), "#my.timer gauge dt.meta.unit=milliseconds");
.containsIgnoringNewLines(
"my.timer,dt.metrics.source=micrometer gauge,min=33,max=44,sum=77,count=2 " + clock.wallTime(),
"#my.timer gauge dt.meta.unit=milliseconds");
}

@Test
Expand All @@ -148,8 +148,9 @@ void shouldNotTrackPercentilesWithDynatraceSummary() throws Throwable {

verify(httpClient).send(assertArg((request -> assertThat(request.getEntity()).asString()
.hasLineCount(2)
.containsIgnoringNewLines("my.timer,dt.metrics.source=micrometer gauge,min=22.0,max=55.0,sum=77.0,count=2 "
+ clock.wallTime(), "#my.timer gauge dt.meta.unit=milliseconds"))));
.containsIgnoringNewLines(
"my.timer,dt.metrics.source=micrometer gauge,min=22,max=55,sum=77,count=2 " + clock.wallTime(),
"#my.timer gauge dt.meta.unit=milliseconds"))));
}

@Test
Expand All @@ -165,8 +166,9 @@ void shouldNotExportLinesWithZeroCount() throws Throwable {

verify(httpClient).send(assertArg(request -> assertThat(request.getEntity()).asString()
.hasLineCount(2)
.containsIgnoringNewLines("my.timer,dt.metrics.source=micrometer gauge,min=44.0,max=44.0,sum=44.0,count=1 "
+ clock.wallTime(), "#my.timer gauge dt.meta.unit=milliseconds")));
.containsIgnoringNewLines(
"my.timer,dt.metrics.source=micrometer gauge,min=44,max=44,sum=44,count=1 " + clock.wallTime(),
"#my.timer gauge dt.meta.unit=milliseconds")));

// reset for next export interval
reset(httpClient);
Expand All @@ -190,8 +192,9 @@ void shouldNotExportLinesWithZeroCount() throws Throwable {

verify(httpClient).send(assertArg(request -> assertThat(request.getEntity()).asString()
.hasLineCount(2)
.containsIgnoringNewLines("my.timer,dt.metrics.source=micrometer gauge,min=33.0,max=33.0,sum=33.0,count=1 "
+ clock.wallTime(), "#my.timer gauge dt.meta.unit=milliseconds")));
.containsIgnoringNewLines(
"my.timer,dt.metrics.source=micrometer gauge,min=33,max=33,sum=33,count=1 " + clock.wallTime(),
"#my.timer gauge dt.meta.unit=milliseconds")));
}

private DynatraceConfig createDefaultDynatraceConfig() {
Expand All @@ -218,4 +221,12 @@ public DynatraceApiVersion apiVersion() {
};
}

private String formatDouble(double value) {
if (value == (long) value) {
return Long.toString((long) value);
}

return Double.toString(value);
}

}