diff --git a/pom.xml b/pom.xml
index fe6877794..064803690 100644
--- a/pom.xml
+++ b/pom.xml
@@ -9,7 +9,7 @@
org.whispersystems.textsecure
TextSecureServer
- 0.7
+ 0.8
diff --git a/src/main/java/org/whispersystems/textsecuregcm/WhisperServerConfiguration.java b/src/main/java/org/whispersystems/textsecuregcm/WhisperServerConfiguration.java
index 24f3d8e73..61c09af19 100644
--- a/src/main/java/org/whispersystems/textsecuregcm/WhisperServerConfiguration.java
+++ b/src/main/java/org/whispersystems/textsecuregcm/WhisperServerConfiguration.java
@@ -20,11 +20,11 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import com.yammer.dropwizard.config.Configuration;
import com.yammer.dropwizard.db.DatabaseConfiguration;
import org.whispersystems.textsecuregcm.configuration.ApnConfiguration;
-import org.whispersystems.textsecuregcm.configuration.DataDogConfiguration;
import org.whispersystems.textsecuregcm.configuration.FederationConfiguration;
import org.whispersystems.textsecuregcm.configuration.GcmConfiguration;
import org.whispersystems.textsecuregcm.configuration.GraphiteConfiguration;
import org.whispersystems.textsecuregcm.configuration.MemcacheConfiguration;
+import org.whispersystems.textsecuregcm.configuration.MetricsConfiguration;
import org.whispersystems.textsecuregcm.configuration.NexmoConfiguration;
import org.whispersystems.textsecuregcm.configuration.RateLimitsConfiguration;
import org.whispersystems.textsecuregcm.configuration.RedisConfiguration;
@@ -87,7 +87,7 @@ public class WhisperServerConfiguration extends Configuration {
@Valid
@JsonProperty
- private DataDogConfiguration datadog = new DataDogConfiguration();
+ private MetricsConfiguration metrics = new MetricsConfiguration();
@Valid
@JsonProperty
@@ -141,7 +141,7 @@ public class WhisperServerConfiguration extends Configuration {
return graphite;
}
- public DataDogConfiguration getDataDogConfiguration() {
- return datadog;
+ public MetricsConfiguration getMetricsConfiguration() {
+ return metrics;
}
}
diff --git a/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java b/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java
index 300918e92..a481a73b7 100644
--- a/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java
+++ b/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java
@@ -24,6 +24,7 @@ import com.yammer.dropwizard.config.HttpConfiguration;
import com.yammer.dropwizard.db.DatabaseConfiguration;
import com.yammer.dropwizard.jdbi.DBIFactory;
import com.yammer.dropwizard.migrations.MigrationsBundle;
+import com.yammer.metrics.Metrics;
import com.yammer.metrics.core.Clock;
import com.yammer.metrics.core.MetricPredicate;
import com.yammer.metrics.reporting.DatadogReporter;
@@ -48,6 +49,7 @@ import org.whispersystems.textsecuregcm.federation.FederatedPeer;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.mappers.IOExceptionMapper;
import org.whispersystems.textsecuregcm.mappers.RateLimitExceededExceptionMapper;
+import org.whispersystems.textsecuregcm.metrics.JsonMetricsReporter;
import org.whispersystems.textsecuregcm.providers.MemcacheHealthCheck;
import org.whispersystems.textsecuregcm.providers.MemcachedClientFactory;
import org.whispersystems.textsecuregcm.providers.RedisClientFactory;
@@ -168,11 +170,11 @@ public class WhisperServerService extends Service {
config.getGraphiteConfiguration().getPort());
}
- if (config.getDataDogConfiguration().isEnabled()) {
- new DatadogReporter.Builder().withApiKey(config.getDataDogConfiguration().getApiKey())
- .withVmMetricsEnabled(true)
- .build()
- .start(15, TimeUnit.SECONDS);
+ if (config.getMetricsConfiguration().isEnabled()) {
+ new JsonMetricsReporter("textsecure", Metrics.defaultRegistry(),
+ config.getMetricsConfiguration().getToken(),
+ config.getMetricsConfiguration().getHost())
+ .start(60, TimeUnit.SECONDS);
}
}
diff --git a/src/main/java/org/whispersystems/textsecuregcm/configuration/MetricsConfiguration.java b/src/main/java/org/whispersystems/textsecuregcm/configuration/MetricsConfiguration.java
new file mode 100644
index 000000000..7594473b0
--- /dev/null
+++ b/src/main/java/org/whispersystems/textsecuregcm/configuration/MetricsConfiguration.java
@@ -0,0 +1,27 @@
+package org.whispersystems.textsecuregcm.configuration;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class MetricsConfiguration {
+
+ @JsonProperty
+ private String token;
+
+ @JsonProperty
+ private String host;
+
+ @JsonProperty
+ private boolean enabled = false;
+
+ public String getHost() {
+ return host;
+ }
+
+ public String getToken() {
+ return token;
+ }
+
+ public boolean isEnabled() {
+ return enabled && token != null && host != null;
+ }
+}
diff --git a/src/main/java/org/whispersystems/textsecuregcm/metrics/JsonMetricsReporter.java b/src/main/java/org/whispersystems/textsecuregcm/metrics/JsonMetricsReporter.java
new file mode 100644
index 000000000..32189ed17
--- /dev/null
+++ b/src/main/java/org/whispersystems/textsecuregcm/metrics/JsonMetricsReporter.java
@@ -0,0 +1,324 @@
+package org.whispersystems.textsecuregcm.metrics;
+
+import com.fasterxml.jackson.core.JsonEncoding;
+import com.fasterxml.jackson.core.JsonFactory;
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.yammer.metrics.core.Clock;
+import com.yammer.metrics.core.Counter;
+import com.yammer.metrics.core.Gauge;
+import com.yammer.metrics.core.Histogram;
+import com.yammer.metrics.core.Metered;
+import com.yammer.metrics.core.Metric;
+import com.yammer.metrics.core.MetricName;
+import com.yammer.metrics.core.MetricProcessor;
+import com.yammer.metrics.core.MetricsRegistry;
+import com.yammer.metrics.core.Sampling;
+import com.yammer.metrics.core.Summarizable;
+import com.yammer.metrics.core.Timer;
+import com.yammer.metrics.core.VirtualMachineMetrics;
+import com.yammer.metrics.reporting.AbstractPollingReporter;
+import com.yammer.metrics.stats.Snapshot;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.InetAddress;
+import java.net.URL;
+import java.net.UnknownHostException;
+import java.util.Map;
+import java.util.SortedMap;
+import java.util.concurrent.TimeUnit;
+import java.util.regex.Pattern;
+
+/**
+ * Adapted from MetricsServlet.
+ */
+public class JsonMetricsReporter extends AbstractPollingReporter implements MetricProcessor {
+ private final Clock clock = Clock.defaultClock();
+ private final VirtualMachineMetrics vm = VirtualMachineMetrics.getInstance();
+ private final String service;
+ private final MetricsRegistry registry;
+ private final JsonFactory factory = new JsonFactory();
+
+ private final String table;
+ private final String sunnylabsHost;
+ private final String host;
+
+ private final boolean includeVMMetrics;
+
+ public JsonMetricsReporter(String service, MetricsRegistry registry, String token, String sunnylabsHost) throws UnknownHostException {
+ this(service, registry, token, sunnylabsHost, true);
+ }
+
+ public JsonMetricsReporter(String service, MetricsRegistry registry, String token, String sunnylabsHost, boolean includeVMMetrics) throws UnknownHostException {
+ super(registry, "jsonmetrics-reporter");
+ this.service = service;
+ this.registry = registry;
+ this.table = token;
+ this.sunnylabsHost = sunnylabsHost;
+ this.host = InetAddress.getLocalHost().getHostName();
+ this.includeVMMetrics = includeVMMetrics;
+ }
+
+ @Override
+ public void run() {
+ try {
+ URL http = new URL("https", sunnylabsHost, 443, "/report/metrics?t=" + table + "&h=" + host);
+ System.out.println("Reporting started to: " + http);
+ HttpURLConnection urlc = (HttpURLConnection) http.openConnection();
+ urlc.setDoOutput(true);
+ urlc.addRequestProperty("Content-Type", "application/json");
+ OutputStream outputStream = urlc.getOutputStream();
+ writeJson(outputStream);
+ outputStream.close();
+ System.out.println("Reporting complete: " + urlc.getResponseCode());
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ static final class Context {
+ final boolean showFullSamples;
+ final JsonGenerator json;
+
+ Context(JsonGenerator json, boolean showFullSamples) {
+ this.json = json;
+ this.showFullSamples = showFullSamples;
+ }
+ }
+
+ public void writeJson(OutputStream out) throws IOException {
+ final JsonGenerator json = factory.createGenerator(out, JsonEncoding.UTF8);
+ json.writeStartObject();
+ if (includeVMMetrics) {
+ writeVmMetrics(json);
+ }
+ writeRegularMetrics(json, false);
+ json.writeEndObject();
+ json.close();
+ }
+
+ private void writeVmMetrics(JsonGenerator json) throws IOException {
+ json.writeFieldName(service);
+ json.writeStartObject();
+ json.writeFieldName("jvm");
+ json.writeStartObject();
+ {
+ json.writeFieldName("vm");
+ json.writeStartObject();
+ {
+ json.writeStringField("name", vm.name());
+ json.writeStringField("version", vm.version());
+ }
+ json.writeEndObject();
+
+ json.writeFieldName("memory");
+ json.writeStartObject();
+ {
+ json.writeNumberField("totalInit", vm.totalInit());
+ json.writeNumberField("totalUsed", vm.totalUsed());
+ json.writeNumberField("totalMax", vm.totalMax());
+ json.writeNumberField("totalCommitted", vm.totalCommitted());
+
+ json.writeNumberField("heapInit", vm.heapInit());
+ json.writeNumberField("heapUsed", vm.heapUsed());
+ json.writeNumberField("heapMax", vm.heapMax());
+ json.writeNumberField("heapCommitted", vm.heapCommitted());
+
+ json.writeNumberField("heap_usage", vm.heapUsage());
+ json.writeNumberField("non_heap_usage", vm.nonHeapUsage());
+ json.writeFieldName("memory_pool_usages");
+ json.writeStartObject();
+ {
+ for (Map.Entry pool : vm.memoryPoolUsage().entrySet()) {
+ json.writeNumberField(pool.getKey(), pool.getValue());
+ }
+ }
+ json.writeEndObject();
+ }
+ json.writeEndObject();
+
+ final Map bufferPoolStats = vm.getBufferPoolStats();
+ if (!bufferPoolStats.isEmpty()) {
+ json.writeFieldName("buffers");
+ json.writeStartObject();
+ {
+ json.writeFieldName("direct");
+ json.writeStartObject();
+ {
+ json.writeNumberField("count", bufferPoolStats.get("direct").getCount());
+ json.writeNumberField("memoryUsed", bufferPoolStats.get("direct").getMemoryUsed());
+ json.writeNumberField("totalCapacity", bufferPoolStats.get("direct").getTotalCapacity());
+ }
+ json.writeEndObject();
+
+ json.writeFieldName("mapped");
+ json.writeStartObject();
+ {
+ json.writeNumberField("count", bufferPoolStats.get("mapped").getCount());
+ json.writeNumberField("memoryUsed", bufferPoolStats.get("mapped").getMemoryUsed());
+ json.writeNumberField("totalCapacity", bufferPoolStats.get("mapped").getTotalCapacity());
+ }
+ json.writeEndObject();
+ }
+ json.writeEndObject();
+ }
+
+
+ json.writeNumberField("daemon_thread_count", vm.daemonThreadCount());
+ json.writeNumberField("thread_count", vm.threadCount());
+ json.writeNumberField("current_time", clock.time());
+ json.writeNumberField("uptime", vm.uptime());
+ json.writeNumberField("fd_usage", vm.fileDescriptorUsage());
+
+ json.writeFieldName("thread-states");
+ json.writeStartObject();
+ {
+ for (Map.Entry entry : vm.threadStatePercentages()
+ .entrySet()) {
+ json.writeNumberField(entry.getKey().toString().toLowerCase(),
+ entry.getValue());
+ }
+ }
+ json.writeEndObject();
+
+ json.writeFieldName("garbage-collectors");
+ json.writeStartObject();
+ {
+ for (Map.Entry entry : vm.garbageCollectors()
+ .entrySet()) {
+ json.writeFieldName(entry.getKey());
+ json.writeStartObject();
+ {
+ final VirtualMachineMetrics.GarbageCollectorStats gc = entry.getValue();
+ json.writeNumberField("runs", gc.getRuns());
+ json.writeNumberField("time", gc.getTime(TimeUnit.MILLISECONDS));
+ }
+ json.writeEndObject();
+ }
+ }
+ json.writeEndObject();
+ }
+ json.writeEndObject();
+ json.writeEndObject();
+ }
+
+ public void writeRegularMetrics(JsonGenerator json, boolean showFullSamples) throws IOException {
+ for (Map.Entry> entry : registry.groupedMetrics().entrySet()) {
+ for (Map.Entry subEntry : entry.getValue().entrySet()) {
+ json.writeFieldName(sanitize(subEntry.getKey()));
+ try {
+ subEntry.getValue()
+ .processWith(this,
+ subEntry.getKey(),
+ new Context(json, showFullSamples));
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ }
+
+ @Override
+ public void processHistogram(MetricName name, Histogram histogram, Context context) throws Exception {
+ final JsonGenerator json = context.json;
+ json.writeStartObject();
+ {
+ json.writeNumberField("count", histogram.count());
+ writeSummarizable(histogram, json);
+ writeSampling(histogram, json);
+ if (context.showFullSamples) {
+ json.writeObjectField("values", histogram.getSnapshot().getValues());
+ }
+ histogram.clear();
+ }
+ json.writeEndObject();
+ }
+
+ @Override
+ public void processCounter(MetricName name, Counter counter, Context context) throws Exception {
+ final JsonGenerator json = context.json;
+ json.writeNumber(counter.count());
+ }
+
+ @Override
+ public void processGauge(MetricName name, Gauge> gauge, Context context) throws Exception {
+ final JsonGenerator json = context.json;
+ json.writeObject(evaluateGauge(gauge));
+ }
+
+ @Override
+ public void processMeter(MetricName name, Metered meter, Context context) throws Exception {
+ final JsonGenerator json = context.json;
+ json.writeStartObject();
+ {
+ writeMeteredFields(meter, json);
+ }
+ json.writeEndObject();
+ }
+
+ @Override
+ public void processTimer(MetricName name, Timer timer, Context context) throws Exception {
+ final JsonGenerator json = context.json;
+ json.writeStartObject();
+ {
+ json.writeFieldName("duration");
+ json.writeStartObject();
+ {
+ json.writeStringField("unit", timer.durationUnit().toString().toLowerCase());
+ writeSummarizable(timer, json);
+ writeSampling(timer, json);
+ if (context.showFullSamples) {
+ json.writeObjectField("values", timer.getSnapshot().getValues());
+ }
+ }
+ json.writeEndObject();
+
+ json.writeFieldName("rate");
+ json.writeStartObject();
+ {
+ writeMeteredFields(timer, json);
+ }
+ json.writeEndObject();
+ }
+ json.writeEndObject();
+ }
+
+ private static Object evaluateGauge(Gauge> gauge) {
+ try {
+ return gauge.value();
+ } catch (RuntimeException e) {
+ return "error reading gauge: " + e.getMessage();
+ }
+ }
+
+ private static void writeSummarizable(Summarizable metric, JsonGenerator json) throws IOException {
+ json.writeNumberField("min", metric.min());
+ json.writeNumberField("max", metric.max());
+ json.writeNumberField("mean", metric.mean());
+ }
+
+ private static void writeSampling(Sampling metric, JsonGenerator json) throws IOException {
+ final Snapshot snapshot = metric.getSnapshot();
+ json.writeNumberField("median", snapshot.getMedian());
+ json.writeNumberField("p75", snapshot.get75thPercentile());
+ json.writeNumberField("p95", snapshot.get95thPercentile());
+ json.writeNumberField("p99", snapshot.get99thPercentile());
+ json.writeNumberField("p999", snapshot.get999thPercentile());
+ }
+
+ private static void writeMeteredFields(Metered metered, JsonGenerator json) throws IOException {
+ json.writeNumberField("count", metered.count());
+ json.writeNumberField("mean", metered.meanRate());
+ json.writeNumberField("m1", metered.oneMinuteRate());
+ json.writeNumberField("m5", metered.fiveMinuteRate());
+ json.writeNumberField("m15", metered.fifteenMinuteRate());
+ }
+
+ private static final Pattern SIMPLE_NAMES = Pattern.compile("[^a-zA-Z0-9_.\\-~]");
+
+ private String sanitize(MetricName metricName) {
+ return SIMPLE_NAMES.matcher(metricName.getGroup() + "." + metricName.getName()).replaceAll("_");
+ }
+
+}
\ No newline at end of file