-
Notifications
You must be signed in to change notification settings - Fork 1k
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
Changes from all commits
8df6874
bed13b9
ae2ca94
20ece09
bf5b067
f64b6c5
4f0ba9c
4ce34bf
a8d74dd
1f9a927
142f0b2
08aa75d
2a44505
cec77b4
7ed29c0
0286420
b2910bc
3c1c067
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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")) { | ||
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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 ({}).", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
"#.#####", | ||
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); | ||
} | ||
|
||
} |
There was a problem hiding this comment.
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?