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;