Skip to content

Commit

Permalink
NewRelicMeterRegistry Java Agent Insights API Support (#1647)
Browse files Browse the repository at this point in the history
Introduces the concept of a ClientProvider for the NewRelicMeterRegistry. This allows for different implementations of publishing metrics to New Relic, including the previous HTTP implementation as well as a new one based on the New Relic Java agent for users of that.

Resolves #1540
  • Loading branch information
neiljpowell committed Mar 12, 2020
1 parent dfd62c1 commit 5d5617d
Show file tree
Hide file tree
Showing 8 changed files with 1,337 additions and 372 deletions.
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.+'

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
//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

0 comments on commit 5d5617d

Please sign in to comment.