-
Notifications
You must be signed in to change notification settings - Fork 981
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
NewRelicMeterRegistry Java Agent Insights API Support (#1647)
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
1 parent
dfd62c1
commit 5d5617d
Showing
8 changed files
with
1,337 additions
and
372 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
/.DS_Store |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
251 changes: 251 additions & 0 deletions
251
...istry-new-relic/src/main/java/io/micrometer/newrelic/NewRelicAgentClientProviderImpl.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} | ||
} | ||
} |
85 changes: 85 additions & 0 deletions
85
...meter-registry-new-relic/src/main/java/io/micrometer/newrelic/NewRelicClientProvider.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.