Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
6d584e3
add New Relic Java Agent API dependency v5.+
neiljpowell Oct 4, 2019
314433e
Introduce ClientProvider interface
neiljpowell Oct 4, 2019
a479eb0
Add Java Agent ClientProvider impl and update tests
neiljpowell Oct 7, 2019
e0029a7
re-apply builder() and start()
neiljpowell Oct 7, 2019
b26f516
Comments
neiljpowell Oct 7, 2019
53adfab
Default meter registry Builder to Http/Rest client
neiljpowell Oct 15, 2019
1107dc0
use registry.getBaseTimeUnit()
neiljpowell Oct 15, 2019
fe79c56
Move publish() up for consistency
neiljpowell Oct 15, 2019
e88586d
Merge branch 'master' of https://github.com/micrometer-metrics/microm…
neiljpowell Oct 17, 2019
2dbf891
clean up
neiljpowell Oct 17, 2019
52ad284
fix formatting build errors
neiljpowell Oct 17, 2019
72d5f9a
fix test formatting build errors
neiljpowell Oct 17, 2019
3e4dc70
merged in polish
neiljpowell Oct 19, 2019
13e134c
Merge branch 'master' of
neiljpowell Oct 19, 2019
e4163ca
Revert back to BaseTimeUnit Seconds
neiljpowell Oct 22, 2019
926e8cb
Merge branch 'master' into newrelic-java-agent-meter-registry
neiljpowell Oct 22, 2019
833924e
Merge branch 'master' into newrelic-java-agent-meter-registry
neiljpowell Oct 31, 2019
e246fe1
make newrelic-api an optional dependency
neiljpowell Oct 31, 2019
fd8ced1
gitignore
neiljpowell Dec 18, 2019
0d812ff
Merge branch 'master' into newrelic-java-agent-meter-registry
neiljpowell Dec 18, 2019
6412faf
MeterRegistry updates from review
neiljpowell Dec 18, 2019
e9a844e
remove redundant modifiers
neiljpowell Dec 18, 2019
3f89a09
specific arg type in sendEvents
neiljpowell Dec 18, 2019
6a5712f
reverted publish to using stream().flatMap for processing batches
neiljpowell Dec 18, 2019
aee20cd
improve http client provider publish test to ensure batching
neiljpowell Dec 18, 2019
5fc6f66
improve http and agent publishing tests
neiljpowell Dec 18, 2019
c5d0dc4
Merge branch 'master' into newrelic-java-agent-meter-registry
neiljpowell Mar 12, 2020
77ff2a9
removed optional
neiljpowell Mar 12, 2020
6292403
switch newrelic-api to compileOnly
neiljpowell Mar 12, 2020
b1437d0
use optionalApi for newrelic-api
neiljpowell Mar 12, 2020
12ea291
implementation newrelic-api
neiljpowell Mar 12, 2020
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
1 change: 1 addition & 0 deletions implementations/micrometer-registry-new-relic/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/.DS_Store
3 changes: 2 additions & 1 deletion implementations/micrometer-registry-new-relic/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ dependencies {
api project(':micrometer-core')

implementation 'org.slf4j:slf4j-api'
implementation 'com.newrelic.agent.java:newrelic-api:5.+'
Copy link
Member

Choose a reason for hiding this comment

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

We want optionalApi here so we are not adding this dependency for all users of micrometer-registry-new-relic. Users who want to use the Agent client provider should include this dependency for their application.


testImplementation project(':micrometer-test')
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
/**
* 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>
* 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.newrelic;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

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

import com.newrelic.api.agent.Agent;
import com.newrelic.api.agent.NewRelic;

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.TimeGauge;
import io.micrometer.core.instrument.Timer;
import io.micrometer.core.instrument.config.MissingRequiredConfigurationException;
import io.micrometer.core.instrument.config.NamingConvention;
import io.micrometer.core.instrument.util.StringUtils;

/**
* Publishes metrics to New Relic Insights via Java Agent API.
*
* @author Neil Powell
*/
public class NewRelicAgentClientProviderImpl implements NewRelicClientProvider {

private final Logger logger = LoggerFactory.getLogger(NewRelicAgentClientProviderImpl.class);

private final Agent newRelicAgent;
private final NewRelicConfig config;
private final NamingConvention namingConvention;

public NewRelicAgentClientProviderImpl(NewRelicConfig config) {
this(config, NewRelic.getAgent(), new NewRelicNamingConvention());
}

public NewRelicAgentClientProviderImpl(NewRelicConfig config, Agent newRelicAgent, NamingConvention namingConvention) {

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

this.newRelicAgent = newRelicAgent;
this.config = config;
this.namingConvention = namingConvention;
}

@Override
public void publish(NewRelicMeterRegistry meterRegistry) {
// New Relic's Java Agent Insights API is backed by a reservoir/buffer
// and handles the actual publishing of events to New Relic.
// 1:1 mapping between Micrometer meters and New Relic events
for (Meter meter : meterRegistry.getMeters()) {
sendEvents(
meter.getId(),
meter.match(
this::writeGauge,
this::writeCounter,
this::writeTimer,
this::writeSummary,
this::writeLongTaskTimer,
this::writeTimeGauge,
this::writeFunctionCounter,
this::writeFunctionTimer,
this::writeMeter)
);
}
}

@Override
public Map<String, Object> writeLongTaskTimer(LongTaskTimer timer) {
Map<String, Object> attributes = new HashMap<String, Object>();
TimeUnit timeUnit = TimeUnit.valueOf(timer.getId().getBaseUnit());
addAttribute(ACTIVE_TASKS, timer.activeTasks(), attributes);
addAttribute(DURATION, timer.duration(timeUnit), attributes);
addAttribute(TIME_UNIT, timeUnit.toString().toLowerCase(), attributes);
//process meter's name, type and tags
addMeterAsAttributes(timer.getId(), attributes);
return attributes;
}

@Override
public Map<String, Object> writeFunctionCounter(FunctionCounter counter) {
return writeCounterValues(counter.getId(), counter.count());
}

@Override
public Map<String, Object> writeCounter(Counter counter) {
return writeCounterValues(counter.getId(), counter.count());
}

Map<String, Object> writeCounterValues(Meter.Id id, double count) {
Map<String, Object> attributes = new HashMap<String, Object>();
if (Double.isFinite(count)) {
addAttribute(THROUGHPUT, count, attributes);
//process meter's name, type and tags
addMeterAsAttributes(id, attributes);
}
return attributes;
}

@Override
public Map<String, Object> writeGauge(Gauge gauge) {
Map<String, Object> attributes = new HashMap<String, Object>();
double value = gauge.value();
if (Double.isFinite(value)) {
addAttribute(VALUE, value, attributes);
//process meter's name, type and tags
addMeterAsAttributes(gauge.getId(), attributes);
}
return attributes;
}

@Override
public Map<String, Object> writeTimeGauge(TimeGauge gauge) {
Map<String, Object> attributes = new HashMap<String, Object>();
double value = gauge.value();
if (Double.isFinite(value)) {
addAttribute(VALUE, value, attributes);
addAttribute(TIME_UNIT, gauge.baseTimeUnit().toString().toLowerCase(), attributes);
//process meter's name, type and tags
addMeterAsAttributes(gauge.getId(), attributes);
}
return attributes;
}

@Override
public Map<String, Object> writeSummary(DistributionSummary summary) {
Map<String, Object> attributes = new HashMap<String, Object>();
addAttribute(COUNT, summary.count(), attributes);
addAttribute(AVG, summary.mean(), attributes);
addAttribute(TOTAL, summary.totalAmount(), attributes);
addAttribute(MAX, summary.max(), attributes);
//process meter's name, type and tags
addMeterAsAttributes(summary.getId(), attributes);
return attributes;
}

@Override
public Map<String, Object> writeTimer(Timer timer) {
Map<String, Object> attributes = new HashMap<String, Object>();
TimeUnit timeUnit = TimeUnit.valueOf(timer.getId().getBaseUnit());
addAttribute(COUNT, (new Double(timer.count())).longValue(), attributes);
addAttribute(AVG, timer.mean(timeUnit), attributes);
addAttribute(TOTAL_TIME, timer.totalTime(timeUnit), attributes);
addAttribute(MAX, timer.max(timeUnit), attributes);
addAttribute(TIME_UNIT, timeUnit.toString().toLowerCase(), attributes);
//process meter's name, type and tags
addMeterAsAttributes(timer.getId(), attributes);
return attributes;
}

@Override
public Map<String, Object> writeFunctionTimer(FunctionTimer timer) {
Map<String, Object> attributes = new HashMap<String, Object>();
TimeUnit timeUnit = TimeUnit.valueOf(timer.getId().getBaseUnit());
addAttribute(COUNT, (new Double(timer.count())).longValue(), attributes);
addAttribute(AVG, timer.mean(timeUnit), attributes);
addAttribute(TOTAL_TIME, timer.totalTime(timeUnit), attributes);
addAttribute(TIME_UNIT, timeUnit.toString().toLowerCase(), attributes);
//process meter's name, type and tags
addMeterAsAttributes(timer.getId(), attributes);
return attributes;
}

@Override
public Map<String, Object> writeMeter(Meter meter) {
Map<String, Object> attributes = new HashMap<String, Object>();
for (Measurement measurement : meter.measure()) {
double value = measurement.getValue();
if (!Double.isFinite(value)) {
continue;
}
addAttribute(measurement.getStatistic().getTagValueRepresentation(), value, attributes);
}
if (attributes.isEmpty()) {
return attributes;
}
//process meter's name, type and tags
addMeterAsAttributes(meter.getId(), attributes);
return attributes;
}

void addMeterAsAttributes(Meter.Id id, Map<String, Object> attributes) {
if (!config.meterNameEventTypeEnabled()) {
// Include contextual attributes when publishing all metrics under a single categorical eventType,
// NOT when publishing an eventType per Meter/metric name
String name = id.getConventionName(namingConvention);
attributes.put(METRIC_NAME, name);
attributes.put(METRIC_TYPE, id.getType().toString());
}
//process meter tags
for (Tag tag : id.getConventionTags(namingConvention)) {
attributes.put(tag.getKey(), tag.getValue());
}
}

void addAttribute(String key, Number value, Map<String, Object> attributes) {
//process other tags

//Replicate DoubleFormat.wholeOrDecimal(value.doubleValue()) formatting behavior
if (Math.floor(value.doubleValue()) == value.doubleValue()) {
//whole number - don't include decimal
attributes.put(namingConvention.tagKey(key), value.intValue());
} else {
//include decimal
attributes.put(namingConvention.tagKey(key), value.doubleValue());
}
}

void addAttribute(String key, String value, Map<String, Object> attributes) {
//process other tags
attributes.put(namingConvention.tagKey(key), namingConvention.tagValue(value));
}

void sendEvents(Meter.Id id, Map<String, Object> attributes) {
//Delegate to New Relic Java Agent
if (attributes != null && attributes.isEmpty() == false) {
String eventType = getEventType(id, config, namingConvention);
try {
newRelicAgent.getInsights().recordCustomEvent(eventType, attributes);
} catch (Throwable e) {
logger.warn("failed to send metrics to new relic", e);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/**
* 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>
* 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.newrelic;

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.Meter;
import io.micrometer.core.instrument.TimeGauge;
import io.micrometer.core.instrument.Timer;
import io.micrometer.core.instrument.config.NamingConvention;

/**
* @author Neil Powell
*/
public interface NewRelicClientProvider {
//long task timer
String DURATION = "duration";
String ACTIVE_TASKS = "activeTasks";
//distribution summary & timer
String MAX = "max";
String TOTAL = "total";
String AVG = "avg";
String COUNT = "count";
//timer
String TOTAL_TIME = "totalTime";
String TIME = "time";
//gauge
String VALUE = "value";
//counter
String THROUGHPUT = "throughput"; //TODO Why not "count"? ..confusing if just counting something
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 probably an oversight. As the philosophy of Micrometer, we want users to focus on rates of counts rather than the raw count. Accordingly, the intention was probably to ship the rate (throughput) of the counter rather than the step count. I'll open a separate issue to look at that. In this pull request, it is probably best to keep the existing behavior and limit its changes to the purpose of the pull request.

//timer
String TIME_UNIT = "timeUnit";
//all
String METRIC_TYPE = "metricType";
String METRIC_NAME = "metricName";

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

void publish(NewRelicMeterRegistry meterRegistry);

Object writeFunctionTimer(FunctionTimer timer);

Object writeTimer(Timer timer);

Object writeSummary(DistributionSummary summary);

Object writeLongTaskTimer(LongTaskTimer timer);

Object writeTimeGauge(TimeGauge gauge);

Object writeGauge(Gauge gauge);

Object writeCounter(Counter counter);

Object writeFunctionCounter(FunctionCounter counter);

Object writeMeter(Meter meter);
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@
*/
package io.micrometer.newrelic;

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

/**
* Configuration for {@link NewRelicMeterRegistry}.
*
* @author Jon Schneider
* @author Neil Powell
* @since 1.0.0
*/
public interface NewRelicConfig extends StepRegistryConfig {
Expand Down Expand Up @@ -55,15 +55,11 @@ default String eventType() {

default String apiKey() {
String v = get(prefix() + ".apiKey");
if (v == null)
throw new MissingRequiredConfigurationException("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 MissingRequiredConfigurationException("accountId must be set to report metrics to New Relic");
return v;
}

Expand All @@ -76,4 +72,5 @@ default String uri() {
String v = get(prefix() + ".uri");
return (v == null) ? "https://insights-collector.newrelic.com" : v;
}

}
Loading