diff --git a/implementations/micrometer-registry-new-relic/.gitignore b/implementations/micrometer-registry-new-relic/.gitignore new file mode 100644 index 0000000000..9bb88d3761 --- /dev/null +++ b/implementations/micrometer-registry-new-relic/.gitignore @@ -0,0 +1 @@ +/.DS_Store diff --git a/implementations/micrometer-registry-new-relic/build.gradle b/implementations/micrometer-registry-new-relic/build.gradle index 7ad798c7c0..2977c51d18 100644 --- a/implementations/micrometer-registry-new-relic/build.gradle +++ b/implementations/micrometer-registry-new-relic/build.gradle @@ -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') -} +} \ No newline at end of file diff --git a/implementations/micrometer-registry-new-relic/src/main/java/io/micrometer/newrelic/NewRelicAgentClientProviderImpl.java b/implementations/micrometer-registry-new-relic/src/main/java/io/micrometer/newrelic/NewRelicAgentClientProviderImpl.java new file mode 100644 index 0000000000..a7f55454b6 --- /dev/null +++ b/implementations/micrometer-registry-new-relic/src/main/java/io/micrometer/newrelic/NewRelicAgentClientProviderImpl.java @@ -0,0 +1,251 @@ +/** + * 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 + *

+ * https://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 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 writeLongTaskTimer(LongTaskTimer timer) { + Map attributes = new HashMap(); + 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 writeFunctionCounter(FunctionCounter counter) { + return writeCounterValues(counter.getId(), counter.count()); + } + + @Override + public Map writeCounter(Counter counter) { + return writeCounterValues(counter.getId(), counter.count()); + } + + Map writeCounterValues(Meter.Id id, double count) { + Map attributes = new HashMap(); + if (Double.isFinite(count)) { + addAttribute(THROUGHPUT, count, attributes); + //process meter's name, type and tags + addMeterAsAttributes(id, attributes); + } + return attributes; + } + + @Override + public Map writeGauge(Gauge gauge) { + Map attributes = new HashMap(); + 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 writeTimeGauge(TimeGauge gauge) { + Map attributes = new HashMap(); + 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 writeSummary(DistributionSummary summary) { + Map attributes = new HashMap(); + 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 writeTimer(Timer timer) { + Map attributes = new HashMap(); + 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 writeFunctionTimer(FunctionTimer timer) { + Map attributes = new HashMap(); + 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 writeMeter(Meter meter) { + Map attributes = new HashMap(); + 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 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 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 attributes) { + //process other tags + attributes.put(namingConvention.tagKey(key), namingConvention.tagValue(value)); + } + + void sendEvents(Meter.Id id, Map 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); + } + } + } +} diff --git a/implementations/micrometer-registry-new-relic/src/main/java/io/micrometer/newrelic/NewRelicClientProvider.java b/implementations/micrometer-registry-new-relic/src/main/java/io/micrometer/newrelic/NewRelicClientProvider.java new file mode 100644 index 0000000000..02635ebd93 --- /dev/null +++ b/implementations/micrometer-registry-new-relic/src/main/java/io/micrometer/newrelic/NewRelicClientProvider.java @@ -0,0 +1,85 @@ +/** + * 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 + *

+ * https://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.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); +} 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 index f7989f3fa3..7911c65245 100644 --- 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 @@ -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 { @@ -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; } @@ -76,4 +72,5 @@ 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/NewRelicHttpClientProviderImpl.java b/implementations/micrometer-registry-new-relic/src/main/java/io/micrometer/newrelic/NewRelicHttpClientProviderImpl.java new file mode 100644 index 0000000000..25dcff1f24 --- /dev/null +++ b/implementations/micrometer-registry-new-relic/src/main/java/io/micrometer/newrelic/NewRelicHttpClientProviderImpl.java @@ -0,0 +1,287 @@ +/** + * 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 + *

+ * https://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 static io.micrometer.core.instrument.util.StringEscapeUtils.escapeJson; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +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.Tags; +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.*; +import io.micrometer.core.ipc.http.HttpSender; +import io.micrometer.core.ipc.http.HttpUrlConnectionSender; + +/** + * Publishes metrics to New Relic Insights REST API. + * + * @author Jon Schneider + * @author Johnny Lim + * @author Neil Powell + */ +public class NewRelicHttpClientProviderImpl implements NewRelicClientProvider { + + private final Logger logger = LoggerFactory.getLogger(NewRelicHttpClientProviderImpl.class); + + private final NewRelicConfig config; + private final HttpSender httpClient; + private final String insightsEndpoint; + private final NamingConvention namingConvention; + + @SuppressWarnings("deprecation") + public NewRelicHttpClientProviderImpl(NewRelicConfig config) { + this(config, new HttpUrlConnectionSender(config.connectTimeout(), config.readTimeout()), new NewRelicNamingConvention()); + } + + public NewRelicHttpClientProviderImpl(NewRelicConfig config, HttpSender httpClient, NamingConvention namingConvention) { + + if (!config.meterNameEventTypeEnabled() && StringUtils.isEmpty(config.eventType())) { + throw new MissingRequiredConfigurationException("eventType must be set to report metrics to New Relic"); + } + if (StringUtils.isEmpty(config.accountId())) { + throw new MissingRequiredConfigurationException("accountId must be set to report metrics to New Relic"); + } + if (StringUtils.isEmpty(config.apiKey())) { + throw new MissingRequiredConfigurationException("apiKey must be set to report metrics to New Relic"); + } + if (StringUtils.isEmpty(config.uri())) { + throw new MissingRequiredConfigurationException("uri must be set to report metrics to New Relic"); + } + + this.config = config; + this.httpClient = httpClient; + this.namingConvention = namingConvention; + this.insightsEndpoint = config.uri() + "/v1/accounts/" + config.accountId() + "/events"; + } + + @Override + public void publish(NewRelicMeterRegistry meterRegistry) { + // New Relic's Insights API limits us to 1000 events per call + // 1:1 mapping between Micrometer meters and New Relic events + for (List batch : MeterPartition.partition(meterRegistry, Math.min(config.batchSize(), 1000))) { + sendEvents(batch.stream().flatMap(meter -> meter.match( + this::writeGauge, + this::writeCounter, + this::writeTimer, + this::writeSummary, + this::writeLongTaskTimer, + this::writeTimeGauge, + this::writeFunctionCounter, + this::writeFunctionTimer, + this::writeMeter))); + } + } + + @Override + public Stream writeLongTaskTimer(LongTaskTimer timer) { + TimeUnit timeUnit = TimeUnit.valueOf(timer.getId().getBaseUnit()); + return Stream.of( + event(timer.getId(), + new Attribute(ACTIVE_TASKS, timer.activeTasks()), + new Attribute(DURATION, timer.duration(timeUnit)), + new Attribute(TIME_UNIT, timeUnit.toString().toLowerCase()) + ) + ); + } + + @Override + public Stream writeFunctionCounter(FunctionCounter counter) { + double count = counter.count(); + if (Double.isFinite(count)) { + return Stream.of(event(counter.getId(), new Attribute(THROUGHPUT, count))); + } + return Stream.empty(); + } + + @Override + public Stream writeCounter(Counter counter) { + return Stream.of(event(counter.getId(), new Attribute(THROUGHPUT, counter.count()))); + } + + @Override + public Stream writeGauge(Gauge gauge) { + Double value = gauge.value(); + if (Double.isFinite(value)) { + return Stream.of(event(gauge.getId(), new Attribute(VALUE, value))); + } + return Stream.empty(); + } + + @Override + public Stream writeTimeGauge(TimeGauge gauge) { + Double value = gauge.value(); + if (Double.isFinite(value)) { + return Stream.of( + event(gauge.getId(), + new Attribute(VALUE, value), + new Attribute(TIME_UNIT, gauge.baseTimeUnit().toString().toLowerCase()) + ) + ); + } + return Stream.empty(); + } + + @Override + public Stream writeSummary(DistributionSummary summary) { + return Stream.of( + event(summary.getId(), + new Attribute(COUNT, summary.count()), + new Attribute(AVG, summary.mean()), + new Attribute(TOTAL, summary.totalAmount()), + new Attribute(MAX, summary.max()) + ) + ); + } + + @Override + public Stream writeTimer(Timer timer) { + TimeUnit timeUnit = TimeUnit.valueOf(timer.getId().getBaseUnit()); + return Stream.of( + event(timer.getId(), + new Attribute(COUNT, timer.count()), + new Attribute(AVG, timer.mean(timeUnit)), + new Attribute(TOTAL_TIME, timer.totalTime(timeUnit)), + new Attribute(MAX, timer.max(timeUnit)), + new Attribute(TIME_UNIT, timeUnit.toString().toLowerCase()) + ) + ); + } + + @Override + public Stream writeFunctionTimer(FunctionTimer timer) { + TimeUnit timeUnit = TimeUnit.valueOf(timer.getId().getBaseUnit()); + return Stream.of( + event(timer.getId(), + new Attribute(COUNT, timer.count()), + new Attribute(AVG, timer.mean(timeUnit)), + new Attribute(TOTAL_TIME, timer.totalTime(timeUnit)), + new Attribute(TIME_UNIT, timeUnit.toString().toLowerCase()) + ) + ); + } + + @Override + public Stream writeMeter(Meter meter) { + // Snapshot values should be used throughout this method as there are chances for values to be changed in-between. + Map attributes = new HashMap<>(); + for (Measurement measurement : meter.measure()) { + double value = measurement.getValue(); + if (!Double.isFinite(value)) { + continue; + } + String name = measurement.getStatistic().getTagValueRepresentation(); + attributes.put(name, new Attribute(name, value)); + } + if (attributes.isEmpty()) { + return Stream.empty(); + } + return Stream.of(event(meter.getId(), attributes.values().toArray(new Attribute[0]))); + } + + private String event(Meter.Id id, Attribute... 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 + int size = attributes.length; + Attribute[] newAttrs = Arrays.copyOf(attributes, size + 2); + + String name = id.getConventionName(namingConvention); + newAttrs[size] = new Attribute(METRIC_NAME, name); + newAttrs[size + 1] = new Attribute(METRIC_TYPE, id.getType().toString()); + + return event(id, Tags.empty(), newAttrs); + } + return event(id, Tags.empty(), attributes); + } + + private String event(Meter.Id id, Iterable extraTags, Attribute... attributes) { + StringBuilder tagsJson = new StringBuilder(); + + for (Tag tag : id.getConventionTags(namingConvention)) { + tagsJson.append(",\"").append(escapeJson(tag.getKey())).append("\":\"").append(escapeJson(tag.getValue())).append("\""); + } + + for (Tag tag : extraTags) { + tagsJson.append(",\"").append(escapeJson(namingConvention.tagKey(tag.getKey()))) + .append("\":\"").append(escapeJson(namingConvention.tagValue(tag.getValue()))).append("\""); + } + + String eventType = getEventType(id, config, namingConvention); + + return Arrays.stream(attributes) + .map(attr -> + (attr.getValue() instanceof Number) + ? ",\"" + attr.getName() + "\":" + DoubleFormat.wholeOrDecimal(((Number)attr.getValue()).doubleValue()) + : ",\"" + attr.getName() + "\":\"" + namingConvention.tagValue(attr.getValue().toString()) + "\"" + ) + .collect(Collectors.joining("", "{\"eventType\":\"" + escapeJson(eventType) + "\"", tagsJson + "}")); + } + + void sendEvents(Stream events) { + try { + AtomicInteger totalEvents = new AtomicInteger(); + + httpClient.post(insightsEndpoint) + .withHeader("X-Insert-Key", config.apiKey()) + .withJsonContent(events.peek(ev -> totalEvents.incrementAndGet()).collect(Collectors.joining(",", "[", "]"))) + .send() + .onSuccess(response -> logger.debug("successfully sent {} metrics to New Relic.", totalEvents)) + .onError(response -> logger.error("failed to send metrics to new relic: http {} {}", response.code(), response.body())); + } catch (Throwable e) { + logger.warn("failed to send metrics to new relic", e); + } + } + + private class Attribute { + private final String name; + private final Object value; + + private Attribute(String name, Object value) { + this.name = name; + this.value = value; + } + + public String getName() { + return name; + } + + public Object getValue() { + return value; + } + } +} 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 index 337ef67801..f74848c0a6 100644 --- 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 @@ -15,82 +15,65 @@ */ package io.micrometer.newrelic; -import io.micrometer.core.instrument.*; -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.step.StepMeterRegistry; -import io.micrometer.core.instrument.util.*; -import io.micrometer.core.ipc.http.HttpSender; -import io.micrometer.core.ipc.http.HttpUrlConnectionSender; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.*; -import java.util.concurrent.ThreadFactory; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import static io.micrometer.core.instrument.util.StringEscapeUtils.escapeJson; +import io.micrometer.core.instrument.Clock; +import io.micrometer.core.instrument.config.MissingRequiredConfigurationException; +import io.micrometer.core.instrument.config.NamingConvention; +import io.micrometer.core.instrument.step.StepMeterRegistry; +import io.micrometer.core.instrument.util.NamedThreadFactory; +import io.micrometer.core.instrument.util.TimeUtils; /** - * Publishes metrics to New Relic Insights. + * Publishes metrics to New Relic Insights based on client provider selected (Http or Java Agent). + * Defaults to the HTTP/REST client provider. * * @author Jon Schneider * @author Johnny Lim - * @since 1.0.0 + * @author Neil Powell */ public class NewRelicMeterRegistry extends StepMeterRegistry { + private static final ThreadFactory DEFAULT_THREAD_FACTORY = new NamedThreadFactory("new-relic-metrics-publisher"); private final NewRelicConfig config; - private final HttpSender httpClient; + private NewRelicClientProvider clientProvider; private final Logger logger = LoggerFactory.getLogger(NewRelicMeterRegistry.class); /** * @param config Configuration options for the registry that are describable as properties. * @param clock The clock to use for timings. */ - @SuppressWarnings("deprecation") public NewRelicMeterRegistry(NewRelicConfig config, Clock clock) { - this(config, clock, DEFAULT_THREAD_FACTORY, - new HttpUrlConnectionSender(config.connectTimeout(), config.readTimeout())); + //default to the HTTP/REST client + this(config, new NewRelicHttpClientProviderImpl(config), clock); } - + /** - * @param config Configuration options for the registry that are describable as properties. - * @param clock The clock to use for timings. - * @param threadFactory The thread factory to use to create the publishing thread. - * @deprecated Use {@link #builder(NewRelicConfig)} instead. + * @param config Configuration options for the registry that are describable as properties. + * @param clientProvider Provider of the HTTP or Agent-based client that publishes metrics to New Relic + * @param clock The clock to use for timings. */ - @Deprecated - public NewRelicMeterRegistry(NewRelicConfig config, Clock clock, ThreadFactory threadFactory) { - this(config, clock, threadFactory, new HttpUrlConnectionSender(config.connectTimeout(), config.readTimeout())); + public NewRelicMeterRegistry(NewRelicConfig config, NewRelicClientProvider clientProvider, Clock clock) { + this(config, clientProvider, new NewRelicNamingConvention(), clock, DEFAULT_THREAD_FACTORY); } // VisibleForTesting - NewRelicMeterRegistry(NewRelicConfig config, Clock clock, ThreadFactory threadFactory, HttpSender httpClient) { + NewRelicMeterRegistry(NewRelicConfig config, NewRelicClientProvider clientProvider, + NamingConvention namingConvention, Clock clock, ThreadFactory threadFactory) { super(config, clock); - if (!config.meterNameEventTypeEnabled() && StringUtils.isEmpty(config.eventType())) { - throw new MissingRequiredConfigurationException("eventType must be set to report metrics to New Relic"); - } - if (StringUtils.isEmpty(config.accountId())) { - throw new MissingRequiredConfigurationException("accountId must be set to report metrics to New Relic"); - } - if (StringUtils.isEmpty(config.apiKey())) { - throw new MissingRequiredConfigurationException("apiKey must be set to report metrics to New Relic"); + if (clientProvider == null) { + throw new MissingRequiredConfigurationException("clientProvider required to report metrics to New Relic"); } - if (StringUtils.isEmpty(config.uri())) { - throw new MissingRequiredConfigurationException("uri must be set to report metrics to New Relic"); - } - + this.config = config; - this.httpClient = httpClient; + this.clientProvider = clientProvider; - config().namingConvention(new NewRelicNamingConvention()); + config().namingConvention(namingConvention); start(threadFactory); } @@ -105,181 +88,10 @@ public void start(ThreadFactory threadFactory) { } super.start(threadFactory); } - + @Override protected void publish() { - String insightsEndpoint = config.uri() + "/v1/accounts/" + config.accountId() + "/events"; - - // New Relic's Insights API limits us to 1000 events per call - // 1:1 mapping between Micrometer meters and New Relic events - for (List batch : MeterPartition.partition(this, Math.min(config.batchSize(), 1000))) { - sendEvents(insightsEndpoint, batch.stream().flatMap(meter -> meter.match( - this::writeGauge, - this::writeCounter, - this::writeTimer, - this::writeSummary, - this::writeLongTaskTimer, - this::writeTimeGauge, - this::writeFunctionCounter, - this::writeFunctionTimer, - this::writeMeter))); - } - } - - private Stream writeLongTaskTimer(LongTaskTimer ltt) { - return Stream.of( - event(ltt.getId(), - new Attribute("activeTasks", ltt.activeTasks()), - new Attribute("duration", ltt.duration(getBaseTimeUnit())), - new Attribute("timeUnit", getBaseTimeUnit().name().toLowerCase())) - ); - } - - // VisibleForTesting - Stream writeFunctionCounter(FunctionCounter counter) { - double count = counter.count(); - if (Double.isFinite(count)) { - return Stream.of(event(counter.getId(), new Attribute("throughput", count))); - } - return Stream.empty(); - } - - private Stream writeCounter(Counter counter) { - return Stream.of(event(counter.getId(), new Attribute("throughput", counter.count()))); - } - - // VisibleForTesting - Stream writeGauge(Gauge gauge) { - Double value = gauge.value(); - if (Double.isFinite(value)) { - return Stream.of(event(gauge.getId(), new Attribute("value", value))); - } - return Stream.empty(); - } - - // VisibleForTesting - Stream writeTimeGauge(TimeGauge gauge) { - Double value = gauge.value(getBaseTimeUnit()); - if (Double.isFinite(value)) { - return Stream.of( - event(gauge.getId(), - new Attribute("value", value), - new Attribute("timeUnit", getBaseTimeUnit().name().toLowerCase()))); - } - return Stream.empty(); - } - - private Stream writeSummary(DistributionSummary summary) { - return Stream.of( - event(summary.getId(), - new Attribute("count", summary.count()), - new Attribute("avg", summary.mean()), - new Attribute("total", summary.totalAmount()), - new Attribute("max", summary.max()) - ) - ); - } - - private Stream writeTimer(Timer timer) { - return Stream.of(event(timer.getId(), - new Attribute("count", timer.count()), - new Attribute("avg", timer.mean(getBaseTimeUnit())), - new Attribute("totalTime", timer.totalTime(getBaseTimeUnit())), - new Attribute("max", timer.max(getBaseTimeUnit())), - new Attribute("timeUnit", getBaseTimeUnit().name().toLowerCase()) - )); - } - - private Stream writeFunctionTimer(FunctionTimer timer) { - return Stream.of( - event(timer.getId(), - new Attribute("count", timer.count()), - new Attribute("avg", timer.mean(getBaseTimeUnit())), - new Attribute("totalTime", timer.totalTime(getBaseTimeUnit())), - new Attribute("timeUnit", getBaseTimeUnit().name().toLowerCase()) - ) - ); - } - - // VisibleForTesting - Stream writeMeter(Meter meter) { - // Snapshot values should be used throughout this method as there are chances for values to be changed in-between. - Map attributes = new HashMap<>(); - for (Measurement measurement : meter.measure()) { - double value = measurement.getValue(); - if (!Double.isFinite(value)) { - continue; - } - String name = measurement.getStatistic().getTagValueRepresentation(); - attributes.put(name, new Attribute(name, value)); - } - if (attributes.isEmpty()) { - return Stream.empty(); - } - return Stream.of(event(meter.getId(), attributes.values().toArray(new Attribute[0]))); - } - - private String event(Meter.Id id, Attribute... 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 - int size = attributes.length; - Attribute[] newAttrs = Arrays.copyOf(attributes, size + 2); - - String name = id.getConventionName(config().namingConvention()); - newAttrs[size] = new Attribute("metricName", name); - newAttrs[size + 1] = new Attribute("metricType", id.getType().toString()); - - return event(id, Tags.empty(), newAttrs); - } - return event(id, Tags.empty(), attributes); - } - - private String event(Meter.Id id, Iterable extraTags, Attribute... attributes) { - StringBuilder tagsJson = new StringBuilder(); - - for (Tag tag : getConventionTags(id)) { - tagsJson.append(",\"").append(escapeJson(tag.getKey())).append("\":\"").append(escapeJson(tag.getValue())).append("\""); - } - - NamingConvention convention = config().namingConvention(); - for (Tag tag : extraTags) { - tagsJson.append(",\"").append(escapeJson(convention.tagKey(tag.getKey()))) - .append("\":\"").append(escapeJson(convention.tagValue(tag.getValue()))).append("\""); - } - - String eventType = getEventType(id); - - return Arrays.stream(attributes) - .map(attr -> - (attr.getValue() instanceof Number) - ? ",\"" + attr.getName() + "\":" + DoubleFormat.wholeOrDecimal(((Number)attr.getValue()).doubleValue()) - : ",\"" + attr.getName() + "\":\"" + convention.tagValue(attr.getValue().toString()) + "\"" - ) - .collect(Collectors.joining("", "{\"eventType\":\"" + escapeJson(eventType) + "\"", tagsJson + "}")); - } - - private String getEventType(Meter.Id id) { - if (config.meterNameEventTypeEnabled()) { - return id.getConventionName(config().namingConvention()); - } else { - return config.eventType(); - } - } - - private void sendEvents(String insightsEndpoint, Stream events) { - try { - AtomicInteger totalEvents = new AtomicInteger(); - - httpClient.post(insightsEndpoint) - .withHeader("X-Insert-Key", config.apiKey()) - .withJsonContent(events.peek(ev -> totalEvents.incrementAndGet()).collect(Collectors.joining(",", "[", "]"))) - .send() - .onSuccess(response -> logger.debug("successfully sent {} metrics to New Relic.", totalEvents)) - .onError(response -> logger.error("failed to send metrics to new relic: http {} {}", response.code(), response.body())); - } catch (Throwable e) { - logger.warn("failed to send metrics to new relic", e); - } + clientProvider.publish(this); } @Override @@ -290,14 +102,31 @@ protected TimeUnit getBaseTimeUnit() { public static class Builder { private final NewRelicConfig config; + private NewRelicClientProvider clientProvider; + private NamingConvention convention = new NewRelicNamingConvention(); private Clock clock = Clock.SYSTEM; private ThreadFactory threadFactory = DEFAULT_THREAD_FACTORY; - private HttpSender httpClient; - @SuppressWarnings("deprecation") Builder(NewRelicConfig config) { this.config = config; - this.httpClient = new HttpUrlConnectionSender(config.connectTimeout(), config.readTimeout()); + } + + public Builder agentClientProvider() { + return clientProvider(new NewRelicAgentClientProviderImpl(config)); + } + + public Builder httpClientProvider() { + return clientProvider(new NewRelicHttpClientProviderImpl(config)); + } + + Builder clientProvider(NewRelicClientProvider clientProvider) { + this.clientProvider = clientProvider; + return this; + } + + public Builder namingConvention(NamingConvention convention) { + this.convention = convention; + return this; } public Builder clock(Clock clock) { @@ -310,31 +139,12 @@ public Builder threadFactory(ThreadFactory threadFactory) { return this; } - public Builder httpClient(HttpSender httpClient) { - this.httpClient = httpClient; - return this; - } - public NewRelicMeterRegistry build() { - return new NewRelicMeterRegistry(config, clock, threadFactory, httpClient); - } - } - - private class Attribute { - private final String name; - private final Object value; - - private Attribute(String name, Object value) { - this.name = name; - this.value = value; - } - - public String getName() { - return name; - } - - public Object getValue() { - return value; + if (clientProvider == null) { + //default to the HTTP/REST client + clientProvider = new NewRelicHttpClientProviderImpl(config); + } + return new NewRelicMeterRegistry(config, clientProvider, convention, clock, threadFactory); } } } diff --git a/implementations/micrometer-registry-new-relic/src/test/java/io/micrometer/newrelic/NewRelicMeterRegistryTest.java b/implementations/micrometer-registry-new-relic/src/test/java/io/micrometer/newrelic/NewRelicMeterRegistryTest.java index 48fafbd88a..bd73ffcbd4 100644 --- a/implementations/micrometer-registry-new-relic/src/test/java/io/micrometer/newrelic/NewRelicMeterRegistryTest.java +++ b/implementations/micrometer-registry-new-relic/src/test/java/io/micrometer/newrelic/NewRelicMeterRegistryTest.java @@ -19,33 +19,56 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import java.util.Arrays; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import org.junit.jupiter.api.Test; +import com.newrelic.api.agent.Agent; +import com.newrelic.api.agent.Config; +import com.newrelic.api.agent.Insights; +import com.newrelic.api.agent.Logger; +import com.newrelic.api.agent.MetricAggregator; +import com.newrelic.api.agent.TraceMetadata; +import com.newrelic.api.agent.TracedMethod; +import com.newrelic.api.agent.Transaction; + +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.MockClock; import io.micrometer.core.instrument.Statistic; import io.micrometer.core.instrument.Tags; import io.micrometer.core.instrument.TimeGauge; +import io.micrometer.core.instrument.Timer; import io.micrometer.core.instrument.config.MissingRequiredConfigurationException; -import io.micrometer.core.instrument.util.NamedThreadFactory; import io.micrometer.core.ipc.http.HttpSender; +import io.micrometer.newrelic.NewRelicMeterRegistryTest.MockNewRelicAgent.MockNewRelicInsights; /** * Tests for {@link NewRelicMeterRegistry}. * * @author Johnny Lim + * @author Neil Powell */ class NewRelicMeterRegistryTest { - private final NewRelicConfig config = new NewRelicConfig() { - + private final NewRelicConfig agentConfig = new NewRelicConfig() { + @Override + public String get(String key) { + return null; + } + }; + + private final NewRelicConfig httpConfig = new NewRelicConfig() { @Override public String get(String key) { return null; @@ -55,22 +78,21 @@ public String get(String key) { public String accountId() { return "accountId"; } - + @Override public String apiKey() { return "apiKey"; } - }; - + private final NewRelicConfig meterNameEventTypeEnabledConfig = new NewRelicConfig() { - + @Override public boolean meterNameEventTypeEnabled() { // Previous behavior for backward compatibility return true; } - + @Override public String get(String key) { return null; @@ -80,173 +102,338 @@ public String get(String key) { public String accountId() { return "accountId"; } - + @Override public String apiKey() { return "apiKey"; } - }; private final MockClock clock = new MockClock(); - private final NewRelicMeterRegistry meterNameEventTypeEnabledRegistry = new NewRelicMeterRegistry(meterNameEventTypeEnabledConfig, clock); - private final NewRelicMeterRegistry registry = new NewRelicMeterRegistry(config, clock); + private final NewRelicMeterRegistry registry = new NewRelicMeterRegistry(httpConfig, new MockClientProvider(), clock); + + NewRelicAgentClientProviderImpl getAgentClientProvider(NewRelicConfig config) { + return new NewRelicAgentClientProviderImpl(config); + } + NewRelicHttpClientProviderImpl getHttpClientProvider(NewRelicConfig config) { + return new NewRelicHttpClientProviderImpl(config); + } @Test void writeGauge() { - writeGauge(this.meterNameEventTypeEnabledRegistry, "{\"eventType\":\"myGauge\",\"value\":1}"); - writeGauge(this.registry, + //test Http clientProvider + writeGauge(meterNameEventTypeEnabledConfig, "{\"eventType\":\"myGauge\",\"value\":1}"); + writeGauge(httpConfig, "{\"eventType\":\"MicrometerSample\",\"value\":1,\"metricName\":\"myGauge\",\"metricType\":\"GAUGE\"}"); + + //test Agent clientProvider + Map expectedEntries = new HashMap<>(); + expectedEntries.put("value", 1); + writeGauge(meterNameEventTypeEnabledConfig, expectedEntries); + expectedEntries.put("metricName", "myGauge2"); + expectedEntries.put("metricType", "GAUGE"); + writeGauge(agentConfig, expectedEntries); } - private void writeGauge(NewRelicMeterRegistry meterRegistry, String expectedJson) { - meterRegistry.gauge("my.gauge", 1d); - Gauge gauge = meterRegistry.find("my.gauge").gauge(); - assertThat(meterRegistry.writeGauge(gauge)).containsExactly(expectedJson); + private void writeGauge(NewRelicConfig config, String expectedJson) { + registry.gauge("my.gauge", 1d); + Gauge gauge = registry.find("my.gauge").gauge(); + assertThat(getHttpClientProvider(config).writeGauge(gauge)).containsExactly(expectedJson); + } + + private void writeGauge(NewRelicConfig config, Map expectedEntries) { + registry.gauge("my.gauge2", 1d); + Gauge gauge = registry.find("my.gauge2").gauge(); + Map result = getAgentClientProvider(config).writeGauge(gauge); + assertThat(result).hasSize(expectedEntries.size()); + assertThat(result).containsExactlyEntriesOf(expectedEntries); } + @Test void writeGaugeShouldDropNanValue() { - writeGaugeShouldDropNanValue(this.meterNameEventTypeEnabledRegistry); - writeGaugeShouldDropNanValue(this.registry); + //test Http clientProvider + writeGaugeShouldDropNanValue(getHttpClientProvider(meterNameEventTypeEnabledConfig)); + writeGaugeShouldDropNanValue(getHttpClientProvider(httpConfig)); + + //test Agent clientProvider + writeGaugeShouldDropNanValue(getAgentClientProvider(meterNameEventTypeEnabledConfig)); + writeGaugeShouldDropNanValue(getAgentClientProvider(agentConfig)); } - - private void writeGaugeShouldDropNanValue(NewRelicMeterRegistry meterRegistry) { - meterRegistry.gauge("my.gauge", Double.NaN); - Gauge gauge = meterRegistry.find("my.gauge").gauge(); - assertThat(meterRegistry.writeGauge(gauge)).isEmpty(); + + private void writeGaugeShouldDropNanValue(NewRelicHttpClientProviderImpl clientProvider) { + registry.gauge("my.gauge", Double.NaN); + Gauge gauge = registry.find("my.gauge").gauge(); + assertThat(clientProvider.writeGauge(gauge)).isEmpty(); } + + private void writeGaugeShouldDropNanValue(NewRelicAgentClientProviderImpl clientProvider) { + registry.gauge("my.gauge2", Double.NaN); + Gauge gauge = registry.find("my.gauge2").gauge(); + assertThat(clientProvider.writeGauge(gauge)).isEmpty(); + } @Test void writeGaugeShouldDropInfiniteValues() { - writeGaugeShouldDropInfiniteValues(this.meterNameEventTypeEnabledRegistry); - writeGaugeShouldDropInfiniteValues(this.registry); + //test Http clientProvider + writeGaugeShouldDropInfiniteValues(getHttpClientProvider(meterNameEventTypeEnabledConfig)); + writeGaugeShouldDropInfiniteValues(getHttpClientProvider(httpConfig)); + + //test Agent clientProvider + writeGaugeShouldDropInfiniteValues(getAgentClientProvider(meterNameEventTypeEnabledConfig)); + writeGaugeShouldDropInfiniteValues(getAgentClientProvider(agentConfig)); } - private void writeGaugeShouldDropInfiniteValues(NewRelicMeterRegistry meterRegistry) { - meterRegistry.gauge("my.gauge", Double.POSITIVE_INFINITY); - Gauge gauge = meterRegistry.find("my.gauge").gauge(); - assertThat(meterRegistry.writeGauge(gauge)).isEmpty(); + private void writeGaugeShouldDropInfiniteValues(NewRelicHttpClientProviderImpl clientProvider) { + registry.gauge("my.gauge", Double.POSITIVE_INFINITY); + Gauge gauge = registry.find("my.gauge").gauge(); + assertThat(clientProvider.writeGauge(gauge)).isEmpty(); - meterRegistry.gauge("my.gauge", Double.NEGATIVE_INFINITY); - gauge = meterRegistry.find("my.gauge").gauge(); - assertThat(meterRegistry.writeGauge(gauge)).isEmpty(); + registry.gauge("my.gauge", Double.NEGATIVE_INFINITY); + gauge = registry.find("my.gauge").gauge(); + assertThat(clientProvider.writeGauge(gauge)).isEmpty(); } - + + private void writeGaugeShouldDropInfiniteValues(NewRelicAgentClientProviderImpl clientProvider) { + registry.gauge("my.gauge2", Double.POSITIVE_INFINITY); + Gauge gauge = registry.find("my.gauge2").gauge(); + assertThat(clientProvider.writeGauge(gauge)).isEmpty(); + + registry.gauge("my.gauge2", Double.NEGATIVE_INFINITY); + gauge = registry.find("my.gauge2").gauge(); + assertThat(clientProvider.writeGauge(gauge)).isEmpty(); + } + @Test void writeGaugeWithTimeGauge() { - writeGaugeWithTimeGauge(this.meterNameEventTypeEnabledRegistry, + //test Http clientProvider + writeGaugeWithTimeGauge(getHttpClientProvider(meterNameEventTypeEnabledConfig), "{\"eventType\":\"myTimeGauge\",\"value\":1,\"timeUnit\":\"seconds\"}"); - writeGaugeWithTimeGauge(this.registry, + writeGaugeWithTimeGauge(getHttpClientProvider(httpConfig), "{\"eventType\":\"MicrometerSample\",\"value\":1,\"timeUnit\":\"seconds\",\"metricName\":\"myTimeGauge\",\"metricType\":\"GAUGE\"}"); + + //test Agent clientProvider + Map expectedEntries = new HashMap<>(); + expectedEntries.put("value", 1); + expectedEntries.put("timeUnit", "seconds"); + writeGaugeWithTimeGauge(getAgentClientProvider(meterNameEventTypeEnabledConfig), expectedEntries); + expectedEntries.put("metricName", "myTimeGauge2"); + expectedEntries.put("metricType", "GAUGE"); + writeGaugeWithTimeGauge(getAgentClientProvider(agentConfig), expectedEntries); } - - private void writeGaugeWithTimeGauge(NewRelicMeterRegistry meterRegistry, String expectedJson) { + + private void writeGaugeWithTimeGauge(NewRelicHttpClientProviderImpl clientProvider, String expectedJson) { AtomicReference obj = new AtomicReference<>(1d); - meterRegistry.more().timeGauge("my.timeGauge", Tags.empty(), obj, TimeUnit.SECONDS, AtomicReference::get); - TimeGauge timeGauge = meterRegistry.find("my.timeGauge").timeGauge(); - assertThat(meterRegistry.writeTimeGauge(timeGauge)).containsExactly(expectedJson); + registry.more().timeGauge("my.timeGauge", Tags.empty(), obj, TimeUnit.SECONDS, AtomicReference::get); + TimeGauge timeGauge = registry.find("my.timeGauge").timeGauge(); + assertThat(clientProvider.writeTimeGauge(timeGauge)).containsExactly(expectedJson); } - + + private void writeGaugeWithTimeGauge(NewRelicAgentClientProviderImpl clientProvider, Map expectedEntries) { + AtomicReference obj = new AtomicReference<>(1d); + registry.more().timeGauge("my.timeGauge2", Tags.empty(), obj, TimeUnit.SECONDS, AtomicReference::get); + TimeGauge timeGauge = registry.find("my.timeGauge2").timeGauge(); + Map result = clientProvider.writeTimeGauge(timeGauge); + assertThat(result).hasSize(expectedEntries.size()); + assertThat(result).containsExactlyEntriesOf(expectedEntries); + } + @Test void writeGaugeWithTimeGaugeShouldDropNanValue() { - writeGaugeWithTimeGaugeShouldDropNanValue(this.meterNameEventTypeEnabledRegistry); - writeGaugeWithTimeGaugeShouldDropNanValue(this.registry); + //test Http clientProvider + writeGaugeWithTimeGaugeShouldDropNanValue(getHttpClientProvider(meterNameEventTypeEnabledConfig)); + writeGaugeWithTimeGaugeShouldDropNanValue(getHttpClientProvider(httpConfig)); + + //test Agent clientProvider + writeGaugeWithTimeGaugeShouldDropNanValue(getAgentClientProvider(meterNameEventTypeEnabledConfig)); + writeGaugeWithTimeGaugeShouldDropNanValue(getAgentClientProvider(agentConfig)); } - - private void writeGaugeWithTimeGaugeShouldDropNanValue(NewRelicMeterRegistry meterRegistry) { + + private void writeGaugeWithTimeGaugeShouldDropNanValue(NewRelicHttpClientProviderImpl clientProvider) { + AtomicReference obj = new AtomicReference<>(Double.NaN); + registry.more().timeGauge("my.timeGauge", Tags.empty(), obj, TimeUnit.SECONDS, AtomicReference::get); + TimeGauge timeGauge = registry.find("my.timeGauge").timeGauge(); + assertThat(clientProvider.writeTimeGauge(timeGauge)).isEmpty(); + } + + private void writeGaugeWithTimeGaugeShouldDropNanValue(NewRelicAgentClientProviderImpl clientProvider) { AtomicReference obj = new AtomicReference<>(Double.NaN); - meterRegistry.more().timeGauge("my.timeGauge", Tags.empty(), obj, TimeUnit.SECONDS, AtomicReference::get); - TimeGauge timeGauge = meterRegistry.find("my.timeGauge").timeGauge(); - assertThat(meterRegistry.writeTimeGauge(timeGauge)).isEmpty(); + registry.more().timeGauge("my.timeGauge2", Tags.empty(), obj, TimeUnit.SECONDS, AtomicReference::get); + TimeGauge timeGauge = registry.find("my.timeGauge2").timeGauge(); + assertThat(clientProvider.writeTimeGauge(timeGauge)).isEmpty(); } @Test void writeGaugeWithTimeGaugeShouldDropInfiniteValues() { - writeGaugeWithTimeGaugeShouldDropInfiniteValues(this.meterNameEventTypeEnabledRegistry); - writeGaugeWithTimeGaugeShouldDropInfiniteValues(this.registry); + //test Http clientProvider + writeGaugeWithTimeGaugeShouldDropInfiniteValues(getHttpClientProvider(meterNameEventTypeEnabledConfig)); + writeGaugeWithTimeGaugeShouldDropInfiniteValues(getHttpClientProvider(httpConfig)); + + //test Agent clientProvider + writeGaugeWithTimeGaugeShouldDropInfiniteValues(getAgentClientProvider(meterNameEventTypeEnabledConfig)); + writeGaugeWithTimeGaugeShouldDropInfiniteValues(getAgentClientProvider(agentConfig)); } + + private void writeGaugeWithTimeGaugeShouldDropInfiniteValues(NewRelicHttpClientProviderImpl clientProvider) { + AtomicReference obj = new AtomicReference<>(Double.POSITIVE_INFINITY); + registry.more().timeGauge("my.timeGauge", Tags.empty(), obj, TimeUnit.SECONDS, AtomicReference::get); + TimeGauge timeGauge = registry.find("my.timeGauge").timeGauge(); + assertThat(clientProvider.writeTimeGauge(timeGauge)).isEmpty(); - private void writeGaugeWithTimeGaugeShouldDropInfiniteValues(NewRelicMeterRegistry meterRegistry) { + obj = new AtomicReference<>(Double.NEGATIVE_INFINITY); + registry.more().timeGauge("my.timeGauge", Tags.empty(), obj, TimeUnit.SECONDS, AtomicReference::get); + timeGauge = registry.find("my.timeGauge").timeGauge(); + assertThat(clientProvider.writeTimeGauge(timeGauge)).isEmpty(); + } + + private void writeGaugeWithTimeGaugeShouldDropInfiniteValues(NewRelicAgentClientProviderImpl clientProvider) { AtomicReference obj = new AtomicReference<>(Double.POSITIVE_INFINITY); - meterRegistry.more().timeGauge("my.timeGauge", Tags.empty(), obj, TimeUnit.SECONDS, AtomicReference::get); - TimeGauge timeGauge = meterRegistry.find("my.timeGauge").timeGauge(); - assertThat(meterRegistry.writeTimeGauge(timeGauge)).isEmpty(); + registry.more().timeGauge("my.timeGauge", Tags.empty(), obj, TimeUnit.SECONDS, AtomicReference::get); + TimeGauge timeGauge = registry.find("my.timeGauge").timeGauge(); + assertThat(clientProvider.writeTimeGauge(timeGauge)).isEmpty(); obj = new AtomicReference<>(Double.NEGATIVE_INFINITY); - meterRegistry.more().timeGauge("my.timeGauge", Tags.empty(), obj, TimeUnit.SECONDS, AtomicReference::get); - timeGauge = meterRegistry.find("my.timeGauge").timeGauge(); - assertThat(meterRegistry.writeTimeGauge(timeGauge)).isEmpty(); + registry.more().timeGauge("my.timeGauge2", Tags.empty(), obj, TimeUnit.SECONDS, AtomicReference::get); + timeGauge = registry.find("my.timeGauge2").timeGauge(); + assertThat(clientProvider.writeTimeGauge(timeGauge)).isEmpty(); } @Test void writeCounterWithFunctionCounter() { - writeCounterWithFunctionCounter(this.meterNameEventTypeEnabledRegistry, + FunctionCounter counter = FunctionCounter.builder("myCounter", 1d, Number::doubleValue).register(registry); + clock.add(httpConfig.step()); + //test Http clientProvider + writeCounterWithFunctionCounter(counter, getHttpClientProvider(meterNameEventTypeEnabledConfig), "{\"eventType\":\"myCounter\",\"throughput\":1}"); - writeCounterWithFunctionCounter(this.registry, + writeCounterWithFunctionCounter(counter, getHttpClientProvider(httpConfig), "{\"eventType\":\"MicrometerSample\",\"throughput\":1,\"metricName\":\"myCounter\",\"metricType\":\"COUNTER\"}"); + + //test Agent clientProvider + Map expectedEntries = new HashMap<>(); + expectedEntries.put("throughput", 1); + writeCounterWithFunctionCounter(counter, getAgentClientProvider(meterNameEventTypeEnabledConfig), expectedEntries); + expectedEntries.put("metricName", "myCounter"); + expectedEntries.put("metricType", "COUNTER"); + writeCounterWithFunctionCounter(counter, getAgentClientProvider(agentConfig), expectedEntries); } - private void writeCounterWithFunctionCounter(NewRelicMeterRegistry meterRegistry, String expectedJson) { - FunctionCounter counter = FunctionCounter.builder("myCounter", 1d, Number::doubleValue).register(meterRegistry); - clock.add(config.step()); - assertThat(meterRegistry.writeFunctionCounter(counter)).containsExactly(expectedJson); + private void writeCounterWithFunctionCounter(FunctionCounter counter, NewRelicHttpClientProviderImpl clientProvider, String expectedJson) { + assertThat(clientProvider.writeFunctionCounter(counter)).containsExactly(expectedJson); } - + + private void writeCounterWithFunctionCounter(FunctionCounter counter, NewRelicAgentClientProviderImpl clientProvider, Map expectedEntries) { + Map result = clientProvider.writeFunctionCounter(counter); + assertThat(result).hasSize(expectedEntries.size()); + assertThat(result).containsExactlyEntriesOf(expectedEntries); + } + @Test void writeCounterWithFunctionCounterShouldDropInfiniteValues() { - writeCounterWithFunctionCounterShouldDropInfiniteValues(this.meterNameEventTypeEnabledRegistry); - writeCounterWithFunctionCounterShouldDropInfiniteValues(this.registry); + //test Http clientProvider + writeCounterWithFunctionCounterShouldDropInfiniteValues(getHttpClientProvider(meterNameEventTypeEnabledConfig)); + writeCounterWithFunctionCounterShouldDropInfiniteValues(getHttpClientProvider(httpConfig)); + + //test Agent clientProvider + writeCounterWithFunctionCounterShouldDropInfiniteValues(getAgentClientProvider(meterNameEventTypeEnabledConfig)); + writeCounterWithFunctionCounterShouldDropInfiniteValues(getAgentClientProvider(agentConfig)); } - private void writeCounterWithFunctionCounterShouldDropInfiniteValues(NewRelicMeterRegistry meterRegistry) { + private void writeCounterWithFunctionCounterShouldDropInfiniteValues(NewRelicHttpClientProviderImpl clientProvider) { + FunctionCounter counter = FunctionCounter.builder("myCounter", Double.POSITIVE_INFINITY, Number::doubleValue) + .register(registry); + clock.add(httpConfig.step()); + assertThat(clientProvider.writeFunctionCounter(counter)).isEmpty(); + + counter = FunctionCounter.builder("myCounter", Double.NEGATIVE_INFINITY, Number::doubleValue) + .register(registry); + clock.add(httpConfig.step()); + assertThat(clientProvider.writeFunctionCounter(counter)).isEmpty(); + } + + private void writeCounterWithFunctionCounterShouldDropInfiniteValues(NewRelicAgentClientProviderImpl clientProvider) { FunctionCounter counter = FunctionCounter.builder("myCounter", Double.POSITIVE_INFINITY, Number::doubleValue) - .register(meterRegistry); - clock.add(config.step()); - assertThat(meterRegistry.writeFunctionCounter(counter)).isEmpty(); + .register(registry); + clock.add(httpConfig.step()); + assertThat(clientProvider.writeFunctionCounter(counter)).isEmpty(); counter = FunctionCounter.builder("myCounter", Double.NEGATIVE_INFINITY, Number::doubleValue) - .register(meterRegistry); - clock.add(config.step()); - assertThat(meterRegistry.writeFunctionCounter(counter)).isEmpty(); + .register(registry); + clock.add(httpConfig.step()); + assertThat(clientProvider.writeFunctionCounter(counter)).isEmpty(); } @Test void writeMeterWhenCustomMeterHasOnlyNonFiniteValuesShouldNotBeWritten() { - writeMeterWhenCustomMeterHasOnlyNonFiniteValuesShouldNotBeWritten(this.meterNameEventTypeEnabledRegistry); - writeMeterWhenCustomMeterHasOnlyNonFiniteValuesShouldNotBeWritten(this.registry); - } - - private void writeMeterWhenCustomMeterHasOnlyNonFiniteValuesShouldNotBeWritten( - NewRelicMeterRegistry meterRegistry) { Measurement measurement1 = new Measurement(() -> Double.POSITIVE_INFINITY, Statistic.VALUE); Measurement measurement2 = new Measurement(() -> Double.NEGATIVE_INFINITY, Statistic.VALUE); Measurement measurement3 = new Measurement(() -> Double.NaN, Statistic.VALUE); List measurements = Arrays.asList(measurement1, measurement2, measurement3); - Meter meter = Meter.builder("my.meter", Meter.Type.GAUGE, measurements).register(meterRegistry); - assertThat(meterRegistry.writeMeter(meter)).isEmpty(); + + //test Http clientProvider + writeMeterWhenCustomMeterHasOnlyNonFiniteValuesShouldNotBeWritten( + measurements, getHttpClientProvider(meterNameEventTypeEnabledConfig)); + writeMeterWhenCustomMeterHasOnlyNonFiniteValuesShouldNotBeWritten( + measurements, getHttpClientProvider(httpConfig)); + + //test Agent clientProvider + writeMeterWhenCustomMeterHasOnlyNonFiniteValuesShouldNotBeWritten( + measurements, getAgentClientProvider(meterNameEventTypeEnabledConfig)); + writeMeterWhenCustomMeterHasOnlyNonFiniteValuesShouldNotBeWritten( + measurements, getAgentClientProvider(agentConfig)); } - @Test - void writeMeterWhenCustomMeterHasMixedFiniteAndNonFiniteValuesShouldSkipOnlyNonFiniteValues() { - writeMeterWhenCustomMeterHasMixedFiniteAndNonFiniteValuesShouldSkipOnlyNonFiniteValues( - this.meterNameEventTypeEnabledRegistry, "{\"eventType\":\"myMeter\",\"value\":1}"); - writeMeterWhenCustomMeterHasMixedFiniteAndNonFiniteValuesShouldSkipOnlyNonFiniteValues( - this.registry, - "{\"eventType\":\"MicrometerSample\",\"value\":1,\"metricName\":\"myMeter\",\"metricType\":\"GAUGE\"}"); + private void writeMeterWhenCustomMeterHasOnlyNonFiniteValuesShouldNotBeWritten( + List measurements, NewRelicHttpClientProviderImpl clientProvider) { + Meter meter = Meter.builder("my.meter", Meter.Type.GAUGE, measurements).register(registry); + assertThat(clientProvider.writeMeter(meter)).isEmpty(); + } + + private void writeMeterWhenCustomMeterHasOnlyNonFiniteValuesShouldNotBeWritten( + List measurements, NewRelicAgentClientProviderImpl clientProvider) { + Meter meter = Meter.builder("my.meter2", Meter.Type.GAUGE, measurements).register(registry); + assertThat(clientProvider.writeMeter(meter)).isEmpty(); } - private void writeMeterWhenCustomMeterHasMixedFiniteAndNonFiniteValuesShouldSkipOnlyNonFiniteValues( - NewRelicMeterRegistry meterRegistry, String expectedJson) { + @Test + void writeMeterWhenCustomMeterHasMixedFiniteAndNonFiniteValuesShouldSkipOnlyNonFiniteValues() { Measurement measurement1 = new Measurement(() -> Double.POSITIVE_INFINITY, Statistic.VALUE); Measurement measurement2 = new Measurement(() -> Double.NEGATIVE_INFINITY, Statistic.VALUE); Measurement measurement3 = new Measurement(() -> Double.NaN, Statistic.VALUE); Measurement measurement4 = new Measurement(() -> 1d, Statistic.VALUE); List measurements = Arrays.asList(measurement1, measurement2, measurement3, measurement4); - Meter meter = Meter.builder("my.meter", Meter.Type.GAUGE, measurements).register(meterRegistry); - assertThat(meterRegistry.writeMeter(meter)).containsExactly(expectedJson); + //test Http clientProvider + writeMeterWhenCustomMeterHasMixedFiniteAndNonFiniteValuesShouldSkipOnlyNonFiniteValues( + measurements, getHttpClientProvider(meterNameEventTypeEnabledConfig), + "{\"eventType\":\"myMeter\",\"value\":1}"); + writeMeterWhenCustomMeterHasMixedFiniteAndNonFiniteValuesShouldSkipOnlyNonFiniteValues( + measurements, getHttpClientProvider(httpConfig), + "{\"eventType\":\"MicrometerSample\",\"value\":1,\"metricName\":\"myMeter\",\"metricType\":\"GAUGE\"}"); + + //test Agent clientProvider + Map expectedEntries = new HashMap<>(); + expectedEntries.put("value", 1); + writeMeterWhenCustomMeterHasMixedFiniteAndNonFiniteValuesShouldSkipOnlyNonFiniteValues( + measurements, getAgentClientProvider(meterNameEventTypeEnabledConfig), expectedEntries); + expectedEntries.put("metricName", "myMeter2"); + expectedEntries.put("metricType", "GAUGE"); + writeMeterWhenCustomMeterHasMixedFiniteAndNonFiniteValuesShouldSkipOnlyNonFiniteValues( + measurements, getAgentClientProvider(agentConfig), expectedEntries); } + private void writeMeterWhenCustomMeterHasMixedFiniteAndNonFiniteValuesShouldSkipOnlyNonFiniteValues( + List measurements, NewRelicHttpClientProviderImpl clientProvider, String expectedJson) { + Meter meter = Meter.builder("my.meter", Meter.Type.GAUGE, measurements).register(registry); + assertThat(clientProvider.writeMeter(meter)).containsExactly(expectedJson); + } + + private void writeMeterWhenCustomMeterHasMixedFiniteAndNonFiniteValuesShouldSkipOnlyNonFiniteValues( + List measurements, NewRelicAgentClientProviderImpl clientProvider, Map expectedEntries) { + Meter meter = Meter.builder("my.meter2", Meter.Type.GAUGE, measurements).register(registry); + Map result = clientProvider.writeMeter(meter); + assertThat(result).hasSize(expectedEntries.size()); + assertThat(result).containsExactlyEntriesOf(expectedEntries); + } + @Test void writeMeterWhenCustomMeterHasDuplicatesKeysShouldWriteOnlyLastValue() { Measurement measurement1 = new Measurement(() -> 3d, Statistic.VALUE); @@ -254,26 +441,152 @@ void writeMeterWhenCustomMeterHasDuplicatesKeysShouldWriteOnlyLastValue() { Measurement measurement3 = new Measurement(() -> 2d, Statistic.VALUE); List measurements = Arrays.asList(measurement1, measurement2, measurement3); Meter meter = Meter.builder("my.meter", Meter.Type.GAUGE, measurements).register(this.registry); - assertThat(registry.writeMeter(meter)).containsExactly("{\"eventType\":\"MicrometerSample\",\"value\":2,\"metricName\":\"myMeter\",\"metricType\":\"GAUGE\"}"); + //test Http clientProvider + assertThat(getHttpClientProvider(httpConfig).writeMeter(meter)).containsExactly( + "{\"eventType\":\"MicrometerSample\",\"value\":2,\"metricName\":\"myMeter\",\"metricType\":\"GAUGE\"}"); + + //test Agent clientProvider + Map expectedEntries = new HashMap<>(); + expectedEntries.put("value", 2); + expectedEntries.put("metricName", "myMeter"); + expectedEntries.put("metricType", "GAUGE"); + Map result = getAgentClientProvider(agentConfig).writeMeter(meter); + assertThat(result).hasSize(expectedEntries.size()); + assertThat(result).containsExactlyEntriesOf(expectedEntries); } + + + + @Test + void sendEventsWithHttpProvider() { + //test meterNameEventTypeEnabledConfig = false (default) + MockHttpSender mockHttpClient = new MockHttpSender(); + NewRelicHttpClientProviderImpl httpProvider = new NewRelicHttpClientProviderImpl( + httpConfig, mockHttpClient, registry.config().namingConvention()); + + NewRelicMeterRegistry registry = new NewRelicMeterRegistry(httpConfig, httpProvider, clock); + + registry.gauge("my.gauge", 1d); + Gauge gauge = registry.find("my.gauge").gauge(); + + httpProvider.sendEvents(httpProvider.writeGauge(gauge)); + assertThat(new String(mockHttpClient.getRequest().getEntity())) + .contains("{\"eventType\":\"MicrometerSample\",\"value\":1,\"metricName\":\"myGauge\",\"metricType\":\"GAUGE\"}"); + + //test meterNameEventTypeEnabledConfig = true + mockHttpClient = new MockHttpSender(); + httpProvider = new NewRelicHttpClientProviderImpl( + meterNameEventTypeEnabledConfig, mockHttpClient, registry.config().namingConvention()); + + registry.gauge("my.gauge2", 1d); + gauge = registry.find("my.gauge2").gauge(); + + httpProvider.sendEvents(httpProvider.writeGauge(gauge)); + + assertThat(new String(mockHttpClient.getRequest().getEntity())) + .contains("{\"eventType\":\"myGauge2\",\"value\":1}"); + } + @Test - void publish() { - MockHttpSender mockHttpSender = new MockHttpSender(); - NewRelicMeterRegistry registry = new NewRelicMeterRegistry(config, clock, new NamedThreadFactory("new-relic-test"), mockHttpSender); + void sendEventsWithAgentProvider() { + //test meterNameEventTypeEnabledConfig = false (default) + MockNewRelicAgent mockNewRelicAgent = new MockNewRelicAgent(); + NewRelicAgentClientProviderImpl agentProvider = new NewRelicAgentClientProviderImpl( + agentConfig, mockNewRelicAgent, registry.config().namingConvention()); + + NewRelicMeterRegistry registry = new NewRelicMeterRegistry(agentConfig, agentProvider, clock); registry.gauge("my.gauge", 1d); Gauge gauge = registry.find("my.gauge").gauge(); + + agentProvider.sendEvents(gauge.getId(), agentProvider.writeGauge(gauge)); + + assertThat(((MockNewRelicInsights)mockNewRelicAgent.getInsights()).getInsightData().getEventType()).isEqualTo("MicrometerSample"); + Map result = ((MockNewRelicInsights)mockNewRelicAgent.getInsights()).getInsightData().getAttributes(); + assertThat(result).hasSize(3); + + //test meterNameEventTypeEnabledConfig = true + mockNewRelicAgent = new MockNewRelicAgent(); + agentProvider = new NewRelicAgentClientProviderImpl( + meterNameEventTypeEnabledConfig, mockNewRelicAgent, registry.config().namingConvention()); + + registry.gauge("my.gauge2", 1d); + gauge = registry.find("my.gauge2").gauge(); + + agentProvider.sendEvents(gauge.getId(), agentProvider.writeGauge(gauge)); + + assertThat(((MockNewRelicInsights)mockNewRelicAgent.getInsights()).getInsightData().getEventType()).isEqualTo("myGauge2"); + result = ((MockNewRelicInsights)mockNewRelicAgent.getInsights()).getInsightData().getAttributes(); + assertThat(result).hasSize(1); + } + + @Test + void publishWithHttpClientProvider() { + //test meterNameEventTypeEnabledConfig = false (default) + MockHttpSender mockHttpClient = new MockHttpSender(); + NewRelicHttpClientProviderImpl httpProvider = new NewRelicHttpClientProviderImpl( + httpConfig, mockHttpClient, registry.config().namingConvention()); + + NewRelicMeterRegistry registry = new NewRelicMeterRegistry(httpConfig, httpProvider, clock); + + registry.gauge("my.gauge", Tags.of("theTag", "theValue"), 1d); + Gauge gauge = registry.find("my.gauge").gauge(); assertThat(gauge).isNotNull(); - registry.publish(); + registry.gauge("other.gauge", 2d); + Gauge other = registry.find("other.gauge").gauge(); + assertThat(other).isNotNull(); - assertThat(new String(mockHttpSender.getRequest().getEntity())) - .contains("{\"eventType\":\"MicrometerSample\",\"value\":1,\"metricName\":\"myGauge\",\"metricType\":\"GAUGE\"}"); + registry.publish(); + + //should send a batch of multiple in one json payload + assertThat(new String(mockHttpClient.getRequest().getEntity())) + .contains("[{\"eventType\":\"MicrometerSample\",\"value\":2,\"metricName\":\"otherGauge\",\"metricType\":\"GAUGE\"}," + + "{\"eventType\":\"MicrometerSample\",\"value\":1,\"metricName\":\"myGauge\",\"metricType\":\"GAUGE\",\"theTag\":\"theValue\"}]"); } @Test - void configMissingEventType() { + void publishWithAgentClientProvider() { + //test meterNameEventTypeEnabledConfig = false (default) + MockNewRelicAgent mockNewRelicAgent = new MockNewRelicAgent(); + NewRelicAgentClientProviderImpl agentProvider = new NewRelicAgentClientProviderImpl( + agentConfig, mockNewRelicAgent, registry.config().namingConvention()); + + NewRelicMeterRegistry registry = new NewRelicMeterRegistry(agentConfig, agentProvider, clock); + + registry.gauge("my.gauge", Tags.of("theTag", "theValue"), 1d); + Gauge gauge = registry.find("my.gauge").gauge(); + assertThat(gauge).isNotNull(); + + registry.gauge("other.gauge", 2d); + Gauge other = registry.find("other.gauge").gauge(); + assertThat(other).isNotNull(); + + registry.publish(); + + //should delegate to the Agent one at a time + assertThat(((MockNewRelicInsights)mockNewRelicAgent.getInsights()).getInsightData().getEventType()).isEqualTo("MicrometerSample"); + Map result = ((MockNewRelicInsights)mockNewRelicAgent.getInsights()).getInsightData().getAttributes(); + assertThat(result).hasSize(4); + } + + @Test + void failsConfigMissingClientProvider() { + NewRelicConfig config = new NewRelicConfig() { + @Override + public String get(String key) { + return null; + } + }; + + assertThatThrownBy(() -> new NewRelicMeterRegistry(config, null, clock)) + .isExactlyInstanceOf(MissingRequiredConfigurationException.class) + .hasMessageContaining("clientProvider"); + } + + @Test + void failsConfigHttpMissingEventType() { NewRelicConfig config = new NewRelicConfig() { @Override public String eventType() { @@ -285,13 +598,41 @@ public String get(String key) { } }; - assertThatThrownBy(() -> new NewRelicMeterRegistry(config, clock)) - .isExactlyInstanceOf(MissingRequiredConfigurationException.class) - .hasMessageContaining("eventType"); + assertThatThrownBy(() -> getHttpClientProvider(config)) + .isExactlyInstanceOf(MissingRequiredConfigurationException.class) + .hasMessageContaining("eventType"); + } + + @Test + void succeedsConfigHttpMissingEventType() { + NewRelicConfig config = new NewRelicConfig() { + @Override + public boolean meterNameEventTypeEnabled() { + return true; + } + @Override + public String eventType() { + return ""; + } + @Override + public String accountId() { + return "accountId"; + } + @Override + public String apiKey() { + return "apiKey"; + } + @Override + public String get(String key) { + return null; + } + }; + + assertThat( getHttpClientProvider(config) ).isNotNull(); } @Test - void configMissingAccountId() { + void failsConfigHttpMissingAccountId() { NewRelicConfig config = new NewRelicConfig() { @Override public String eventType() { @@ -306,14 +647,14 @@ public String get(String key) { return null; } }; - - assertThatThrownBy(() -> new NewRelicMeterRegistry(config, clock)) - .isExactlyInstanceOf(MissingRequiredConfigurationException.class) - .hasMessageContaining("accountId"); + + assertThatThrownBy(() -> getHttpClientProvider(config)) + .isExactlyInstanceOf(MissingRequiredConfigurationException.class) + .hasMessageContaining("accountId"); } @Test - void configMissingApiKey() { + void failsConfigHttpMissingApiKey() { NewRelicConfig config = new NewRelicConfig() { @Override public String eventType() { @@ -332,14 +673,14 @@ public String get(String key) { return null; } }; - - assertThatThrownBy(() -> new NewRelicMeterRegistry(config, clock)) - .isExactlyInstanceOf(MissingRequiredConfigurationException.class) - .hasMessageContaining("apiKey"); + + assertThatThrownBy(() -> getHttpClientProvider(config)) + .isExactlyInstanceOf(MissingRequiredConfigurationException.class) + .hasMessageContaining("apiKey"); } @Test - void configMissingUri() { + void failsConfigHttpMissingUri() { NewRelicConfig config = new NewRelicConfig() { @Override public String eventType() { @@ -362,18 +703,56 @@ public String get(String key) { return null; } }; + + assertThatThrownBy(() -> getHttpClientProvider(config)) + .isExactlyInstanceOf(MissingRequiredConfigurationException.class) + .hasMessageContaining("uri"); + } + + @Test + void failsConfigAgentMissingEventType() { + NewRelicConfig config = new NewRelicConfig() { + @Override + public String eventType() { + return ""; + } + @Override + public String get(String key) { + return null; + } + }; + + assertThatThrownBy(() -> getAgentClientProvider(config)) + .isExactlyInstanceOf(MissingRequiredConfigurationException.class) + .hasMessageContaining("eventType"); + } + + @Test + void succeedsConfigAgentMissingEventType() { + NewRelicConfig config = new NewRelicConfig() { + @Override + public boolean meterNameEventTypeEnabled() { + return true; + } + @Override + public String eventType() { + return ""; + } + @Override + public String get(String key) { + return null; + } + }; - assertThatThrownBy(() -> new NewRelicMeterRegistry(config, clock)) - .isExactlyInstanceOf(MissingRequiredConfigurationException.class) - .hasMessageContaining("uri"); + assertThat( getAgentClientProvider(config) ).isNotNull(); } - static class MockHttpSender implements HttpSender { + class MockHttpSender implements HttpSender { private Request request; @Override - public Response send(Request request) { + public Response send(Request request) throws Throwable { this.request = request; return new Response(200, "body"); } @@ -383,4 +762,158 @@ public Request getRequest() { } } + class MockClientProvider implements NewRelicClientProvider { + + @Override + public void publish(NewRelicMeterRegistry meterRegistry) { + //No-op + } + + @Override + public Object writeFunctionTimer(FunctionTimer timer) { + //No-op + return null; + } + + @Override + public Object writeTimer(Timer timer) { + //No-op + return null; + } + + @Override + public Object writeSummary(DistributionSummary summary) { + //No-op + return null; + } + + @Override + public Object writeLongTaskTimer(LongTaskTimer timer) { + //No-op + return null; + } + + @Override + public Object writeTimeGauge(TimeGauge gauge) { + //No-op + return null; + } + + @Override + public Object writeGauge(Gauge gauge) { + //No-op + return null; + } + + @Override + public Object writeCounter(Counter counter) { + //No-op + return null; + } + + @Override + public Object writeFunctionCounter(FunctionCounter counter) { + //No-op + return null; + } + + @Override + public Object writeMeter(Meter meter) { + //No-op + return null; + } + + } + + class MockNewRelicAgent implements Agent { + + private final Insights insights; + + public MockNewRelicAgent() { + this.insights = new MockNewRelicInsights(); + } + + @Override + public Config getConfig() { + //No-op + return null; + } + + @Override + public Insights getInsights() { + return insights; + } + + public class MockNewRelicInsights implements Insights { + + private InsightData insightData; + + public InsightData getInsightData() { + return insightData; + } + + @Override + public void recordCustomEvent(String eventType, Map attributes) { + this.insightData = new InsightData(eventType, attributes); + } + + public void setInsightData(InsightData insightData) { + this.insightData = insightData; + } + + class InsightData { + private String eventType; + private Map attributes; + + public InsightData(String eventType, Map attributes) { + this.eventType = eventType; + this.attributes = attributes; + } + + public String getEventType() { + return eventType; + } + public Map getAttributes() { + return attributes; + } + } + + } + + @Override + public Logger getLogger() { + //No-op + return null; + } + + @Override + public MetricAggregator getMetricAggregator() { + //No-op + return null; + } + + @Override + public TracedMethod getTracedMethod() { + //No-op + return null; + } + + @Override + public Transaction getTransaction() { + //No-op + return null; + } + + @Override + public Map getLinkingMetadata() { + //No-op + return null; + } + + @Override + public TraceMetadata getTraceMetadata() { + //No-op + return null; + } + } }