Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,18 @@ default String prefix() {
return "newrelic";
}

default boolean meterNameEventTypeEnabled() {
String v = get(prefix() + ".meterNameEventTypeEnabled");
return (v == null) ? false : new Boolean(v);
}

default String eventType() {
String v = get(prefix() + ".eventType");
if (v == null)
v = "MicrometerSample";
return v;
}

default String apiKey() {
String v = get(prefix() + ".apiKey");
if (v == null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,33 +15,7 @@
*/
package io.micrometer.newrelic;

import static io.micrometer.core.instrument.util.StringEscapeUtils.escapeJson;

import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import io.micrometer.core.instrument.Clock;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.DistributionSummary;
import io.micrometer.core.instrument.FunctionCounter;
import io.micrometer.core.instrument.FunctionTimer;
import io.micrometer.core.instrument.Gauge;
import io.micrometer.core.instrument.LongTaskTimer;
import io.micrometer.core.instrument.Measurement;
import io.micrometer.core.instrument.Meter;
import io.micrometer.core.instrument.Tag;
import io.micrometer.core.instrument.Tags;
import io.micrometer.core.instrument.TimeGauge;
import io.micrometer.core.instrument.*;
import io.micrometer.core.instrument.Timer;
import io.micrometer.core.instrument.config.MissingRequiredConfigurationException;
import io.micrometer.core.instrument.config.NamingConvention;
Expand All @@ -53,6 +27,18 @@
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.*;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static io.micrometer.core.instrument.util.StringEscapeUtils.escapeJson;

/**
* Publishes metrics to New Relic Insights.
*
Expand Down Expand Up @@ -87,15 +73,23 @@ public NewRelicMeterRegistry(NewRelicConfig config, Clock clock, ThreadFactory t
this(config, clock, threadFactory, new HttpUrlConnectionSender(config.connectTimeout(), config.readTimeout()));
}

private NewRelicMeterRegistry(NewRelicConfig config, Clock clock, ThreadFactory threadFactory, HttpSender httpClient) {
// VisibleForTesting
NewRelicMeterRegistry(NewRelicConfig config, Clock clock, ThreadFactory threadFactory, HttpSender httpClient) {
super(config, clock);

if (config.accountId() == null) {
if (config.meterNameEventTypeEnabled() == false
&& (config.eventType() == null || config.eventType().isEmpty())) {
throw new MissingRequiredConfigurationException("eventType must be set to report metrics to New Relic");
}
if (config.accountId() == null || config.accountId().isEmpty()) {
throw new MissingRequiredConfigurationException("accountId must be set to report metrics to New Relic");
}
if (config.apiKey() == null) {
if (config.apiKey() == null || config.apiKey().isEmpty()) {
throw new MissingRequiredConfigurationException("apiKey must be set to report metrics to New Relic");
}
if (config.uri() == null || config.uri().isEmpty()) {
throw new MissingRequiredConfigurationException("uri must be set to report metrics to New Relic");
}

this.config = config;
this.httpClient = httpClient;
Expand Down Expand Up @@ -140,7 +134,8 @@ private Stream<String> writeLongTaskTimer(LongTaskTimer ltt) {
return Stream.of(
event(ltt.getId(),
new Attribute("activeTasks", ltt.activeTasks()),
new Attribute("duration", ltt.duration(getBaseTimeUnit())))
new Attribute("duration", ltt.duration(getBaseTimeUnit())),
new Attribute("timeUnit", getBaseTimeUnit().name().toLowerCase()))
);
}

Expand All @@ -154,6 +149,7 @@ Stream<String> writeFunctionCounter(FunctionCounter counter) {
}

private Stream<String> writeCounter(Counter counter) {
//TODO: Double.isFinite() check here like writeFunctionCounter ???
return Stream.of(event(counter.getId(), new Attribute("throughput", counter.count())));
}

Expand All @@ -170,7 +166,10 @@ Stream<String> writeGauge(Gauge gauge) {
Stream<String> writeTimeGauge(TimeGauge gauge) {
Double value = gauge.value(getBaseTimeUnit());
if (Double.isFinite(value)) {
return Stream.of(event(gauge.getId(), new Attribute("value", value)));
return Stream.of(
event(gauge.getId(),
new Attribute("value", value),
new Attribute("timeUnit", getBaseTimeUnit().name().toLowerCase())));
}
return Stream.empty();
}
Expand All @@ -191,7 +190,8 @@ private Stream<String> writeTimer(Timer timer) {
new Attribute("count", timer.count()),
new Attribute("avg", timer.mean(getBaseTimeUnit())),
new Attribute("totalTime", timer.totalTime(getBaseTimeUnit())),
new Attribute("max", timer.max(getBaseTimeUnit()))
new Attribute("max", timer.max(getBaseTimeUnit())),
new Attribute("timeUnit", getBaseTimeUnit().name().toLowerCase())
));
}

Expand All @@ -200,7 +200,8 @@ private Stream<String> writeFunctionTimer(FunctionTimer timer) {
event(timer.getId(),
new Attribute("count", timer.count()),
new Attribute("avg", timer.mean(getBaseTimeUnit())),
new Attribute("totalTime", timer.totalTime(getBaseTimeUnit()))
new Attribute("totalTime", timer.totalTime(getBaseTimeUnit())),
new Attribute("timeUnit", getBaseTimeUnit().name().toLowerCase())
)
);
}
Expand All @@ -224,6 +225,18 @@ Stream<String> writeMeter(Meter meter) {
}

private String event(Meter.Id id, Attribute... attributes) {
if (config.meterNameEventTypeEnabled() == false) {
//Include contextual attributes when publishing all metrics under a single categorical eventType,
// NOT when publishing an eventType per Meter/metric name
int size = attributes.length;
Attribute[] newAttrs = Arrays.copyOf(attributes, size + 2);

String name = id.getConventionName(config().namingConvention());
newAttrs[size] = new Attribute("metricName", name);
newAttrs[size + 1] = new Attribute("metricType", id.getType().toString());

return event(id, Tags.empty(), newAttrs);
}
return event(id, Tags.empty(), attributes);
}

Expand All @@ -240,11 +253,29 @@ private String event(Meter.Id id, Iterable<Tag> extraTags, Attribute... attribut
.append("\":\"").append(escapeJson(convention.tagValue(tag.getValue()))).append("\"");
}

String eventType = getEventType(id, config, convention);

return Arrays.stream(attributes)
.map(attr -> ",\"" + attr.getName() + "\":" + DoubleFormat.wholeOrDecimal(attr.getValue().doubleValue()))
.collect(Collectors.joining("", "{\"eventType\":\"" + escapeJson(getConventionName(id)) + "\"", tagsJson + "}"));
.map(attr ->
(attr.getValue() instanceof Number)
? ",\"" + attr.getName() + "\":" + DoubleFormat.wholeOrDecimal(((Number)attr.getValue()).doubleValue())
: ",\"" + attr.getName() + "\":\"" + convention.tagValue(attr.getValue().toString()) + "\""
)
.collect(Collectors.joining("", "{\"eventType\":\"" + escapeJson(eventType) + "\"", tagsJson + "}"));
}

String getEventType(Meter.Id id, NewRelicConfig config, NamingConvention convention) {
String eventType = null;
if (config.meterNameEventTypeEnabled()) {
//meter/metric name event type
eventType = id.getConventionName(convention);
} else {
//static eventType "category"
eventType = config.eventType();
}
return eventType;
}

private void sendEvents(String insightsEndpoint, Stream<String> events) {
try {
AtomicInteger totalEvents = new AtomicInteger();
Expand Down Expand Up @@ -300,9 +331,9 @@ public NewRelicMeterRegistry build() {

private class Attribute {
private final String name;
private final Number value;
private final Object value;

private Attribute(String name, Number value) {
private Attribute(String name, Object value) {
this.name = name;
this.value = value;
}
Expand All @@ -311,7 +342,7 @@ public String getName() {
return name;
}

public Number getValue() {
public Object getValue() {
return value;
}
}
Expand Down
Loading