diff --git a/build.gradle b/build.gradle index 90c2f04057..4455adbda0 100644 --- a/build.gradle +++ b/build.gradle @@ -225,7 +225,7 @@ subprojects { } if (project.extensions.findByName('bintray')) { - bintray.labels = ['micrometer', 'atlas', 'metrics', 'prometheus', 'spectator', 'influx', 'new-relic', 'signalfx', 'wavefront', 'elastic', 'dynatrace', 'azure-monitor', 'appoptics', 'kairos', 'stackdriver'] + bintray.labels = ['micrometer', 'atlas', 'metrics', 'prometheus', 'spectator', 'influx', 'new-relic', 'signalfx', 'wavefront', 'elastic', 'dynatrace', 'dynatrace2', 'azure-monitor', 'appoptics', 'kairos', 'stackdriver'] bintray.packageName = 'io.micrometer' } } 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..f2533bab47 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 @@ -42,7 +42,7 @@ import static java.nio.charset.StandardCharsets.UTF_8; /** - * {@link StepMeterRegistry} for Dynatrace. + * {@link StepMeterRegistry} for Dynatrace metric API v1. * * @author Oriol Barcelona * @author Jon Schneider diff --git a/implementations/micrometer-registry-dynatrace2/build.gradle b/implementations/micrometer-registry-dynatrace2/build.gradle new file mode 100644 index 0000000000..584bea256f --- /dev/null +++ b/implementations/micrometer-registry-dynatrace2/build.gradle @@ -0,0 +1,8 @@ +dependencies { + api project(':micrometer-core') + + implementation 'org.slf4j:slf4j-api' + + testImplementation project(':micrometer-test') + testImplementation 'com.fasterxml.jackson.core:jackson-databind' +} diff --git a/implementations/micrometer-registry-dynatrace2/src/main/java/io/micrometer/dynatrace2/DynatraceConfig.java b/implementations/micrometer-registry-dynatrace2/src/main/java/io/micrometer/dynatrace2/DynatraceConfig.java new file mode 100644 index 0000000000..e4f3b67ee7 --- /dev/null +++ b/implementations/micrometer-registry-dynatrace2/src/main/java/io/micrometer/dynatrace2/DynatraceConfig.java @@ -0,0 +1,80 @@ +/** + * Copyright 2020 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.dynatrace2;
+
+import io.micrometer.core.instrument.config.validate.InvalidReason;
+import io.micrometer.core.instrument.config.validate.Validated;
+import io.micrometer.core.instrument.step.StepRegistryConfig;
+
+import java.util.function.Function;
+
+import static io.micrometer.core.instrument.config.MeterRegistryConfigValidator.*;
+import static io.micrometer.core.instrument.config.validate.PropertyValidator.*;
+import static io.micrometer.dynatrace2.LineProtocolIngestionLimits.MAX_METRIC_LINES_PER_REQUEST;
+
+/**
+ * Configuration for {@link DynatraceMeterRegistry}
+ *
+ * @author Oriol Barcelona
+ * @author David Mass
+ */
+public interface DynatraceConfig extends StepRegistryConfig {
+
+ @Override
+ default String prefix() {
+ return "dynatrace2";
+ }
+
+ default String deviceName() { return getString(this,"deviceName").orElse(""); }
+
+ default String groupName() { return getString(this,"groupName").orElse(""); }
+
+ default String entityId() { return getString(this,"entityId").orElse(""); }
+
+ default String apiToken() {
+ if (this.uri().contains("localhost")) {
+ return getSecret(this, "apiToken").orElse("");
+ }
+ return getSecret(this, "apiToken").required().get();
+ }
+
+ default String uri() {
+ return getUrlString(this, "uri").required().get();
+ }
+
+ @Override
+ default int batchSize() {
+ return getInteger(this, "batchSize").orElse(MAX_METRIC_LINES_PER_REQUEST);
+ }
+
+ @Override
+ default Validated> validate() {
+ return checkAll(this,
+ c -> StepRegistryConfig.validate(c),
+ check("deviceName", DynatraceConfig::deviceName),
+ check("groupName", DynatraceConfig::groupName),
+ check("entityId", DynatraceConfig::entityId),
+ check("apiToken", DynatraceConfig::apiToken),
+ checkRequired("uri", DynatraceConfig::uri),
+ check("batchSize", DynatraceConfig::batchSize)
+ .andThen(invalidateWhenGreaterThan(MAX_METRIC_LINES_PER_REQUEST))
+ );
+ }
+
+ default Function
+ * 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.dynatrace2;
+
+import io.micrometer.core.instrument.Clock;
+import io.micrometer.core.instrument.config.NamingConvention;
+import io.micrometer.core.instrument.step.StepMeterRegistry;
+import io.micrometer.core.instrument.util.NamedThreadFactory;
+import io.micrometer.core.ipc.http.HttpSender;
+import io.micrometer.core.ipc.http.HttpUrlConnectionSender;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+/**
+ * {@link StepMeterRegistry} for Dynatrace metric API v2
+ * https://dev-wiki.dynatrace.org/display/MET/MINT+Specification#MINTSpecification-IngestFormat
+ *
+ * @author Oriol Barcelona
+ * @author David Mass
+ * @see Dynatrace metric ingestion v2
+ * @since ?
+ */
+public class DynatraceMeterRegistry extends StepMeterRegistry {
+
+ private static final ThreadFactory DEFAULT_THREAD_FACTORY = new NamedThreadFactory("dynatrace2-metrics-publisher");
+ private final Logger logger = LoggerFactory.getLogger(DynatraceMeterRegistry.class);
+ private final DynatraceConfig config;
+ private final HttpSender httpClient;
+
+ @SuppressWarnings("deprecation")
+ public DynatraceMeterRegistry(DynatraceConfig config, Clock clock) {
+ this(config, clock, DEFAULT_THREAD_FACTORY, new HttpUrlConnectionSender(config.connectTimeout(), config.readTimeout()));
+ }
+
+ public DynatraceMeterRegistry(DynatraceConfig config, Clock clock, ThreadFactory threadFactory, HttpSender httpClient) {
+ super(config, clock);
+ this.config = config;
+ this.httpClient = httpClient;
+ addCommonTags();
+
+ config().namingConvention(new LineProtocolNamingConvention());
+ start(threadFactory);
+ }
+
+ @Override
+ protected void publish() {
+ NamingConvention lineProtocolNamingConvention = config().namingConvention();
+ MetricLineFactory metricLineFactory = new MetricLineFactory(clock, lineProtocolNamingConvention);
+
+ Map
+ * 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.dynatrace2;
+
+import io.micrometer.core.instrument.Measurement;
+import io.micrometer.core.instrument.Meter;
+import io.micrometer.core.instrument.Tag;
+import io.micrometer.core.instrument.config.NamingConvention;
+
+import java.text.DecimalFormat;
+import java.text.DecimalFormatSymbols;
+import java.util.List;
+import java.util.Locale;
+import java.util.stream.Collectors;
+
+
+/**
+ * Static formatters for line protocol
+ * Formatters are not responsible of formatting metric keys, dimension keys nor dimension values as
+ * this is provided by Micrometer core when implementing a corresponding
+ * {@link io.micrometer.core.instrument.config.NamingConvention}
+ *
+ * @author Oriol Barcelona
+ * @author David Mass
+ * @see LineProtocolNamingConvention
+ */
+class LineProtocolFormatters {
+
+ private static final DecimalFormat METRIC_VALUE_FORMAT = new DecimalFormat(
+ "#.#####",
+ DecimalFormatSymbols.getInstance(Locale.US));
+
+ static String formatGaugeMetricLine(NamingConvention namingConvention, Meter meter, Measurement measurement, long timestamp) {
+ return String.format(
+ "%s %s %d",
+ formatMetricAndDimensions(namingConvention.name(formatMetricName(meter, measurement), meter.getId().getType()), meter.getId().getTags(), namingConvention),
+ formatMetricValue(measurement.getValue()),
+ timestamp);
+ }
+
+ static String formatCounterMetricLine(NamingConvention namingConvention, Meter meter, Measurement measurement, long timestamp) {
+ return String.format(
+ "%s count,delta=%s %d",
+ formatMetricAndDimensions(namingConvention.name(formatMetricName(meter, measurement), meter.getId().getType()), meter.getId().getTags(), namingConvention),
+ formatMetricValue(measurement.getValue()),
+ timestamp);
+ }
+
+ static String formatTimerMetricLine(NamingConvention namingConvention, Meter meter, Measurement measurement, long timestamp) {
+ return String.format(
+ "%s gauge,%s %d",
+ formatMetricAndDimensions(namingConvention.name(formatMetricName(meter, measurement), meter.getId().getType()), meter.getId().getTags(), namingConvention),
+ formatMetricValue(measurement.getValue()),
+ timestamp);
+ }
+
+ private static String formatMetricAndDimensions(String metric, List
+ * 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.dynatrace2;
+
+/**
+ * Relevant limits for metric ingestion
+ *
+ * @author Oriol Barcelona
+ * @see metric ingestion limits
+ */
+class LineProtocolIngestionLimits {
+ static int METRIC_KEY_MAX_LENGTH = 250;
+ static int DIMENSION_KEY_MAX_LENGTH = 100;
+ static int DIMENSION_VALUE_MAX_LENGTH = 250;
+ static int METRIC_LINE_MAX_DIMENSIONS = 50;
+ static int METRIC_LINE_MAX_LENGTH = 2000;
+ static int MAX_METRIC_LINES_PER_REQUEST = 1000;
+}
diff --git a/implementations/micrometer-registry-dynatrace2/src/main/java/io/micrometer/dynatrace2/LineProtocolNamingConvention.java b/implementations/micrometer-registry-dynatrace2/src/main/java/io/micrometer/dynatrace2/LineProtocolNamingConvention.java
new file mode 100644
index 0000000000..2733bef02a
--- /dev/null
+++ b/implementations/micrometer-registry-dynatrace2/src/main/java/io/micrometer/dynatrace2/LineProtocolNamingConvention.java
@@ -0,0 +1,86 @@
+/**
+ * Copyright 2020 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.dynatrace2;
+
+import io.micrometer.core.instrument.Meter;
+import io.micrometer.core.instrument.config.NamingConvention;
+import io.micrometer.core.lang.Nullable;
+
+import java.util.Locale;
+import java.util.regex.Pattern;
+
+import static io.micrometer.dynatrace2.LineProtocolIngestionLimits.DIMENSION_KEY_MAX_LENGTH;
+import static io.micrometer.dynatrace2.LineProtocolIngestionLimits.DIMENSION_VALUE_MAX_LENGTH;
+import static io.micrometer.dynatrace2.LineProtocolIngestionLimits.METRIC_KEY_MAX_LENGTH;
+
+/**
+ * Naming convention for line protocol ingestion into Dynatrace
+ * @see metric key naming convention
+ *
+ * @author Oriol Barcelona
+ * @author David Mass
+ */
+public class LineProtocolNamingConvention implements NamingConvention {
+
+ private static final Pattern KEY_CLEANUP_PATTERN = Pattern.compile("[^a-z0-9-_.]");
+ private static final Pattern NAME_CLEANUP_PATTERN = Pattern.compile("[^a-z0-9-_.]");
+
+ private final NamingConvention delegate;
+
+ public LineProtocolNamingConvention(NamingConvention delegate) {
+ this.delegate = delegate;
+ }
+
+ public LineProtocolNamingConvention() {
+ this(NamingConvention.dot);
+ }
+
+ @Override
+ public String name(String name, Meter.Type type, @Nullable String baseUnit) {
+ String conventionName = name.toLowerCase(Locale.US);
+ String sanitized = NAME_CLEANUP_PATTERN.matcher(conventionName).replaceAll("_");
+ try {
+ return sanitized.substring(0,METRIC_KEY_MAX_LENGTH);
+ }
+ catch (IndexOutOfBoundsException e) {
+ return sanitized;
+ }
+ }
+
+ @Override
+ public String tagKey(String key) {
+ String conventionKey = key.toLowerCase(Locale.US);
+ String sanitized = KEY_CLEANUP_PATTERN.matcher(conventionKey).replaceAll("_");
+ try {
+ return sanitized.substring(0, DIMENSION_KEY_MAX_LENGTH);
+ }
+ catch (IndexOutOfBoundsException e) {
+ return sanitized;
+ }
+ }
+
+ @Override
+ public String tagValue(String value) {
+ String sanitized = value.replace("\\","\\\\");
+ sanitized = sanitized.replace("\"", "\\\"");
+ try {
+ return sanitized.substring(0, DIMENSION_VALUE_MAX_LENGTH);
+ }
+ catch (IndexOutOfBoundsException e) {
+ return sanitized;
+ }
+ }
+}
diff --git a/implementations/micrometer-registry-dynatrace2/src/main/java/io/micrometer/dynatrace2/MetricLineFactory.java b/implementations/micrometer-registry-dynatrace2/src/main/java/io/micrometer/dynatrace2/MetricLineFactory.java
new file mode 100644
index 0000000000..a8af50b128
--- /dev/null
+++ b/implementations/micrometer-registry-dynatrace2/src/main/java/io/micrometer/dynatrace2/MetricLineFactory.java
@@ -0,0 +1,157 @@
+/**
+ * Copyright 2020 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.dynatrace2;
+
+import io.micrometer.core.instrument.*;
+import io.micrometer.core.instrument.config.NamingConvention;
+
+import java.util.stream.Stream;
+
+/**
+ * Metric line factory which maps from micrometer domain to expected format in line protocol
+ *
+ * @author Oriol Barcelona
+ * @author David Mass
+ */
+class MetricLineFactory {
+ private final Clock clock;
+ private final NamingConvention namingConvention;
+
+ MetricLineFactory(Clock clock, NamingConvention lineProtocolNamingConvention) {
+ this.clock = clock;
+ this.namingConvention = lineProtocolNamingConvention;
+ }
+
+ /**
+ * Creates the formatted metric lines for the corresponding meter. A meter will have multiple
+ * metric lines considering the measurements within.
+ *
+ * @param meter to extract the measurements
+ * @return a stream of formatted metric lines
+ */
+ Stream
+ * 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.dynatrace2;
+
+import io.micrometer.core.instrument.util.AbstractPartition;
+import io.micrometer.core.ipc.http.HttpSender;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+class MetricsApiIngestion {
+ public static final String METRICS_INGESTION_URL = "/api/v2/metrics/ingest";
+
+ private final Logger logger = LoggerFactory.getLogger(MetricsApiIngestion.class);
+ private final HttpSender httpSender;
+ private final DynatraceConfig config;
+
+ MetricsApiIngestion(HttpSender httpSender, DynatraceConfig config) {
+ this.httpSender = httpSender;
+ this.config = config;
+ }
+
+ void sendInBatches(List
+ * 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.dynatrace2;
+
+import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
+
+class Streams {
+ static
+ * 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.
+ */
+@NonNullApi
+@NonNullFields
+package io.micrometer.dynatrace2;
+
+import io.micrometer.core.lang.NonNullApi;
+import io.micrometer.core.lang.NonNullFields;
diff --git a/implementations/micrometer-registry-dynatrace2/src/test/java/io/micrometer/dynatrace2/DynatraceConfigTest.java b/implementations/micrometer-registry-dynatrace2/src/test/java/io/micrometer/dynatrace2/DynatraceConfigTest.java
new file mode 100644
index 0000000000..eed11a1335
--- /dev/null
+++ b/implementations/micrometer-registry-dynatrace2/src/test/java/io/micrometer/dynatrace2/DynatraceConfigTest.java
@@ -0,0 +1,77 @@
+/**
+ * Copyright 2020 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.dynatrace2;
+
+import io.micrometer.core.instrument.config.validate.Validated;
+import org.assertj.core.api.WithAssertions;
+import org.junit.jupiter.api.Test;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static io.micrometer.dynatrace2.LineProtocolIngestionLimits.MAX_METRIC_LINES_PER_REQUEST;
+
+class DynatraceConfigTest implements WithAssertions {
+ private final Map
+ * 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.dynatrace2;
+
+import com.github.tomakehurst.wiremock.WireMockServer;
+import com.github.tomakehurst.wiremock.matching.StringValuePattern;
+import com.github.tomakehurst.wiremock.matching.UrlPattern;
+import io.micrometer.core.instrument.Clock;
+import io.micrometer.core.instrument.MockClock;
+import io.micrometer.core.instrument.Tag;
+import io.micrometer.core.instrument.Tags;
+import io.micrometer.core.instrument.config.validate.ValidationException;
+import org.assertj.core.api.WithAssertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import ru.lanwen.wiremock.ext.WiremockResolver;
+import ru.lanwen.wiremock.ext.WiremockResolver.Wiremock;
+
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
+import static com.github.tomakehurst.wiremock.client.WireMock.anyRequestedFor;
+import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
+import static com.github.tomakehurst.wiremock.client.WireMock.post;
+import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor;
+import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
+import static io.micrometer.dynatrace2.LineProtocolIngestionLimits.DIMENSION_KEY_MAX_LENGTH;
+import static io.micrometer.dynatrace2.LineProtocolIngestionLimits.DIMENSION_VALUE_MAX_LENGTH;
+import static io.micrometer.dynatrace2.LineProtocolIngestionLimits.METRIC_KEY_MAX_LENGTH;
+import static io.micrometer.dynatrace2.LineProtocolIngestionLimits.METRIC_LINE_MAX_LENGTH;
+
+/**
+ * Tests for {@link DynatraceMeterRegistry}.
+ *
+ * @author Oriol Barcelona
+ * @author David Mass
+ */
+@ExtendWith(WiremockResolver.class)
+class DynatraceMeterRegistryTest implements WithAssertions {
+
+ private static final String API_TOKEN = "DT-API-TOKEN";
+ private static final UrlPattern METRICS_INGESTION_URL = urlEqualTo(MetricsApiIngestion.METRICS_INGESTION_URL);
+ private static final StringValuePattern TEXT_PLAIN_CONTENT_TYPE = equalTo("text/plain");
+
+ WireMockServer dtApiServer;
+ Clock clock;
+ DynatraceMeterRegistry meterRegistry;
+
+ @BeforeEach
+ void setupServerAndConfig(@Wiremock WireMockServer server) {
+ this.dtApiServer = server;
+ DynatraceConfig config = new DynatraceConfig() {
+ @Override
+ public String get(String key) {
+ return null;
+ }
+
+ @Override
+ public String apiToken() {
+ return API_TOKEN;
+ }
+
+ @Override
+ public String uri() {
+ return server.baseUrl();
+ }
+
+ @Override
+ public String entityId() { return "HOST-06F288EE2A930951"; }
+ };
+ clock = new MockClock();
+ meterRegistry = DynatraceMeterRegistry.builder(config)
+ .clock(clock)
+ .build();
+ dtApiServer.stubFor(post(METRICS_INGESTION_URL)
+ .willReturn(aResponse().withStatus(202)));
+ }
+
+ @Test
+ void shouldThrowValidationException_whenUriIsMissingInConfig() {
+ DynatraceConfig config = new DynatraceConfig() {
+ @Override
+ public String get(String key) {
+ return null;
+ }
+
+ @Override
+ public String apiToken() {
+ return "apiToken";
+ }
+ };
+
+ assertThatThrownBy(() -> DynatraceMeterRegistry.builder(config).build())
+ .isExactlyInstanceOf(ValidationException.class);
+ }
+
+ @Test
+ void shouldThrowValidationException_whenApiTokenIsMissingInConfig() {
+ DynatraceConfig config = new DynatraceConfig() {
+ @Override
+ public String get(String key) {
+ return null;
+ }
+
+ @Override
+ public String uri() {
+ return "uri";
+ }
+ };
+
+ assertThatThrownBy(() -> DynatraceMeterRegistry.builder(config).build())
+ .isExactlyInstanceOf(ValidationException.class);
+ }
+
+ @Test
+ void shouldIngestAMetricThroughTheApi() {
+ meterRegistry.gauge("cpu.temperature", 55);
+
+ meterRegistry.publish();
+
+ dtApiServer.verify(postRequestedFor(METRICS_INGESTION_URL)
+ .withHeader("Content-Type", TEXT_PLAIN_CONTENT_TYPE)
+ .withRequestBody(equalToMetricLines("cpu.temperature,dt.entity.host=\"HOST-06F288EE2A930951\" 55"))
+ );
+ }
+
+ @Test
+ void shouldIngestAMetricThroughTheApi_whenHasDimensions() {
+ meterRegistry.gauge(
+ "cpu.temperature",
+ Tags.of("hostname", "server01", "cpu", "1"),
+ 55);
+
+ meterRegistry.publish();
+
+ dtApiServer.verify(postRequestedFor(METRICS_INGESTION_URL)
+ .withHeader("Content-Type", TEXT_PLAIN_CONTENT_TYPE)
+ .withRequestBody(equalToMetricLines("cpu.temperature,cpu=\"1\",dt.entity.host=\"HOST-06F288EE2A930951\",hostname=\"server01\" 55"))
+ );
+ }
+
+ @Test
+ void shouldIngestMultipleMetricsThroughTheApi_whenSameMetricButDifferentDimensions() {
+ meterRegistry.gauge(
+ "cpu.temperature",
+ Tags.of("hostname", "server01", "cpu", "1"),
+ 55);
+ meterRegistry.gauge(
+ "cpu.temperature",
+ Tags.of("hostname", "server01", "cpu", "2"),
+ 50);
+
+
+ meterRegistry.publish();
+
+ dtApiServer.verify(postRequestedFor(METRICS_INGESTION_URL)
+ .withHeader("Content-Type", TEXT_PLAIN_CONTENT_TYPE)
+ .withRequestBody(equalToMetricLines(
+ "cpu.temperature,cpu=\"1\",dt.entity.host=\"HOST-06F288EE2A930951\",hostname=\"server01\" 55",
+ "cpu.temperature,cpu=\"2\",dt.entity.host=\"HOST-06F288EE2A930951\",hostname=\"server01\" 50"
+ ))
+ );
+ }
+
+
+ @Test
+ void shouldMetricNameBeSanitized_whenSpecialChars() {
+ meterRegistry.gauge(
+ "cpu#temperature",
+ Tags.of("hostname", "server01", "cpu", "1"),
+ 55);
+
+ meterRegistry.publish();
+
+ dtApiServer.verify(postRequestedFor(METRICS_INGESTION_URL)
+ .withHeader("Content-Type", TEXT_PLAIN_CONTENT_TYPE)
+ .withRequestBody(equalToMetricLines("cpu_temperature,cpu=\"1\",dt.entity.host=\"HOST-06F288EE2A930951\",hostname=\"server01\" 55"))
+ );
+ }
+
+
+ @Test
+ void shouldAddCommonTag_whenEntityIdPropIsAdded() {
+ meterRegistry.gauge(
+ "cpu_temperature", 55);
+
+ meterRegistry.publish();
+
+ dtApiServer.verify(postRequestedFor(METRICS_INGESTION_URL)
+ .withHeader("Content-Type", TEXT_PLAIN_CONTENT_TYPE)
+ .withRequestBody(equalToMetricLines("cpu_temperature,dt.entity.host=\"HOST-06F288EE2A930951\" 55"))
+ );
+ }
+
+ @Test
+ void shouldSkipMetricLines_whenAreBiggerThanMaxLengthLimit() {
+ String metricName = newString(METRIC_KEY_MAX_LENGTH);
+ long metricValue = 55;
+ int lengthForTags = METRIC_LINE_MAX_LENGTH - metricName.length() - String.valueOf(metricValue).length();
+ int maxSizeTagLength = DIMENSION_KEY_MAX_LENGTH + DIMENSION_VALUE_MAX_LENGTH;
+ int numberOfTags = (lengthForTags / maxSizeTagLength) + 1;
+
+ List
+ * 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.dynatrace2;
+
+import io.micrometer.core.instrument.*;
+import io.micrometer.core.instrument.config.NamingConvention;
+import org.assertj.core.api.WithAssertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static io.micrometer.dynatrace2.LineProtocolFormatters.formatCounterMetricLine;
+import static io.micrometer.dynatrace2.LineProtocolFormatters.formatGaugeMetricLine;
+import static io.micrometer.dynatrace2.LineProtocolFormatters.formatTimerMetricLine;
+
+
+class LineProtocolFormattersTest implements WithAssertions {
+
+ DynatraceMeterRegistry meterRegistry;
+ Clock clock;
+ NamingConvention namingConvention = new LineProtocolNamingConvention();
+
+ @BeforeEach
+ void setUpConfigAndMeterRegistry() {
+ DynatraceConfig config = new DynatraceConfig() {
+ @Override
+ public String get(String key) {
+ return null;
+ }
+
+ @Override
+ public String apiToken() {
+ return "API_TOKEN";
+ }
+
+ @Override
+ public String uri() {
+ return "https://dynatrace.com";
+ }
+
+ @Override
+ public String entityId() { return ""; }
+ };
+ clock = new MockClock();
+ meterRegistry = DynatraceMeterRegistry.builder(config)
+ .clock(clock)
+ .build();
+ }
+
+ @Test
+ void shouldCreateAGaugeMetricLine_whenDimensionsAreEmpty() {
+ meterRegistry.gauge("my.metric", 55);
+
+ List
+ * 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.dynatrace2;
+
+import io.micrometer.core.instrument.Meter;
+import io.micrometer.core.instrument.config.NamingConvention;
+import org.assertj.core.api.WithAssertions;
+import org.junit.jupiter.api.Test;
+
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+import static io.micrometer.dynatrace2.LineProtocolIngestionLimits.DIMENSION_KEY_MAX_LENGTH;
+import static io.micrometer.dynatrace2.LineProtocolIngestionLimits.DIMENSION_VALUE_MAX_LENGTH;
+import static io.micrometer.dynatrace2.LineProtocolIngestionLimits.METRIC_KEY_MAX_LENGTH;
+
+class LineProtocolNamingConventionTest implements WithAssertions {
+
+ NamingConvention namingConvention = new LineProtocolNamingConvention();
+
+ @Test
+ void shouldMetricKeyBeTrimmed_whenIsGreaterThanMaxLength() {
+ String name = stringOfSize(METRIC_KEY_MAX_LENGTH + 1);
+
+ String metricKey = namingConvention.name(name, Meter.Type.GAUGE);
+
+ assertThat(metricKey).hasSize(METRIC_KEY_MAX_LENGTH);
+ }
+
+ @Test
+ void shouldMetricKeyBeSanitized_whenItContainsSpecialChars() {
+ String name = "my,metric";
+
+ String metricKey = namingConvention.name(name, Meter.Type.GAUGE);
+
+ assertThat(metricKey).matches("my_metric");
+ }
+
+ @Test
+ void shouldDimensionNameBeTrimmed_whenIsGreaterThanMaxLength() {
+ String key = stringOfSize(DIMENSION_KEY_MAX_LENGTH + 1);
+
+ String dimensionName = namingConvention.tagKey(key);
+
+ assertThat(dimensionName).hasSize(DIMENSION_KEY_MAX_LENGTH);
+ }
+
+ @Test
+ void shouldDimensionNameBeSanitized_whenItContainsSpecialChars() {
+ String key = "country#lang";
+
+ String dimensionName = namingConvention.tagKey(key);
+
+ assertThat(dimensionName).matches("country_lang");
+ }
+
+ @Test
+ void shouldDimensionValueBeTrimmed_whenIsGreaterThanMaxLength() {
+ String value = stringOfSize(DIMENSION_VALUE_MAX_LENGTH + 1);
+
+ String dimensionValue = namingConvention.tagValue(value);
+
+ assertThat(dimensionValue).hasSize(DIMENSION_VALUE_MAX_LENGTH);
+ }
+
+ private String stringOfSize(int size) {
+ return IntStream.range(0, size)
+ .mapToObj(doesnotmatter -> "a")
+ .collect(Collectors.joining());
+ }
+}
diff --git a/implementations/micrometer-registry-dynatrace2/src/test/java/io/micrometer/dynatrace2/MetricsApiIngestionTest.java b/implementations/micrometer-registry-dynatrace2/src/test/java/io/micrometer/dynatrace2/MetricsApiIngestionTest.java
new file mode 100644
index 0000000000..d5dc5504b9
--- /dev/null
+++ b/implementations/micrometer-registry-dynatrace2/src/test/java/io/micrometer/dynatrace2/MetricsApiIngestionTest.java
@@ -0,0 +1,127 @@
+/**
+ * Copyright 2020 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.dynatrace2;
+
+import io.micrometer.core.ipc.http.HttpSender;
+import io.micrometer.core.ipc.http.HttpSender.Method;
+import io.micrometer.core.ipc.http.HttpSender.Request;
+import org.assertj.core.api.WithAssertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import wiremock.com.google.common.collect.ImmutableMap;
+
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+import static io.micrometer.dynatrace2.MetricsApiIngestion.METRICS_INGESTION_URL;
+import static java.util.Arrays.asList;
+import static java.util.Collections.emptyList;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.when;
+
+class MetricsApiIngestionTest implements WithAssertions {
+
+ HttpSender httpSender;
+ DynatraceConfig config;
+ MetricsApiIngestion metricsApiIngestion;
+
+ @BeforeEach
+ void setUp() throws Throwable {
+ httpSender = spy(HttpSender.class);
+ when(httpSender.send(any())).thenReturn(new HttpSender.Response(202, null));
+
+ config = mock(DynatraceConfig.class);
+ when(config.batchSize()).thenReturn(2);
+ when(config.uri()).thenReturn("https://micrometer.dynatrace.com");
+ when(config.apiToken()).thenReturn("my-token");
+
+ metricsApiIngestion = new MetricsApiIngestion(httpSender, config);
+ }
+
+ @Test
+ void shouldNotSendRequests_whenNoMetricLines() {
+ List
+ * 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.boot2.samples;
+
+import io.micrometer.boot2.samples.components.PersonController;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.builder.SpringApplicationBuilder;
+import org.springframework.scheduling.annotation.EnableScheduling;
+
+@SpringBootApplication(scanBasePackageClasses = PersonController.class)
+@EnableScheduling
+public class Dynatrace2Sample {
+ public static void main(String[] args) {
+ new SpringApplicationBuilder(Dynatrace2Sample.class).profiles("dynatrace2").run(args);
+ }
+}
diff --git a/samples/micrometer-samples-boot2/src/main/resources/application-dynatrace2.yml b/samples/micrometer-samples-boot2/src/main/resources/application-dynatrace2.yml
new file mode 100644
index 0000000000..552a29bb35
--- /dev/null
+++ b/samples/micrometer-samples-boot2/src/main/resources/application-dynatrace2.yml
@@ -0,0 +1,20 @@
+#
+# Copyright 2020 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.
+#
+
+management.metrics.export.dynatrace2:
+ enabled: true
+ apiToken: my-api-token
+ uri: https://my-environment-id.live.dynatrace.com
diff --git a/samples/micrometer-samples-boot2/src/main/resources/application.yml b/samples/micrometer-samples-boot2/src/main/resources/application.yml
index 8b0a9e3a88..03b951080e 100644
--- a/samples/micrometer-samples-boot2/src/main/resources/application.yml
+++ b/samples/micrometer-samples-boot2/src/main/resources/application.yml
@@ -27,6 +27,7 @@ management:
azuremonitor.enabled: false
datadog.enabled: false
dynatrace.enabled: false
+ dynatrace2.enabled: false
elastic.enabled: false
ganglia.enabled: false
graphite.enabled: false
diff --git a/samples/micrometer-samples-core/build.gradle b/samples/micrometer-samples-core/build.gradle
index be12415917..93bd4751ae 100644
--- a/samples/micrometer-samples-core/build.gradle
+++ b/samples/micrometer-samples-core/build.gradle
@@ -8,7 +8,7 @@ dependencies {
implementation('ch.qos.logback:logback-classic')
implementation('org.slf4j:slf4j-api')
- ['atlas', 'prometheus', 'datadog', 'ganglia', 'elastic', 'graphite', 'jmx', 'influx', 'statsd', 'new-relic', 'cloudwatch', 'cloudwatch2', 'signalfx', 'wavefront', 'dynatrace', 'azure-monitor', 'humio', 'appoptics', 'kairos', 'stackdriver'].each { sys ->
+ ['atlas', 'prometheus', 'datadog', 'ganglia', 'elastic', 'graphite', 'jmx', 'influx', 'statsd', 'new-relic', 'cloudwatch', 'cloudwatch2', 'signalfx', 'wavefront', 'dynatrace', 'dynatrace2', 'azure-monitor', 'humio', 'appoptics', 'kairos', 'stackdriver'].each { sys ->
implementation project(":micrometer-registry-$sys")
}
diff --git a/samples/micrometer-samples-core/src/main/java/io/micrometer/core/samples/utils/SampleRegistries.java b/samples/micrometer-samples-core/src/main/java/io/micrometer/core/samples/utils/SampleRegistries.java
index 95ad4dd18d..775b5d32c1 100644
--- a/samples/micrometer-samples-core/src/main/java/io/micrometer/core/samples/utils/SampleRegistries.java
+++ b/samples/micrometer-samples-core/src/main/java/io/micrometer/core/samples/utils/SampleRegistries.java
@@ -32,8 +32,6 @@
import io.micrometer.core.lang.Nullable;
import io.micrometer.datadog.DatadogConfig;
import io.micrometer.datadog.DatadogMeterRegistry;
-import io.micrometer.dynatrace.DynatraceConfig;
-import io.micrometer.dynatrace.DynatraceMeterRegistry;
import io.micrometer.elastic.ElasticConfig;
import io.micrometer.elastic.ElasticMeterRegistry;
import io.micrometer.ganglia.GangliaConfig;
@@ -424,8 +422,8 @@ public Duration step() {
}, Clock.SYSTEM);
}
- public static DynatraceMeterRegistry dynatrace(String apiToken, String uri) {
- return new DynatraceMeterRegistry(new DynatraceConfig() {
+ public static io.micrometer.dynatrace.DynatraceMeterRegistry dynatrace(String apiToken, String uri) {
+ return new io.micrometer.dynatrace.DynatraceMeterRegistry(new io.micrometer.dynatrace.DynatraceConfig() {
@Override
public String get(String key) {
return null;
@@ -453,6 +451,45 @@ public Duration step() {
}, Clock.SYSTEM);
}
+ public static io.micrometer.dynatrace2.DynatraceMeterRegistry dynatrace2(String apiToken, String uri, String entityId, String deviceName, String groupName) {
+ return io.micrometer.dynatrace2.DynatraceMeterRegistry.builder(new io.micrometer.dynatrace2.DynatraceConfig() {
+ @Override
+ public String get(String key) {
+ return null;
+ }
+
+ @Override
+ public String apiToken() {
+ return apiToken;
+ }
+
+ @Override
+ public String uri() {
+ return uri;
+ }
+
+ @Override
+ public String entityId() {
+ return entityId;
+ }
+
+ @Override
+ public String deviceName() {
+ return deviceName;
+ }
+
+ @Override
+ public String groupName() {
+ return groupName;
+ }
+
+ @Override
+ public Duration step() {
+ return Duration.ofSeconds(5);
+ }
+ }).build();
+ }
+
public static HumioMeterRegistry humio(String apiToken) {
return new HumioMeterRegistry(new HumioConfig() {
@Override
diff --git a/scripts/sync-to-maven-central.sh b/scripts/sync-to-maven-central.sh
index 2b626a3ada..70e3d34295 100755
--- a/scripts/sync-to-maven-central.sh
+++ b/scripts/sync-to-maven-central.sh
@@ -30,6 +30,7 @@ MODULES=(
micrometer-registry-elastic
micrometer-registry-kairos
micrometer-registry-dynatrace
+ micrometer-registry-dynatrace2
micrometer-registry-humio
micrometer-registry-azure-monitor
micrometer-registry-appoptics
diff --git a/settings.gradle b/settings.gradle
index 295c448c6f..71769270ae 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -10,7 +10,7 @@ include 'micrometer-jersey2'
include 'micrometer-test'
-['atlas', 'prometheus', 'datadog', 'elastic', 'ganglia', 'graphite', 'health', 'jmx', 'influx', 'statsd', 'new-relic', 'cloudwatch', 'cloudwatch2', 'signalfx', 'wavefront', 'dynatrace', 'azure-monitor', 'humio', 'appoptics', 'kairos', 'stackdriver', 'opentsdb'].each { sys ->
+['atlas', 'prometheus', 'datadog', 'elastic', 'ganglia', 'graphite', 'health', 'jmx', 'influx', 'statsd', 'new-relic', 'cloudwatch', 'cloudwatch2', 'signalfx', 'wavefront', 'dynatrace', 'dynatrace2', 'azure-monitor', 'humio', 'appoptics', 'kairos', 'stackdriver', 'opentsdb'].each { sys ->
include "micrometer-registry-$sys"
project(":micrometer-registry-$sys").projectDir = new File(rootProject.projectDir, "implementations/micrometer-registry-$sys")
}
> partition(List