/* * 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.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; import org.osgi.framework.BundleContext; import org.osgi.framework.ServiceReference; import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Deactivate; import org.osgi.util.tracker.ServiceTracker; import org.osgi.util.tracker.ServiceTrackerCustomizer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @Component(service = {InventoryPrinter.class, Servlet.class}, property = { "felix.webconsole.label=slingmetrics", "felix.webconsole.title=Metrics", "felix.webconsole.category=Sling", InventoryPrinter.FORMAT + "=TEXT", InventoryPrinter.FORMAT + "=JSON", InventoryPrinter.TITLE + "=Sling Metrics", InventoryPrinter.NAME + "=slingmetrics" } ) public class MetricWebConsolePlugin 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 */ public static final String METRIC_REGISTRY_NAME = "name"; private final Logger log = LoggerFactory.getLogger(getClass()); private BundleContext context; private ServiceTracker<MetricRegistry, MetricRegistry> tracker; private ConcurrentMap<ServiceReference, MetricRegistry> registries = new ConcurrentHashMap<>(); 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<>(context, MetricRegistry.class, this); tracker.open(); } @Deactivate private void deactivate(BundleContext context){ tracker.close(); } //~--------------------------------------------< InventoryPrinter > @Override public void print(PrintWriter printWriter, Format format, boolean isZip) { if (format == Format.TEXT) { MetricRegistry registry = getConsolidatedRegistry(); ConsoleReporter reporter = ConsoleReporter.forRegistry(registry) .outputTo(new PrintStream(new WriterOutputStream(printWriter))) .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(); } } //~---------------------------------------------< ServiceTracker > @Override public MetricRegistry addingService(ServiceReference<MetricRegistry> serviceReference) { MetricRegistry registry = context.getService(serviceReference); registries.put(serviceReference, registry); return registry; } @Override public void modifiedService(ServiceReference<MetricRegistry> serviceReference, MetricRegistry registry) { registries.put(serviceReference, registry); } @Override public void removedService(ServiceReference<MetricRegistry> serviceReference, MetricRegistry registry) { 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' id='data-meters'>"); 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' id='data-timers'>"); 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' id='data-histograms'>"); 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' id='data-counters'>"); 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'>Gauges</div>"); pw.println("<table class='nicetable' id='data-gauges'>"); 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 > MetricRegistry getConsolidatedRegistry() { MetricRegistry registry = new MetricRegistry(); for (Map.Entry<ServiceReference, MetricRegistry> registryEntry : registries.entrySet()){ String metricRegistryName = (String) registryEntry.getKey().getProperty(METRIC_REGISTRY_NAME); for (Map.Entry<String, Metric> metricEntry : registryEntry.getValue().getMetrics().entrySet()){ String metricName = metricEntry.getKey(); try{ if (metricRegistryName != null){ metricName = metricRegistryName + ":" + metricName; } registry.register(metricName, metricEntry.getValue()); }catch (IllegalArgumentException ex){ log.warn("Duplicate Metric name found {}", metricName, ex); } } } 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; } } }