Last active
December 20, 2018 20:54
-
-
Save jonnywray/1e999823cdb12c6d9d12 to your computer and use it in GitHub Desktop.
Generic JSON reporter for Dropwizard metrics (https://dropwizard.github.io/metrics/3.1.0/) for sending JSON representation to arbitrary HTTP endpoint
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
import com.codahale.metrics.MetricFilter; | |
import com.codahale.metrics.MetricRegistry; | |
import com.fasterxml.jackson.databind.SerializationFeature; | |
import java.io.PrintStream; | |
import java.util.Map; | |
import java.util.Set; | |
import java.util.concurrent.TimeUnit; | |
/** | |
* Simple JSON sender that just prints to an output stream with the default being stdout | |
* | |
* @author Jonny Wray | |
*/ | |
public class ConsoleJsonWriter extends JsonWriter { | |
private PrintStream output; | |
private boolean prettyPrint = true; | |
/** | |
* Construct a writer that prints to stdout | |
*/ | |
public ConsoleJsonWriter(){ | |
this(System.out); | |
} | |
/** | |
* Construct a writer that prints to the specified stream | |
*/ | |
public ConsoleJsonWriter(PrintStream stream){ | |
output = stream; | |
} | |
/** | |
* Set a specific value for pretty printing over-riding the default of true | |
*/ | |
public void setPrettyPrint(boolean prettyPrint) { | |
this.prettyPrint = prettyPrint; | |
} | |
@Override | |
public void configure(TimeUnit rateUnit, TimeUnit durationUnit, boolean showSamples, MetricFilter filter, Set<String> tags, Map<String, String> attributes){ | |
super.configure(rateUnit, durationUnit, showSamples, filter, tags, attributes); | |
if(prettyPrint) { | |
getObjectMapper().enable(SerializationFeature.INDENT_OUTPUT); | |
} | |
} | |
@Override | |
public void writeData(MetricRegistry registry){ | |
String json = constructJson(registry); | |
output.println(json); | |
} | |
} |
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
import java.io.IOException; | |
import java.net.MalformedURLException; | |
import java.net.URI; | |
import java.net.URISyntaxException; | |
import java.net.URL; | |
import com.codahale.metrics.MetricRegistry; | |
import org.apache.http.client.methods.CloseableHttpResponse; | |
import org.apache.http.client.methods.HttpPost; | |
import org.apache.http.entity.StringEntity; | |
import org.apache.http.impl.client.CloseableHttpClient; | |
import org.apache.http.impl.client.HttpClients; | |
import org.slf4j.Logger; | |
import org.slf4j.LoggerFactory; | |
/** | |
* JsonSender that POSTs the JSON to a HTTP endpoint | |
* | |
* @author Jonny Wray | |
*/ | |
public class HttpJsonWriter extends JsonWriter { | |
private static final Logger LOGGER = LoggerFactory.getLogger(HttpJsonWriter.class); | |
private URI uri; | |
/** | |
* Construct the JSON writer via specification of a hostname and port | |
* | |
* @param hostname the hostname to POST the JSON to | |
* @param port the port the HTTP server is listening on | |
*/ | |
public HttpJsonWriter(String hostname, int port){ | |
try { | |
this.uri = new URL("http", hostname, port, "/").toURI(); | |
} | |
catch (MalformedURLException | URISyntaxException e){ | |
throw new IllegalArgumentException("Cannot construct valid URI", e); | |
} | |
} | |
/** | |
* POST JSON data to the configured HTTP endpoint. The POST is performed asynchronously allowing this method | |
* to return immediately. The response or any errors are logged at the appropriate level. | |
*/ | |
public void writeData(MetricRegistry registry) { | |
String json = constructJson(registry); | |
LOGGER.debug("Sending metrics to URI {} with content {}", uri, json); | |
CloseableHttpClient client = HttpClients.createMinimal(); | |
try { | |
HttpPost request = new HttpPost(uri); | |
request.setEntity(new StringEntity(json)); | |
request.setHeader("Content-Type", "application/json"); | |
CloseableHttpResponse response = client.execute(request); | |
response.close(); | |
} | |
catch (IOException | RuntimeException e){ | |
LOGGER.error("Error sending metric to URI {}", uri, e); | |
} | |
finally { | |
try{ | |
client.close(); | |
} | |
catch (IOException e){ | |
LOGGER.error("Unable to close HTTP connection to metrics server", e); | |
} | |
} | |
LOGGER.debug("Sending metrics done"); | |
} | |
} |
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
import com.codahale.metrics.*; | |
import com.codahale.metrics.Timer; | |
import org.slf4j.Logger; | |
import org.slf4j.LoggerFactory; | |
import java.util.*; | |
import java.util.concurrent.TimeUnit; | |
/** | |
* {@link Reporter} that sends JSON files to a specific endpoint which is defined by the implementation | |
* of {@link JsonWriter} that is used. | |
* | |
* @author Jonny Wray | |
*/ | |
public class JsonReporter extends ScheduledReporter { | |
public static final class Builder { | |
private final MetricRegistry registry; | |
private Map<String, String> attributes; | |
private Set<String> tags; | |
private TimeUnit rateUnit; | |
private TimeUnit durationUnit; | |
private MetricFilter filter; | |
private Builder(MetricRegistry registry) { | |
this.registry = registry; | |
this.attributes = new HashMap<>(); | |
this.tags = new HashSet<>(); | |
this.rateUnit = TimeUnit.SECONDS; | |
this.durationUnit = TimeUnit.MILLISECONDS; | |
this.filter = MetricFilter.ALL; | |
} | |
/** | |
* Add these attributes, or (name,value) pairs, to all metrics. | |
* | |
* @param attributes a map containing attributes common to all metrics | |
* @return {@code this} | |
*/ | |
public Builder withAttributes(Map<String, String> attributes) { | |
this.attributes = Collections.unmodifiableMap(attributes); | |
return this; | |
} | |
/** | |
* Add these tags, or a set of strings, to all metrics. | |
* | |
* @param tags a set containing tags common to all metrics | |
* @return {@code this} | |
*/ | |
public Builder withTags(Set<String> tags) { | |
this.tags = Collections.unmodifiableSet(tags); | |
return this; | |
} | |
/** | |
* Convert rates to the given time unit. | |
* | |
* @param rateUnit a unit of time | |
* @return {@code this} | |
*/ | |
public Builder convertRatesTo(TimeUnit rateUnit) { | |
this.rateUnit = rateUnit; | |
return this; | |
} | |
/** | |
* Convert durations to the given time unit. | |
* | |
* @param durationUnit a unit of time | |
* @return {@code this} | |
*/ | |
public Builder convertDurationsTo(TimeUnit durationUnit) { | |
this.durationUnit = durationUnit; | |
return this; | |
} | |
/** | |
* Only report metrics which match the given filter. | |
* | |
* @param filter a {@link MetricFilter} | |
* @return {@code this} | |
*/ | |
public Builder filter(MetricFilter filter) { | |
this.filter = filter; | |
return this; | |
} | |
public JsonReporter build(final JsonWriter sender) { | |
sender.configure(rateUnit, durationUnit, false, filter, tags, attributes); | |
return new JsonReporter(registry, sender, rateUnit, durationUnit, filter); | |
} | |
} | |
private static final Logger LOGGER = LoggerFactory.getLogger(JsonReporter.class); | |
private final JsonWriter sender; | |
private final MetricRegistry registry; | |
private JsonReporter(final MetricRegistry registry, final JsonWriter sender, | |
final TimeUnit rateUnit, final TimeUnit durationUnit, final MetricFilter filter) { | |
super(registry, "json-reporter", filter, rateUnit, durationUnit); | |
this.sender = sender; | |
this.registry = registry; | |
} | |
public static Builder forRegistry(MetricRegistry registry) { | |
return new Builder(registry); | |
} | |
@Override | |
public void report(final SortedMap<String, Gauge> gauges, final SortedMap<String, Counter> counters, | |
final SortedMap<String, Histogram> histograms, final SortedMap<String, Meter> meters, final SortedMap<String, Timer> timers) { | |
LOGGER.debug("Writing registry data"); | |
sender.writeData(registry); | |
} | |
} |
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
import com.codahale.metrics.MetricFilter; | |
import com.codahale.metrics.MetricRegistry; | |
import com.fasterxml.jackson.annotation.JsonInclude; | |
import com.fasterxml.jackson.databind.ObjectMapper; | |
import org.slf4j.Logger; | |
import org.slf4j.LoggerFactory; | |
import java.io.IOException; | |
import java.util.*; | |
import java.util.concurrent.TimeUnit; | |
/** | |
* Abstract class for any specific implementation of a sender of JSON to a specific location. | |
* | |
* @author Jonny Wray | |
*/ | |
public abstract class JsonWriter { | |
private static final Logger LOGGER = LoggerFactory.getLogger(JsonWriter.class); | |
private ObjectMapper objectMapper = new ObjectMapper(); | |
public ObjectMapper getObjectMapper() { | |
return objectMapper; | |
} | |
/** | |
* Configure the JsonWriter by registering a {@link com.fasterxml.jackson.databind.Module} with | |
* the Jackson {@link ObjectMapper} that performs serialization of the {@link MetricRegistry} | |
*/ | |
public void configure(TimeUnit rateUnit, TimeUnit durationUnit, boolean showSamples, MetricFilter filter, Set<String> tags, Map<String, String> attributes){ | |
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY); | |
MetricsRegistryModule module = new MetricsRegistryModule(rateUnit, durationUnit, showSamples, filter, tags, attributes); | |
objectMapper.registerModule(module); | |
} | |
public String constructJson(MetricRegistry registry){ | |
try { | |
return objectMapper.writeValueAsString(registry); | |
} | |
catch (IOException e){ | |
LOGGER.error("Error creating string representation of JSON", e); | |
return null; | |
} | |
} | |
/** | |
* Implement this method to write the registry data, and associated tags and attributes, to the desired destination. | |
*/ | |
public abstract void writeData(MetricRegistry registry); | |
} |
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
import com.codahale.metrics.*; | |
import com.codahale.metrics.Timer; | |
import com.fasterxml.jackson.core.JsonGenerator; | |
import com.fasterxml.jackson.core.Version; | |
import com.fasterxml.jackson.databind.JsonSerializer; | |
import com.fasterxml.jackson.databind.Module; | |
import com.fasterxml.jackson.databind.SerializerProvider; | |
import com.fasterxml.jackson.databind.module.SimpleSerializers; | |
import com.fasterxml.jackson.databind.ser.std.StdSerializer; | |
import java.io.IOException; | |
import java.util.*; | |
import java.util.concurrent.TimeUnit; | |
/** | |
* This is a modification of the {@see com.codahale.metrics.json.MetricModule} class to eliminate dots in field | |
* names as they aren't supported by Elastic Search, and add support for tags and attributes. | |
* | |
* @author Jonny Wray | |
*/ | |
public class MetricsRegistryModule extends Module { | |
static final Version VERSION = new Version(1, 0, 2, "", "metrics.reporter", "metrics-json"); | |
private final TimeUnit rateUnit; | |
private final TimeUnit durationUnit; | |
private final boolean showSamples; | |
private final MetricFilter filter; | |
private final Set<String> tags = new HashSet<>(); | |
private final Map<String, String> attributes = new HashMap<>(); | |
public MetricsRegistryModule(TimeUnit rateUnit, TimeUnit durationUnit, boolean showSamples) { | |
this(rateUnit, durationUnit, showSamples, MetricFilter.ALL, new HashSet<>(), new HashMap<>()); | |
} | |
public MetricsRegistryModule(TimeUnit rateUnit, TimeUnit durationUnit, boolean showSamples, MetricFilter filter, Set<String> tags, Map<String, String> attributes) { | |
this.rateUnit = rateUnit; | |
this.durationUnit = durationUnit; | |
this.showSamples = showSamples; | |
this.filter = filter; | |
this.tags.addAll(tags); | |
this.attributes.putAll(attributes); | |
} | |
public String getModuleName() { | |
return "metrics"; | |
} | |
public Version version() { | |
return VERSION; | |
} | |
public void setupModule(SetupContext context) { | |
context.addSerializers(new SimpleSerializers(Arrays.asList(new JsonSerializer[]{new MetricsRegistryModule.GaugeSerializer(), | |
new MetricsRegistryModule.CounterSerializer(), new MetricsRegistryModule.HistogramSerializer(this.showSamples), new MetricsRegistryModule.MeterSerializer(this.rateUnit), | |
new MetricsRegistryModule.TimerSerializer(this.rateUnit, this.durationUnit, this.showSamples), new MetricsRegistryModule.MetricRegistrySerializer(this.filter, this.tags, this.attributes)}))); | |
} | |
private static String calculateRateUnit(TimeUnit unit, String name) { | |
String s = unit.toString().toLowerCase(Locale.US); | |
return name + '/' + s.substring(0, s.length() - 1); | |
} | |
private static class MetricRegistrySerializer extends StdSerializer<MetricRegistry> { | |
private final MetricFilter filter; | |
private final Set<String> tags = new HashSet<>(); | |
private final Map<String, String> attributes = new HashMap<>(); | |
private static class MetricRegistrySerializer extends StdSerializer<MetricRegistry> { | |
private final MetricFilter filter; | |
private final Set<String> tags = new HashSet<>(); | |
private final Map<String, String> attributes = new HashMap<>(); | |
private MetricRegistrySerializer(MetricFilter filter, Set<String> tags, Map<String, String> attributes) { | |
super(MetricRegistry.class); | |
this.filter = filter; | |
this.tags.addAll(tags); | |
this.attributes.putAll(attributes); | |
} | |
public void serialize(MetricRegistry registry, JsonGenerator json, SerializerProvider provider) throws IOException { | |
json.writeStartObject(); | |
if(tags != null && !tags.isEmpty()) { | |
json.writeObjectField("tags", tags); | |
} | |
if(attributes != null && !attributes.isEmpty()){ | |
for(Map.Entry<String, String> attribute: attributes.entrySet()){ | |
json.writeStringField(attribute.getKey(), attribute.getValue()); | |
} | |
} | |
writeMetricMap(json, modify(registry.getGauges(this.filter))); | |
writeMetricMap(json, modify(registry.getCounters(this.filter))); | |
writeMetricMap(json, modify(registry.getHistograms(this.filter))); | |
writeMetricMap(json, modify(registry.getMeters(this.filter))); | |
writeMetricMap(json, modify(registry.getTimers(this.filter))); | |
json.writeEndObject(); | |
} | |
private <T extends Metric> void writeMetricMap(JsonGenerator json, SortedMap<String, T> metrics) throws IOException{ | |
for(Map.Entry<String, T> metric: metrics.entrySet()){ | |
json.writeObjectField(metric.getKey(), metric.getValue()); | |
} | |
} | |
private <T extends Metric> SortedMap<String, T> modify(SortedMap<String, T> original){ | |
SortedMap<String, T> modifiedMap = new TreeMap<>(); | |
for(Map.Entry<String, T> entry: original.entrySet()){ | |
modifiedMap.put(entry.getKey().replace(".", "_"), entry.getValue()); | |
} | |
return modifiedMap; | |
} | |
} | |
private static class TimerSerializer extends StdSerializer<Timer> { | |
private final String rateUnit; | |
private final double rateFactor; | |
private final String durationUnit; | |
private final double durationFactor; | |
private final boolean showSamples; | |
private TimerSerializer(TimeUnit rateUnit, TimeUnit durationUnit, boolean showSamples) { | |
super(Timer.class); | |
this.rateUnit = MetricsRegistryModule.calculateRateUnit(rateUnit, "calls"); | |
this.rateFactor = (double)rateUnit.toSeconds(1L); | |
this.durationUnit = durationUnit.toString().toLowerCase(Locale.US); | |
this.durationFactor = 1.0D / (double)durationUnit.toNanos(1L); | |
this.showSamples = showSamples; | |
} | |
public void serialize(Timer timer, JsonGenerator json, SerializerProvider provider) throws IOException { | |
json.writeStartObject(); | |
Snapshot snapshot = timer.getSnapshot(); | |
json.writeNumberField("count", timer.getCount()); | |
json.writeNumberField("max", (double)snapshot.getMax() * this.durationFactor); | |
json.writeNumberField("mean", snapshot.getMean() * this.durationFactor); | |
json.writeNumberField("min", (double)snapshot.getMin() * this.durationFactor); | |
json.writeNumberField("p50", snapshot.getMedian() * this.durationFactor); | |
json.writeNumberField("p75", snapshot.get75thPercentile() * this.durationFactor); | |
json.writeNumberField("p95", snapshot.get95thPercentile() * this.durationFactor); | |
json.writeNumberField("p98", snapshot.get98thPercentile() * this.durationFactor); | |
json.writeNumberField("p99", snapshot.get99thPercentile() * this.durationFactor); | |
json.writeNumberField("p999", snapshot.get999thPercentile() * this.durationFactor); | |
if(this.showSamples) { | |
long[] values = snapshot.getValues(); | |
double[] scaledValues = new double[values.length]; | |
for(int i = 0; i < values.length; ++i) { | |
scaledValues[i] = (double)values[i] * this.durationFactor; | |
} | |
json.writeObjectField("values", scaledValues); | |
} | |
json.writeNumberField("stddev", snapshot.getStdDev() * this.durationFactor); | |
json.writeNumberField("m15_rate", timer.getFifteenMinuteRate() * this.rateFactor); | |
json.writeNumberField("m1_rate", timer.getOneMinuteRate() * this.rateFactor); | |
json.writeNumberField("m5_rate", timer.getFiveMinuteRate() * this.rateFactor); | |
json.writeNumberField("mean_rate", timer.getMeanRate() * this.rateFactor); | |
json.writeStringField("duration_units", this.durationUnit); | |
json.writeStringField("rate_units", this.rateUnit); | |
json.writeEndObject(); | |
} | |
} | |
private static class MeterSerializer extends StdSerializer<Meter> { | |
private final String rateUnit; | |
private final double rateFactor; | |
public MeterSerializer(TimeUnit rateUnit) { | |
super(Meter.class); | |
this.rateFactor = (double)rateUnit.toSeconds(1L); | |
this.rateUnit = MetricsRegistryModule.calculateRateUnit(rateUnit, "events"); | |
} | |
public void serialize(Meter meter, JsonGenerator json, SerializerProvider provider) throws IOException { | |
json.writeStartObject(); | |
json.writeNumberField("count", meter.getCount()); | |
json.writeNumberField("m15_rate", meter.getFifteenMinuteRate() * this.rateFactor); | |
json.writeNumberField("m1_rate", meter.getOneMinuteRate() * this.rateFactor); | |
json.writeNumberField("m5_rate", meter.getFiveMinuteRate() * this.rateFactor); | |
json.writeNumberField("mean_rate", meter.getMeanRate() * this.rateFactor); | |
json.writeStringField("units", this.rateUnit); | |
json.writeEndObject(); | |
} | |
} | |
private static class HistogramSerializer extends StdSerializer<Histogram> { | |
private final boolean showSamples; | |
private HistogramSerializer(boolean showSamples) { | |
super(Histogram.class); | |
this.showSamples = showSamples; | |
} | |
public void serialize(Histogram histogram, JsonGenerator json, SerializerProvider provider) throws IOException { | |
json.writeStartObject(); | |
Snapshot snapshot = histogram.getSnapshot(); | |
json.writeNumberField("count", histogram.getCount()); | |
json.writeNumberField("max", snapshot.getMax()); | |
json.writeNumberField("mean", snapshot.getMean()); | |
json.writeNumberField("min", snapshot.getMin()); | |
json.writeNumberField("p50", snapshot.getMedian()); | |
json.writeNumberField("p75", snapshot.get75thPercentile()); | |
json.writeNumberField("p95", snapshot.get95thPercentile()); | |
json.writeNumberField("p98", snapshot.get98thPercentile()); | |
json.writeNumberField("p99", snapshot.get99thPercentile()); | |
json.writeNumberField("p999", snapshot.get999thPercentile()); | |
if(this.showSamples) { | |
json.writeObjectField("values", snapshot.getValues()); | |
} | |
json.writeNumberField("stddev", snapshot.getStdDev()); | |
json.writeEndObject(); | |
} | |
} | |
private static class CounterSerializer extends StdSerializer<Counter> { | |
private CounterSerializer() { | |
super(Counter.class); | |
} | |
public void serialize(Counter counter, JsonGenerator json, SerializerProvider provider) throws IOException { | |
json.writeStartObject(); | |
json.writeNumberField("count", counter.getCount()); | |
json.writeEndObject(); | |
} | |
} | |
private static class GaugeSerializer extends StdSerializer<Gauge> { | |
private GaugeSerializer() { | |
super(Gauge.class); | |
} | |
public void serialize(Gauge gauge, JsonGenerator json, SerializerProvider provider) throws IOException { | |
json.writeStartObject(); | |
try { | |
Object value = gauge.getValue(); | |
json.writeObjectField("value", value); | |
} catch (RuntimeException var6) { | |
json.writeObjectField("error", var6.toString()); | |
} | |
json.writeEndObject(); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment