Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add dynatrace meter registry for metrics api v2 #2406

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions implementations/micrometer-registry-dynatrace2/build.gradle
Original file line number Diff line number Diff line change
@@ -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'
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/**
* Copyright 2020 VMware, Inc.
* <p>
* 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
* <p>
* https://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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")) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is always required for V1, did V1 mandate auth while V2 does not?

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<Validated<Integer>, Validated<Integer>> invalidateWhenGreaterThan(int value) {
return v -> v.invalidateWhen(b -> b > value, "cannot be greater than " + value, InvalidReason.MALFORMED);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
/**
* Copyright 2020 VMware, Inc.
* <p>
* 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
* <p>
* https://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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 <a href="https://www.dynatrace.com/support/help/dynatrace-api/environment-api/metric-v2/post-ingest-metrics/">Dynatrace metric ingestion v2</a>
* @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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need a new instance every time?


Map<Boolean, List<String>> metricLines = getMeters()
.stream()
.flatMap(metricLineFactory::toMetricLines)
.collect(Collectors.partitioningBy(this::lineLengthGreaterThanLimit));

List<String> metricLinesToSkip = metricLines.get(true);
if (!metricLinesToSkip.isEmpty()) {
logger.warn(
"Dropping {} metric lines because are greater than line protocol max length limit ({}).",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you sure this limit still exist? I did not see it in the docs.

metricLinesToSkip.size(),
LineProtocolIngestionLimits.METRIC_LINE_MAX_LENGTH);
}

List<String> metricLinesToSend = metricLines.get(false);
new MetricsApiIngestion(httpClient, config)
.sendInBatches(metricLinesToSend);
}

private boolean lineLengthGreaterThanLimit(String line) {
return line.length() > LineProtocolIngestionLimits.METRIC_LINE_MAX_LENGTH;
}

private void addCommonTags() {
addCommonTags_entityId();
addCommonTags_deviceName();
addCommonTags_groupName();
}

private void addCommonTags_groupName() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please do camelCase?

if (!config.deviceName().equals("")) {
config().commonTags("group-name", config.groupName());
}
}

private void addCommonTags_deviceName() {
if (!config.deviceName().equals("")) {
config().commonTags("device-name", config.deviceName());
}
}

private void addCommonTags_entityId() {
String[] cases = {"HOST","PROCESS_GROUP_INSTANCE","PROCESS_GROUP","CUSTOM_DEVICE_GROUP","CUSTOM_DEVICE"};
if (!config.entityId().equals("")) {
int index;
for (index = 0;index < cases.length; index++) {
if (config.entityId().startsWith(cases[index])) break;
}
switch (index) {
case 0:
config().commonTags("dt.entity.host", config.entityId());
break;
case 1:
config().commonTags("dt.entity.process_group_instance", config.entityId());
break;
case 2:
config().commonTags("dt.entity.process_group", config.entityId());
break;
case 3:
config().commonTags("dt.entity.custom_device_group", config.entityId());
break;
case 4:
config().commonTags("dt.entity.custom_device", config.entityId());
break;
default:
logger.debug("Entity ID Not Available");
}
}
}

@Override
protected TimeUnit getBaseTimeUnit() {
return TimeUnit.MILLISECONDS;
}

public static Builder builder(DynatraceConfig config) {
return new Builder(config);
}

public static class Builder {
private final DynatraceConfig config;

private Clock clock = Clock.SYSTEM;
private ThreadFactory threadFactory = DEFAULT_THREAD_FACTORY;
private HttpSender httpClient;

@SuppressWarnings("deprecation")
Builder(DynatraceConfig config) {
this.config = config;
this.httpClient = new HttpUrlConnectionSender(config.connectTimeout(), config.readTimeout());
}

public Builder clock(Clock clock) {
this.clock = clock;
return this;
}

public Builder threadFactory(ThreadFactory threadFactory) {
this.threadFactory = threadFactory;
return this;
}

public Builder httpClient(HttpSender httpClient) {
this.httpClient = httpClient;
return this;
}

public DynatraceMeterRegistry build() {
return new DynatraceMeterRegistry(config, clock, threadFactory, httpClient);
}
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/**
* Copyright 2020 VMware, Inc.
* <p>
* 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
* <p>
* https://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DecimalFormat is not thread-safe, can it be used by multiple thread?

"#.#####",
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<Tag> tags, NamingConvention namingConvention) {
if (tags.isEmpty() ) {
return metric;
}
return String.format("%s,%s", metric, formatTags(tags, namingConvention));
}

private static String formatMetricName(Meter meter, Measurement measurement) {
String meterName = meter.getId().getName();
if (Streams.of(meter.measure()).count() == 1) {
return meterName;
}

return String.format("%s.%s", meterName, measurement.getStatistic().getTagValueRepresentation());
}

private static String formatTags(List<Tag> tags, NamingConvention namingConvention) {
return tags.stream()
.map(tag -> String.format("%s=\"%s\"", namingConvention.tagKey(tag.getKey()), namingConvention.tagValue(tag.getValue())))
.limit(LineProtocolIngestionLimits.METRIC_LINE_MAX_DIMENSIONS)
.collect(Collectors.joining(","));
}

private static String formatMetricValue(double value) {
return METRIC_VALUE_FORMAT.format(value);
}

}
Loading