diff --git a/implementations/micrometer-registry-dynatrace/src/main/java/io/micrometer/dynatrace/AbstractDynatraceExporter.java b/implementations/micrometer-registry-dynatrace/src/main/java/io/micrometer/dynatrace/AbstractDynatraceExporter.java
new file mode 100644
index 0000000000..0242febbc7
--- /dev/null
+++ b/implementations/micrometer-registry-dynatrace/src/main/java/io/micrometer/dynatrace/AbstractDynatraceExporter.java
@@ -0,0 +1,47 @@
+/**
+ * Copyright 2021 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;
+
+import io.micrometer.core.instrument.Clock;
+import io.micrometer.core.instrument.MeterRegistry;
+import io.micrometer.core.ipc.http.HttpSender;
+
+import javax.annotation.Nonnull;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Base class for implementations of Dynatrace exporters.
+ *
+ * @author Georg Pirklbauer
+ */
+public abstract class AbstractDynatraceExporter {
+
+ protected DynatraceConfig config;
+ protected Clock clock;
+ protected HttpSender httpClient;
+
+ public abstract void export(@Nonnull MeterRegistry registry);
+
+ public TimeUnit getBaseTimeUnit() {
+ return TimeUnit.MILLISECONDS;
+ }
+
+ public AbstractDynatraceExporter(DynatraceConfig config, Clock clock, HttpSender httpClient) {
+ this.config = config;
+ this.clock = clock;
+ this.httpClient = httpClient;
+ }
+}
diff --git a/implementations/micrometer-registry-dynatrace/src/main/java/io/micrometer/dynatrace/DynatraceApiVersion.java b/implementations/micrometer-registry-dynatrace/src/main/java/io/micrometer/dynatrace/DynatraceApiVersion.java
new file mode 100644
index 0000000000..f28255261b
--- /dev/null
+++ b/implementations/micrometer-registry-dynatrace/src/main/java/io/micrometer/dynatrace/DynatraceApiVersion.java
@@ -0,0 +1,24 @@
+/**
+ * Copyright 2021 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;
+
+/**
+ * An enum containing valid Dynatrace API versions.
+ */
+public enum DynatraceApiVersion {
+ V1,
+}
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 24d6cdc925..0e58b8cec6 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
@@ -27,6 +27,7 @@
* Configuration for {@link DynatraceMeterRegistry}
*
* @author Oriol Barcelona
+ * @author Georg Pirklbauer
*/
public interface DynatraceConfig extends StepRegistryConfig {
@@ -49,7 +50,7 @@ default String deviceId() {
default String technologyType() {
return getSecret(this, "technologyType")
- .map(v -> StringUtils.isEmpty(v) ? "java" : v)
+ .map(val -> StringUtils.isEmpty(val) ? "java" : val)
.get();
}
@@ -64,8 +65,28 @@ default String group() {
return get(prefix() + ".group");
}
+ /**
+ * Return the version of the target Dynatrace API.
+ *
+ * @return a {@link DynatraceApiVersion} containing the version of the targeted Dynatrace API.
+ */
+ default DynatraceApiVersion apiVersion() {
+ // if not specified, defaults to v1 for backwards compatibility.
+ return getEnum(this, DynatraceApiVersion.class, "apiVersion")
+ .orElse(DynatraceApiVersion.V1);
+ }
+
@Override
default Validated> validate() {
+ Validated apiVersionValidation = checkRequired("apiVersion", DynatraceConfig::apiVersion).apply(this);
+ if (apiVersionValidation.isInvalid()) {
+ return apiVersionValidation;
+ }
+
+ return validateV1();
+ }
+
+ default Validated> validateV1() {
return checkAll(this,
c -> StepRegistryConfig.validate(c),
checkRequired("apiToken", DynatraceConfig::apiToken),
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 88be9aa225..8f632ca71f 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
@@ -15,31 +15,17 @@
*/
package io.micrometer.dynatrace;
-import io.micrometer.core.instrument.*;
-import io.micrometer.core.instrument.distribution.HistogramSnapshot;
+import io.micrometer.core.instrument.Clock;
import io.micrometer.core.instrument.step.StepMeterRegistry;
-import io.micrometer.core.instrument.util.MeterPartition;
import io.micrometer.core.instrument.util.NamedThreadFactory;
-import io.micrometer.core.instrument.util.StringUtils;
import io.micrometer.core.ipc.http.HttpSender;
import io.micrometer.core.ipc.http.HttpUrlConnectionSender;
-import io.micrometer.core.lang.Nullable;
+import io.micrometer.dynatrace.v1.DynatraceExporterV1;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
-import java.util.stream.StreamSupport;
-
-import static io.micrometer.dynatrace.DynatraceMetricDefinition.DynatraceUnit;
-import static java.nio.charset.StandardCharsets.UTF_8;
/**
* {@link StepMeterRegistry} for Dynatrace.
@@ -48,259 +34,45 @@
* @author Jon Schneider
* @author Johnny Lim
* @author PJ Fanning
+ * @author Georg Pirklbauer
* @since 1.1.0
*/
public class DynatraceMeterRegistry extends StepMeterRegistry {
private static final ThreadFactory DEFAULT_THREAD_FACTORY = new NamedThreadFactory("dynatrace-metrics-publisher");
- private static final int MAX_MESSAGE_SIZE = 15360; //max message size in bytes that Dynatrace will accept
- private final Logger logger = LoggerFactory.getLogger(DynatraceMeterRegistry.class);
- private final DynatraceConfig config;
- private final HttpSender httpClient;
- /**
- * Metric names for which we have created the custom metric in the API
- */
- private final Set createdCustomMetrics = ConcurrentHashMap.newKeySet();
- private final String customMetricEndpointTemplate;
-
- @SuppressWarnings("deprecation")
- public DynatraceMeterRegistry(DynatraceConfig config, Clock clock) {
- this(config, clock, DEFAULT_THREAD_FACTORY, new HttpUrlConnectionSender(config.connectTimeout(), config.readTimeout()));
- }
+ private final AbstractDynatraceExporter exporter;
+ private static final Logger logger = LoggerFactory.getLogger(DynatraceMeterRegistry.class.getName());
private DynatraceMeterRegistry(DynatraceConfig config, Clock clock, ThreadFactory threadFactory, HttpSender httpClient) {
super(config, clock);
-
- this.config = config;
- this.httpClient = httpClient;
-
- config().namingConvention(new DynatraceNamingConvention());
-
- this.customMetricEndpointTemplate = config.uri() + "/api/v1/timeseries/";
-
start(threadFactory);
- }
-
- public static Builder builder(DynatraceConfig config) {
- return new Builder(config);
- }
-
- @Override
- protected void publish() {
- String customDeviceMetricEndpoint = config.uri() + "/api/v1/entity/infrastructure/custom/" +
- config.deviceId() + "?api-token=" + config.apiToken();
-
- for (List batch : MeterPartition.partition(this, config.batchSize())) {
- final List series = batch.stream()
- .flatMap(meter -> meter.match(
- this::writeMeter,
- this::writeMeter,
- this::writeTimer,
- this::writeSummary,
- this::writeLongTaskTimer,
- this::writeMeter,
- this::writeMeter,
- this::writeFunctionTimer,
- this::writeMeter)
- )
- .collect(Collectors.toList());
-
- // TODO is there a way to batch submissions of multiple metrics?
- series.stream()
- .map(DynatraceCustomMetric::getMetricDefinition)
- .filter(this::isCustomMetricNotCreated)
- .forEach(this::putCustomMetric);
-
- if (!createdCustomMetrics.isEmpty() && !series.isEmpty()) {
- postCustomMetricValues(
- config.technologyType(),
- config.group(),
- series.stream()
- .map(DynatraceCustomMetric::getTimeSeries)
- .filter(this::isCustomMetricCreated)
- .collect(Collectors.toList()),
- customDeviceMetricEndpoint);
- }
- }
- }
-
- // VisibleForTesting
- Stream writeMeter(Meter meter) {
- final long wallTime = clock.wallTime();
- return StreamSupport.stream(meter.measure().spliterator(), false)
- .map(Measurement::getValue)
- .filter(Double::isFinite)
- .map(value -> createCustomMetric(meter.getId(), wallTime, value));
- }
-
- private Stream writeLongTaskTimer(LongTaskTimer longTaskTimer) {
- final long wallTime = clock.wallTime();
- final Meter.Id id = longTaskTimer.getId();
- return Stream.of(
- createCustomMetric(idWithSuffix(id, "activeTasks"), wallTime, longTaskTimer.activeTasks(), DynatraceUnit.Count),
- createCustomMetric(idWithSuffix(id, "count"), wallTime, longTaskTimer.duration(getBaseTimeUnit())));
- }
-
- private Stream writeSummary(DistributionSummary summary) {
- final long wallTime = clock.wallTime();
- final Meter.Id id = summary.getId();
- final HistogramSnapshot snapshot = summary.takeSnapshot();
-
- return Stream.of(
- createCustomMetric(idWithSuffix(id, "sum"), wallTime, snapshot.total(getBaseTimeUnit())),
- createCustomMetric(idWithSuffix(id, "count"), wallTime, snapshot.count(), DynatraceUnit.Count),
- createCustomMetric(idWithSuffix(id, "avg"), wallTime, snapshot.mean(getBaseTimeUnit())),
- createCustomMetric(idWithSuffix(id, "max"), wallTime, snapshot.max(getBaseTimeUnit())));
- }
-
- private Stream writeFunctionTimer(FunctionTimer timer) {
- final long wallTime = clock.wallTime();
- final Meter.Id id = timer.getId();
-
- return Stream.of(
- createCustomMetric(idWithSuffix(id, "count"), wallTime, timer.count(), DynatraceUnit.Count),
- createCustomMetric(idWithSuffix(id, "avg"), wallTime, timer.mean(getBaseTimeUnit())),
- createCustomMetric(idWithSuffix(id, "sum"), wallTime, timer.totalTime(getBaseTimeUnit())));
- }
-
- private Stream writeTimer(Timer timer) {
- final long wallTime = clock.wallTime();
- final Meter.Id id = timer.getId();
- final HistogramSnapshot snapshot = timer.takeSnapshot();
-
- return Stream.of(
- createCustomMetric(idWithSuffix(id, "sum"), wallTime, snapshot.total(getBaseTimeUnit())),
- createCustomMetric(idWithSuffix(id, "count"), wallTime, snapshot.count(), DynatraceUnit.Count),
- createCustomMetric(idWithSuffix(id, "avg"), wallTime, snapshot.mean(getBaseTimeUnit())),
- createCustomMetric(idWithSuffix(id, "max"), wallTime, snapshot.max(getBaseTimeUnit())));
- }
- private DynatraceCustomMetric createCustomMetric(Meter.Id id, long time, Number value) {
- return createCustomMetric(id, time, value, DynatraceUnit.fromPlural(id.getBaseUnit()));
- }
-
- private DynatraceCustomMetric createCustomMetric(Meter.Id id, long time, Number value, @Nullable DynatraceUnit unit) {
- final String metricId = getConventionName(id);
- final List tags = getConventionTags(id);
- return new DynatraceCustomMetric(
- new DynatraceMetricDefinition(metricId, id.getDescription(), unit, extractDimensions(tags), new String[]{config.technologyType()}, config.group()),
- new DynatraceTimeSeries(metricId, time, value.doubleValue(), extractDimensionValues(tags)));
- }
-
- private Set extractDimensions(List tags) {
- return tags.stream().map(Tag::getKey).collect(Collectors.toSet());
- }
-
- private Map extractDimensionValues(List tags) {
- return tags.stream().collect(Collectors.toMap(Tag::getKey, Tag::getValue));
- }
-
- private boolean isCustomMetricNotCreated(final DynatraceMetricDefinition metric) {
- return !createdCustomMetrics.contains(metric.getMetricId());
- }
-
- private boolean isCustomMetricCreated(final DynatraceTimeSeries timeSeries) {
- return createdCustomMetrics.contains(timeSeries.getMetricId());
- }
-
- // VisibleForTesting
- void putCustomMetric(final DynatraceMetricDefinition customMetric) {
- try {
- httpClient.put(customMetricEndpointTemplate + customMetric.getMetricId() + "?api-token=" + config.apiToken())
- .withJsonContent(customMetric.asJson())
- .send()
- .onSuccess(response -> {
- logger.debug("created {} as custom metric in dynatrace", customMetric.getMetricId());
- createdCustomMetrics.add(customMetric.getMetricId());
- })
- .onError(response -> {
- if (logger.isErrorEnabled()) {
- logger.error("failed to create custom metric {} in dynatrace: {}", customMetric.getMetricId(),
- response.body());
- }
- });
- } catch (Throwable e) {
- logger.error("failed to create custom metric in dynatrace: {}", customMetric.getMetricId(), e);
+ if (config.apiVersion() == DynatraceApiVersion.V1) {
+ logger.info("Using Dynatrace v1 exporter.");
+ this.exporter = new DynatraceExporterV1(config, clock, httpClient);
+ } else {
+ throw new IllegalArgumentException("Only v1 export is available at the moment.");
}
}
- private void postCustomMetricValues(String type, String group, List timeSeries, String customDeviceMetricEndpoint) {
- try {
- for (DynatraceBatchedPayload postMessage : createPostMessages(type, group, timeSeries)) {
- httpClient.post(customDeviceMetricEndpoint)
- .withJsonContent(postMessage.payload)
- .send()
- .onSuccess(response -> {
- if (logger.isDebugEnabled()) {
- logger.debug("successfully sent {} metrics to Dynatrace ({} bytes).",
- postMessage.metricCount, postMessage.payload.getBytes(UTF_8).length);
- }
- })
- .onError(response -> {
- logger.error("failed to send metrics to dynatrace: {}", response.body());
- logger.debug("failed metrics payload: {}", postMessage.payload);
- });
- }
- } catch (Throwable e) {
- logger.error("failed to send metrics to dynatrace", e);
- }
- }
-
- // VisibleForTesting
- List createPostMessages(String type, String group, List timeSeries) {
- final String header = "{\"type\":\"" + type + '\"'
- + (StringUtils.isNotBlank(group) ? ",\"group\":\"" + group + '\"' : "")
- + ",\"series\":[";
- final String footer = "]}";
- final int headerFooterBytes = header.getBytes(UTF_8).length + footer.getBytes(UTF_8).length;
- final int maxMessageSize = MAX_MESSAGE_SIZE - headerFooterBytes;
- List payloadBodies = createPostMessageBodies(timeSeries, maxMessageSize);
- return payloadBodies.stream().map(body -> {
- String message = header + body.payload + footer;
- return new DynatraceBatchedPayload(message, body.metricCount);
- }).collect(Collectors.toList());
+ @SuppressWarnings("deprecation")
+ public DynatraceMeterRegistry(DynatraceConfig config, Clock clock) {
+ this(config, clock, DEFAULT_THREAD_FACTORY, new HttpUrlConnectionSender(config.connectTimeout(), config.readTimeout()));
}
- private List createPostMessageBodies(List timeSeries, long maxSize) {
- ArrayList messages = new ArrayList<>();
- StringBuilder payload = new StringBuilder();
- int metricCount = 0;
- long totalByteCount = 0;
- for (DynatraceTimeSeries ts : timeSeries) {
- String json = ts.asJson();
- int jsonByteCount = json.getBytes(UTF_8).length;
- if (jsonByteCount > maxSize) {
- logger.debug("Time series data for metric '{}' is too large ({} bytes) to send to Dynatrace.", ts.getMetricId(), jsonByteCount);
- continue;
- }
- if ((payload.length() == 0 && totalByteCount + jsonByteCount > maxSize) ||
- (payload.length() > 0 && totalByteCount + jsonByteCount + 1 > maxSize)) {
- messages.add(new DynatraceBatchedPayload(payload.toString(), metricCount));
- payload.setLength(0);
- totalByteCount = 0;
- metricCount = 0;
- }
- if (payload.length() > 0) {
- payload.append(',');
- totalByteCount++;
- }
- payload.append(json);
- totalByteCount += jsonByteCount;
- metricCount++;
- }
- if (payload.length() > 0) {
- messages.add(new DynatraceBatchedPayload(payload.toString(), metricCount));
- }
- return messages;
+ @Override
+ protected TimeUnit getBaseTimeUnit() {
+ return this.exporter.getBaseTimeUnit();
}
- private Meter.Id idWithSuffix(Meter.Id id, String suffix) {
- return id.withName(id.getName() + "." + suffix);
+ @Override
+ protected void publish() {
+ exporter.export(this);
}
- @Override
- protected TimeUnit getBaseTimeUnit() {
- return TimeUnit.MILLISECONDS;
+ // the builder is used by spring boot to create the class.
+ public static Builder builder(DynatraceConfig config) {
+ return new Builder(config);
}
public static class Builder {
@@ -335,23 +107,5 @@ public DynatraceMeterRegistry build() {
return new DynatraceMeterRegistry(config, clock, threadFactory, httpClient);
}
}
-
- class DynatraceCustomMetric {
- private final DynatraceMetricDefinition metricDefinition;
- private final DynatraceTimeSeries timeSeries;
-
- DynatraceCustomMetric(final DynatraceMetricDefinition metricDefinition, final DynatraceTimeSeries timeSeries) {
- this.metricDefinition = metricDefinition;
- this.timeSeries = timeSeries;
- }
-
- DynatraceMetricDefinition getMetricDefinition() {
- return metricDefinition;
- }
-
- DynatraceTimeSeries getTimeSeries() {
- return timeSeries;
- }
- }
}
diff --git a/implementations/micrometer-registry-dynatrace/src/main/java/io/micrometer/dynatrace/DynatraceNamingConvention.java b/implementations/micrometer-registry-dynatrace/src/main/java/io/micrometer/dynatrace/DynatraceNamingConvention.java
index 348f6022da..df8a0ab6ea 100644
--- a/implementations/micrometer-registry-dynatrace/src/main/java/io/micrometer/dynatrace/DynatraceNamingConvention.java
+++ b/implementations/micrometer-registry-dynatrace/src/main/java/io/micrometer/dynatrace/DynatraceNamingConvention.java
@@ -1,5 +1,5 @@
/**
- * Copyright 2017 VMware, Inc.
+ * Copyright 2021 VMware, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -18,30 +18,28 @@
import io.micrometer.core.instrument.Meter;
import io.micrometer.core.instrument.config.NamingConvention;
import io.micrometer.core.lang.Nullable;
-import io.micrometer.core.util.internal.logging.WarnThenDebugLogger;
-
-import java.util.regex.Pattern;
+import io.micrometer.dynatrace.v1.DynatraceNamingConventionV1;
/**
- * {@link NamingConvention} for Dynatrace.
+ * {@link NamingConvention} for Dynatrace. Delegates to the API-specific naming convention.
*
* @author Oriol Barcelona Palau
* @author Jon Schneider
* @author Johnny Lim
+ * @author Georg Pirklbauer
* @since 1.1.0
*/
public class DynatraceNamingConvention implements NamingConvention {
+ private final NamingConvention namingConvention;
- private static final WarnThenDebugLogger logger = new WarnThenDebugLogger(DynatraceNamingConvention.class);
-
- private static final Pattern NAME_CLEANUP_PATTERN = Pattern.compile("[^\\w._-]");
- private static final Pattern LEADING_NUMERIC_PATTERN = Pattern.compile("[._-]([\\d])+");
- private static final Pattern KEY_CLEANUP_PATTERN = Pattern.compile("[^\\w.-]");
-
- private final NamingConvention delegate;
+ public DynatraceNamingConvention(NamingConvention delegate, DynatraceApiVersion version) {
+ // if (version == DynatraceApiVersion.V1) ...
+ // for now, this check does not make sense, but it will when more naming conventions are added.
+ this.namingConvention = new DynatraceNamingConventionV1(delegate);
+ }
public DynatraceNamingConvention(NamingConvention delegate) {
- this.delegate = delegate;
+ this(delegate, DynatraceApiVersion.V1);
}
public DynatraceNamingConvention() {
@@ -50,25 +48,22 @@ public DynatraceNamingConvention() {
@Override
public String name(String name, Meter.Type type, @Nullable String baseUnit) {
- return "custom:" + sanitizeName(delegate.name(name, type, baseUnit));
+ return namingConvention.name(name, type, baseUnit);
}
- private String sanitizeName(String name) {
- if (name.equals("system.load.average.1m")) {
- return "system.load.average.oneminute";
- }
- String sanitized = NAME_CLEANUP_PATTERN.matcher(name).replaceAll("_");
- if (LEADING_NUMERIC_PATTERN.matcher(sanitized).find()) {
- logger.log("'" + sanitized + "' (original name: '" + name + "') is not a valid meter name. "
- + "Dynatrace doesn't allow leading numeric characters after non-alphabets. "
- + "Please rename it to conform to the constraints. "
- + "If it comes from a third party, please use MeterFilter to rename it.");
- }
- return sanitized;
+ @Override
+ public String name(String name, Meter.Type type) {
+ return namingConvention.name(name, type);
}
@Override
public String tagKey(String key) {
- return KEY_CLEANUP_PATTERN.matcher(delegate.tagKey(key)).replaceAll("_");
+ return namingConvention.tagKey(key);
+ }
+
+ @Override
+ public String tagValue(String value) {
+ return namingConvention.tagValue(value);
}
}
+
diff --git a/implementations/micrometer-registry-dynatrace/src/main/java/io/micrometer/dynatrace/DynatraceBatchedPayload.java b/implementations/micrometer-registry-dynatrace/src/main/java/io/micrometer/dynatrace/v1/DynatraceBatchedPayload.java
similarity index 95%
rename from implementations/micrometer-registry-dynatrace/src/main/java/io/micrometer/dynatrace/DynatraceBatchedPayload.java
rename to implementations/micrometer-registry-dynatrace/src/main/java/io/micrometer/dynatrace/v1/DynatraceBatchedPayload.java
index fdf4bcb87d..5c5c6a661a 100644
--- a/implementations/micrometer-registry-dynatrace/src/main/java/io/micrometer/dynatrace/DynatraceBatchedPayload.java
+++ b/implementations/micrometer-registry-dynatrace/src/main/java/io/micrometer/dynatrace/v1/DynatraceBatchedPayload.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package io.micrometer.dynatrace;
+package io.micrometer.dynatrace.v1;
class DynatraceBatchedPayload {
final String payload;
diff --git a/implementations/micrometer-registry-dynatrace/src/main/java/io/micrometer/dynatrace/v1/DynatraceCustomMetric.java b/implementations/micrometer-registry-dynatrace/src/main/java/io/micrometer/dynatrace/v1/DynatraceCustomMetric.java
new file mode 100644
index 0000000000..5a67fe0c52
--- /dev/null
+++ b/implementations/micrometer-registry-dynatrace/src/main/java/io/micrometer/dynatrace/v1/DynatraceCustomMetric.java
@@ -0,0 +1,35 @@
+/**
+ * Copyright 2021 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.v1;
+
+class DynatraceCustomMetric {
+ private final DynatraceMetricDefinition metricDefinition;
+ private final DynatraceTimeSeries timeSeries;
+
+ DynatraceCustomMetric(final DynatraceMetricDefinition metricDefinition, final DynatraceTimeSeries timeSeries) {
+ this.metricDefinition = metricDefinition;
+ this.timeSeries = timeSeries;
+ }
+
+ DynatraceMetricDefinition getMetricDefinition() {
+ return metricDefinition;
+ }
+
+ DynatraceTimeSeries getTimeSeries() {
+ return timeSeries;
+ }
+}
diff --git a/implementations/micrometer-registry-dynatrace/src/main/java/io/micrometer/dynatrace/v1/DynatraceExporterV1.java b/implementations/micrometer-registry-dynatrace/src/main/java/io/micrometer/dynatrace/v1/DynatraceExporterV1.java
new file mode 100644
index 0000000000..a00b17e328
--- /dev/null
+++ b/implementations/micrometer-registry-dynatrace/src/main/java/io/micrometer/dynatrace/v1/DynatraceExporterV1.java
@@ -0,0 +1,294 @@
+/**
+ * Copyright 2021 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.v1;
+
+import io.micrometer.core.instrument.*;
+import io.micrometer.core.instrument.config.NamingConvention;
+import io.micrometer.core.instrument.distribution.HistogramSnapshot;
+import io.micrometer.core.instrument.util.MeterPartition;
+import io.micrometer.core.instrument.util.StringUtils;
+import io.micrometer.core.ipc.http.HttpSender;
+import io.micrometer.core.lang.Nullable;
+import io.micrometer.dynatrace.AbstractDynatraceExporter;
+import io.micrometer.dynatrace.DynatraceConfig;
+import io.micrometer.dynatrace.DynatraceNamingConvention;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.annotation.Nonnull;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+/**
+ * Implementation for Dynatrace v1 metrics API export.
+ *
+ * @author Oriol Barcelona
+ * @author Jon Schneider
+ * @author Johnny Lim
+ * @author PJ Fanning
+ * @author Georg Pirklbauer
+ */
+public class DynatraceExporterV1 extends AbstractDynatraceExporter {
+ private static final int MAX_MESSAGE_SIZE = 15360; //max message size in bytes that Dynatrace will accept
+ private final Logger logger = LoggerFactory.getLogger(DynatraceExporterV1.class.getName());
+
+ /**
+ * Metric names for which we have created the custom metric in the API
+ */
+ private final Set createdCustomMetrics = ConcurrentHashMap.newKeySet();
+ private final String customMetricEndpointTemplate;
+
+ private final NamingConvention namingConvention;
+
+ public DynatraceExporterV1(DynatraceConfig config, Clock clock, HttpSender httpClient) {
+ super(config, clock, httpClient);
+
+ this.customMetricEndpointTemplate = config.uri() + "/api/v1/timeseries/";
+ this.namingConvention = new DynatraceNamingConvention();
+ }
+
+ @Override
+ public void export(@Nonnull MeterRegistry registry) {
+ String customDeviceMetricEndpoint = config.uri() + "/api/v1/entity/infrastructure/custom/" +
+ config.deviceId() + "?api-token=" + config.apiToken();
+
+ for (List batch : MeterPartition.partition(registry, config.batchSize())) {
+ final List series = batch.stream()
+ .flatMap(meter -> meter.match(
+ this::writeMeter,
+ this::writeMeter,
+ this::writeTimer,
+ this::writeSummary,
+ this::writeLongTaskTimer,
+ this::writeMeter,
+ this::writeMeter,
+ this::writeFunctionTimer,
+ this::writeMeter)
+ )
+ .collect(Collectors.toList());
+
+ // TODO is there a way to batch submissions of multiple metrics?
+ series.stream()
+ .map(DynatraceCustomMetric::getMetricDefinition)
+ .filter(this::isCustomMetricNotCreated)
+ .forEach(this::putCustomMetric);
+
+ if (!createdCustomMetrics.isEmpty() && !series.isEmpty()) {
+ postCustomMetricValues(
+ config.technologyType(),
+ config.group(),
+ series.stream()
+ .map(DynatraceCustomMetric::getTimeSeries)
+ .filter(this::isCustomMetricCreated)
+ .collect(Collectors.toList()),
+ customDeviceMetricEndpoint);
+ }
+ }
+ }
+
+ // VisibleForTesting
+ Stream writeMeter(Meter meter) {
+ final long wallTime = clock.wallTime();
+ return StreamSupport.stream(meter.measure().spliterator(), false)
+ .map(Measurement::getValue)
+ .filter(Double::isFinite)
+ .map(value -> createCustomMetric(meter.getId(), wallTime, value));
+ }
+
+ private Stream writeLongTaskTimer(LongTaskTimer longTaskTimer) {
+ final long wallTime = clock.wallTime();
+ final Meter.Id id = longTaskTimer.getId();
+ return Stream.of(
+ createCustomMetric(idWithSuffix(id, "activeTasks"), wallTime, longTaskTimer.activeTasks(), DynatraceMetricDefinition.DynatraceUnit.Count),
+ createCustomMetric(idWithSuffix(id, "count"), wallTime, longTaskTimer.duration(getBaseTimeUnit())));
+ }
+
+ private Stream writeSummary(DistributionSummary summary) {
+ final long wallTime = clock.wallTime();
+ final Meter.Id id = summary.getId();
+ final HistogramSnapshot snapshot = summary.takeSnapshot();
+
+ return Stream.of(
+ createCustomMetric(idWithSuffix(id, "sum"), wallTime, snapshot.total(getBaseTimeUnit())),
+ createCustomMetric(idWithSuffix(id, "count"), wallTime, snapshot.count(), DynatraceMetricDefinition.DynatraceUnit.Count),
+ createCustomMetric(idWithSuffix(id, "avg"), wallTime, snapshot.mean(getBaseTimeUnit())),
+ createCustomMetric(idWithSuffix(id, "max"), wallTime, snapshot.max(getBaseTimeUnit())));
+ }
+
+ private Stream writeFunctionTimer(FunctionTimer timer) {
+ final long wallTime = clock.wallTime();
+ final Meter.Id id = timer.getId();
+
+ return Stream.of(
+ createCustomMetric(idWithSuffix(id, "count"), wallTime, timer.count(), DynatraceMetricDefinition.DynatraceUnit.Count),
+ createCustomMetric(idWithSuffix(id, "avg"), wallTime, timer.mean(getBaseTimeUnit())),
+ createCustomMetric(idWithSuffix(id, "sum"), wallTime, timer.totalTime(getBaseTimeUnit())));
+ }
+
+ private Stream writeTimer(Timer timer) {
+ final long wallTime = clock.wallTime();
+ final Meter.Id id = timer.getId();
+ final HistogramSnapshot snapshot = timer.takeSnapshot();
+
+ return Stream.of(
+ createCustomMetric(idWithSuffix(id, "sum"), wallTime, snapshot.total(getBaseTimeUnit())),
+ createCustomMetric(idWithSuffix(id, "count"), wallTime, snapshot.count(), DynatraceMetricDefinition.DynatraceUnit.Count),
+ createCustomMetric(idWithSuffix(id, "avg"), wallTime, snapshot.mean(getBaseTimeUnit())),
+ createCustomMetric(idWithSuffix(id, "max"), wallTime, snapshot.max(getBaseTimeUnit())));
+ }
+
+ private DynatraceCustomMetric createCustomMetric(Meter.Id id, long time, Number value) {
+ return createCustomMetric(id, time, value, DynatraceMetricDefinition.DynatraceUnit.fromPlural(id.getBaseUnit()));
+ }
+
+ private DynatraceCustomMetric createCustomMetric(Meter.Id id, long time, Number value, @Nullable DynatraceMetricDefinition.DynatraceUnit unit) {
+ final String metricId = getConventionName(id);
+ final List tags = getConventionTags(id);
+ return new DynatraceCustomMetric(
+ new DynatraceMetricDefinition(metricId, id.getDescription(), unit, extractDimensions(tags), new String[]{config.technologyType()}, config.group()),
+ new DynatraceTimeSeries(metricId, time, value.doubleValue(), extractDimensionValues(tags)));
+ }
+
+ private List getConventionTags(Meter.Id id) {
+ return id.getConventionTags(namingConvention);
+ }
+
+ private String getConventionName(Meter.Id id) {
+ return id.getConventionName(namingConvention);
+ }
+
+ private Set extractDimensions(List tags) {
+ return tags.stream().map(Tag::getKey).collect(Collectors.toSet());
+ }
+
+ private Map extractDimensionValues(List tags) {
+ return tags.stream().collect(Collectors.toMap(Tag::getKey, Tag::getValue));
+ }
+
+ private boolean isCustomMetricNotCreated(final DynatraceMetricDefinition metric) {
+ return !createdCustomMetrics.contains(metric.getMetricId());
+ }
+
+ private boolean isCustomMetricCreated(final DynatraceTimeSeries timeSeries) {
+ return createdCustomMetrics.contains(timeSeries.getMetricId());
+ }
+
+ // VisibleForTesting
+ void putCustomMetric(final DynatraceMetricDefinition customMetric) {
+ try {
+ httpClient.put(customMetricEndpointTemplate + customMetric.getMetricId() + "?api-token=" + config.apiToken())
+ .withJsonContent(customMetric.asJson())
+ .send()
+ .onSuccess(response -> {
+ logger.debug("created {} as custom metric in dynatrace", customMetric.getMetricId());
+ createdCustomMetrics.add(customMetric.getMetricId());
+ })
+ .onError(response -> {
+ if (logger.isErrorEnabled()) {
+ logger.error("failed to create custom metric {} in dynatrace: {}", customMetric.getMetricId(),
+ response.body());
+ }
+ });
+ } catch (Throwable e) {
+ logger.error("failed to create custom metric in dynatrace: {}", customMetric.getMetricId(), e);
+ }
+ }
+
+ private void postCustomMetricValues(String type, String group, List timeSeries, String customDeviceMetricEndpoint) {
+ try {
+ for (DynatraceBatchedPayload postMessage : createPostMessages(type, group, timeSeries)) {
+ httpClient.post(customDeviceMetricEndpoint)
+ .withJsonContent(postMessage.payload)
+ .send()
+ .onSuccess(response -> {
+ if (logger.isDebugEnabled()) {
+ logger.debug("successfully sent {} metrics to Dynatrace ({} bytes).",
+ postMessage.metricCount, postMessage.payload.getBytes(UTF_8).length);
+ }
+ })
+ .onError(response -> {
+ logger.error("failed to send metrics to dynatrace: {}", response.body());
+ logger.debug("failed metrics payload: {}", postMessage.payload);
+ });
+ }
+ } catch (Throwable e) {
+ logger.error("failed to send metrics to dynatrace", e);
+ }
+ }
+
+ // VisibleForTesting
+ List createPostMessages(String type, String group, List timeSeries) {
+ final String header = "{\"type\":\"" + type + '\"'
+ + (StringUtils.isNotBlank(group) ? ",\"group\":\"" + group + '\"' : "")
+ + ",\"series\":[";
+ final String footer = "]}";
+ final int headerFooterBytes = header.getBytes(UTF_8).length + footer.getBytes(UTF_8).length;
+ final int maxMessageSize = MAX_MESSAGE_SIZE - headerFooterBytes;
+ List payloadBodies = createPostMessageBodies(timeSeries, maxMessageSize);
+ return payloadBodies.stream().map(body -> {
+ String message = header + body.payload + footer;
+ return new DynatraceBatchedPayload(message, body.metricCount);
+ }).collect(Collectors.toList());
+ }
+
+ private List createPostMessageBodies(List timeSeries, long maxSize) {
+ ArrayList messages = new ArrayList<>();
+ StringBuilder payload = new StringBuilder();
+ int metricCount = 0;
+ long totalByteCount = 0;
+ for (DynatraceTimeSeries ts : timeSeries) {
+ String json = ts.asJson();
+ int jsonByteCount = json.getBytes(UTF_8).length;
+ if (jsonByteCount > maxSize) {
+ logger.debug("Time series data for metric '{}' is too large ({} bytes) to send to Dynatrace.", ts.getMetricId(), jsonByteCount);
+ continue;
+ }
+ if ((payload.length() == 0 && totalByteCount + jsonByteCount > maxSize) ||
+ (payload.length() > 0 && totalByteCount + jsonByteCount + 1 > maxSize)) {
+ messages.add(new DynatraceBatchedPayload(payload.toString(), metricCount));
+ payload.setLength(0);
+ totalByteCount = 0;
+ metricCount = 0;
+ }
+ if (payload.length() > 0) {
+ payload.append(',');
+ totalByteCount++;
+ }
+ payload.append(json);
+ totalByteCount += jsonByteCount;
+ metricCount++;
+ }
+ if (payload.length() > 0) {
+ messages.add(new DynatraceBatchedPayload(payload.toString(), metricCount));
+ }
+ return messages;
+ }
+
+ private Meter.Id idWithSuffix(Meter.Id id, String suffix) {
+ return id.withName(id.getName() + "." + suffix);
+ }
+
+}
diff --git a/implementations/micrometer-registry-dynatrace/src/main/java/io/micrometer/dynatrace/DynatraceMetricDefinition.java b/implementations/micrometer-registry-dynatrace/src/main/java/io/micrometer/dynatrace/v1/DynatraceMetricDefinition.java
similarity index 99%
rename from implementations/micrometer-registry-dynatrace/src/main/java/io/micrometer/dynatrace/DynatraceMetricDefinition.java
rename to implementations/micrometer-registry-dynatrace/src/main/java/io/micrometer/dynatrace/v1/DynatraceMetricDefinition.java
index dd99fb00e6..5c45ba504a 100644
--- a/implementations/micrometer-registry-dynatrace/src/main/java/io/micrometer/dynatrace/DynatraceMetricDefinition.java
+++ b/implementations/micrometer-registry-dynatrace/src/main/java/io/micrometer/dynatrace/v1/DynatraceMetricDefinition.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package io.micrometer.dynatrace;
+package io.micrometer.dynatrace.v1;
import io.micrometer.core.instrument.util.StringEscapeUtils;
import io.micrometer.core.instrument.util.StringUtils;
@@ -32,7 +32,6 @@
* @author Oriol Barcelona
*/
class DynatraceMetricDefinition {
-
private static final int MAX_DISPLAY_NAME = 256;
private static final int MAX_GROUP_NAME = 256;
private final String metricId;
diff --git a/implementations/micrometer-registry-dynatrace/src/main/java/io/micrometer/dynatrace/v1/DynatraceNamingConventionV1.java b/implementations/micrometer-registry-dynatrace/src/main/java/io/micrometer/dynatrace/v1/DynatraceNamingConventionV1.java
new file mode 100644
index 0000000000..65bc3e68d9
--- /dev/null
+++ b/implementations/micrometer-registry-dynatrace/src/main/java/io/micrometer/dynatrace/v1/DynatraceNamingConventionV1.java
@@ -0,0 +1,74 @@
+/**
+ * Copyright 2021 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.v1;
+
+import io.micrometer.core.instrument.Meter;
+import io.micrometer.core.instrument.config.NamingConvention;
+import io.micrometer.core.lang.Nullable;
+import io.micrometer.core.util.internal.logging.WarnThenDebugLogger;
+import io.micrometer.dynatrace.DynatraceNamingConvention;
+
+import java.util.regex.Pattern;
+
+/**
+ * {@link NamingConvention} for Dynatrace API v1.
+ *
+ * @author Oriol Barcelona Palau
+ * @author Jon Schneider
+ * @author Johnny Lim
+ */
+public class DynatraceNamingConventionV1 implements NamingConvention {
+ private static final WarnThenDebugLogger logger = new WarnThenDebugLogger(DynatraceNamingConvention.class);
+
+ private static final Pattern NAME_CLEANUP_PATTERN = Pattern.compile("[^\\w._-]");
+ private static final Pattern LEADING_NUMERIC_PATTERN = Pattern.compile("[._-]([\\d])+");
+ private static final Pattern KEY_CLEANUP_PATTERN = Pattern.compile("[^\\w.-]");
+
+ private final NamingConvention delegate;
+
+ public DynatraceNamingConventionV1(NamingConvention delegate) {
+ this.delegate = delegate;
+ }
+
+ public DynatraceNamingConventionV1() {
+ this(NamingConvention.dot);
+ }
+
+ @Override
+ public String name(String name, Meter.Type type, @Nullable String baseUnit) {
+ return "custom:" + sanitizeName(delegate.name(name, type, baseUnit));
+ }
+
+ private String sanitizeName(String name) {
+ if (name.equals("system.load.average.1m")) {
+ return "system.load.average.oneminute";
+ }
+ String sanitized = NAME_CLEANUP_PATTERN.matcher(name).replaceAll("_");
+ if (LEADING_NUMERIC_PATTERN.matcher(sanitized).find()) {
+ logger.log("'" + sanitized + "' (original name: '" + name + "') is not a valid meter name. "
+ + "Dynatrace doesn't allow leading numeric characters after non-alphabets. "
+ + "Please rename it to conform to the constraints. "
+ + "If it comes from a third party, please use MeterFilter to rename it.");
+ }
+ return sanitized;
+ }
+
+ @Override
+ public String tagKey(String key) {
+ return KEY_CLEANUP_PATTERN.matcher(delegate.tagKey(key)).replaceAll("_");
+ }
+}
diff --git a/implementations/micrometer-registry-dynatrace/src/main/java/io/micrometer/dynatrace/DynatraceTimeSeries.java b/implementations/micrometer-registry-dynatrace/src/main/java/io/micrometer/dynatrace/v1/DynatraceTimeSeries.java
similarity index 98%
rename from implementations/micrometer-registry-dynatrace/src/main/java/io/micrometer/dynatrace/DynatraceTimeSeries.java
rename to implementations/micrometer-registry-dynatrace/src/main/java/io/micrometer/dynatrace/v1/DynatraceTimeSeries.java
index 28b7a2bd49..4132d07986 100644
--- a/implementations/micrometer-registry-dynatrace/src/main/java/io/micrometer/dynatrace/DynatraceTimeSeries.java
+++ b/implementations/micrometer-registry-dynatrace/src/main/java/io/micrometer/dynatrace/v1/DynatraceTimeSeries.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package io.micrometer.dynatrace;
+package io.micrometer.dynatrace.v1;
import io.micrometer.core.instrument.util.DoubleFormat;
import io.micrometer.core.instrument.util.StringEscapeUtils;
diff --git a/implementations/micrometer-registry-dynatrace/src/test/java/io/micrometer/dynatrace/DynatraceConfigTest.java b/implementations/micrometer-registry-dynatrace/src/test/java/io/micrometer/dynatrace/DynatraceConfigTest.java
index 6d44f68fb6..39c72a2b52 100644
--- a/implementations/micrometer-registry-dynatrace/src/test/java/io/micrometer/dynatrace/DynatraceConfigTest.java
+++ b/implementations/micrometer-registry-dynatrace/src/test/java/io/micrometer/dynatrace/DynatraceConfigTest.java
@@ -15,6 +15,7 @@
*/
package io.micrometer.dynatrace;
+import io.micrometer.core.instrument.config.validate.InvalidReason;
import io.micrometer.core.instrument.config.validate.Validated;
import org.junit.jupiter.api.Test;
@@ -54,6 +55,26 @@ public String get(String key) {
.contains("cannot be blank");
}
+ @Test
+ void invalidVersion() {
+ Map properties = new HashMap() {{
+ put("dynatrace.apiToken", "secret");
+ put("dynatrace.uri", "https://uri.dynatrace.com");
+ put("dynatrace.deviceId", "device");
+ put("dynatrace.apiVersion", "v-INVALID");
+ }};
+ DynatraceConfig config = properties::get;
+
+ List> failures = config.validate().failures();
+ assertThat(failures).hasSize(1);
+ Validated.Invalid> failure = failures.get(0);
+ assertThat(failure.getProperty()).isEqualTo("dynatrace.apiVersion");
+ assertThat(failure.getValue()).isEqualTo("v-INVALID");
+ assertThat(failure.getMessage()).startsWith("should be one of ");
+ assertThat(failure.getReason()).isSameAs(InvalidReason.MALFORMED);
+ assertThat(failure.getException()).isNull();
+ }
+
@Test
void valid() {
props.put("dynatrace.apiToken", "secret");
@@ -62,4 +83,18 @@ void valid() {
assertThat(config.validate().isValid()).isTrue();
}
+
+ @Test
+ void testFallbackToV1() {
+ Map properties = new HashMap() {{
+ put("dynatrace.apiToken", "secret");
+ put("dynatrace.uri", "https://uri.dynatrace.com");
+ put("dynatrace.deviceId", "device");
+ }};
+
+ DynatraceConfig config = properties::get;
+
+ assertThat(config.validate().isValid()).isTrue();
+ assertThat(config.apiVersion()).isSameAs(DynatraceApiVersion.V1);
+ }
}
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/v1/DynatraceExporterV1Test.java
similarity index 83%
rename from implementations/micrometer-registry-dynatrace/src/test/java/io/micrometer/dynatrace/DynatraceMeterRegistryTest.java
rename to implementations/micrometer-registry-dynatrace/src/test/java/io/micrometer/dynatrace/v1/DynatraceExporterV1Test.java
index d0685aadfb..ab186f2e6a 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/v1/DynatraceExporterV1Test.java
@@ -13,12 +13,15 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package io.micrometer.dynatrace;
+package io.micrometer.dynatrace.v1;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.micrometer.core.instrument.*;
import io.micrometer.core.instrument.config.validate.ValidationException;
import io.micrometer.core.ipc.http.HttpSender;
+import io.micrometer.dynatrace.DynatraceApiVersion;
+import io.micrometer.dynatrace.DynatraceConfig;
+import io.micrometer.dynatrace.DynatraceMeterRegistry;
import org.junit.jupiter.api.Test;
import java.io.IOException;
@@ -40,9 +43,10 @@
*
* @author Johnny Lim
*/
-class DynatraceMeterRegistryTest {
+class DynatraceExporterV1Test {
private final DynatraceMeterRegistry meterRegistry = createMeterRegistry();
+ private final DynatraceExporterV1 exporter = createExporter();
private final ObjectMapper mapper = new ObjectMapper();
@@ -108,14 +112,14 @@ public String deviceId() {
@Test
void putCustomMetricOnSuccessShouldAddMetricIdToCreatedCustomMetrics() throws NoSuchFieldException, IllegalAccessException {
- Field createdCustomMetricsField = DynatraceMeterRegistry.class.getDeclaredField("createdCustomMetrics");
+ Field createdCustomMetricsField = DynatraceExporterV1.class.getDeclaredField("createdCustomMetrics");
createdCustomMetricsField.setAccessible(true);
@SuppressWarnings("unchecked")
- Set createdCustomMetrics = (Set) createdCustomMetricsField.get(meterRegistry);
+ Set createdCustomMetrics = (Set) createdCustomMetricsField.get(exporter);
assertThat(createdCustomMetrics).isEmpty();
DynatraceMetricDefinition customMetric = new DynatraceMetricDefinition("metricId", null, null, null, new String[]{"type"}, null);
- meterRegistry.putCustomMetric(customMetric);
+ exporter.putCustomMetric(customMetric);
assertThat(createdCustomMetrics).containsExactly("metricId");
}
@@ -123,14 +127,14 @@ void putCustomMetricOnSuccessShouldAddMetricIdToCreatedCustomMetrics() throws No
void writeMeterWithGauge() {
meterRegistry.gauge("my.gauge", 1d);
Gauge gauge = meterRegistry.find("my.gauge").gauge();
- assertThat(meterRegistry.writeMeter(gauge)).hasSize(1);
+ assertThat(exporter.writeMeter(gauge)).hasSize(1);
}
@Test
void writeMeterWithGaugeShouldDropNanValue() {
meterRegistry.gauge("my.gauge", Double.NaN);
Gauge gauge = meterRegistry.find("my.gauge").gauge();
- assertThat(meterRegistry.writeMeter(gauge)).isEmpty();
+ assertThat(exporter.writeMeter(gauge)).isEmpty();
}
@SuppressWarnings("unchecked")
@@ -139,10 +143,10 @@ void writeMeterWithGaugeWhenChangingFiniteToNaNShouldWork() {
AtomicBoolean first = new AtomicBoolean(true);
meterRegistry.gauge("my.gauge", first, (b) -> b.getAndSet(false) ? 1d : Double.NaN);
Gauge gauge = meterRegistry.find("my.gauge").gauge();
- Stream stream = meterRegistry.writeMeter(gauge);
- List metrics = stream.collect(Collectors.toList());
+ Stream stream = exporter.writeMeter(gauge);
+ List metrics = stream.collect(Collectors.toList());
assertThat(metrics).hasSize(1);
- DynatraceMeterRegistry.DynatraceCustomMetric metric = metrics.get(0);
+ DynatraceCustomMetric metric = metrics.get(0);
DynatraceTimeSeries timeSeries = metric.getTimeSeries();
try {
Map map = mapper.readValue(timeSeries.asJson(), Map.class);
@@ -157,11 +161,11 @@ void writeMeterWithGaugeWhenChangingFiniteToNaNShouldWork() {
void writeMeterWithGaugeShouldDropInfiniteValues() {
meterRegistry.gauge("my.gauge", Double.POSITIVE_INFINITY);
Gauge gauge = meterRegistry.find("my.gauge").gauge();
- assertThat(meterRegistry.writeMeter(gauge)).isEmpty();
+ assertThat(exporter.writeMeter(gauge)).isEmpty();
meterRegistry.gauge("my.gauge", Double.NEGATIVE_INFINITY);
gauge = meterRegistry.find("my.gauge").gauge();
- assertThat(meterRegistry.writeMeter(gauge)).isEmpty();
+ assertThat(exporter.writeMeter(gauge)).isEmpty();
}
@Test
@@ -169,7 +173,7 @@ void writeMeterWithTimeGauge() {
AtomicReference obj = new AtomicReference<>(1d);
meterRegistry.more().timeGauge("my.timeGauge", Tags.empty(), obj, TimeUnit.SECONDS, AtomicReference::get);
TimeGauge timeGauge = meterRegistry.find("my.timeGauge").timeGauge();
- assertThat(meterRegistry.writeMeter(timeGauge)).hasSize(1);
+ assertThat(exporter.writeMeter(timeGauge)).hasSize(1);
}
@Test
@@ -177,7 +181,7 @@ void writeMeterWithTimeGaugeShouldDropNanValue() {
AtomicReference obj = new AtomicReference<>(Double.NaN);
meterRegistry.more().timeGauge("my.timeGauge", Tags.empty(), obj, TimeUnit.SECONDS, AtomicReference::get);
TimeGauge timeGauge = meterRegistry.find("my.timeGauge").timeGauge();
- assertThat(meterRegistry.writeMeter(timeGauge)).isEmpty();
+ assertThat(exporter.writeMeter(timeGauge)).isEmpty();
}
@Test
@@ -185,12 +189,12 @@ void writeMeterWithTimeGaugeShouldDropInfiniteValues() {
AtomicReference obj = new AtomicReference<>(Double.POSITIVE_INFINITY);
meterRegistry.more().timeGauge("my.timeGauge", Tags.empty(), obj, TimeUnit.SECONDS, AtomicReference::get);
TimeGauge timeGauge = meterRegistry.find("my.timeGauge").timeGauge();
- assertThat(meterRegistry.writeMeter(timeGauge)).isEmpty();
+ assertThat(exporter.writeMeter(timeGauge)).isEmpty();
obj = new AtomicReference<>(Double.NEGATIVE_INFINITY);
meterRegistry.more().timeGauge("my.timeGauge", Tags.empty(), obj, TimeUnit.SECONDS, AtomicReference::get);
timeGauge = meterRegistry.find("my.timeGauge").timeGauge();
- assertThat(meterRegistry.writeMeter(timeGauge)).isEmpty();
+ assertThat(exporter.writeMeter(timeGauge)).isEmpty();
}
@Test
@@ -198,11 +202,11 @@ void writeCustomMetrics() {
Double number = 1d;
meterRegistry.gauge("my.gauge", number);
Gauge gauge = meterRegistry.find("my.gauge").gauge();
- Stream series = meterRegistry.writeMeter(gauge);
+ Stream series = exporter.writeMeter(gauge);
List timeSeries = series
- .map(DynatraceMeterRegistry.DynatraceCustomMetric::getTimeSeries)
+ .map(DynatraceCustomMetric::getTimeSeries)
.collect(Collectors.toList());
- List entries = meterRegistry.createPostMessages("my.type", null, timeSeries);
+ List entries = exporter.createPostMessages("my.type", null, timeSeries);
assertThat(entries).hasSize(1);
assertThat(entries.get(0).metricCount).isEqualTo(1);
assertThat(isValidJson(entries.get(0).payload)).isEqualTo(true);
@@ -210,14 +214,14 @@ void writeCustomMetrics() {
@Test
void whenAllTsTooLargeEmptyMessageListReturned() {
- List messages = meterRegistry.createPostMessages("my.type", null, Collections.singletonList(createTimeSeriesWithDimensions(10_000)));
+ List messages = exporter.createPostMessages("my.type", null, Collections.singletonList(createTimeSeriesWithDimensions(10_000)));
assertThat(messages).isEmpty();
}
@Test
void splitsWhenExactlyExceedingMaxByComma() {
// comma needs to be considered when there is more than one time series
- List messages = meterRegistry.createPostMessages("my.type", "my.group",
+ List messages = exporter.createPostMessages("my.type", "my.group",
// Max bytes: 15330 (excluding header/footer, 15360 with header/footer)
Arrays.asList(createTimeSeriesWithDimensions(750), // 14861 bytes
createTimeSeriesWithDimensions(23, "asdfg"), // 469 bytes (overflows due to comma)
@@ -234,7 +238,7 @@ void splitsWhenExactlyExceedingMaxByComma() {
@Test
void countsPreviousAndNextComma() {
- List messages = meterRegistry.createPostMessages("my.type", null,
+ List messages = exporter.createPostMessages("my.type", null,
// Max bytes: 15330 (excluding header/footer, 15360 with header/footer)
Arrays.asList(createTimeSeriesWithDimensions(750), // 14861 bytes
createTimeSeriesWithDimensions(10, "asdf"), // 234 bytes + comma
@@ -253,7 +257,7 @@ void writeMeterWhenCustomMeterHasOnlyNonFiniteValuesShouldNotBeWritten() {
Measurement measurement3 = new Measurement(() -> Double.NaN, Statistic.VALUE);
List measurements = Arrays.asList(measurement1, measurement2, measurement3);
Meter meter = Meter.builder("my.meter", Meter.Type.GAUGE, measurements).register(this.meterRegistry);
- assertThat(meterRegistry.writeMeter(meter)).isEmpty();
+ assertThat(exporter.writeMeter(meter)).isEmpty();
}
@Test
@@ -265,7 +269,7 @@ void writeMeterWhenCustomMeterHasMixedFiniteAndNonFiniteValuesShouldSkipOnlyNonF
Measurement measurement5 = new Measurement(() -> 2d, Statistic.VALUE);
List measurements = Arrays.asList(measurement1, measurement2, measurement3, measurement4, measurement5);
Meter meter = Meter.builder("my.meter", Meter.Type.GAUGE, measurements).register(this.meterRegistry);
- assertThat(meterRegistry.writeMeter(meter)).hasSize(2);
+ assertThat(exporter.writeMeter(meter)).hasSize(2);
}
private DynatraceTimeSeries createTimeSeriesWithDimensions(int numberOfDimensions) {
@@ -283,7 +287,22 @@ private Map createDimensionsMap(int numberOfDimensions) {
}
private DynatraceMeterRegistry createMeterRegistry() {
- DynatraceConfig config = new DynatraceConfig() {
+ DynatraceConfig config = createDynatraceConfig();
+
+ return DynatraceMeterRegistry.builder(config)
+ .httpClient(request -> new HttpSender.Response(200, null))
+ .build();
+ }
+
+ private DynatraceExporterV1 createExporter() {
+ DynatraceConfig config = createDynatraceConfig();
+
+ return new DynatraceExporterV1(config, Clock.SYSTEM, request -> new HttpSender.Response(200, null));
+
+ }
+
+ private DynatraceConfig createDynatraceConfig() {
+ return new DynatraceConfig() {
@Override
public String get(String key) {
return null;
@@ -303,10 +322,12 @@ public String deviceId() {
public String apiToken() {
return "apiToken";
}
+
+ @Override
+ public DynatraceApiVersion apiVersion() {
+ return DynatraceApiVersion.V1;
+ }
};
- return DynatraceMeterRegistry.builder(config)
- .httpClient(request -> new HttpSender.Response(200, null))
- .build();
}
private boolean isValidJson(String json) {
@@ -317,5 +338,4 @@ private boolean isValidJson(String json) {
return false;
}
}
-
}
diff --git a/implementations/micrometer-registry-dynatrace/src/test/java/io/micrometer/dynatrace/DynatraceMetricDefinitionTest.java b/implementations/micrometer-registry-dynatrace/src/test/java/io/micrometer/dynatrace/v1/DynatraceMetricDefinitionTest.java
similarity index 98%
rename from implementations/micrometer-registry-dynatrace/src/test/java/io/micrometer/dynatrace/DynatraceMetricDefinitionTest.java
rename to implementations/micrometer-registry-dynatrace/src/test/java/io/micrometer/dynatrace/v1/DynatraceMetricDefinitionTest.java
index 499af08c4b..f3f426cc10 100644
--- a/implementations/micrometer-registry-dynatrace/src/test/java/io/micrometer/dynatrace/DynatraceMetricDefinitionTest.java
+++ b/implementations/micrometer-registry-dynatrace/src/test/java/io/micrometer/dynatrace/v1/DynatraceMetricDefinitionTest.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package io.micrometer.dynatrace;
+package io.micrometer.dynatrace.v1;
import org.junit.jupiter.api.Test;
diff --git a/implementations/micrometer-registry-dynatrace/src/test/java/io/micrometer/dynatrace/DynatraceNamingConventionTest.java b/implementations/micrometer-registry-dynatrace/src/test/java/io/micrometer/dynatrace/v1/DynatraceNamingConventionV1Test.java
similarity index 58%
rename from implementations/micrometer-registry-dynatrace/src/test/java/io/micrometer/dynatrace/DynatraceNamingConventionTest.java
rename to implementations/micrometer-registry-dynatrace/src/test/java/io/micrometer/dynatrace/v1/DynatraceNamingConventionV1Test.java
index 463ec86b67..519ab6912e 100644
--- a/implementations/micrometer-registry-dynatrace/src/test/java/io/micrometer/dynatrace/DynatraceNamingConventionTest.java
+++ b/implementations/micrometer-registry-dynatrace/src/test/java/io/micrometer/dynatrace/v1/DynatraceNamingConventionV1Test.java
@@ -1,5 +1,5 @@
/**
- * Copyright 2017 VMware, Inc.
+ * Copyright 2021 VMware, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -13,9 +13,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package io.micrometer.dynatrace;
+package io.micrometer.dynatrace.v1;
import io.micrometer.core.instrument.Meter;
+import io.micrometer.dynatrace.DynatraceNamingConvention;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
@@ -27,9 +28,8 @@
* @author Jon Schneider
* @author Johnny Lim
*/
-class DynatraceNamingConventionTest {
-
- private final DynatraceNamingConvention convention = new DynatraceNamingConvention();
+class DynatraceNamingConventionV1Test {
+ private final DynatraceNamingConventionV1 convention = new DynatraceNamingConventionV1();
@Test
void nameStartsWithCustomAndColon() {
@@ -58,4 +58,18 @@ void nameWithSystemLoadAverageOneMintueShouldSanitize() {
void tagKeysAreSanitized() {
assertThat(convention.tagKey("{tagTag0}.-")).isEqualTo("_tagTag0_.-");
}
+
+ @Test
+ void testDelegate() {
+ DynatraceNamingConvention dynatraceConvention = new DynatraceNamingConvention();
+ DynatraceNamingConventionV1 v1Convention = convention;
+ assertThat(dynatraceConvention.name("mymetric", Meter.Type.COUNTER, null)).isEqualTo(v1Convention.name("mymetric", Meter.Type.COUNTER, null));
+ assertThat(dynatraceConvention.name("my.name1", Meter.Type.COUNTER, null)).isEqualTo(v1Convention.name("my.name1", Meter.Type.COUNTER, null));
+ assertThat(dynatraceConvention.name("my_name1", Meter.Type.COUNTER, null)).isEqualTo(v1Convention.name("my_name1", Meter.Type.COUNTER, null));
+ assertThat(dynatraceConvention.name("my-name1", Meter.Type.COUNTER, null)).isEqualTo(v1Convention.name("my-name1", Meter.Type.COUNTER, null));
+ assertThat(dynatraceConvention.name("system.load.average.1m", Meter.Type.COUNTER, null))
+ .isEqualTo(v1Convention.name("system.load.average.1m", Meter.Type.COUNTER, null));
+ assertThat(dynatraceConvention.tagKey("{tagTag0}.-")).isEqualTo(v1Convention.tagKey("_tagTag0_.-"));
+ }
}
+
diff --git a/implementations/micrometer-registry-dynatrace/src/test/java/io/micrometer/dynatrace/DynatraceTimeSeriesTest.java b/implementations/micrometer-registry-dynatrace/src/test/java/io/micrometer/dynatrace/v1/DynatraceTimeSeriesTest.java
similarity index 98%
rename from implementations/micrometer-registry-dynatrace/src/test/java/io/micrometer/dynatrace/DynatraceTimeSeriesTest.java
rename to implementations/micrometer-registry-dynatrace/src/test/java/io/micrometer/dynatrace/v1/DynatraceTimeSeriesTest.java
index e50101f0fb..8c04ef048b 100644
--- a/implementations/micrometer-registry-dynatrace/src/test/java/io/micrometer/dynatrace/DynatraceTimeSeriesTest.java
+++ b/implementations/micrometer-registry-dynatrace/src/test/java/io/micrometer/dynatrace/v1/DynatraceTimeSeriesTest.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package io.micrometer.dynatrace;
+package io.micrometer.dynatrace.v1;
import org.junit.jupiter.api.Test;