Skip to content

Instantly share code, notes, and snippets.

@jonnywray
Last active December 20, 2018 20:54
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save jonnywray/1e999823cdb12c6d9d12 to your computer and use it in GitHub Desktop.
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
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);
}
}
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");
}
}
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);
}
}
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);
}
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