diff --git a/implementations/micrometer-registry-dynatrace/src/main/java/io/micrometer/dynatrace/DynatraceConfig.java b/implementations/micrometer-registry-dynatrace/src/main/java/io/micrometer/dynatrace/DynatraceConfig.java index c314bdfa4e..f9c06e768c 100644 --- a/implementations/micrometer-registry-dynatrace/src/main/java/io/micrometer/dynatrace/DynatraceConfig.java +++ b/implementations/micrometer-registry-dynatrace/src/main/java/io/micrometer/dynatrace/DynatraceConfig.java @@ -128,6 +128,20 @@ default boolean enrichWithDynatraceMetadata() { return getBoolean(this, "enrichWithDynatraceMetadata").orElse(true); } + /** + * Return whether to fall back to the built-in micrometer instruments for Timer and DistributionSummary. + * + * @return {@code true} if the resetting Dynatrace instruments should be used, and false if the registry should + * fall back to the built-in Micrometer instruments. + * @since 1.9.0 + */ + default boolean useDynatraceSummaryInstruments() { + if (apiVersion() == V1) { + return false; + } + return getBoolean(this, "useDynatraceSummaryInstruments").orElse(true); + } + @Override default Validated validate() { return checkAll(this, diff --git a/implementations/micrometer-registry-dynatrace/src/main/java/io/micrometer/dynatrace/DynatraceMeterRegistry.java b/implementations/micrometer-registry-dynatrace/src/main/java/io/micrometer/dynatrace/DynatraceMeterRegistry.java index eaf8edaca2..0f71e0dbcb 100644 --- a/implementations/micrometer-registry-dynatrace/src/main/java/io/micrometer/dynatrace/DynatraceMeterRegistry.java +++ b/implementations/micrometer-registry-dynatrace/src/main/java/io/micrometer/dynatrace/DynatraceMeterRegistry.java @@ -16,20 +16,26 @@ package io.micrometer.dynatrace; import io.micrometer.core.instrument.Clock; +import io.micrometer.core.instrument.DistributionSummary; import io.micrometer.core.instrument.Meter; +import io.micrometer.core.instrument.Timer; import io.micrometer.core.instrument.config.MeterFilter; import io.micrometer.core.instrument.config.MeterFilterReply; import io.micrometer.core.instrument.distribution.DistributionStatisticConfig; +import io.micrometer.core.instrument.distribution.pause.PauseDetector; import io.micrometer.core.instrument.step.StepMeterRegistry; import io.micrometer.core.instrument.util.NamedThreadFactory; import io.micrometer.core.ipc.http.HttpSender; import io.micrometer.core.ipc.http.HttpUrlConnectionSender; import io.micrometer.core.util.internal.logging.InternalLogger; import io.micrometer.core.util.internal.logging.InternalLoggerFactory; +import io.micrometer.dynatrace.types.DynatraceDistributionSummary; +import io.micrometer.dynatrace.types.DynatraceTimer; import io.micrometer.dynatrace.v1.DynatraceExporterV1; import io.micrometer.dynatrace.v2.DynatraceExporterV2; import java.util.Arrays; +import java.util.Objects; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ThreadFactory; @@ -53,6 +59,8 @@ public class DynatraceMeterRegistry extends StepMeterRegistry { private static final ThreadFactory DEFAULT_THREAD_FACTORY = new NamedThreadFactory("dynatrace-metrics-publisher"); private static final InternalLogger logger = InternalLoggerFactory.getInstance(DynatraceMeterRegistry.class); + private final DynatraceApiVersion apiVersion; + private final boolean useDynatraceSummaryInstruments; private final AbstractDynatraceExporter exporter; @SuppressWarnings("deprecation") @@ -63,9 +71,13 @@ public DynatraceMeterRegistry(DynatraceConfig config, Clock clock) { private DynatraceMeterRegistry(DynatraceConfig config, Clock clock, ThreadFactory threadFactory, HttpSender httpClient) { super(config, clock); - if (config.apiVersion() == DynatraceApiVersion.V2) { + apiVersion = config.apiVersion(); + useDynatraceSummaryInstruments = config.useDynatraceSummaryInstruments(); + + if (apiVersion == DynatraceApiVersion.V2) { logger.info("Exporting to Dynatrace metrics API v2"); this.exporter = new DynatraceExporterV2(config, clock, httpClient); + // Not used for Timer and DistributionSummary in V2 anymore, but still used for the other timer types. registerMinPercentile(); } else { logger.info("Exporting to Dynatrace metrics API v1"); @@ -89,6 +101,22 @@ protected TimeUnit getBaseTimeUnit() { return this.exporter.getBaseTimeUnit(); } + @Override + protected DistributionSummary newDistributionSummary(Meter.Id id, DistributionStatisticConfig distributionStatisticConfig, double scale) { + if (apiVersion == DynatraceApiVersion.V2 && useDynatraceSummaryInstruments) { + return new DynatraceDistributionSummary(id, clock, distributionStatisticConfig, scale); + } + return super.newDistributionSummary(id, distributionStatisticConfig, scale); + } + + @Override + protected Timer newTimer(Meter.Id id, DistributionStatisticConfig distributionStatisticConfig, PauseDetector pauseDetector) { + if (apiVersion == DynatraceApiVersion.V2 && useDynatraceSummaryInstruments) { + return new DynatraceTimer(id, clock, distributionStatisticConfig, pauseDetector, exporter.getBaseTimeUnit()); + } + return super.newTimer(id, distributionStatisticConfig, pauseDetector); + } + /** * As the micrometer summary statistics (DistributionSummary, and a number of timer meter types) * do not provide the minimum values that are required by Dynatrace to ingest summary metrics, @@ -106,16 +134,17 @@ private void registerMinPercentile() { public DistributionStatisticConfig configure(Meter.Id id, DistributionStatisticConfig config) { double[] percentiles; - if (config.getPercentiles() == null) { - percentiles = new double[] {0}; + double[] configPercentiles = config.getPercentiles(); + if (configPercentiles == null) { + percentiles = new double[]{0}; metersWithArtificialZeroPercentile.add(id.getName() + ".percentile"); } else if (!containsZeroPercentile(config)) { - percentiles = new double[config.getPercentiles().length + 1]; - System.arraycopy(config.getPercentiles(), 0, percentiles, 0, config.getPercentiles().length); - percentiles[config.getPercentiles().length] = 0; // theoretically this is already zero + percentiles = new double[configPercentiles.length + 1]; + System.arraycopy(configPercentiles, 0, percentiles, 0, configPercentiles.length); + percentiles[configPercentiles.length] = 0; // theoretically this is already zero metersWithArtificialZeroPercentile.add(id.getName() + ".percentile"); } else { - percentiles = config.getPercentiles(); + percentiles = configPercentiles; } return DistributionStatisticConfig.builder() @@ -133,7 +162,7 @@ public MeterFilterReply accept(Meter.Id id) { } private boolean containsZeroPercentile(DistributionStatisticConfig config) { - return Arrays.stream(config.getPercentiles()).anyMatch(percentile -> percentile == 0); + return Arrays.stream(Objects.requireNonNull(config.getPercentiles())).anyMatch(percentile -> percentile == 0); } private boolean hasArtificialZerothPercentile(Meter.Id id) { diff --git a/implementations/micrometer-registry-dynatrace/src/main/java/io/micrometer/dynatrace/types/DynatraceDistributionSummary.java b/implementations/micrometer-registry-dynatrace/src/main/java/io/micrometer/dynatrace/types/DynatraceDistributionSummary.java new file mode 100644 index 0000000000..a4186e411b --- /dev/null +++ b/implementations/micrometer-registry-dynatrace/src/main/java/io/micrometer/dynatrace/types/DynatraceDistributionSummary.java @@ -0,0 +1,109 @@ +/* + * Copyright 2022 VMware, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micrometer.dynatrace.types; + +import io.micrometer.core.instrument.AbstractDistributionSummary; +import io.micrometer.core.instrument.Clock; +import io.micrometer.core.instrument.DistributionSummary; +import io.micrometer.core.instrument.distribution.DistributionStatisticConfig; +import io.micrometer.core.instrument.distribution.HistogramSnapshot; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.TimeUnit; + +/** + * Resettable {@link DistributionSummary} implementation for Dynatrace exporters. + * + * @author Georg Pirklbauer + * @since 1.9.0 + */ +public final class DynatraceDistributionSummary extends AbstractDistributionSummary implements DynatraceSummarySnapshotSupport { + private final DynatraceSummary summary = new DynatraceSummary(); + private static final Logger LOGGER = LoggerFactory.getLogger(DynatraceDistributionSummary.class.getName()); + + // Configuration that will set the Histogram in AbstractTimer to a NoopHistogram. + private static final DistributionStatisticConfig NOOP_HISTOGRAM_CONFIG = + DistributionStatisticConfig.builder().percentilesHistogram(false).percentiles().build(); + + public DynatraceDistributionSummary(Id id, Clock clock, DistributionStatisticConfig distributionStatisticConfig, double scale) { + super(id, clock, NOOP_HISTOGRAM_CONFIG, scale, false); + + if (distributionStatisticConfig != DistributionStatisticConfig.NONE) { + LOGGER.warn("Distribution statistic config is currently ignored."); + } + } + + @Override + protected void recordNonNegative(double amount) { + summary.recordNonNegative(amount); + } + + @Override + public long count() { + return summary.getCount(); + } + + @Override + public double totalAmount() { + return summary.getTotal(); + } + + @Override + public double max() { + return summary.getMax(); + } + + public double min() { + return summary.getMin(); + } + + @Override + public boolean hasValues() { + return count() > 0; + } + + @Override + public DynatraceSummarySnapshot takeSummarySnapshot() { + return new DynatraceSummarySnapshot(min(), max(), totalAmount(), count()); + } + + @Override + public DynatraceSummarySnapshot takeSummarySnapshot(TimeUnit timeUnit) { + LOGGER.debug("Called takeSummarySnapshot with a TimeUnit on a DistributionSummary. Ignoring TimeUnit."); + return takeSummarySnapshot(); + } + + @Override + public DynatraceSummarySnapshot takeSummarySnapshotAndReset() { + DynatraceSummarySnapshot snapshot = takeSummarySnapshot(); + summary.reset(); + return snapshot; + } + + @Override + public DynatraceSummarySnapshot takeSummarySnapshotAndReset(TimeUnit unit) { + LOGGER.debug("Called takeSummarySnapshot with a TimeUnit on a DistributionSummary. Ignoring TimeUnit."); + return takeSummarySnapshotAndReset(); + } + + @Override + public HistogramSnapshot takeSnapshot() { + LOGGER.warn("Called takeSnapshot on a Dynatrace Distribution Summary, no percentiles will be exported."); + DynatraceSummarySnapshot dtSnapshot = takeSummarySnapshot(); + return HistogramSnapshot.empty(dtSnapshot.getCount(), dtSnapshot.getTotal(), dtSnapshot.getMax()); + } +} diff --git a/implementations/micrometer-registry-dynatrace/src/main/java/io/micrometer/dynatrace/types/DynatraceSummary.java b/implementations/micrometer-registry-dynatrace/src/main/java/io/micrometer/dynatrace/types/DynatraceSummary.java new file mode 100644 index 0000000000..01772ad13c --- /dev/null +++ b/implementations/micrometer-registry-dynatrace/src/main/java/io/micrometer/dynatrace/types/DynatraceSummary.java @@ -0,0 +1,76 @@ +/* + * Copyright 2022 VMware, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micrometer.dynatrace.types; + +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.DoubleAdder; +import java.util.concurrent.atomic.LongAdder; + +/** + * Internal class for resettable summary statistics + * + * @author Georg Pirklbauer + * @since 1.9.0 + */ +final class DynatraceSummary { + private final LongAdder count = new LongAdder(); + private final DoubleAdder total = new DoubleAdder(); + private final AtomicLong min = new AtomicLong(0); + private final AtomicLong max = new AtomicLong(0); + + void recordNonNegative(double amount) { + if (amount < 0) { + return; + } + + synchronized (this) { + long longBits = Double.doubleToLongBits(amount); + + max.getAndUpdate(prev -> Math.max(prev, longBits)); + // have to check if a value was already recorded before, otherwise min will always stay 0 (because the default is 0). + min.getAndUpdate(prev -> count.longValue() > 0 ? Math.min(prev, longBits) : longBits); + + total.add(amount); + count.increment(); + } + } + + + public long getCount() { + return count.longValue(); + } + + public double getTotal() { + return total.doubleValue(); + } + + public double getMin() { + return Double.longBitsToDouble(min.longValue()); + } + + public double getMax() { + return Double.longBitsToDouble(max.longValue()); + } + + void reset() { + synchronized (this) { + min.set(0); + max.set(0); + total.reset(); + count.reset(); + } + } +} diff --git a/implementations/micrometer-registry-dynatrace/src/main/java/io/micrometer/dynatrace/types/DynatraceSummarySnapshot.java b/implementations/micrometer-registry-dynatrace/src/main/java/io/micrometer/dynatrace/types/DynatraceSummarySnapshot.java new file mode 100644 index 0000000000..a6d64e6c61 --- /dev/null +++ b/implementations/micrometer-registry-dynatrace/src/main/java/io/micrometer/dynatrace/types/DynatraceSummarySnapshot.java @@ -0,0 +1,57 @@ +/* + * Copyright 2022 VMware, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micrometer.dynatrace.types; + +import javax.annotation.concurrent.Immutable; + + +/** + * Snapshot of a Dynatrace summary object. + * + * @author Georg Pirklbauer + * @since 1.9.0 + */ +@Immutable +public final class DynatraceSummarySnapshot { + private final double min; + private final double max; + private final double total; + private final long count; + + DynatraceSummarySnapshot(double min, double max, double total, long count) { + this.min = min; + this.max = max; + this.total = total; + this.count = count; + } + + public double getMin() { + return min; + } + + public double getMax() { + return max; + } + + public double getTotal() { + return total; + } + + public long getCount() { + return count; + } +} + diff --git a/implementations/micrometer-registry-dynatrace/src/main/java/io/micrometer/dynatrace/types/DynatraceSummarySnapshotSupport.java b/implementations/micrometer-registry-dynatrace/src/main/java/io/micrometer/dynatrace/types/DynatraceSummarySnapshotSupport.java new file mode 100644 index 0000000000..d377aa421f --- /dev/null +++ b/implementations/micrometer-registry-dynatrace/src/main/java/io/micrometer/dynatrace/types/DynatraceSummarySnapshotSupport.java @@ -0,0 +1,36 @@ +/* + * Copyright 2022 VMware, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micrometer.dynatrace.types; + +import java.util.concurrent.TimeUnit; + +/** + * Interface for retrieving a {@link DynatraceSummarySnapshot}. + * + * @author Georg Pirklbauer + * @since 1.9.0 + */ +public interface DynatraceSummarySnapshotSupport { + boolean hasValues(); + + DynatraceSummarySnapshot takeSummarySnapshot(); + + DynatraceSummarySnapshot takeSummarySnapshot(TimeUnit unit); + + DynatraceSummarySnapshot takeSummarySnapshotAndReset(); + + DynatraceSummarySnapshot takeSummarySnapshotAndReset(TimeUnit unit); +} diff --git a/implementations/micrometer-registry-dynatrace/src/main/java/io/micrometer/dynatrace/types/DynatraceTimer.java b/implementations/micrometer-registry-dynatrace/src/main/java/io/micrometer/dynatrace/types/DynatraceTimer.java new file mode 100644 index 0000000000..ea4b799def --- /dev/null +++ b/implementations/micrometer-registry-dynatrace/src/main/java/io/micrometer/dynatrace/types/DynatraceTimer.java @@ -0,0 +1,110 @@ +/* + * Copyright 2022 VMware, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micrometer.dynatrace.types; + +import io.micrometer.core.instrument.AbstractTimer; +import io.micrometer.core.instrument.Clock; +import io.micrometer.core.instrument.Timer; +import io.micrometer.core.instrument.distribution.DistributionStatisticConfig; +import io.micrometer.core.instrument.distribution.HistogramSnapshot; +import io.micrometer.core.instrument.distribution.pause.PauseDetector; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.TimeUnit; + +/** + * Resettable {@link Timer} implementation for Dynatrace exporters. + * + * @author Georg Pirklbauer + * @since 1.9.0 + */ +public final class DynatraceTimer extends AbstractTimer implements DynatraceSummarySnapshotSupport { + private static final Logger LOGGER = LoggerFactory.getLogger(DynatraceTimer.class.getName()); + private final DynatraceSummary summary = new DynatraceSummary(); + + // Configuration that will set the Histogram in AbstractTimer to a NoopHistogram. + private static final DistributionStatisticConfig NOOP_HISTOGRAM_CONFIG = + DistributionStatisticConfig.builder().percentilesHistogram(false).percentiles().build(); + + public DynatraceTimer(Id id, Clock clock, DistributionStatisticConfig distributionStatisticConfig, PauseDetector pauseDetector, TimeUnit baseTimeUnit) { + // make sure the Histogram in AbstractTimer is always a NoopHistogram by disabling the respective config options + super(id, clock, NOOP_HISTOGRAM_CONFIG, pauseDetector, baseTimeUnit, false); + + if (distributionStatisticConfig != DistributionStatisticConfig.NONE) { + LOGGER.warn("Distribution statistic config is currently ignored."); + } + } + + @Override + public boolean hasValues() { + return count() > 0; + } + + @Override + public DynatraceSummarySnapshot takeSummarySnapshot() { + return takeSummarySnapshot(baseTimeUnit()); + } + + @Override + public DynatraceSummarySnapshot takeSummarySnapshot(TimeUnit unit) { + return new DynatraceSummarySnapshot(min(unit), max(unit), totalTime(unit), count()); + } + + @Override + public DynatraceSummarySnapshot takeSummarySnapshotAndReset() { + return takeSummarySnapshotAndReset(baseTimeUnit()); + } + + @Override + public DynatraceSummarySnapshot takeSummarySnapshotAndReset(TimeUnit unit) { + DynatraceSummarySnapshot snapshot = takeSummarySnapshot(unit); + summary.reset(); + return snapshot; + } + + @Override + protected void recordNonNegative(long amount, TimeUnit unit) { + // store everything in baseTimeUnit + long inBaseUnit = baseTimeUnit().convert(amount, unit); + summary.recordNonNegative(inBaseUnit); + } + + @Override + public long count() { + return summary.getCount(); + } + + @Override + public double totalTime(TimeUnit unit) { + return unit.convert((long) summary.getTotal(), baseTimeUnit()); + } + + @Override + public double max(TimeUnit unit) { + return unit.convert((long) summary.getMax(), baseTimeUnit()); + } + + public double min(TimeUnit unit) { + return unit.convert((long) summary.getMin(), baseTimeUnit()); + } + + @Override + public HistogramSnapshot takeSnapshot() { + DynatraceSummarySnapshot dtSnapshot = takeSummarySnapshot(); + return HistogramSnapshot.empty(dtSnapshot.getCount(), dtSnapshot.getTotal(), dtSnapshot.getMax()); + } +} 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 d78e5b4120..dadafc3f92 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 @@ -16,7 +16,6 @@ package io.micrometer.dynatrace.v2; import com.dynatrace.metric.util.*; -import io.micrometer.core.instrument.Timer; import io.micrometer.core.instrument.*; import io.micrometer.core.instrument.distribution.HistogramSnapshot; import io.micrometer.core.instrument.distribution.ValueAtPercentile; @@ -26,11 +25,16 @@ import io.micrometer.core.util.internal.logging.InternalLoggerFactory; import io.micrometer.dynatrace.AbstractDynatraceExporter; import io.micrometer.dynatrace.DynatraceConfig; +import io.micrometer.dynatrace.types.DynatraceSummarySnapshot; +import io.micrometer.dynatrace.types.DynatraceSummarySnapshotSupport; import java.net.MalformedURLException; import java.net.URI; import java.time.Instant; -import java.util.*; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; import java.util.concurrent.TimeUnit; import java.util.function.BiFunction; import java.util.regex.Matcher; @@ -77,8 +81,7 @@ private boolean isValidEndpoint(String uri) { try { //noinspection ResultOfMethodCallIgnored URI.create(uri).toURL(); - } - catch (IllegalArgumentException | MalformedURLException ex) { + } catch (IllegalArgumentException | MalformedURLException ex) { return false; } @@ -99,9 +102,9 @@ private boolean shouldIgnoreToken(DynatraceConfig config) { private DimensionList parseDefaultDimensions(Map defaultDimensions) { List dimensions = Stream.concat( - defaultDimensions.entrySet().stream(), - staticDimensions.entrySet().stream() - ) + defaultDimensions.entrySet().stream(), + staticDimensions.entrySet().stream() + ) .map(entry -> Dimension.create(entry.getKey(), entry.getValue())) .collect(Collectors.toList()); return DimensionList.fromCollection(dimensions); @@ -169,7 +172,19 @@ private String createCounterLine(Meter meter, Measurement measurement) { } Stream toTimerLine(Timer meter) { - return toSummaryLine(meter, meter.takeSnapshot(), getBaseTimeUnit()); + if (!(meter instanceof DynatraceSummarySnapshotSupport)) { + // fall back to previous behaviour + return toSummaryLine(meter, meter.takeSnapshot(), getBaseTimeUnit()); + } + + DynatraceSummarySnapshotSupport summary = (DynatraceSummarySnapshotSupport) meter; + if (!summary.hasValues()) { + return Stream.empty(); + } + + // Take a snapshot and reset the Timer for the next export + DynatraceSummarySnapshot snapshot = summary.takeSummarySnapshotAndReset(getBaseTimeUnit()); + return createSummaryLine(meter, snapshot.getMin(), snapshot.getMax(), snapshot.getTotal(), snapshot.getCount()); } private Stream toSummaryLine(Meter meter, HistogramSnapshot histogramSnapshot, TimeUnit timeUnit) { @@ -206,7 +221,20 @@ private Stream createSummaryLine(Meter meter, double min, double max, do } Stream toDistributionSummaryLine(DistributionSummary meter) { - return toSummaryLine(meter, meter.takeSnapshot(), null); + if (!(meter instanceof DynatraceSummarySnapshotSupport)) { + // fall back to previous behaviour + return toSummaryLine(meter, meter.takeSnapshot(), null); + } + + DynatraceSummarySnapshotSupport summary = (DynatraceSummarySnapshotSupport) meter; + + if (!summary.hasValues()) { + return Stream.empty(); + } + + // Take a snapshot and reset the DistributionSummary for the next export + DynatraceSummarySnapshot snapshot = ((DynatraceSummarySnapshotSupport) meter).takeSummarySnapshotAndReset(); + return createSummaryLine(meter, snapshot.getMin(), snapshot.getMax(), snapshot.getTotal(), snapshot.getCount()); } Stream toLongTaskTimerLine(LongTaskTimer meter) { 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 265a3e8b89..930347f840 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 @@ -26,10 +26,9 @@ import static java.util.concurrent.TimeUnit.MILLISECONDS; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.entry; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.isA; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; /** * Tests for {@link DynatraceMeterRegistry}. @@ -59,7 +58,7 @@ void shouldSendProperRequest() throws Throwable { HttpSender.Request.Builder builder = HttpSender.Request.build(config.uri(), httpClient); when(httpClient.post(config.uri())).thenReturn(builder); when(httpClient.send(isA(HttpSender.Request.class))).thenReturn(new HttpSender.Response(202, - "{ \"linesOk\": 4, \"linesInvalid\": 0, \"error\": null }" + "{ \"linesOk\": 3, \"linesInvalid\": 0, \"error\": null }" )); Double gauge = meterRegistry.gauge("my.gauge", 42d); @@ -86,18 +85,21 @@ void shouldSendProperRequest() throws Throwable { assertThat(request.getEntity()).asString() .hasLineCount(3) .contains("my.counter,dt.metrics.source=micrometer count,delta=12.0 " + clock.wallTime()) - .contains("my.gauge,dt.metrics.source=micrometer gauge," + gauge.doubleValue() + " " + clock.wallTime()) - .contains("my.timer,dt.metrics.source=micrometer gauge,min=0.0,max=42.0,sum=108.0,count=4 " + clock.wallTime()); + .contains("my.gauge,dt.metrics.source=micrometer gauge," + gauge + " " + clock.wallTime()) + .contains("my.timer,dt.metrics.source=micrometer gauge,min=12.0,max=42.0,sum=108.0,count=4 " + clock.wallTime()); } @Test - void shouldTrackZerothPercentileButShouldNotPublishIt() throws Throwable { + void shouldResetBetweenRequests() throws Throwable { HttpSender.Request.Builder builder = HttpSender.Request.build(config.uri(), httpClient); when(httpClient.post(config.uri())).thenReturn(builder); - Timer timer = Timer.builder("my.timer") - .publishPercentiles(0.5) - .register(meterRegistry); + when(httpClient.send(isA(HttpSender.Request.class))).thenReturn(new HttpSender.Response(202, + "{ \"linesOk\": 1, \"linesInvalid\": 0, \"error\": null }" + )); + + Timer timer = Timer.builder("my.timer").register(meterRegistry); timer.record(22, MILLISECONDS); + timer.record(50, MILLISECONDS); clock.add(config.step()); meterRegistry.publish(); @@ -107,21 +109,38 @@ void shouldTrackZerothPercentileButShouldNotPublishIt() throws Throwable { HttpSender.Request request = argumentCaptor.getValue(); assertThat(request.getEntity()).asString() - .hasLineCount(2) - .contains("my.timer,dt.metrics.source=micrometer gauge,min=22.0,max=22.0,sum=22.0,count=1 " + clock.wallTime()) - .contains("my.timer.percentile,phi=0.5,dt.metrics.source=micrometer gauge,0.0 " + clock.wallTime()); + .hasLineCount(1) + .contains("my.timer,dt.metrics.source=micrometer gauge,min=22.0,max=50.0,sum=72.0,count=2 " + clock.wallTime()); + + // both are bigger than the previous min and smaller than the previous max. They will only show up if the + // summary was reset in between exports. + timer.record(33, MILLISECONDS); + timer.record(44, MILLISECONDS); + clock.add(config.step()); + + meterRegistry.publish(); + ArgumentCaptor argumentCaptor2 = ArgumentCaptor.forClass(HttpSender.Request.class); + // needs to be two, since the previous request is also counted. + verify(httpClient, times(2)).send(argumentCaptor2.capture()); + HttpSender.Request request2 = argumentCaptor2.getValue(); + + assertThat(request2.getEntity()).asString() + .hasLineCount(1) + .contains("my.timer,dt.metrics.source=micrometer gauge,min=33.0,max=44.0,sum=77.0,count=2 " + clock.wallTime()); } + @Test - void shouldPublishZerothPercentileIfAlreadyDefined() throws Throwable { + void shouldNotTrackPercentilesWithDynatraceSummary() throws Throwable { HttpSender.Request.Builder builder = HttpSender.Request.build(config.uri(), httpClient); when(httpClient.post(config.uri())).thenReturn(builder); Timer timer = Timer.builder("my.timer") - .publishPercentiles(0.5, 0.0) + .publishPercentiles(0.5, 0.75, 0.9, 0.99) .register(meterRegistry); timer.record(22, MILLISECONDS); - clock.add(config.step()); + timer.record(55, MILLISECONDS); + clock.add(config.step()); meterRegistry.publish(); ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(HttpSender.Request.class); @@ -129,22 +148,19 @@ void shouldPublishZerothPercentileIfAlreadyDefined() throws Throwable { HttpSender.Request request = argumentCaptor.getValue(); assertThat(request.getEntity()).asString() - .hasLineCount(3) - .contains("my.timer,dt.metrics.source=micrometer gauge,min=22.0,max=22.0,sum=22.0,count=1 " + clock.wallTime()) - .contains("my.timer.percentile,phi=0,dt.metrics.source=micrometer gauge,0.0 " + clock.wallTime()) - .contains("my.timer.percentile,phi=0.5,dt.metrics.source=micrometer gauge,0.0 " + clock.wallTime()); + .hasLineCount(1) + .contains("my.timer,dt.metrics.source=micrometer gauge,min=22.0,max=55.0,sum=77.0,count=2 " + clock.wallTime()); } @Test - void shouldPublishZerothPercentileIfExclusivelyDefined() throws Throwable { + void shouldNotExportLinesWithZeroCount() throws Throwable { HttpSender.Request.Builder builder = HttpSender.Request.build(config.uri(), httpClient); when(httpClient.post(config.uri())).thenReturn(builder); - Timer timer = Timer.builder("my.timer") - .publishPercentiles(0.0) - .register(meterRegistry); - timer.record(22, MILLISECONDS); - clock.add(config.step()); + Timer timer = Timer.builder("my.timer").register(meterRegistry); + // ---> first export interval, one request is sent: + timer.record(44, MILLISECONDS); + clock.add(config.step()); meterRegistry.publish(); ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(HttpSender.Request.class); @@ -152,9 +168,36 @@ void shouldPublishZerothPercentileIfExclusivelyDefined() throws Throwable { HttpSender.Request request = argumentCaptor.getValue(); assertThat(request.getEntity()).asString() - .hasLineCount(2) - .contains("my.timer,dt.metrics.source=micrometer gauge,min=22.0,max=22.0,sum=22.0,count=1 " + clock.wallTime()) - .contains("my.timer.percentile,phi=0,dt.metrics.source=micrometer gauge,0.0 " + clock.wallTime()); + .hasLineCount(1) + .contains("my.timer,dt.metrics.source=micrometer gauge,min=44.0,max=44.0,sum=44.0,count=1 " + clock.wallTime()); + + // reset for next export interval + reset(httpClient); + when(httpClient.post(config.uri())).thenReturn(builder); + + // ---> second export interval, no values are recorded + clock.add(config.step()); + meterRegistry.publish(); + + // if the line has 0 count, dont send anything + verify(httpClient, never()).send(any()); + + // reset for next export interval + reset(httpClient); + when(httpClient.post(config.uri())).thenReturn(builder); + + // ---> third export interval + timer.record(33, MILLISECONDS); + clock.add(config.step()); + meterRegistry.publish(); + + ArgumentCaptor argumentCaptor2 = ArgumentCaptor.forClass(HttpSender.Request.class); + verify(httpClient).send(argumentCaptor2.capture()); + HttpSender.Request request2 = argumentCaptor2.getValue(); + + assertThat(request2.getEntity()).asString() + .hasLineCount(1) + .contains("my.timer,dt.metrics.source=micrometer gauge,min=33.0,max=33.0,sum=33.0,count=1 " + clock.wallTime()); } private DynatraceConfig createDefaultDynatraceConfig() { diff --git a/implementations/micrometer-registry-dynatrace/src/test/java/io/micrometer/dynatrace/types/DynatraceDistributionSummaryTest.java b/implementations/micrometer-registry-dynatrace/src/test/java/io/micrometer/dynatrace/types/DynatraceDistributionSummaryTest.java new file mode 100644 index 0000000000..7f34aade25 --- /dev/null +++ b/implementations/micrometer-registry-dynatrace/src/test/java/io/micrometer/dynatrace/types/DynatraceDistributionSummaryTest.java @@ -0,0 +1,158 @@ +/* + * Copyright 2022 VMware, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micrometer.dynatrace.types; + +import io.micrometer.core.instrument.Clock; +import io.micrometer.core.instrument.Meter; +import io.micrometer.core.instrument.MockClock; +import io.micrometer.core.instrument.Tags; +import io.micrometer.core.instrument.distribution.DistributionStatisticConfig; +import org.assertj.core.data.Offset; +import org.junit.jupiter.api.Test; + +import java.util.concurrent.TimeUnit; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +/** + * Tests for {@link DynatraceDistributionSummary}. + * + * @author Georg Pirklbauer + * @since 1.9.0 + */ +class DynatraceDistributionSummaryTest { + private static final Offset OFFSET = Offset.offset(0.0001); + private static final Meter.Id ID = new Meter.Id("test.id", Tags.empty(), "1", "desc", Meter.Type.DISTRIBUTION_SUMMARY); + private static final DistributionStatisticConfig DISTRIBUTION_STATISTIC_CONFIG = DistributionStatisticConfig.NONE; + private static final Clock CLOCK = new MockClock(); + + @Test + void testHasValues() { + DynatraceDistributionSummary ds = new DynatraceDistributionSummary(ID, CLOCK, DISTRIBUTION_STATISTIC_CONFIG, 1); + assertThat(ds.hasValues()).isFalse(); + ds.record(3.14); + assertThat(ds.hasValues()).isTrue(); + + // reset, hasValues should be initially false + ds.takeSummarySnapshotAndReset(); + assertThat(ds.hasValues()).isFalse(); + + // add invalid value, hasValues stays false + ds.record(-1.234); + assertThat(ds.hasValues()).isFalse(); + } + + @Test + void testDynatraceDistributionSummary() { + DynatraceDistributionSummary ds = new DynatraceDistributionSummary(ID, CLOCK, DISTRIBUTION_STATISTIC_CONFIG, 1); + ds.record(3.14); + ds.record(4.76); + + assertMinMaxSumCount(ds, 3.14, 4.76, 7.9, 2); + } + + @Test + void testDynatraceDistributionSummaryScaled() { + double scale = 1.5; + DynatraceDistributionSummary ds = new DynatraceDistributionSummary(ID, CLOCK, DISTRIBUTION_STATISTIC_CONFIG, scale); + ds.record(3.14); + ds.record(4.76); + + assertMinMaxSumCount(ds, 3.14 * scale, 4.76 * scale, 7.9 * scale, 2); + } + + @Test + void testRecordNegativeIgnored() { + DynatraceDistributionSummary ds = new DynatraceDistributionSummary(ID, CLOCK, DISTRIBUTION_STATISTIC_CONFIG, 1); + ds.record(3.14); + ds.record(-1.234); + ds.record(4.76); + ds.record(-6.789); + + assertMinMaxSumCount(ds, 3.14, 4.76, 7.9, 2); + } + + @Test + void testMinMaxAreOverwritten() { + DynatraceDistributionSummary ds = new DynatraceDistributionSummary(ID, CLOCK, DISTRIBUTION_STATISTIC_CONFIG, 1); + ds.record(3.14); + ds.record(4.76); + ds.record(0.123); + ds.record(8.93); + + assertMinMaxSumCount(ds, 0.123, 8.93, 16.953, 4); + } + + + @Test + void testGetSnapshotNoReset() { + DynatraceDistributionSummary ds = new DynatraceDistributionSummary(ID, CLOCK, DISTRIBUTION_STATISTIC_CONFIG, 1); + ds.record(3.14); + ds.record(4.76); + + assertMinMaxSumCount(ds.takeSummarySnapshot(), 3.14, 4.76, 7.9, 2); + // run twice to make sure its not reset in between + assertMinMaxSumCount(ds.takeSummarySnapshot(), 3.14, 4.76, 7.9, 2); + } + + @Test + void testGetSnapshotNoResetWithTimeUnitIgnored() { + DynatraceDistributionSummary ds = new DynatraceDistributionSummary(ID, CLOCK, DISTRIBUTION_STATISTIC_CONFIG, 1); + ds.record(3.14); + ds.record(4.76); + + assertMinMaxSumCount(ds.takeSummarySnapshot(TimeUnit.MINUTES), 3.14, 4.76, 7.9, 2); + // run twice to make sure its not reset in between + assertMinMaxSumCount(ds.takeSummarySnapshot(TimeUnit.MINUTES), 3.14, 4.76, 7.9, 2); + } + + + @Test + void testGetSnapshotAndReset() { + DynatraceDistributionSummary ds = new DynatraceDistributionSummary(ID, CLOCK, DISTRIBUTION_STATISTIC_CONFIG, 1); + ds.record(3.14); + ds.record(4.76); + + assertMinMaxSumCount(ds.takeSummarySnapshotAndReset(), 3.14, 4.76, 7.9, 2); + // run twice to make sure its not reset in between + assertMinMaxSumCount(ds.takeSummarySnapshotAndReset(), 0d, 0d, 0d, 0); + } + + @Test + void testGetSnapshotAndResetWithTimeUnitIgnored() { + DynatraceDistributionSummary ds = new DynatraceDistributionSummary(ID, CLOCK, DISTRIBUTION_STATISTIC_CONFIG, 1); + ds.record(3.14); + ds.record(4.76); + + assertMinMaxSumCount(ds.takeSummarySnapshotAndReset(TimeUnit.MINUTES), 3.14, 4.76, 7.9, 2); + // run twice to make sure its not reset in between + assertMinMaxSumCount(ds.takeSummarySnapshotAndReset(TimeUnit.MINUTES), 0d, 0d, 0d, 0); + } + + private void assertMinMaxSumCount(DynatraceDistributionSummary ds, double expMin, double expMax, double expTotal, long expCount) { + assertThat(ds.min()).isCloseTo(expMin, OFFSET); + assertThat(ds.max()).isCloseTo(expMax, OFFSET); + assertThat(ds.totalAmount()).isCloseTo(expTotal, OFFSET); + assertThat(ds.count()).isEqualTo(expCount); + } + + private void assertMinMaxSumCount(DynatraceSummarySnapshot snapshot, double expMin, double expMax, double expTotal, long expCount) { + assertThat(snapshot.getMin()).isCloseTo(expMin, OFFSET); + assertThat(snapshot.getMax()).isCloseTo(expMax, OFFSET); + assertThat(snapshot.getTotal()).isCloseTo(expTotal, OFFSET); + assertThat(snapshot.getCount()).isEqualTo(expCount); + } +} diff --git a/implementations/micrometer-registry-dynatrace/src/test/java/io/micrometer/dynatrace/types/DynatraceSummaryTest.java b/implementations/micrometer-registry-dynatrace/src/test/java/io/micrometer/dynatrace/types/DynatraceSummaryTest.java new file mode 100644 index 0000000000..8c955515d5 --- /dev/null +++ b/implementations/micrometer-registry-dynatrace/src/test/java/io/micrometer/dynatrace/types/DynatraceSummaryTest.java @@ -0,0 +1,82 @@ +/* + * Copyright 2022 VMware, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micrometer.dynatrace.types; + +import org.assertj.core.data.Offset; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; + + +/** + * Tests for {@link DynatraceSummary}. + * + * @author Georg Pirklbauer + * @since 1.9.0 + */ +class DynatraceSummaryTest { + private static final Offset OFFSET = Offset.offset(0.0001); + + @Test + void testRecordValues() { + DynatraceSummary summary = new DynatraceSummary(); + summary.recordNonNegative(3.14); + summary.recordNonNegative(4.76); + + assertMinMaxSumCount(summary, 3.14, 4.76, 7.9, 2); + } + + @Test + void testRecordNegativeIgnored() { + DynatraceSummary summary = new DynatraceSummary(); + summary.recordNonNegative(3.14); + summary.recordNonNegative(-1.234); + summary.recordNonNegative(4.76); + summary.recordNonNegative(-6.789); + + assertMinMaxSumCount(summary, 3.14, 4.76, 7.9, 2); + } + + @Test + void testReset() { + DynatraceSummary summary = new DynatraceSummary(); + summary.recordNonNegative(3.14); + summary.recordNonNegative(4.76); + + assertMinMaxSumCount(summary, 3.14, 4.76, 7.9, 2); + summary.reset(); + assertMinMaxSumCount(summary, 0d, 0d, 0d, 0); + } + + @Test + void testMinMaxAreOverwritten() { + DynatraceSummary summary = new DynatraceSummary(); + summary.recordNonNegative(3.14); + summary.recordNonNegative(4.76); + summary.recordNonNegative(0.123); + summary.recordNonNegative(8.93); + + assertMinMaxSumCount(summary, 0.123, 8.93, 16.953, 4); + } + + + private void assertMinMaxSumCount(DynatraceSummary summary, Double expMin, Double expMax, Double expTotal, long expCount) { + assertThat(summary.getMin()).isCloseTo(expMin, OFFSET); + assertThat(summary.getMax()).isCloseTo(expMax, OFFSET); + assertThat(summary.getCount()).isEqualTo(expCount); + assertThat(summary.getTotal()).isCloseTo(expTotal, OFFSET); + } +} diff --git a/implementations/micrometer-registry-dynatrace/src/test/java/io/micrometer/dynatrace/types/DynatraceTimerTest.java b/implementations/micrometer-registry-dynatrace/src/test/java/io/micrometer/dynatrace/types/DynatraceTimerTest.java new file mode 100644 index 0000000000..c263bcedfc --- /dev/null +++ b/implementations/micrometer-registry-dynatrace/src/test/java/io/micrometer/dynatrace/types/DynatraceTimerTest.java @@ -0,0 +1,184 @@ +/* + * Copyright 2022 VMware, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micrometer.dynatrace.types; + +import io.micrometer.core.instrument.Clock; +import io.micrometer.core.instrument.Meter; +import io.micrometer.core.instrument.MockClock; +import io.micrometer.core.instrument.Tags; +import io.micrometer.core.instrument.distribution.DistributionStatisticConfig; +import io.micrometer.core.instrument.distribution.pause.NoPauseDetector; +import io.micrometer.core.instrument.distribution.pause.PauseDetector; +import org.assertj.core.data.Offset; +import org.junit.jupiter.api.Test; + +import java.time.Duration; +import java.util.concurrent.TimeUnit; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +/** + * Tests for {@link DynatraceTimer}. + * + * @author Georg Pirklbauer + * @since 1.9.0 + */ +class DynatraceTimerTest { + private static final Offset OFFSET = Offset.offset(0.0001); + private static final Clock CLOCK = new MockClock(); + private static final TimeUnit BASE_TIME_UNIT = TimeUnit.MILLISECONDS; + private static final Meter.Id ID = new Meter.Id("test.id", Tags.empty(), "1", "desc", Meter.Type.TIMER); + private static final DistributionStatisticConfig DISTRIBUTION_STATISTIC_CONFIG = DistributionStatisticConfig.NONE; + private static final PauseDetector PAUSE_DETECTOR = new NoPauseDetector(); + + @Test + void testHasValues() { + DynatraceTimer timer = new DynatraceTimer(ID, CLOCK, DISTRIBUTION_STATISTIC_CONFIG, PAUSE_DETECTOR, BASE_TIME_UNIT); + assertThat(timer.hasValues()).isFalse(); + timer.record(Duration.ofMillis(314)); + assertThat(timer.hasValues()).isTrue(); + timer.record(Duration.ofMillis(476)); + assertThat(timer.hasValues()).isTrue(); + + // checks that the recorded values are returned in the TimeUnit used to set up the instrument + assertMinMaxSumCount(timer, 314, 476, 790, 2); + assertThat(timer.hasValues()).isTrue(); + assertMinMaxSumCount(timer.takeSummarySnapshotAndReset(), 314, 476, 790, 2); + assertThat(timer.hasValues()).isFalse(); + + timer.record(-100, TimeUnit.MILLISECONDS); + assertThat(timer.hasValues()).isFalse(); + } + + @Test + void testNegativeValuesIgnored() { + DynatraceTimer timer = new DynatraceTimer(ID, CLOCK, DISTRIBUTION_STATISTIC_CONFIG, PAUSE_DETECTOR, BASE_TIME_UNIT); + timer.record(-100, TimeUnit.MILLISECONDS); + timer.record(Duration.ofMillis(-100)); + assertMinMaxSumCount(timer, 0, 0, 0, 0); + } + + @Test + void testMinMaxAreOverwritten() { + DynatraceTimer timer = new DynatraceTimer(ID, CLOCK, DISTRIBUTION_STATISTIC_CONFIG, PAUSE_DETECTOR, BASE_TIME_UNIT); + timer.record(Duration.ofMillis(314)); + timer.record(Duration.ofMillis(476)); + assertMinMaxSumCount(timer, 314, 476, 790, 2); + timer.record(Duration.ofMillis(123)); + timer.record(Duration.ofMillis(579)); + assertMinMaxSumCount(timer, 123, 579, 1492, 4); + } + + @Test + void testGetSnapshotAndReset() { + DynatraceTimer timer = new DynatraceTimer(ID, CLOCK, DISTRIBUTION_STATISTIC_CONFIG, PAUSE_DETECTOR, BASE_TIME_UNIT); + timer.record(Duration.ofMillis(314)); + timer.record(Duration.ofMillis(476)); + + assertMinMaxSumCount(timer.takeSummarySnapshotAndReset(BASE_TIME_UNIT), 314, 476, 790, 2); + // check that the timer was indeed reset + assertMinMaxSumCount(timer.takeSummarySnapshot(BASE_TIME_UNIT), 0d, 0d, 0d, 0); + } + + @Test + void testGetSnapshotAndResetWithNoTimeUnit() { + DynatraceTimer timer = new DynatraceTimer(ID, CLOCK, DISTRIBUTION_STATISTIC_CONFIG, PAUSE_DETECTOR, BASE_TIME_UNIT); + // record in a time unit that is not the base time unit + timer.record(Duration.ofSeconds(1)); + timer.record(Duration.ofSeconds(2)); + + // checks that the recorded values are returned in the TimeUnit used to set up the instrument + assertMinMaxSumCount(timer.takeSummarySnapshotAndReset(), 1000, 2000, 3000, 2); + // check that the timer was indeed reset + assertMinMaxSumCount(timer.takeSummarySnapshot(), 0d, 0d, 0d, 0); + } + + @Test + void testGetSnapshot() { + DynatraceTimer timer = new DynatraceTimer(ID, CLOCK, DISTRIBUTION_STATISTIC_CONFIG, PAUSE_DETECTOR, BASE_TIME_UNIT); + timer.record(Duration.ofMillis(314)); + timer.record(Duration.ofMillis(476)); + + assertMinMaxSumCount(timer.takeSummarySnapshot(BASE_TIME_UNIT), 314, 476, 790, 2); + // check that the timer was not reset + assertMinMaxSumCount(timer.takeSummarySnapshot(BASE_TIME_UNIT), 314, 476, 790, 2); + } + + @Test + void testGetSnapshotWithNoTimeUnit() { + DynatraceTimer timer = new DynatraceTimer(ID, CLOCK, DISTRIBUTION_STATISTIC_CONFIG, PAUSE_DETECTOR, BASE_TIME_UNIT); + // record in a time unit that is not the base time unit + timer.record(Duration.ofSeconds(1)); + timer.record(Duration.ofSeconds(2)); + + // checks that the recorded values are returned in the TimeUnit used to set up the instrument + assertMinMaxSumCount(timer.takeSummarySnapshot(), 1000, 2000, 3000, 2); + // check that the timer was not reset + assertMinMaxSumCount(timer.takeSummarySnapshot(), 1000, 2000, 3000, 2); + } + + @Test + void testDifferentTimeUnits() { + // set up the timer to record in Nanoseconds + DynatraceTimer timer = new DynatraceTimer(ID, CLOCK, DISTRIBUTION_STATISTIC_CONFIG, PAUSE_DETECTOR, TimeUnit.NANOSECONDS); + Duration oneSecond = Duration.ofSeconds(1); + timer.record(oneSecond); + assertThat(timer.totalTime(TimeUnit.NANOSECONDS)).isCloseTo(1_000_000_000, OFFSET); + // when requesting the time in a different unit, it is converted to that unit before being returned + assertThat(timer.totalTime(TimeUnit.SECONDS)).isCloseTo(1d, OFFSET); + assertThat(timer.totalTime(TimeUnit.MILLISECONDS)).isCloseTo(1000, OFFSET); + } + + @Test + void testUseAllRecordInterfaces() { + MockClock clock = new MockClock(); + DynatraceTimer timer = new DynatraceTimer(ID, clock, DISTRIBUTION_STATISTIC_CONFIG, PAUSE_DETECTOR, BASE_TIME_UNIT); + + // Runnable + timer.record(() -> { + // Simulate the passing of time by using the MockClock interface + clock.add(Duration.ofMillis(100)); + }); + + // Supplier + timer.record(() -> { + clock.add(Duration.ofMillis(200)); + return 0; + }); + + // Duration + timer.record(Duration.ofMillis(300)); + + // Amount & Unit + timer.record(400, TimeUnit.MILLISECONDS); + + assertMinMaxSumCount(timer.takeSummarySnapshot(), 100, 400, 1000, 4); + } + + private static void assertMinMaxSumCount(DynatraceTimer timer, double expMin, double expMax, double expTotal, long expCount) { + assertThat(timer.min(BASE_TIME_UNIT)).isCloseTo(expMin, OFFSET); + assertThat(timer.max(BASE_TIME_UNIT)).isCloseTo(expMax, OFFSET); + assertThat(timer.totalTime(BASE_TIME_UNIT)).isCloseTo(expTotal, OFFSET); + assertThat(timer.count()).isEqualTo(expCount); + } + + private void assertMinMaxSumCount(DynatraceSummarySnapshot snapshot, double expMin, double expMax, double expTotal, long expCount) { + assertThat(snapshot.getMin()).isCloseTo(expMin, OFFSET); + assertThat(snapshot.getMax()).isCloseTo(expMax, OFFSET); + assertThat(snapshot.getTotal()).isCloseTo(expTotal, OFFSET); + assertThat(snapshot.getCount()).isEqualTo(expCount); + } +} 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 063eca57f0..81930b5993 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 @@ -227,7 +227,7 @@ void toTimerLine() { List lines = exporter.toTimerLine(timer).collect(Collectors.toList()); assertThat(lines).hasSize(1); - assertThat(lines.get(0)).isEqualTo("my.timer,dt.metrics.source=micrometer gauge,min=0.0,max=60.0,sum=90.0,count=3 " + clock.wallTime()); + assertThat(lines.get(0)).isEqualTo("my.timer,dt.metrics.source=micrometer gauge,min=10.0,max=60.0,sum=90.0,count=3 " + clock.wallTime()); } @Test @@ -341,7 +341,7 @@ void testToDistributionSummaryLine() { List lines = exporter.toDistributionSummaryLine(summary).collect(Collectors.toList()); assertThat(lines).hasSize(1); - assertThat(lines.get(0)).isEqualTo("my.summary,dt.metrics.source=micrometer gauge,min=0.0,max=5.4,sum=10.9,count=4 " + clock.wallTime()); + assertThat(lines.get(0)).isEqualTo("my.summary,dt.metrics.source=micrometer gauge,min=0.1,max=5.4,sum=10.9,count=4 " + clock.wallTime()); } @Test