diff --git a/build.gradle b/build.gradle index fbbc370532..1d8c6e18aa 100644 --- a/build.gradle +++ b/build.gradle @@ -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'] } } diff --git a/implementations/micrometer-registry-new-relic/build.gradle b/implementations/micrometer-registry-new-relic/build.gradle new file mode 100644 index 0000000000..3cf50bc31a --- /dev/null +++ b/implementations/micrometer-registry-new-relic/build.gradle @@ -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') +} \ No newline at end of file diff --git a/implementations/micrometer-registry-new-relic/src/main/java/io/micrometer/newrelic/NewRelicConfig.java b/implementations/micrometer-registry-new-relic/src/main/java/io/micrometer/newrelic/NewRelicConfig.java new file mode 100644 index 0000000000..aab4e17fc9 --- /dev/null +++ b/implementations/micrometer-registry-new-relic/src/main/java/io/micrometer/newrelic/NewRelicConfig.java @@ -0,0 +1,49 @@ +/** + * Copyright 2017 Pivotal Software, 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 + *

+ * http://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.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; + } +} diff --git a/implementations/micrometer-registry-new-relic/src/main/java/io/micrometer/newrelic/NewRelicMeterRegistry.java b/implementations/micrometer-registry-new-relic/src/main/java/io/micrometer/newrelic/NewRelicMeterRegistry.java new file mode 100644 index 0000000000..968ce3deb8 --- /dev/null +++ b/implementations/micrometer-registry-new-relic/src/main/java/io/micrometer/newrelic/NewRelicMeterRegistry.java @@ -0,0 +1,185 @@ +/** + * Copyright 2017 Pivotal Software, 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 + *

+ * http://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.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 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 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 { + } +} diff --git a/implementations/micrometer-registry-new-relic/src/test/java/io/micrometer/newrelic/NewRelicMeterRegistryCompatibilityTest.java b/implementations/micrometer-registry-new-relic/src/test/java/io/micrometer/newrelic/NewRelicMeterRegistryCompatibilityTest.java new file mode 100644 index 0000000000..e138e9e894 --- /dev/null +++ b/implementations/micrometer-registry-new-relic/src/test/java/io/micrometer/newrelic/NewRelicMeterRegistryCompatibilityTest.java @@ -0,0 +1,54 @@ +/** + * Copyright 2017 Pivotal Software, 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 + *

+ * http://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.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()); + } +} diff --git a/implementations/micrometer-registry-statsd/src/main/java/io/micrometer/statsd/StatsdLineBuilder.java b/implementations/micrometer-registry-statsd/src/main/java/io/micrometer/statsd/StatsdLineBuilder.java index b20b156386..05e2529a4d 100644 --- a/implementations/micrometer-registry-statsd/src/main/java/io/micrometer/statsd/StatsdLineBuilder.java +++ b/implementations/micrometer-registry-statsd/src/main/java/io/micrometer/statsd/StatsdLineBuilder.java @@ -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; diff --git a/implementations/micrometer-registry-statsd/src/main/java/io/micrometer/statsd/internal/MemoizingSupplier.java b/implementations/micrometer-registry-statsd/src/main/java/io/micrometer/statsd/internal/MemoizingFunction.java similarity index 85% rename from implementations/micrometer-registry-statsd/src/main/java/io/micrometer/statsd/internal/MemoizingSupplier.java rename to implementations/micrometer-registry-statsd/src/main/java/io/micrometer/statsd/internal/MemoizingFunction.java index 1dce36ecd9..79ae9ac73e 100644 --- a/implementations/micrometer-registry-statsd/src/main/java/io/micrometer/statsd/internal/MemoizingSupplier.java +++ b/implementations/micrometer-registry-statsd/src/main/java/io/micrometer/statsd/internal/MemoizingFunction.java @@ -18,10 +18,10 @@ import java.util.function.Function; /** - * Modified from Guava's MemoizingSupplier + * Modified from Guava's MemoizingFunction * @param */ -public class MemoizingSupplier implements Function { +public class MemoizingFunction implements Function { private final Function delegate; private transient volatile boolean initialized; @@ -31,12 +31,12 @@ public class MemoizingSupplier implements Function { // on volatile read of "initialized". private transient R value; - public MemoizingSupplier(Function delegate) { + public MemoizingFunction(Function delegate) { this.delegate = delegate; } - public static MemoizingSupplier memoize(Function delegate) { - return new MemoizingSupplier<>(delegate); + public static MemoizingFunction memoize(Function delegate) { + return new MemoizingFunction<>(delegate); } @Override diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/Measurement.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/Measurement.java index 2d34c73777..ea23cb1e50 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/Measurement.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/Measurement.java @@ -32,10 +32,6 @@ public Measurement(Supplier f, Statistic statistic) { this.statistic = statistic; } - public Supplier getValueFunction() { - return f; - } - /** * Value for the measurement. */ diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/MeterRegistry.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/MeterRegistry.java index c5cfa6d326..c34509e60a 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/MeterRegistry.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/MeterRegistry.java @@ -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; diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/config/NamingConvention.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/config/NamingConvention.java index 9a068494e1..ea1b1ef26a 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/config/NamingConvention.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/config/NamingConvention.java @@ -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); } diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/simple/CumulativeCounter.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/cumulative/CumulativeCounter.java similarity index 95% rename from micrometer-core/src/main/java/io/micrometer/core/instrument/simple/CumulativeCounter.java rename to micrometer-core/src/main/java/io/micrometer/core/instrument/cumulative/CumulativeCounter.java index d140588e17..d131027ac5 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/simple/CumulativeCounter.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/cumulative/CumulativeCounter.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.micrometer.core.instrument.simple; +package io.micrometer.core.instrument.cumulative; import io.micrometer.core.instrument.AbstractMeter; import io.micrometer.core.instrument.Counter; diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/simple/CumulativeDistributionSummary.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/cumulative/CumulativeDistributionSummary.java similarity index 98% rename from micrometer-core/src/main/java/io/micrometer/core/instrument/simple/CumulativeDistributionSummary.java rename to micrometer-core/src/main/java/io/micrometer/core/instrument/cumulative/CumulativeDistributionSummary.java index f2518b8b7f..ae383fe517 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/simple/CumulativeDistributionSummary.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/cumulative/CumulativeDistributionSummary.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.micrometer.core.instrument.simple; +package io.micrometer.core.instrument.cumulative; import com.google.common.util.concurrent.AtomicDouble; import io.micrometer.core.instrument.AbstractDistributionSummary; diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/simple/CumulativeTimer.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/cumulative/CumulativeTimer.java similarity index 98% rename from micrometer-core/src/main/java/io/micrometer/core/instrument/simple/CumulativeTimer.java rename to micrometer-core/src/main/java/io/micrometer/core/instrument/cumulative/CumulativeTimer.java index 4aa85afc9b..c49faf5834 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/simple/CumulativeTimer.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/cumulative/CumulativeTimer.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.micrometer.core.instrument.simple; +package io.micrometer.core.instrument.cumulative; import io.micrometer.core.instrument.AbstractTimer; import io.micrometer.core.instrument.Clock; diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/simple/SimpleMeterRegistry.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/simple/SimpleMeterRegistry.java index b1c5583374..72526102b8 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/simple/SimpleMeterRegistry.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/simple/SimpleMeterRegistry.java @@ -16,6 +16,9 @@ package io.micrometer.core.instrument.simple; import io.micrometer.core.instrument.*; +import io.micrometer.core.instrument.cumulative.CumulativeCounter; +import io.micrometer.core.instrument.cumulative.CumulativeDistributionSummary; +import io.micrometer.core.instrument.cumulative.CumulativeTimer; import io.micrometer.core.instrument.histogram.HistogramConfig; import io.micrometer.core.instrument.internal.DefaultGauge; import io.micrometer.core.instrument.internal.DefaultLongTaskTimer; diff --git a/micrometer-core/src/test/java/io/micrometer/core/instrument/NamingConventionTest.java b/micrometer-core/src/test/java/io/micrometer/core/instrument/NamingConventionTest.java index 0765c75875..d5527727da 100644 --- a/micrometer-core/src/test/java/io/micrometer/core/instrument/NamingConventionTest.java +++ b/micrometer-core/src/test/java/io/micrometer/core/instrument/NamingConventionTest.java @@ -32,4 +32,10 @@ void snakeCase() { String name = NamingConvention.snakeCase.name("a.Name.with.Words", Meter.Type.Counter); assertThat(name).isEqualTo("a_Name_with_Words"); } + + @Test + void upperCamelCase() { + String name = NamingConvention.upperCamelCase.name("a.name.with.words", Meter.Type.Counter); + assertThat(name).isEqualTo("ANameWithWords"); + } } \ No newline at end of file diff --git a/micrometer-samples/build.gradle b/micrometer-samples/build.gradle index 787923e6e3..6c03ce11f1 100644 --- a/micrometer-samples/build.gradle +++ b/micrometer-samples/build.gradle @@ -1,9 +1,13 @@ dependencies { compile project(':micrometer-core') compile 'colt:colt:1.2.0' - compile 'ch.qos.logback:logback-classic:latest.release' + compile 'ch.qos.logback:logback-classic:1.0.13' + compile('org.slf4j:slf4j-api:1.7+') { + // logback doesn't yet work with slf4j 1.8 + force = true + } - ['atlas', 'prometheus', 'datadog', 'ganglia', 'graphite', 'jmx', 'influx', 'statsd'].each { sys -> + ['atlas', 'prometheus', 'datadog', 'ganglia', 'graphite', 'jmx', 'influx', 'statsd', 'new-relic'].each { sys -> compile project(":micrometer-registry-$sys") } diff --git a/micrometer-samples/src/main/java/io/micrometer/core/samples/CounterSample.java b/micrometer-samples/src/main/java/io/micrometer/core/samples/CounterSample.java index f22c5d65ee..ce7e81acc9 100644 --- a/micrometer-samples/src/main/java/io/micrometer/core/samples/CounterSample.java +++ b/micrometer-samples/src/main/java/io/micrometer/core/samples/CounterSample.java @@ -26,7 +26,7 @@ public class CounterSample { public static void main(String[] args) { - Counter counter = SampleRegistries.prometheus().counter("counter"); + Counter counter = SampleRegistries.newRelic().counter("counter"); RandomEngine r = new MersenneTwister64(0); Normal dist = new Normal(0, 1, r); diff --git a/micrometer-samples/src/main/java/io/micrometer/core/samples/LatencySample.java b/micrometer-samples/src/main/java/io/micrometer/core/samples/LatencySample.java index c222ceefb1..1741410ac9 100644 --- a/micrometer-samples/src/main/java/io/micrometer/core/samples/LatencySample.java +++ b/micrometer-samples/src/main/java/io/micrometer/core/samples/LatencySample.java @@ -35,16 +35,7 @@ public static void main(String[] args) { .publishPercentileHistogram() .register(registry); - void bar() { - registry.timer("bar.latency") - .record(() -> { - // do something here - }); - } - void run() { - - Flux.interval(Duration.ofMillis(1)) .doOnEach(n -> recordGaussian(10)) .subscribe(); diff --git a/micrometer-samples/src/main/java/io/micrometer/core/samples/SimulatedEndpointInstrumentation.java b/micrometer-samples/src/main/java/io/micrometer/core/samples/SimulatedEndpointInstrumentation.java new file mode 100644 index 0000000000..3afb67ea4c --- /dev/null +++ b/micrometer-samples/src/main/java/io/micrometer/core/samples/SimulatedEndpointInstrumentation.java @@ -0,0 +1,110 @@ +/** + * Copyright 2017 Pivotal Software, 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 + *

+ * http://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.core.samples; + +import cern.jet.random.Normal; +import cern.jet.random.engine.MersenneTwister64; +import cern.jet.random.engine.RandomEngine; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Timer; +import io.micrometer.core.samples.utils.SampleRegistries; +import reactor.core.publisher.Flux; + +import java.time.Duration; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +public class SimulatedEndpointInstrumentation { + public static void main(String[] args) { + MeterRegistry registry = SampleRegistries.newRelic(); + + Timer e1Success = Timer.builder("http.server.requests") + .tags("uri", "/api/bar") + .tags("response", "200") + .publishPercentiles(0.5, 0.95) + .register(registry); + + Timer e2Success = Timer.builder("http.server.requests") + .tags("uri", "/api/foo") + .tags("response", "200") + .publishPercentiles(0.5, 0.95) + .register(registry); + + Timer e1Fail = Timer.builder("http.server.requests") + .tags("uri", "/api/bar") + .tags("response", "500") + .publishPercentiles(0.5, 0.95) + .register(registry); + + Timer e2Fail = Timer.builder("http.server.requests") + .tags("uri", "/api/foo") + .tags("response", "500") + .publishPercentiles(0.5, 0.95) + .register(registry); + + RandomEngine r = new MersenneTwister64(0); + Normal incomingRequests = new Normal(0, 1, r); + Normal successOrFail = new Normal(0, 1, r); + + Normal duration = new Normal(250, 50, r); + Normal duration2 = new Normal(250, 50, r); + + AtomicInteger latencyForThisSecond = new AtomicInteger(duration.nextInt()); + Flux.interval(Duration.ofSeconds(1)) + .doOnEach(d -> latencyForThisSecond.set(duration.nextInt())) + .subscribe(); + + AtomicInteger latencyForThisSecond2 = new AtomicInteger(duration2.nextInt()); + Flux.interval(Duration.ofSeconds(1)) + .doOnEach(d -> latencyForThisSecond2.set(duration2.nextInt())) + .subscribe(); + + // the potential for an "incoming request" every 10 ms + Flux.interval(Duration.ofMillis(10)) + .doOnEach(d -> { + // are we going to receive a request for /api/foo? + if (incomingRequests.nextDouble() + 0.4 > 0) { + if(successOrFail.nextDouble() + 0.8 > 0) { + // pretend the request took some amount of time, such that the time is + // distributed normally with a mean of 250ms + e1Success.record(latencyForThisSecond.get(), TimeUnit.MILLISECONDS); + } + else { + e1Fail.record(latencyForThisSecond.get(), TimeUnit.MILLISECONDS); + } + } + }) + .subscribe(); + + // the potential for an "incoming request" every 1 ms + Flux.interval(Duration.ofMillis(1)) + .doOnEach(d -> { + // are we going to receive a request for /api/bar? + if (incomingRequests.nextDouble() + 0.4 > 0) { + if(successOrFail.nextDouble() + 0.8 > 0) { + // pretend the request took some amount of time, such that the time is + // distributed normally with a mean of 250ms + e2Success.record(latencyForThisSecond2.get(), TimeUnit.MILLISECONDS); + } + else { + e2Fail.record(latencyForThisSecond2.get(), TimeUnit.MILLISECONDS); + } + } + }) + .blockLast(); + } + +} diff --git a/micrometer-samples/src/main/java/io/micrometer/core/samples/utils/SampleRegistries.java b/micrometer-samples/src/main/java/io/micrometer/core/samples/utils/SampleRegistries.java index 974a9407dc..50460ccc29 100644 --- a/micrometer-samples/src/main/java/io/micrometer/core/samples/utils/SampleRegistries.java +++ b/micrometer-samples/src/main/java/io/micrometer/core/samples/utils/SampleRegistries.java @@ -19,6 +19,7 @@ import com.sun.net.httpserver.HttpServer; import io.micrometer.atlas.AtlasMeterRegistry; import io.micrometer.core.instrument.Clock; +import io.micrometer.core.instrument.MockClock; import io.micrometer.datadog.DatadogConfig; import io.micrometer.datadog.DatadogMeterRegistry; import io.micrometer.ganglia.GangliaMeterRegistry; @@ -26,6 +27,8 @@ import io.micrometer.influx.InfluxConfig; import io.micrometer.influx.InfluxMeterRegistry; import io.micrometer.jmx.JmxMeterRegistry; +import io.micrometer.newrelic.NewRelicConfig; +import io.micrometer.newrelic.NewRelicMeterRegistry; import io.micrometer.prometheus.PrometheusConfig; import io.micrometer.prometheus.PrometheusMeterRegistry; import io.micrometer.statsd.StatsdConfig; @@ -82,7 +85,7 @@ public static DatadogMeterRegistry datadog() { try { props.load(SampleRegistries.class.getResourceAsStream("/datadog.properties")); } catch (IOException e) { - throw new RuntimeException("must have application.properties with datadog.apiKey defined", e); + throw new RuntimeException("must have datadog.properties with datadog.apiKey defined", e); } } @@ -109,17 +112,19 @@ public StatsdFlavor flavor() { }, Clock.SYSTEM); } - public static StatsdMeterRegistry telegrafStatsd = new StatsdMeterRegistry(new StatsdConfig() { - @Override - public String get(String k) { - return null; - } + public static StatsdMeterRegistry telegrafStatsd() { + return new StatsdMeterRegistry(new StatsdConfig() { + @Override + public String get(String k) { + return null; + } - @Override - public StatsdFlavor flavor() { - return StatsdFlavor.Telegraf; - } - }, Clock.SYSTEM); + @Override + public StatsdFlavor flavor() { + return StatsdFlavor.Telegraf; + } + }, Clock.SYSTEM); + } public static GangliaMeterRegistry ganglia() { return new GangliaMeterRegistry(); @@ -149,4 +154,28 @@ public String get(String k) { } }); } + + public static NewRelicMeterRegistry newRelic() { + return new NewRelicMeterRegistry(new NewRelicConfig() { + private final Properties props = new Properties(); + + { + try { + props.load(SampleRegistries.class.getResourceAsStream("/new-relic.properties")); + } catch (IOException e) { + throw new RuntimeException("must have new-relic.properties with newrelic.licenseKey defined", e); + } + } + + @Override + public String get (String k){ + return props.getProperty(k); + } + + @Override + public Duration step() { + return Duration.ofSeconds(10); + } + }, Clock.SYSTEM); + } } diff --git a/micrometer-samples/src/main/resources/.gitignore b/micrometer-samples/src/main/resources/.gitignore index ea78148f28..102b6fc94a 100644 --- a/micrometer-samples/src/main/resources/.gitignore +++ b/micrometer-samples/src/main/resources/.gitignore @@ -1 +1 @@ -datadog.properties \ No newline at end of file +*.properties \ No newline at end of file diff --git a/micrometer-spring-legacy/build.gradle b/micrometer-spring-legacy/build.gradle index 2e6f1ab743..acc4c1d1da 100644 --- a/micrometer-spring-legacy/build.gradle +++ b/micrometer-spring-legacy/build.gradle @@ -37,7 +37,7 @@ dependencies { compile 'javax.servlet:javax.servlet-api:3.1.0', optional compile 'org.aspectj:aspectjweaver:1.8.+', optional - ['atlas', 'prometheus', 'datadog', 'ganglia', 'graphite', 'jmx', 'influx', 'statsd', 'cloudwatch'].each { sys -> + ['atlas', 'prometheus', 'datadog', 'ganglia', 'graphite', 'jmx', 'influx', 'statsd', 'new-relic', 'cloudwatch'].each { sys -> compile project(":micrometer-registry-$sys"), optional } diff --git a/micrometer-spring-legacy/src/main/java/io/micrometer/spring/autoconfigure/MetricsAutoConfiguration.java b/micrometer-spring-legacy/src/main/java/io/micrometer/spring/autoconfigure/MetricsAutoConfiguration.java index f1f264a827..e5ac70948f 100644 --- a/micrometer-spring-legacy/src/main/java/io/micrometer/spring/autoconfigure/MetricsAutoConfiguration.java +++ b/micrometer-spring-legacy/src/main/java/io/micrometer/spring/autoconfigure/MetricsAutoConfiguration.java @@ -25,6 +25,7 @@ import io.micrometer.spring.autoconfigure.export.MetricsExporter; import io.micrometer.spring.autoconfigure.export.atlas.AtlasExportConfiguration; import io.micrometer.spring.autoconfigure.export.datadog.DatadogExportConfiguration; +import io.micrometer.spring.autoconfigure.export.datadog.NewRelicExportConfiguration; import io.micrometer.spring.autoconfigure.export.ganglia.GangliaExportConfiguration; import io.micrometer.spring.autoconfigure.export.graphite.GraphiteExportConfiguration; import io.micrometer.spring.autoconfigure.export.influx.InfluxExportConfiguration; @@ -66,8 +67,9 @@ RestTemplateMetricsConfiguration.class, AtlasExportConfiguration.class, DatadogExportConfiguration.class, GangliaExportConfiguration.class, GraphiteExportConfiguration.class, InfluxExportConfiguration.class, - JmxExportConfiguration.class, StatsdExportConfiguration.class, - PrometheusExportConfiguration.class, SimpleExportConfiguration.class}) + NewRelicExportConfiguration.class, JmxExportConfiguration.class, + StatsdExportConfiguration.class, PrometheusExportConfiguration.class, + SimpleExportConfiguration.class}) public class MetricsAutoConfiguration { @Bean @Order(0) diff --git a/micrometer-spring-legacy/src/main/java/io/micrometer/spring/autoconfigure/export/newrelic/NewRelicExportConfiguration.java b/micrometer-spring-legacy/src/main/java/io/micrometer/spring/autoconfigure/export/newrelic/NewRelicExportConfiguration.java new file mode 100644 index 0000000000..b7dffc2399 --- /dev/null +++ b/micrometer-spring-legacy/src/main/java/io/micrometer/spring/autoconfigure/export/newrelic/NewRelicExportConfiguration.java @@ -0,0 +1,87 @@ +/** + * Copyright 2017 Pivotal Software, 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 + *

+ * http://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.spring.autoconfigure.export.datadog; + +import io.micrometer.core.instrument.Clock; +import io.micrometer.newrelic.NewRelicConfig; +import io.micrometer.newrelic.NewRelicMeterRegistry; +import io.micrometer.spring.autoconfigure.export.DefaultStepRegistryConfig; +import io.micrometer.spring.autoconfigure.export.MetricsExporter; +import io.micrometer.spring.autoconfigure.export.StringToDurationConverter; +import io.micrometer.spring.autoconfigure.export.newrelic.NewRelicProperties; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +/** + * Configuration for exporting metrics to Datadog. + * + * @author Jon Schneider + */ +@Configuration +@ConditionalOnClass(NewRelicMeterRegistry.class) +@Import(StringToDurationConverter.class) +@EnableConfigurationProperties(NewRelicProperties.class) +public class NewRelicExportConfiguration { + + private class DefaultNewRelicConfig extends DefaultStepRegistryConfig implements NewRelicConfig { + private final NewRelicProperties props; + private final NewRelicConfig defaults = k -> null; + + private DefaultNewRelicConfig(NewRelicProperties props) { + super(props); + this.props = props; + } + + @Override + public String apiKey() { + return props.getApiKey() == null ? defaults.apiKey() : props.getApiKey(); + } + + @Override + public String accountId() { + return props.getAccountId() == null ? defaults.accountId() : props.getAccountId(); + } + + @Override + public String uri() { + return props.getUri() == null ? defaults.uri() : props.getUri(); + } + } + + @Bean + @ConditionalOnMissingBean + public NewRelicConfig newRelicConfig(NewRelicProperties props) { + return new DefaultNewRelicConfig(props); + } + + @Bean + @ConditionalOnProperty(value = "spring.metrics.newrelic.enabled", matchIfMissing = true) + public MetricsExporter newRelicExporter(NewRelicConfig config, Clock clock) { + return () -> new NewRelicMeterRegistry(config, clock); + } + + @Bean + @ConditionalOnMissingBean + public Clock micrometerClock() { + return Clock.SYSTEM; + } + +} diff --git a/micrometer-spring-legacy/src/main/java/io/micrometer/spring/autoconfigure/export/newrelic/NewRelicProperties.java b/micrometer-spring-legacy/src/main/java/io/micrometer/spring/autoconfigure/export/newrelic/NewRelicProperties.java new file mode 100644 index 0000000000..347ce521cf --- /dev/null +++ b/micrometer-spring-legacy/src/main/java/io/micrometer/spring/autoconfigure/export/newrelic/NewRelicProperties.java @@ -0,0 +1,60 @@ +/** + * Copyright 2017 Pivotal Software, 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 + *

+ * http://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.spring.autoconfigure.export.newrelic; + +import io.micrometer.spring.autoconfigure.export.StepRegistryProperties; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * {@link ConfigurationProperties} for configuring New Relic metrics export. + * + * @author Jon Schneider + */ +@ConfigurationProperties(prefix = "spring.metrics.newrelic") +public class NewRelicProperties extends StepRegistryProperties { + /** + * Your API key, found in your account settings at New Relic. This property is required. + */ + private String apiKey; + + private String accountId; + + private String uri; + + public String getApiKey() { + return apiKey; + } + + public void setApiKey(String apiKey) { + this.apiKey = apiKey; + } + + public String getAccountId() { + return accountId; + } + + public void setAccountId(String accountId) { + this.accountId = accountId; + } + + public String getUri() { + return uri; + } + + public void setUri(String uri) { + this.uri = uri; + } +} diff --git a/micrometer-spring-legacy/src/samples/java/io/micrometer/spring/samples/NewRelicSample.java b/micrometer-spring-legacy/src/samples/java/io/micrometer/spring/samples/NewRelicSample.java new file mode 100644 index 0000000000..ee54b80f44 --- /dev/null +++ b/micrometer-spring-legacy/src/samples/java/io/micrometer/spring/samples/NewRelicSample.java @@ -0,0 +1,28 @@ +/** + * Copyright 2017 Pivotal Software, 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 + *

+ * http://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.spring.samples; + +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.scheduling.annotation.EnableScheduling; + +@SpringBootApplication(scanBasePackages = "io.micrometer.spring.samples.components") +@EnableScheduling +public class NewRelicSample { + public static void main(String[] args) { + new SpringApplicationBuilder(PrometheusSample.class).profiles("newrelic").run(args); + } +} diff --git a/micrometer-spring-legacy/src/samples/resources/.gitignore b/micrometer-spring-legacy/src/samples/resources/.gitignore new file mode 100644 index 0000000000..325f1d0b95 --- /dev/null +++ b/micrometer-spring-legacy/src/samples/resources/.gitignore @@ -0,0 +1 @@ +application-newrelic.yml \ No newline at end of file diff --git a/micrometer-spring-legacy/src/samples/resources/application.yml b/micrometer-spring-legacy/src/samples/resources/application.yml index e53eacf2c2..5d20f430a2 100644 --- a/micrometer-spring-legacy/src/samples/resources/application.yml +++ b/micrometer-spring-legacy/src/samples/resources/application.yml @@ -30,5 +30,6 @@ spring.metrics: jmx.enabled: false prometheus.enabled: false statsd.enabled: false + newrelic.enabled: false security.ignored: /** \ No newline at end of file diff --git a/micrometer-spring-legacy/src/test/java/io/micrometer/spring/autoconfigure/MeterRegistryConfigurerTest.java b/micrometer-spring-legacy/src/test/java/io/micrometer/spring/autoconfigure/MeterRegistryConfigurerTest.java index f9c78c1e99..133fc7b8c1 100644 --- a/micrometer-spring-legacy/src/test/java/io/micrometer/spring/autoconfigure/MeterRegistryConfigurerTest.java +++ b/micrometer-spring-legacy/src/test/java/io/micrometer/spring/autoconfigure/MeterRegistryConfigurerTest.java @@ -42,7 +42,8 @@ "spring.metrics.graphite.enabled=false", "spring.metrics.influx.enabled=false", "spring.metrics.jmx.enabled=false", - "spring.metrics.statsd.enabled=false" + "spring.metrics.statsd.enabled=false", + "spring.metrics.newrelic.enabled=false" }) public class MeterRegistryConfigurerTest { diff --git a/micrometer-spring-legacy/src/test/java/io/micrometer/spring/autoconfigure/MetricsConfigurationCompositeTest.java b/micrometer-spring-legacy/src/test/java/io/micrometer/spring/autoconfigure/MetricsConfigurationCompositeTest.java index 7f28b9e8d8..3ac548f59f 100644 --- a/micrometer-spring-legacy/src/test/java/io/micrometer/spring/autoconfigure/MetricsConfigurationCompositeTest.java +++ b/micrometer-spring-legacy/src/test/java/io/micrometer/spring/autoconfigure/MetricsConfigurationCompositeTest.java @@ -42,7 +42,8 @@ "spring.metrics.influx.enabled=false", "spring.metrics.jmx.enabled=false", "spring.metrics.statsd.enabled=false", - "spring.metrics.prometheus.enabled=true" + "spring.metrics.prometheus.enabled=true", + "spring.metrics.newrelic.enabled=false" }) public class MetricsConfigurationCompositeTest { @Autowired diff --git a/micrometer-spring-legacy/src/test/java/io/micrometer/spring/autoconfigure/export/simple/SimpleExportConfigurationTest.java b/micrometer-spring-legacy/src/test/java/io/micrometer/spring/autoconfigure/export/simple/SimpleExportConfigurationTest.java index ec1a1ad1f9..f26e4edb8f 100644 --- a/micrometer-spring-legacy/src/test/java/io/micrometer/spring/autoconfigure/export/simple/SimpleExportConfigurationTest.java +++ b/micrometer-spring-legacy/src/test/java/io/micrometer/spring/autoconfigure/export/simple/SimpleExportConfigurationTest.java @@ -41,6 +41,7 @@ "spring.metrics.influx.enabled=false", "spring.metrics.jmx.enabled=false", "spring.metrics.statsd.enabled=false", + "spring.metrics.newrelic.enabled=false" }) public class SimpleExportConfigurationTest { @Autowired diff --git a/micrometer-spring-legacy/src/test/java/io/micrometer/spring/integration/SpringIntegrationMetricsTest.java b/micrometer-spring-legacy/src/test/java/io/micrometer/spring/integration/SpringIntegrationMetricsTest.java index d59a04b7b2..8fb6f129fd 100644 --- a/micrometer-spring-legacy/src/test/java/io/micrometer/spring/integration/SpringIntegrationMetricsTest.java +++ b/micrometer-spring-legacy/src/test/java/io/micrometer/spring/integration/SpringIntegrationMetricsTest.java @@ -47,7 +47,8 @@ "spring.metrics.ganglia.enabled=false", "spring.metrics.influx.enabled=false", "spring.metrics.jmx.enabled=false", - "spring.metrics.statsd.enabled=false" + "spring.metrics.statsd.enabled=false", + "spring.metrics.newrelic.enabled=false" }) public class SpringIntegrationMetricsTest { @Autowired diff --git a/micrometer-spring-legacy/src/test/resources/application.yml b/micrometer-spring-legacy/src/test/resources/application.yml index b340eb9b0a..09d9a2d751 100644 --- a/micrometer-spring-legacy/src/test/resources/application.yml +++ b/micrometer-spring-legacy/src/test/resources/application.yml @@ -27,4 +27,5 @@ spring.metrics: jmx.enabled: false prometheus.enabled: false statsd.enabled: false - cloudwatch.enabled: false \ No newline at end of file + cloudwatch.enabled: false + newrelic.enabled: false \ No newline at end of file diff --git a/scripts/newrelic/.gitignore b/scripts/newrelic/.gitignore new file mode 100644 index 0000000000..6fff5705a1 --- /dev/null +++ b/scripts/newrelic/.gitignore @@ -0,0 +1,2 @@ +newrelic.yml +logs/ \ No newline at end of file diff --git a/scripts/newrelic/newrelic.jar b/scripts/newrelic/newrelic.jar new file mode 100644 index 0000000000..23ab97ba1a Binary files /dev/null and b/scripts/newrelic/newrelic.jar differ diff --git a/settings.gradle b/settings.gradle index e555bed99b..d572c4f11f 100644 --- a/settings.gradle +++ b/settings.gradle @@ -5,7 +5,7 @@ include 'micrometer-spring-legacy' include 'micrometer-samples' include 'micrometer-test' -['atlas', 'prometheus', 'datadog', 'ganglia', 'graphite', 'jmx', 'influx', 'statsd', 'cloudwatch'].each { sys -> +['atlas', 'prometheus', 'datadog', 'ganglia', 'graphite', 'jmx', 'influx', 'statsd', 'new-relic', 'cloudwatch'].each { sys -> include "micrometer-registry-$sys" project(":micrometer-registry-$sys").projectDir = new File(rootProject.projectDir, "implementations/micrometer-registry-$sys") }