Skip to content

Commit

Permalink
New relic integration (fixes micrometer-metrics#122)
Browse files Browse the repository at this point in the history
  • Loading branch information
jkschneider committed Nov 29, 2017
1 parent 0d125ae commit 8d1402f
Show file tree
Hide file tree
Showing 36 changed files with 685 additions and 51 deletions.
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ subprojects {
}

if (project.extensions.findByName('bintray')) {
bintray.labels = ['micrometer', 'atlas', 'metrics', 'prometheus', 'spectator', 'influx']
bintray.labels = ['micrometer', 'atlas', 'metrics', 'prometheus', 'spectator', 'influx', 'new-relic']
}
}

Expand Down
8 changes: 8 additions & 0 deletions implementations/micrometer-registry-new-relic/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
apply plugin: 'org.junit.platform.gradle.plugin'

dependencies {
compile project(':micrometer-core')
compile 'com.fasterxml.jackson.core:jackson-databind:latest.release'

testCompile project(':micrometer-test')
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/**
* Copyright 2017 Pivotal Software, 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>
* http://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.newrelic;

import io.micrometer.core.instrument.step.StepRegistryConfig;

public interface NewRelicConfig extends StepRegistryConfig {
@Override
default String prefix() {
return "newrelic";
}

default String apiKey() {
String v = get(prefix() + ".apiKey");
if (v == null)
throw new IllegalStateException(prefix() + ".apiKey must be set to report metrics to New Relic");
return v;
}

default String accountId() {
String v = get(prefix() + ".accountId");
if (v == null)
throw new IllegalStateException(prefix() + ".accountId must be set to report metrics to New Relic");
return v;
}

/**
* Returns the URI for the New Relic insights API. The default is
* {@code https://insights-collector.newrelic.com}. If you need to pass through
* a proxy, you can change this value.
*/
default String uri() {
String v = get(prefix() + ".uri");
return (v == null) ? "https://insights-collector.newrelic.com" : v;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
/**
* Copyright 2017 Pivotal Software, 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>
* http://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.newrelic;

import com.fasterxml.jackson.databind.ObjectMapper;
import io.micrometer.core.annotation.Incubating;
import io.micrometer.core.instrument.*;
import io.micrometer.core.instrument.config.NamingConvention;
import io.micrometer.core.instrument.step.StepMeterRegistry;
import io.micrometer.core.instrument.util.DoubleFormat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.TimeUnit;

import static java.util.stream.Collectors.joining;

/**
* @author Jon Schneider
*/
@Incubating(since = "1.0.0-rc.5")
public class NewRelicMeterRegistry extends StepMeterRegistry {
private final NewRelicConfig config;
private final ObjectMapper mapper = new ObjectMapper();
private final Logger logger = LoggerFactory.getLogger(NewRelicMeterRegistry.class);

public NewRelicMeterRegistry(NewRelicConfig config, Clock clock) {
super(config, clock);
this.config = config;
config().namingConvention(NamingConvention.camelCase);
start();
}

@Override
protected void publish() {
try {
URL insightsEndpoint = URI.create(config.uri() + "/v1/accounts/" + config.accountId() + "/events").toURL();

// New Relic's Insights API limits us to 1000 events per call
final int batchSize = Math.min(config.batchSize(), 1000);

List<Event> events = new ArrayList<>();

for (Meter meter : getMeters()) {
Meter.Id id = meter.getId();

if (meter instanceof Timer) {
HistogramSnapshot t = ((Timer) meter).takeSnapshot(false);

events.add(event(id, "count", t.count()));
events.add(event(id, "sum", t.total(getBaseTimeUnit())));
events.add(event(id, "avg", t.mean(getBaseTimeUnit())));
events.add(event(id, "max", t.max(getBaseTimeUnit())));

for (ValueAtPercentile valueAtPercentile : t.percentileValues()) {
events.add(event(id, "percentile", valueAtPercentile.value(getBaseTimeUnit()), "phi",
DoubleFormat.toString(valueAtPercentile.percentile())));
}
} else if (meter instanceof FunctionTimer) {
FunctionTimer t = (FunctionTimer) meter;
events.add(event(id, "count", t.count()));
events.add(event(id, "sum", t.count()));
events.add(event(id, "mean", t.mean(getBaseTimeUnit())));
} else if (meter instanceof DistributionSummary) {
HistogramSnapshot t = ((DistributionSummary) meter).takeSnapshot(false);

events.add(event(id, "count", t.count()));
events.add(event(id, "sum", t.total()));
events.add(event(id, "avg", t.mean()));
events.add(event(id, "max", t.max()));

for (ValueAtPercentile valueAtPercentile : t.percentileValues()) {
events.add(event(id, "percentile", valueAtPercentile.value(), "phi",
DoubleFormat.toString(valueAtPercentile.percentile())));
}
} else {
for (Measurement measurement : meter.measure()) {
events.add(event(id, measurement.getStatistic().toString(), measurement.getValue()));
}
}

if (events.size() > batchSize) {
sendEvents(insightsEndpoint, events.subList(0, batchSize));
events = new ArrayList<>(events.subList(batchSize, events.size()));
} else if (events.size() == batchSize) {
sendEvents(insightsEndpoint, events);
events = new ArrayList<>();
}
}

// drain the remaining event list
if (!events.isEmpty()) {
sendEvents(insightsEndpoint, events);
}
} catch (MalformedURLException e) {
throw new IllegalArgumentException("Malformed New Relic insights endpoint, see '" + config.prefix() + ".uri'", e);
}
}

private Event event(Meter.Id id, String statistic, Number value, String... additionalTags) {
Event event = new Event();

event.put("eventType", getConventionName(id));
event.put("statistic", statistic);
event.put("value", value);

for (int i = 0; i < additionalTags.length; i += 2) {
event.put(additionalTags[i], additionalTags[i + 1]);
}

id.getTags().forEach(t -> event.put(t.getKey(), t.getValue()));

return event;
}

// TODO HTTP/1.1 Persistent connections are supported
private void sendEvents(URL insightsEndpoint, List<Event> events) {
try {
HttpURLConnection con = (HttpURLConnection) insightsEndpoint.openConnection();
con.setConnectTimeout((int) config.connectTimeout().toMillis());
con.setReadTimeout((int) config.readTimeout().toMillis());
con.setRequestMethod("POST");
con.setRequestProperty("Content-Type", "application/json");
con.setRequestProperty("X-Insert-Key", config.apiKey());

con.setDoOutput(true);

String body = mapper.writeValueAsString(events);

try (OutputStream os = con.getOutputStream()) {
os.write(body.getBytes());
os.flush();
}

int status = con.getResponseCode();

if (status >= 200 && status < 300) {
logger.info("successfully sent {} events to New Relic", events.size());
} else if (status >= 400) {
try (InputStream in = con.getErrorStream()) {
logger.error("failed to send metrics: " + new BufferedReader(new InputStreamReader(in))
.lines().collect(joining("\n")));
}
} else {
logger.error("failed to send metrics: http " + status);
}

con.disconnect();
} catch (Throwable e) {
logger.warn("failed to send metrics", e);
}
}

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

private class Event extends HashMap<String, Object> {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/**
* Copyright 2017 Pivotal Software, 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>
* http://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.newrelic;

import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.MockClock;
import io.micrometer.core.tck.MeterRegistryCompatibilityKit;

import java.time.Duration;

public class NewRelicMeterRegistryCompatibilityTest extends MeterRegistryCompatibilityKit {
@Override
public MeterRegistry registry() {
return new NewRelicMeterRegistry(new NewRelicConfig() {
@Override
public boolean enabled() {
return false;
}

@Override
public String apiKey() {
return "DOESNOTMATTER";
}

@Override
public String accountId() {
return "DOESNOTMATTER";
}

@Override
public String get(String k) {
return null;
}

@Override
public Duration step() {
return Duration.ofMillis(800);
}
}, new MockClock());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
import java.util.function.Function;
import java.util.stream.Collectors;

import static io.micrometer.statsd.internal.MemoizingSupplier.memoize;
import static io.micrometer.statsd.internal.MemoizingFunction.memoize;
import static java.beans.Introspector.decapitalize;
import static java.util.stream.Stream.of;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@
import java.util.function.Function;

/**
* Modified from Guava's MemoizingSupplier
* Modified from Guava's MemoizingFunction
* @param <R>
*/
public class MemoizingSupplier<T, R> implements Function<T, R> {
public class MemoizingFunction<T, R> implements Function<T, R> {

private final Function<T, R> delegate;
private transient volatile boolean initialized;
Expand All @@ -31,12 +31,12 @@ public class MemoizingSupplier<T, R> implements Function<T, R> {
// on volatile read of "initialized".
private transient R value;

public MemoizingSupplier(Function<T, R> delegate) {
public MemoizingFunction(Function<T, R> delegate) {
this.delegate = delegate;
}

public static <U, V> MemoizingSupplier<U, V> memoize(Function<U, V> delegate) {
return new MemoizingSupplier<>(delegate);
public static <U, V> MemoizingFunction<U, V> memoize(Function<U, V> delegate) {
return new MemoizingFunction<>(delegate);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,6 @@ public Measurement(Supplier<Double> f, Statistic statistic) {
this.statistic = statistic;
}

public Supplier<Double> getValueFunction() {
return f;
}

/**
* Value for the measurement.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,7 @@
import java.util.*;
import java.util.Map.Entry;
import java.util.concurrent.TimeUnit;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.ToDoubleFunction;
import java.util.function.ToLongFunction;
import java.util.function.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,23 @@ private String toCamelCase(String value) {
}
};

NamingConvention upperCamelCase = new NamingConvention() {
@Override
public String name(String name, Meter.Type type, String baseUnit) {
String lowerCamel = camelCase.name(name, type, baseUnit);
return capitalize(lowerCamel);
}

private String capitalize(String name) {
if (name == null || name.length() == 0) {
return name;
}
char chars[] = name.toCharArray();
chars[0] = Character.toUpperCase(chars[0]);
return new String(chars);
}
};

default String name(String name, Meter.Type type) {
return name(name, type, null);
}
Expand Down
Loading

0 comments on commit 8d1402f

Please sign in to comment.