SLING-5693 - Add support for exporting MetricRegistry data as JSON
git-svn-id: https://svn.apache.org/repos/asf/sling/trunk@1740952 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/pom.xml b/pom.xml
index c76858f..7b9fef1 100644
--- a/pom.xml
+++ b/pom.xml
@@ -113,6 +113,12 @@
<version>2.2</version>
<optional>true</optional>
</dependency>
+ <dependency>
+ <groupId>org.json</groupId>
+ <artifactId>json</artifactId>
+ <version>20090211</version>
+ <optional>true</optional>
+ </dependency>
<dependency>
<groupId>org.hamcrest</groupId>
diff --git a/src/main/java/org/apache/sling/commons/metrics/internal/JSONReporter.java b/src/main/java/org/apache/sling/commons/metrics/internal/JSONReporter.java
new file mode 100644
index 0000000..9c631f7
--- /dev/null
+++ b/src/main/java/org/apache/sling/commons/metrics/internal/JSONReporter.java
@@ -0,0 +1,298 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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
+ *
+ * http://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 org.apache.sling.commons.metrics.internal;
+
+import java.io.Closeable;
+import java.io.PrintStream;
+import java.io.PrintWriter;
+import java.util.Locale;
+import java.util.Map;
+import java.util.SortedMap;
+import java.util.concurrent.TimeUnit;
+
+import com.codahale.metrics.ConsoleReporter;
+import com.codahale.metrics.Counter;
+import com.codahale.metrics.Gauge;
+import com.codahale.metrics.Histogram;
+import com.codahale.metrics.Meter;
+import com.codahale.metrics.MetricFilter;
+import com.codahale.metrics.MetricRegistry;
+import com.codahale.metrics.Reporter;
+import com.codahale.metrics.Snapshot;
+import com.codahale.metrics.Timer;
+import org.json.JSONException;
+import org.json.JSONWriter;
+
+class JSONReporter implements Reporter, Closeable {
+
+ public static JSONReporter.Builder forRegistry(MetricRegistry registry) {
+ return new JSONReporter.Builder(registry);
+ }
+
+ public static class Builder {
+ private final MetricRegistry registry;
+ private PrintStream output;
+ private MetricFilter filter;
+ private TimeUnit rateUnit;
+ private TimeUnit durationUnit;
+
+ private Builder(MetricRegistry registry) {
+ this.registry = registry;
+ this.output = System.out;
+ this.filter = MetricFilter.ALL;
+ this.rateUnit = TimeUnit.SECONDS;
+ this.durationUnit = TimeUnit.MILLISECONDS;
+ }
+
+ /**
+ * Write to the given {@link PrintStream}.
+ *
+ * @param output a {@link PrintStream} instance.
+ * @return {@code this}
+ */
+ public Builder outputTo(PrintStream output) {
+ this.output = output;
+ 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;
+ }
+
+ /**
+ * Builds a {@link ConsoleReporter} with the given properties.
+ *
+ * @return a {@link ConsoleReporter}
+ */
+ public JSONReporter build() {
+ return new JSONReporter(registry,
+ output,
+ rateUnit,
+ durationUnit,
+ filter);
+ }
+ }
+
+ private final MetricRegistry registry;
+ private final MetricFilter filter;
+ private final double durationFactor;
+ private final String durationUnit;
+ private final double rateFactor;
+ private final String rateUnit;
+ private final JSONWriter json;
+ private final PrintWriter pw;
+
+ private JSONReporter(MetricRegistry registry,
+ PrintStream output, TimeUnit rateUnit, TimeUnit durationUnit, MetricFilter filter){
+ this.registry = registry;
+ this.filter = filter;
+ this.pw = new PrintWriter(output);
+ this.json = new JSONWriter(pw);
+ this.rateFactor = rateUnit.toSeconds(1);
+ this.rateUnit = calculateRateUnit(rateUnit);
+ this.durationFactor = 1.0 / durationUnit.toNanos(1);
+ this.durationUnit = durationUnit.toString().toLowerCase(Locale.US);
+ }
+
+ public void report() {
+ try {
+ report(registry.getGauges(filter),
+ registry.getCounters(filter),
+ registry.getHistograms(filter),
+ registry.getMeters(filter),
+ registry.getTimers(filter));
+ } catch (JSONException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public void close(){
+ pw.close();
+ }
+
+ private void report(SortedMap<String, Gauge> gauges, SortedMap<String, Counter> counters,
+ SortedMap<String, Histogram> histograms, SortedMap<String, Meter> meters,
+ SortedMap<String, Timer> timers) throws JSONException {
+ json.object();
+ if (!gauges.isEmpty()) {
+ json.key("guages").object();
+ for (Map.Entry<String, Gauge> entry : gauges.entrySet()) {
+ printGauge(entry);
+ }
+ json.endObject();
+ }
+
+ if (!counters.isEmpty()) {
+ json.key("counters").object();
+ for (Map.Entry<String, Counter> entry : counters.entrySet()) {
+ printCounter(entry);
+ }
+ json.endObject();
+ }
+
+ if (!histograms.isEmpty()) {
+ json.key("histograms").object();
+ for (Map.Entry<String, Histogram> entry : histograms.entrySet()) {
+ printHistogram(entry);
+ }
+ json.endObject();
+ }
+
+ if (!meters.isEmpty()) {
+ json.key("meters").object();
+ for (Map.Entry<String, Meter> entry : meters.entrySet()) {
+ printMeter(entry);
+ }
+ json.endObject();
+ }
+
+ if (!timers.isEmpty()) {
+ json.key("timers").object();
+ for (Map.Entry<String, Timer> entry : timers.entrySet()) {
+ printTimer(entry);
+ }
+ json.endObject();
+ }
+
+ json.endObject();
+
+ }
+
+ private void printTimer(Map.Entry<String, Timer> e) throws JSONException {
+ json.key(e.getKey()).object();
+ Timer timer = e.getValue();
+ Snapshot snapshot = timer.getSnapshot();
+
+ json.key("count").value(timer.getCount());
+ json.key("max").value(snapshot.getMax() * durationFactor);
+ json.key("mean").value(snapshot.getMean() * durationFactor);
+ json.key("min").value(snapshot.getMin() * durationFactor);
+
+ json.key("p50").value(snapshot.getMedian() * durationFactor);
+ json.key("p75").value(snapshot.get75thPercentile() * durationFactor);
+ json.key("p95").value(snapshot.get95thPercentile() * durationFactor);
+ json.key("p98").value(snapshot.get98thPercentile() * durationFactor);
+ json.key("p99").value(snapshot.get99thPercentile() * durationFactor);
+ json.key("p999").value(snapshot.get999thPercentile() * durationFactor);
+
+ json.key("stddev").value(snapshot.getStdDev() * durationFactor);
+ json.key("m1_rate").value(timer.getOneMinuteRate() * rateFactor);
+ json.key("m5_rate").value(timer.getFiveMinuteRate() * rateFactor);
+ json.key("m15_rate").value(timer.getFifteenMinuteRate() * rateFactor);
+ json.key("mean_rate").value(timer.getMeanRate() * rateFactor);
+ json.key("duration_units").value(durationUnit);
+ json.key("rate_units").value(rateUnit);
+
+ json.endObject();
+ }
+
+ private void printMeter(Map.Entry<String, Meter> e) throws JSONException {
+ json.key(e.getKey()).object();
+ Meter meter = e.getValue();
+ json.key("count").value(e.getValue().getCount());
+ json.key("m1_rate").value(meter.getOneMinuteRate() * rateFactor);
+ json.key("m5_rate").value(meter.getFiveMinuteRate() * rateFactor);
+ json.key("m15_rate").value(meter.getFifteenMinuteRate() * rateFactor);
+ json.key("mean_rate").value(meter.getMeanRate() * rateFactor);
+ json.key("units").value(rateUnit);
+ json.endObject();
+ }
+
+ private void printHistogram(Map.Entry<String, Histogram> e) throws JSONException {
+ json.key(e.getKey()).object();
+ json.key("count").value(e.getValue().getCount());
+
+ Snapshot snapshot = e.getValue().getSnapshot();
+ json.key("max").value(snapshot.getMax());
+ json.key("mean").value(snapshot.getMean());
+ json.key("min").value(snapshot.getMin());
+ json.key("p50").value(snapshot.getMedian());
+ json.key("p75").value(snapshot.get75thPercentile());
+ json.key("p95").value(snapshot.get95thPercentile());
+ json.key("p98").value(snapshot.get98thPercentile());
+ json.key("p99").value(snapshot.get99thPercentile());
+ json.key("p999").value(snapshot.get999thPercentile());
+ json.key("stddev").value(snapshot.getStdDev());
+
+ json.endObject();
+ }
+
+ private void printCounter(Map.Entry<String, Counter> e) throws JSONException {
+ json.key(e.getKey()).object();
+ json.key("count").value(e.getValue().getCount());
+ json.endObject();
+ }
+
+ private void printGauge(Map.Entry<String, Gauge> e) throws JSONException {
+ json.key(e.getKey()).object();
+ Object v = e.getValue().getValue();
+ json.key("value").value(jsonSafeValue(v));
+ json.endObject();
+ }
+
+ private static Object jsonSafeValue(Object v){
+ //Json does not allow NaN or infinite doubles. So take care of that
+ if (v instanceof Number){
+ if (v instanceof Double){
+ Double d = (Double) v;
+ if (d.isInfinite() || d.isNaN()){
+ return d.toString();
+ }
+ }
+ }
+ return v;
+ }
+
+ private static String calculateRateUnit(TimeUnit unit) {
+ final String s = unit.toString().toLowerCase(Locale.US);
+ return s.substring(0, s.length() - 1);
+ }
+
+}
diff --git a/src/main/java/org/apache/sling/commons/metrics/internal/MetricWebConsolePlugin.java b/src/main/java/org/apache/sling/commons/metrics/internal/MetricWebConsolePlugin.java
index f5261c5..c33eb18 100644
--- a/src/main/java/org/apache/sling/commons/metrics/internal/MetricWebConsolePlugin.java
+++ b/src/main/java/org/apache/sling/commons/metrics/internal/MetricWebConsolePlugin.java
@@ -66,7 +66,7 @@
@Property(name = "felix.webconsole.label", value = "slingmetrics"),
@Property(name = "felix.webconsole.title", value = "Metrics"),
@Property(name = "felix.webconsole.category", value = "Sling"),
- @Property(name = InventoryPrinter.FORMAT, value = {"TEXT" }),
+ @Property(name = InventoryPrinter.FORMAT, value = {"TEXT" , "JSON"}),
@Property(name = InventoryPrinter.NAME, value = "slingmetrics"),
@Property(name = InventoryPrinter.TITLE, value = "Sling Metrics"),
@Property(name = InventoryPrinter.WEBCONSOLE, boolValue = true)
@@ -114,6 +114,13 @@
.build();
reporter.report();
reporter.close();
+ } else if (format == Format.JSON) {
+ MetricRegistry registry = getConsolidatedRegistry();
+ JSONReporter reporter = JSONReporter.forRegistry(registry)
+ .outputTo(new PrintStream(new WriterOutputStream(printWriter)))
+ .build();
+ reporter.report();
+ reporter.close();
}
}
diff --git a/src/test/java/org/apache/sling/commons/metrics/internal/JSONReporterTest.java b/src/test/java/org/apache/sling/commons/metrics/internal/JSONReporterTest.java
new file mode 100644
index 0000000..1f13872
--- /dev/null
+++ b/src/test/java/org/apache/sling/commons/metrics/internal/JSONReporterTest.java
@@ -0,0 +1,87 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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
+ *
+ * http://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 org.apache.sling.commons.metrics.internal;
+
+import java.io.PrintStream;
+import java.io.StringWriter;
+
+import com.codahale.metrics.Gauge;
+import com.codahale.metrics.JvmAttributeGaugeSet;
+import com.codahale.metrics.MetricRegistry;
+import org.apache.commons.io.output.WriterOutputStream;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.junit.Test;
+
+import static org.junit.Assert.assertTrue;
+
+public class JSONReporterTest {
+
+ @Test
+ public void jsonOutput() throws Exception {
+ MetricRegistry registry = new MetricRegistry();
+ registry.meter("test1").mark(5);
+ registry.timer("test2").time().close();
+ registry.histogram("test3").update(743);
+ registry.counter("test4").inc(9);
+ registry.registerAll(new JvmAttributeGaugeSet());
+
+ JSONObject json = getJSON(registry);
+
+ assertTrue(json.has("meters"));
+ assertTrue(json.has("guages"));
+ assertTrue(json.has("timers"));
+ assertTrue(json.has("counters"));
+ assertTrue(json.has("histograms"));
+ assertTrue(json.has("meters"));
+
+ assertTrue(json.getJSONObject("meters").has("test1"));
+ assertTrue(json.getJSONObject("timers").has("test2"));
+ assertTrue(json.getJSONObject("counters").has("test4"));
+ assertTrue(json.getJSONObject("histograms").has("test3"));
+ }
+
+ @Test
+ public void nan_value() throws Exception{
+ MetricRegistry registry = new MetricRegistry();
+
+ registry.register("test", new Gauge<Double>() {
+ @Override
+ public Double getValue() {
+ return Double.POSITIVE_INFINITY;
+ }
+ });
+
+
+ JSONObject json = getJSON(registry);
+ assertTrue(json.getJSONObject("guages").has("test"));
+ }
+
+ private static JSONObject getJSON(MetricRegistry registry) throws JSONException {
+ StringWriter sw = new StringWriter();
+ JSONReporter reporter = JSONReporter.forRegistry(registry)
+ .outputTo(new PrintStream(new WriterOutputStream(sw)))
+ .build();
+ reporter.report();
+ reporter.close();
+ return new JSONObject(sw.toString());
+ }
+
+}
\ No newline at end of file