Author: chetanm Date: Tue Jan 5 15:21:34 2016 New Revision: 1723093 URL: http://svn.apache.org/viewvc?rev=1723093&view=rev Log: SLING-4080 - API to capture/measure application-level metrics
Add a web console plugin to display all metrics in a nice table Modified: sling/whiteboard/chetanm/metrics/src/main/java/org/apache/sling/metrics/internal/MetricPrinter.java Modified: sling/whiteboard/chetanm/metrics/src/main/java/org/apache/sling/metrics/internal/MetricPrinter.java URL: http://svn.apache.org/viewvc/sling/whiteboard/chetanm/metrics/src/main/java/org/apache/sling/metrics/internal/MetricPrinter.java?rev=1723093&r1=1723092&r2=1723093&view=diff ============================================================================== --- sling/whiteboard/chetanm/metrics/src/main/java/org/apache/sling/metrics/internal/MetricPrinter.java (original) +++ sling/whiteboard/chetanm/metrics/src/main/java/org/apache/sling/metrics/internal/MetricPrinter.java Tue Jan 5 15:21:34 2016 @@ -19,15 +19,31 @@ package org.apache.sling.metrics.internal; +import java.io.IOException; import java.io.PrintStream; import java.io.PrintWriter; +import java.util.Collections; +import java.util.Locale; import java.util.Map; +import java.util.SortedMap; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.TimeUnit; + +import javax.servlet.Servlet; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; 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.Metric; import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.Snapshot; +import com.codahale.metrics.Timer; import org.apache.commons.io.output.WriterOutputStream; import org.apache.felix.inventory.Format; import org.apache.felix.inventory.InventoryPrinter; @@ -45,14 +61,18 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; @Component -@Service(value = InventoryPrinter.class) +@Service(value = {InventoryPrinter.class, Servlet.class}) @Properties({ + @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.NAME, value = "slingmetrics"), @Property(name = InventoryPrinter.TITLE, value = "Sling Metrics"), @Property(name = InventoryPrinter.WEBCONSOLE, boolValue = true) }) -public class MetricPrinter implements InventoryPrinter, ServiceTrackerCustomizer<MetricRegistry, MetricRegistry>{ +public class MetricPrinter extends HttpServlet implements + InventoryPrinter, ServiceTrackerCustomizer<MetricRegistry, MetricRegistry>{ /** * Service property name which stores the MetricRegistry name as a given OSGi * ServiceRegistry might have multiple instances of MetricRegistry @@ -64,9 +84,16 @@ public class MetricPrinter implements In private ConcurrentMap<ServiceReference, MetricRegistry> registries = new ConcurrentHashMap<ServiceReference, MetricRegistry>(); + private TimeUnit rateUnit = TimeUnit.SECONDS; + private TimeUnit durationUnit = TimeUnit.MILLISECONDS; + private Map<String, TimeUnit> specificDurationUnits = Collections.emptyMap(); + private Map<String, TimeUnit> specificRateUnits = Collections.emptyMap(); + private MetricTimeUnits timeUnit; + @Activate private void activate(BundleContext context){ this.context = context; + this.timeUnit = new MetricTimeUnits(rateUnit, durationUnit, specificRateUnits, specificDurationUnits); tracker = new ServiceTracker<MetricRegistry, MetricRegistry>(context, MetricRegistry.class, this); tracker.open(); } @@ -110,6 +137,291 @@ public class MetricPrinter implements In registries.remove(serviceReference); } + //~----------------------------------------------< Servlet > + + @Override + protected void doGet(final HttpServletRequest req, final HttpServletResponse resp) throws IOException { + final PrintWriter pw = resp.getWriter(); + MetricRegistry registry = getConsolidatedRegistry(); + + appendMetricStatus(pw, registry); + addCounterDetails(pw, registry.getCounters()); + addGaugeDetails(pw, registry.getGauges()); + addMeterDetails(pw, registry.getMeters()); + addTimerDetails(pw, registry.getTimers()); + addHistogramDetails(pw, registry.getHistograms()); + } + + private static void appendMetricStatus(PrintWriter pw, MetricRegistry registry) { + pw.printf( + "<p class='statline'>Metrics: %d gauges, %d timers, %d meters, %d counters, %d histograms</p>%n", + registry.getGauges().size(), + registry.getTimers().size(), + registry.getMeters().size(), + registry.getCounters().size(), + registry.getHistograms().size()); + } + + private void addMeterDetails(PrintWriter pw, SortedMap<String, Meter> meters) { + if (meters.isEmpty()) { + return; + } + pw.println("<br>"); + pw.println("<div class='table'>"); + pw.println("<div class='ui-widget-header ui-corner-top buttonGroup'>Meters</div>"); + pw.println("<table class='nicetable'>"); + pw.println("<thead>"); + pw.println("<tr>"); + pw.println("<th class='header'>Name</th>"); + pw.println("<th class='header'>Count</th>"); + pw.println("<th class='header'>Mean Rate</th>"); + pw.println("<th class='header'>OneMinuteRate</th>"); + pw.println("<th class='header'>FiveMinuteRate</th>"); + pw.println("<th class='header'>FifteenMinuteRate</ th>"); + pw.println("<th>RateUnit</th>"); + pw.println("</tr>"); + pw.println("</thead>"); + pw.println("<tbody>"); + + String rowClass = "odd"; + for (Map.Entry<String, Meter> e : meters.entrySet()) { + Meter m = e.getValue(); + String name = e.getKey(); + + double rateFactor = timeUnit.rateFor(name).toSeconds(1); + String rateUnit = "events/" + calculateRateUnit(timeUnit.rateFor(name)); + pw.printf("<tr class='%s ui-state-default'>%n", rowClass); + + pw.printf("<td>%s</td>", name); + pw.printf("<td>%d</td>", m.getCount()); + pw.printf("<td>%f</td>", m.getMeanRate() * rateFactor); + pw.printf("<td>%f</td>", m.getOneMinuteRate() * rateFactor); + pw.printf("<td>%f</td>", m.getFiveMinuteRate() * rateFactor); + pw.printf("<td>%f</td>", m.getFifteenMinuteRate() * rateFactor); + pw.printf("<td>%s</td>", rateUnit); + + pw.println("</tr>"); + rowClass = "odd".equals(rowClass) ? "even" : "odd"; + } + + pw.println("</tbody>"); + pw.println("</table>"); + pw.println("</div>"); + } + + private void addTimerDetails(PrintWriter pw, SortedMap<String, Timer> timers) { + if (timers.isEmpty()) { + return; + } + + pw.println("<br>"); + pw.println("<div class='table'>"); + pw.println("<div class='ui-widget-header ui-corner-top buttonGroup'>Timers</div>"); + pw.println("<table class='nicetable'>"); + pw.println("<thead>"); + pw.println("<tr>"); + pw.println("<th class='header'>Name</th>"); + pw.println("<th class='header'>Count</th>"); + pw.println("<th class='header'>Mean Rate</th>"); + pw.println("<th class='header'>1 min rate</th>"); + pw.println("<th class='header'>5 mins rate</th>"); + pw.println("<th class='header'>15 mins rate</th>"); + pw.println("<th class='header'>50%</th>"); + pw.println("<th class='header'>Min</th>"); + pw.println("<th class='header'>Max</th>"); + pw.println("<th class='header'>Mean</th>"); + pw.println("<th class='header'>StdDev</th>"); + pw.println("<th class='header'>75%</th>"); + pw.println("<th class='header'>95%</th>"); + pw.println("<th class='header'>98%</th>"); + pw.println("<th class='header'>99%</th>"); + pw.println("<th class='header'>999%</th>"); + pw.println("<th>Rate Unit</th>"); + pw.println("<th>Duration Unit</th>"); + pw.println("</tr>"); + pw.println("</thead>"); + pw.println("<tbody>"); + + String rowClass = "odd"; + for (Map.Entry<String, Timer> e : timers.entrySet()) { + Timer t = e.getValue(); + Snapshot s = t.getSnapshot(); + String name = e.getKey(); + + double rateFactor = timeUnit.rateFor(name).toSeconds(1); + String rateUnit = "events/" + calculateRateUnit(timeUnit.rateFor(name)); + + double durationFactor = 1.0 / timeUnit.durationFor(name).toNanos(1); + String durationUnit = timeUnit.durationFor(name).toString().toLowerCase(Locale.US); + + pw.printf("<tr class='%s ui-state-default'>%n", rowClass); + + pw.printf("<td>%s</td>", name); + pw.printf("<td>%d</td>", t.getCount()); + pw.printf("<td>%f</td>", t.getMeanRate() * rateFactor); + pw.printf("<td>%f</td>", t.getOneMinuteRate() * rateFactor); + pw.printf("<td>%f</td>", t.getFiveMinuteRate() * rateFactor); + pw.printf("<td>%f</td>", t.getFifteenMinuteRate() * rateFactor); + + pw.printf("<td>%f</td>", s.getMedian() * durationFactor); + pw.printf("<td>%f</td>", s.getMin() * durationFactor); + pw.printf("<td>%f</td>", s.getMax() * durationFactor); + pw.printf("<td>%f</td>", s.getMean() * durationFactor); + pw.printf("<td>%f</td>", s.getStdDev() * durationFactor); + + pw.printf("<td>%f</td>", s.get75thPercentile() * durationFactor); + pw.printf("<td>%f</td>", s.get95thPercentile() * durationFactor); + pw.printf("<td>%f</td>", s.get98thPercentile() * durationFactor); + pw.printf("<td>%f</td>", s.get99thPercentile() * durationFactor); + pw.printf("<td>%f</td>", s.get999thPercentile() * durationFactor); + + pw.printf("<td>%s</td>", rateUnit); + pw.printf("<td>%s</td>", durationUnit); + + pw.println("</tr>"); + rowClass = "odd".equals(rowClass) ? "even" : "odd"; + } + + pw.println("</tbody>"); + pw.println("</table>"); + pw.println("</div>"); + } + + private void addHistogramDetails(PrintWriter pw, SortedMap<String, Histogram> histograms) { + if (histograms.isEmpty()) { + return; + } + + pw.println("<br>"); + pw.println("<div class='table'>"); + pw.println("<div class='ui-widget-header ui-corner-top buttonGroup'>Histograms</div>"); + pw.println("<table class='nicetable'>"); + pw.println("<thead>"); + pw.println("<tr>"); + pw.println("<th class='header'>Name</th>"); + pw.println("<th class='header'>Count</th>"); + pw.println("<th class='header'>50%</th>"); + pw.println("<th class='header'>Min</th>"); + pw.println("<th class='header'>Max</th>"); + pw.println("<th class='header'>Mean</th>"); + pw.println("<th class='header'>StdDev</th>"); + pw.println("<th class='header'>75%</th>"); + pw.println("<th class='header'>95%</th>"); + pw.println("<th class='header'>98%</th>"); + pw.println("<th class='header'>99%</th>"); + pw.println("<th class='header'>999%</th>"); + pw.println("<th>Duration Unit</th>"); + pw.println("</tr>"); + pw.println("</thead>"); + pw.println("<tbody>"); + + String rowClass = "odd"; + for (Map.Entry<String, Histogram> e : histograms.entrySet()) { + Histogram h = e.getValue(); + Snapshot s = h.getSnapshot(); + String name = e.getKey(); + + double durationFactor = 1.0 / timeUnit.durationFor(name).toNanos(1); + String durationUnit = timeUnit.durationFor(name).toString().toLowerCase(Locale.US); + pw.printf("<tr class='%s ui-state-default'>%n", rowClass); + + pw.printf("<td>%s</td>", name); + pw.printf("<td>%d</td>", h.getCount()); + pw.printf("<td>%f</td>", s.getMedian() * durationFactor); + pw.printf("<td>%f</td>", s.getMin() * durationFactor); + pw.printf("<td>%f</td>", s.getMax() * durationFactor); + pw.printf("<td>%f</td>", s.getMean() * durationFactor); + pw.printf("<td>%f</td>", s.getStdDev() * durationFactor); + + pw.printf("<td>%f</td>", s.get75thPercentile() * durationFactor); + pw.printf("<td>%f</td>", s.get95thPercentile() * durationFactor); + pw.printf("<td>%f</td>", s.get98thPercentile() * durationFactor); + pw.printf("<td>%f</td>", s.get99thPercentile() * durationFactor); + pw.printf("<td>%f</td>", s.get999thPercentile() * durationFactor); + + pw.printf("<td>%s</td>", durationUnit); + + pw.println("</tr>"); + rowClass = "odd".equals(rowClass) ? "even" : "odd"; + } + + pw.println("</tbody>"); + pw.println("</table>"); + pw.println("</div>"); + } + + private void addCounterDetails(PrintWriter pw, SortedMap<String, Counter> counters) { + if (counters.isEmpty()) { + return; + } + pw.println("<br>"); + pw.println("<div class='table'>"); + pw.println("<div class='ui-widget-header ui-corner-top buttonGroup'>Counters</div>"); + pw.println("<table class='nicetable'>"); + pw.println("<thead>"); + pw.println("<tr>"); + pw.println("<th class='header'>Name</th>"); + pw.println("<th class='header'>Count</th>"); + pw.println("</tr>"); + pw.println("</thead>"); + pw.println("<tbody>"); + + String rowClass = "odd"; + for (Map.Entry<String, Counter> e : counters.entrySet()) { + Counter c = e.getValue(); + String name = e.getKey(); + + pw.printf("<tr class='%s ui-state-default'>%n", rowClass); + + pw.printf("<td>%s</td>", name); + pw.printf("<td>%d</td>", c.getCount()); + + pw.println("</tr>"); + rowClass = "odd".equals(rowClass) ? "even" : "odd"; + } + + pw.println("</tbody>"); + pw.println("</table>"); + pw.println("</div>"); + } + + private void addGaugeDetails(PrintWriter pw, SortedMap<String, Gauge> gauges) { + if (gauges.isEmpty()) { + return; + } + + pw.println("<br>"); + pw.println("<div class='table'>"); + pw.println("<div class='ui-widget-header ui-corner-top buttonGroup'>Guages</div>"); + pw.println("<table class='nicetable'>"); + pw.println("<thead>"); + pw.println("<tr>"); + pw.println("<th class='header'>Name</th>"); + pw.println("<th class='header'>Value</th>"); + pw.println("</tr>"); + pw.println("</thead>"); + pw.println("<tbody>"); + + String rowClass = "odd"; + for (Map.Entry<String, Gauge> e : gauges.entrySet()) { + Gauge g = e.getValue(); + String name = e.getKey(); + + pw.printf("<tr class='%s ui-state-default'>%n", rowClass); + + pw.printf("<td>%s</td>", name); + pw.printf("<td>%s</td>", g.getValue()); + + pw.println("</tr>"); + rowClass = "odd".equals(rowClass) ? "even" : "odd"; + } + + pw.println("</tbody>"); + pw.println("</table>"); + pw.println("</div>"); + } + + //~----------------------------------------------< internal > private MetricRegistry getConsolidatedRegistry() { @@ -130,4 +442,34 @@ public class MetricPrinter implements In } return registry; } + + private static String calculateRateUnit(TimeUnit unit) { + final String s = unit.toString().toLowerCase(Locale.US); + return s.substring(0, s.length() - 1); + } + + private static class MetricTimeUnits { + private final TimeUnit defaultRate; + private final TimeUnit defaultDuration; + private final Map<String, TimeUnit> rateOverrides; + private final Map<String, TimeUnit> durationOverrides; + + MetricTimeUnits(TimeUnit defaultRate, + TimeUnit defaultDuration, + Map<String, TimeUnit> rateOverrides, + Map<String, TimeUnit> durationOverrides) { + this.defaultRate = defaultRate; + this.defaultDuration = defaultDuration; + this.rateOverrides = rateOverrides; + this.durationOverrides = durationOverrides; + } + + public TimeUnit durationFor(String name) { + return durationOverrides.containsKey(name) ? durationOverrides.get(name) : defaultDuration; + } + + public TimeUnit rateFor(String name) { + return rateOverrides.containsKey(name) ? rateOverrides.get(name) : defaultRate; + } + } }