Skip to content

Commit

Permalink
[Dynatrace v2] Export unit/description to Dynatrace (#4006)
Browse files Browse the repository at this point in the history
Export Meter's unit and description as Dynatrace metadata by default. This new behavior can be toggled by a new configuration method `exportMeterMetadata` on DynatraceConfig.

Resolves gh-3979

Co-authored-by: Armin Ruech <armin.ruech@dynatrace.com>
  • Loading branch information
pirgeo and arminru committed Sep 1, 2023
1 parent a56b968 commit 44c334a
Show file tree
Hide file tree
Showing 5 changed files with 475 additions and 95 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -120,11 +120,16 @@ default Map<String, String> defaultDimensions() {
}

/**
* Return whether to enrich with Dynatrace metadata.
* Return whether to enrich with Dynatrace metadata. Dynatrace metadata is provided by
* the Dynatrace OneAgent or Dynatrace Kubernetes Operator and helps put metrics
* emitted via the Micrometer exporter in context more easily.
* @return whether to enrich with Dynatrace metadata
* @since 1.8.0
*/
default boolean enrichWithDynatraceMetadata() {
if (apiVersion() == V1) {
return false;
}
return getBoolean(this, "enrichWithDynatraceMetadata").orElse(true);
}

Expand All @@ -143,6 +148,23 @@ default boolean useDynatraceSummaryInstruments() {
return getBoolean(this, "useDynatraceSummaryInstruments").orElse(true);
}

/**
* Toggle whether to export meter metadata (unit and description) to the Dynatrace
* backend for the V2 version of this exporter. Metadata will be exported by default
* from Micrometer version 1.12.0. This setting has no effect for the (legacy)
* Dynatrace Exporter v1. Setting this toggle to {@code false} has a similar effect to
* registering a MeterFilter that removes unit and description from all registered
* meters.
* @return true if metadata should be exported, false otherwise.
* @since 1.12.0
*/
default boolean exportMeterMetadata() {
if (apiVersion() == V1) {
return false;
}
return getBoolean(this, "exportMeterMetadata").orElse(true);
}

@Override
default Validated<?> validate() {
return checkAll(this, config -> StepRegistryConfig.validate(config),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,40 +133,63 @@ private DimensionList parseDefaultDimensions(Map<String, String> defaultDimensio
*/
@Override
public void export(List<Meter> meters) {
Map<String, String> seenMetadata = null;
if (config.exportMeterMetadata()) {
seenMetadata = new HashMap<>();
}

int partitionSize = Math.min(config.batchSize(), DynatraceMetricApiConstants.getPayloadLinesLimit());
List<String> batch = new ArrayList<>(partitionSize);

for (Meter meter : meters) {
// Lines that are too long to be ingested into Dynatrace, as well as lines
// that contain NaN or Inf values are not returned from "toMetricLines",
// and are therefore dropped.
Stream<String> metricLines = toMetricLines(meter);
Stream<String> metricLines = toMetricLines(meter, seenMetadata);

metricLines.forEach(line -> {
batch.add(line);
if (batch.size() == partitionSize) {
send(batch);
batch.clear();
sendBatchIfFull(batch, partitionSize);
});
}

// if the config to export metadata is turned off, the seenMetadata map will be
// null.
if (seenMetadata != null) {
seenMetadata.values().forEach(line -> {
if (line != null) {
batch.add(line);
sendBatchIfFull(batch, partitionSize);
}
});
}

// push remaining lines if any.
if (!batch.isEmpty()) {
send(batch);
}
}

private Stream<String> toMetricLines(Meter meter) {
return meter.match(this::toGaugeLine, this::toCounterLine, this::toTimerLine, this::toDistributionSummaryLine,
this::toLongTaskTimerLine, this::toTimeGaugeLine, this::toFunctionCounterLine,
this::toFunctionTimerLine, this::toMeterLine);
private void sendBatchIfFull(List<String> batch, int partitionSize) {
if (batch.size() == partitionSize) {
send(batch);
batch.clear();
}
}

Stream<String> toGaugeLine(Gauge meter) {
return toMeterLine(meter, this::createGaugeLine);
private Stream<String> toMetricLines(Meter meter, Map<String, String> seenMetadata) {
return meter.match(m -> toGaugeLine(m, seenMetadata), m -> toCounterLine(m, seenMetadata),
m -> toTimerLine(m, seenMetadata), m -> toDistributionSummaryLine(m, seenMetadata),
m -> toLongTaskTimerLine(m, seenMetadata), m -> toTimeGaugeLine(m, seenMetadata),
m -> toFunctionCounterLine(m, seenMetadata), m -> toFunctionTimerLine(m, seenMetadata),
m -> toGaugeLine(m, seenMetadata));
}

private String createGaugeLine(Meter meter, Measurement measurement) {
Stream<String> toGaugeLine(Meter meter, Map<String, String> seenMetadata) {
return toMeterLine(meter, (theMeter, measurement) -> createGaugeLine(theMeter, seenMetadata, measurement));
}

private String createGaugeLine(Meter meter, Map<String, String> seenMetadata, Measurement measurement) {
try {
double value = measurement.getValue();
if (Double.isNaN(value)) {
Expand All @@ -186,7 +209,11 @@ private String createGaugeLine(Meter meter, Measurement measurement) {
meter.getId().getName()));
return null;
}
return createMetricBuilder(meter).setDoubleGaugeValue(value).serialize();
Metric.Builder metricBuilder = createMetricBuilder(meter).setDoubleGaugeValue(value);

storeMetadataLine(metricBuilder, seenMetadata);

return metricBuilder.serializeMetricLine();
}
catch (MetricException e) {
logger.warn(METER_EXCEPTION_LOG_FORMAT, meter.getId(), e.getMessage());
Expand All @@ -195,13 +222,19 @@ private String createGaugeLine(Meter meter, Measurement measurement) {
return null;
}

Stream<String> toCounterLine(Counter meter) {
return toMeterLine(meter, this::createCounterLine);
Stream<String> toCounterLine(Counter counter, Map<String, String> seenMetadata) {
return toMeterLine(counter,
(Meter meter, Measurement measurement) -> this.createCounterLine(meter, seenMetadata, measurement));
}

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

storeMetadataLine(metricBuilder, seenMetadata);

return metricBuilder.serializeMetricLine();
}
catch (MetricException e) {
logger.warn(METER_EXCEPTION_LOG_FORMAT, meter.getId(), e.getMessage());
Expand All @@ -210,9 +243,9 @@ private String createCounterLine(Meter meter, Measurement measurement) {
return null;
}

Stream<String> toTimerLine(Timer meter) {
Stream<String> toTimerLine(Timer meter, Map<String, String> seenMetadata) {
if (!(meter instanceof DynatraceSummarySnapshotSupport)) {
return toSummaryLine(meter, meter.takeSnapshot(), getBaseTimeUnit());
return toSummaryLine(meter, seenMetadata, meter.takeSnapshot(), getBaseTimeUnit());
}

DynatraceSummarySnapshot snapshot = ((DynatraceSummarySnapshotSupport) meter)
Expand All @@ -222,10 +255,12 @@ Stream<String> toTimerLine(Timer meter) {
return Stream.empty();
}

return createSummaryLine(meter, snapshot.getMin(), snapshot.getMax(), snapshot.getTotal(), snapshot.getCount());
return createSummaryLine(meter, seenMetadata, snapshot.getMin(), snapshot.getMax(), snapshot.getTotal(),
snapshot.getCount());
}

private Stream<String> toSummaryLine(Meter meter, HistogramSnapshot histogramSnapshot, TimeUnit timeUnit) {
private Stream<String> toSummaryLine(Meter meter, Map<String, String> seenMetadata,
HistogramSnapshot histogramSnapshot, TimeUnit timeUnit) {
long count = histogramSnapshot.count();
if (count < 1) {
logger.debug("Summary with 0 count dropped: {}", meter.getId().getName());
Expand All @@ -234,7 +269,7 @@ private Stream<String> toSummaryLine(Meter meter, HistogramSnapshot histogramSna
double total = (timeUnit != null) ? histogramSnapshot.total(timeUnit) : histogramSnapshot.total();
double max = (timeUnit != null) ? histogramSnapshot.max(timeUnit) : histogramSnapshot.max();
double min = (count == 1) ? max : minFromHistogramSnapshot(histogramSnapshot, timeUnit);
return createSummaryLine(meter, min, max, total, count);
return createSummaryLine(meter, seenMetadata, min, max, total, count);
}

private double minFromHistogramSnapshot(HistogramSnapshot histogramSnapshot, TimeUnit timeUnit) {
Expand All @@ -247,10 +282,14 @@ private double minFromHistogramSnapshot(HistogramSnapshot histogramSnapshot, Tim
return Double.NaN;
}

private Stream<String> createSummaryLine(Meter meter, double min, double max, double total, long count) {
private Stream<String> createSummaryLine(Meter meter, Map<String, String> seenMetadata, double min, double max,
double total, long count) {
try {
String line = createMetricBuilder(meter).setDoubleSummaryValue(min, max, total, count).serialize();
return Stream.of(line);
Metric.Builder builder = createMetricBuilder(meter).setDoubleSummaryValue(min, max, total, count);

storeMetadataLine(builder, seenMetadata);

return Stream.of(builder.serializeMetricLine());
}
catch (MetricException e) {
logger.warn(METER_EXCEPTION_LOG_FORMAT, meter.getId(), e.getMessage());
Expand All @@ -259,9 +298,9 @@ private Stream<String> createSummaryLine(Meter meter, double min, double max, do
return Stream.empty();
}

Stream<String> toDistributionSummaryLine(DistributionSummary meter) {
Stream<String> toDistributionSummaryLine(DistributionSummary meter, Map<String, String> seenMetadata) {
if (!(meter instanceof DynatraceSummarySnapshotSupport)) {
return toSummaryLine(meter, meter.takeSnapshot(), null);
return toSummaryLine(meter, seenMetadata, meter.takeSnapshot(), null);
}

DynatraceSummarySnapshot snapshot = ((DynatraceSummarySnapshotSupport) meter).takeSummarySnapshotAndReset();
Expand All @@ -270,22 +309,23 @@ Stream<String> toDistributionSummaryLine(DistributionSummary meter) {
return Stream.empty();
}

return createSummaryLine(meter, snapshot.getMin(), snapshot.getMax(), snapshot.getTotal(), snapshot.getCount());
return createSummaryLine(meter, seenMetadata, snapshot.getMin(), snapshot.getMax(), snapshot.getTotal(),
snapshot.getCount());
}

Stream<String> toLongTaskTimerLine(LongTaskTimer meter) {
return toSummaryLine(meter, meter.takeSnapshot(), getBaseTimeUnit());
Stream<String> toLongTaskTimerLine(LongTaskTimer meter, Map<String, String> seenMetadata) {
return toSummaryLine(meter, seenMetadata, meter.takeSnapshot(), getBaseTimeUnit());
}

Stream<String> toTimeGaugeLine(TimeGauge meter) {
return toMeterLine(meter, this::createGaugeLine);
Stream<String> toTimeGaugeLine(TimeGauge meter, Map<String, String> seenMetadata) {
return toMeterLine(meter, (theMeter, measurement) -> createGaugeLine(theMeter, seenMetadata, measurement));
}

Stream<String> toFunctionCounterLine(FunctionCounter meter) {
return toMeterLine(meter, this::createCounterLine);
Stream<String> toFunctionCounterLine(FunctionCounter meter, Map<String, String> seenMetadata) {
return toMeterLine(meter, (theMeter, measurement) -> createCounterLine(theMeter, seenMetadata, measurement));
}

Stream<String> toFunctionTimerLine(FunctionTimer meter) {
Stream<String> toFunctionTimerLine(FunctionTimer meter, Map<String, String> seenMetadata) {
long count = (long) meter.count();
if (count < 1) {
logger.debug("Timer with 0 count dropped: {}", meter.getId().getName());
Expand All @@ -294,11 +334,11 @@ Stream<String> toFunctionTimerLine(FunctionTimer meter) {
double total = meter.totalTime(getBaseTimeUnit());
double average = meter.mean(getBaseTimeUnit());

return createSummaryLine(meter, average, average, total, count);
return createSummaryLine(meter, seenMetadata, average, average, total, count);
}

Stream<String> toMeterLine(Meter meter) {
return toMeterLine(meter, this::createGaugeLine);
Stream<String> toMeterLine(Meter meter, Map<String, String> seenMetadata) {
return toMeterLine(meter, (theMeter, measurement) -> createGaugeLine(theMeter, seenMetadata, measurement));
}

private Stream<String> toMeterLine(Meter meter, BiFunction<Meter, Measurement, String> measurementConverter) {
Expand All @@ -309,7 +349,9 @@ private Stream<String> toMeterLine(Meter meter, BiFunction<Meter, Measurement, S
private Metric.Builder createMetricBuilder(Meter meter) {
return metricBuilderFactory.newMetricBuilder(meter.getId().getName())
.setDimensions(fromTags(meter.getId().getTags()))
.setTimestamp(Instant.ofEpochMilli(clock.wallTime()));
.setTimestamp(Instant.ofEpochMilli(clock.wallTime()))
.setUnit(meter.getId().getBaseUnit())
.setDescription(meter.getId().getDescription());
}

private DimensionList fromTags(List<Tag> tags) {
Expand Down Expand Up @@ -382,4 +424,44 @@ private void handleSuccess(int totalSent, HttpSender.Response response) {
}
}

private void storeMetadataLine(Metric.Builder metricBuilder, 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();

if (!seenMetadata.containsKey(key)) {
// if there is no metadata associated with the key, add it.
seenMetadata.put(key, metricBuilder.serializeMetadataLine());
}
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)) {
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);
}
}
// else:
// the key exists, but the value is null, so a conflicting state has been
// identified before. we will ignore any other metadata for this key, so there
// is nothing to do here.
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -171,9 +171,33 @@ void testV2Defaults() {
assertThat(config.apiVersion()).isEqualTo(DynatraceApiVersion.V2);
assertThat(config.apiToken()).isEmpty();
assertThat(config.uri()).isSameAs(DynatraceMetricApiConstants.getDefaultOneAgentEndpoint());
assertThat(config.deviceId()).isEmpty();
assertThat(config.metricKeyPrefix()).isEmpty();
assertThat(config.defaultDimensions()).isEmpty();
assertThat(config.enrichWithDynatraceMetadata()).isTrue();
assertThat(config.exportMeterMetadata()).isTrue();

Validated<?> validated = config.validate();
assertThat(validated.isValid()).isTrue();
}

@Test
void testV1Defaults() {
Map<String, String> properties = new HashMap<>();
properties.put("dynatrace.apiVersion", "v1");
properties.put("dynatrace.apiToken", "my.token");
properties.put("dynatrace.uri", "https://my.uri.com");
properties.put("dynatrace.deviceId", "my.device.id");
DynatraceConfig config = properties::get;

assertThat(config.apiVersion()).isEqualTo(DynatraceApiVersion.V1);
assertThat(config.apiToken()).isEqualTo("my.token");
assertThat(config.uri()).isEqualTo("https://my.uri.com");
assertThat(config.deviceId()).isEqualTo("my.device.id");
assertThat(config.metricKeyPrefix()).isEmpty();
assertThat(config.defaultDimensions()).isEmpty();
assertThat(config.enrichWithDynatraceMetadata()).isFalse();
assertThat(config.exportMeterMetadata()).isFalse();

Validated<?> validated = config.validate();
assertThat(validated.isValid()).isTrue();
Expand Down

0 comments on commit 44c334a

Please sign in to comment.