package com.bigdata.counters.render; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.StringWriter; import java.io.Writer; import java.lang.reflect.Field; import java.text.DateFormat; import java.text.Format; import java.util.Arrays; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.regex.Pattern; import org.apache.log4j.Logger; import com.bigdata.counters.CounterSet; import com.bigdata.counters.HistoryInstrument; import com.bigdata.counters.ICounter; import com.bigdata.counters.ICounterNode; import com.bigdata.counters.ICounterSet; import com.bigdata.counters.IHistoryEntry; import com.bigdata.counters.IServiceCounters; import com.bigdata.counters.PeriodEnum; import com.bigdata.counters.query.CSet; import com.bigdata.counters.query.CounterSetSelector; import com.bigdata.counters.query.HistoryTable; import com.bigdata.counters.query.ICounterSelector; import com.bigdata.counters.query.PivotTable; import com.bigdata.counters.query.ReportEnum; import com.bigdata.counters.query.TimestampFormatEnum; import com.bigdata.counters.query.URLQueryModel; import com.bigdata.counters.query.URLQueryParam; import com.bigdata.service.Event; import com.bigdata.service.IEventReportingService; import com.bigdata.util.HTMLUtility; /** * (X)HTML rendering of a {@link CounterSet}. * * @todo UI widgets for regex filters, depth, correlated. * * @todo make documentation available on the counters via click through on their * name. * * @author <a href="mailto:thompsonbry@users.sourceforge.net">Bryan Thompson</a> */ public class XHTMLRenderer implements IRenderer { final static private Logger log = Logger.getLogger(XHTMLRenderer.class); final public static String ps = ICounterSet.pathSeparator; static final private String encoding = "UTF-8"; /* * Note: the page is valid for any of these doctypes. */ // final private DoctypeEnum doctype = DoctypeEnum.html_4_01_strict // final private DoctypeEnum doctype = DoctypeEnum.html_4_01_transitional static final private DoctypeEnum doctype = DoctypeEnum.xhtml_1_0_strict; /** * Describes the state of the controller. */ private final URLQueryModel model; /** * Selects the counters to be rendered. */ private final ICounterSelector counterSelector; /** * @param model * Describes the state of the controller (e.g., as parsed from * the URL query parameters). * @param counterSelector * Selects the counters to be rendered. */ public XHTMLRenderer(final URLQueryModel model, final ICounterSelector counterSelector) { if (model == null) throw new IllegalArgumentException(); if (counterSelector == null) throw new IllegalArgumentException(); this.model = model; this.counterSelector = counterSelector; } /** * @param w * @throws IOException */ @Override public void render(final Writer w) throws IOException { writeXmlDecl(w); writeDocType(w); writeHtml(w); writeHead(w); writeBody(w); w.write("</html\n>"); } protected void writeXmlDecl(final Writer w) throws IOException { w.write("<?xml version=\"1.0\" encoding=\"" + encoding + "\"?>\n"); } /** * * @param w * @throws IOException */ protected void writeDocType(final Writer w) throws IOException { // if(true) return; w.write("<!DOCTYPE html PUBLIC"); w.write(" \""+doctype.publicId()+"\""); w.write(" \""+doctype.systemId()+"\""); w.write(">\n"); } /** The start <code>html</code> tag. */ protected void writeHtml(final Writer w) throws IOException { w.write("<html "); if(doctype.isXML()) { w.write(" xmlns=\"http://www.w3.org/1999/xhtml\" xml:lang=\"en\""); } w.write(" lang=\"en\""); w.write("\n>"); } protected void writeHead(final Writer w) throws IOException { w.write("<head\n>"); writeTitle(w); writeScripts(w); w.write("</head\n>"); } protected void writeTitle(final Writer w) throws IOException { w.write("<title>bigdata(tm) telemetry : "+cdata(model.path)+"</title\n>"); } protected void writeScripts(final Writer w) throws IOException { if (model.flot) { final String s = model.getRequestURL().toString(); w.write("<script\n type=\"text/javascript\" src=\""+s+"jquery.js\"></script\n>"); w.write("<script\n type=\"text/javascript\" src=\""+s+"jquery.flot.js\"></script\n>"); w.write("<!--[if IE]><script type=\"text/javascript\" src=\""+s+"excanvas.pack.js\"></script><![endif]-->"); } } protected void writeBody(final Writer w) throws IOException { w.write("<body\n>"); // Navigate to the node selected by the path. final ICounterNode node = ((CounterSetSelector) counterSelector) .getRoot().getPath(model.path); if(node == null) { /* * Used when the path does not evaluate to anything in the * hierarchy. The generated markup at least lets you choose a parent * from the path. */ w.write("<p>"); w.write("No such counter or counter set: "); writeFullPath(w, model.path); w.write("</p>"); return; } if(node instanceof ICounter) { writeCounter(w, (ICounter<?>) node); } else { switch (model.reportType) { case hierarchy: /* * @todo rewrite to use node.getDepth() + model.depth so that * the relative depth is maintained during navigation. */ writeCounterSet(w, (CounterSet) node, model.depth); break; case correlated: writeHistoryTable(w, counterSelector.selectCounters( model.depth, model.pattern, model.fromTime, model.toTime, model.period, true/* historyRequired */), model.period, model.timestampFormat); break; case pivot: writePivotTable(w, counterSelector.selectCounters(model.depth, model.pattern, model.fromTime, model.toTime, model.period, true/* historyRequired */)); break; case events: // render the time-series chart : FIXME respect fromTime/toTime. writeFlot(w, model.eventReportingService); break; } } /* * This is valid XHTML, but having the image here this makes copy and * paste a little more difficult. */ // doctype.writeValid(w); w.write("</body\n>"); } /** * A clickable trail of the path from the root. * * @deprecated by refactor inside of a rendering object. */ protected void writeFullPath(final Writer w, final String path) throws IOException { writePath(w, path, 0/* root */); } /** * A clickable trail of the path. * * @param rootDepth * The path components will be shown beginning at this depth - * ZERO (0) is the root. * * @deprecated by refactor inside of a rendering object. */ protected void writePath(final Writer w, final String path, final int rootDepth) throws IOException { final String[] a = path.split(ps); if (rootDepth == 0) { // click through to the root of the counter hierarchy w.write("<a href=\"" + model.getRequestURL(new URLQueryParam[] { new URLQueryParam(URLQueryModel.PATH, ps) }) + "\">"); w.write(ps); w.write("</a>"); } // builds up the path query parameter for each split. final StringBuilder sb = new StringBuilder(ps); for (int n = 1; n < a.length; n++) { final String name = a[n]; if (n > 1) { if ((n+1) > rootDepth) { w.write(" "); w.write(ps); } sb.append(ps); } final String prefix = sb.toString(); sb.append(name); if ((n+1) > rootDepth) { if(rootDepth!=0 && n==rootDepth) { w.write("<a href=\"" + model.getRequestURL(new URLQueryParam[] { new URLQueryParam(URLQueryModel.PATH, prefix) }) + "\">"); w.write("..."); w.write("</a>"); w.write(" "+ps); } w.write(" "); w.write("<a href=\"" + model.getRequestURL(new URLQueryParam[] { new URLQueryParam(URLQueryModel.PATH, sb .toString()) }) + "\">"); // current path component. w.write(cdata(name)); w.write("</a>"); } } } // protected void writeCounterNode(Writer w, ICounterNode node) throws // IOException { // // if(node instanceof ICounterSet) { // // writeCounterSet(w, (CounterSet)node); // // } else { // // /* // * How to render a single counter? // */ // // throw new UnsupportedOperationException(); // // } // // } /** * Writes all counters in the hierarchy starting with the specified * {@link CounterSet} in a single table (this is the navigational view of * the counter set hierarchy). */ protected void writeCounterSet(final Writer w, final CounterSet counterSet, final int depth) throws IOException { // depth of the hierarchy at the point where we are starting. final int ourDepth = counterSet.getDepth(); if (log.isInfoEnabled()) log.info("path=" + counterSet.getPath() + ", depth=" + depth + ", ourDepth=" + ourDepth); final String summary = "Showing counters for path=" + counterSet.getPath(); w.write("<table border=\"1\" summary=\""+attrib(summary)+"\"\n>"); // @todo use css to left justify the path. w.write(" <caption>"); writeFullPath(w,counterSet.getPath()); w.write("</caption\n>"); w.write(" <tr\n>"); w.write(" <th rowspan=\"2\" >Name</th\n>"); w.write(" <th colspan=\"3\">Averages</th\n>"); w.write(" <th rowspan=\"2\">Current</th\n>"); w.write(" </tr\n>"); w.write(" <tr\n>"); w.write(" <th>Minutes</th\n>"); w.write(" <th>Hours</th\n>"); w.write(" <th>Days</th\n>"); w.write(" </tr\n>"); final Iterator<ICounterNode> itr = counterSet.getNodes(model.pattern); // final Iterator<ICounter> itr = counterSet.directChildIterator( // true/* sorted */, ICounter.class/* type */); while(itr.hasNext()) { final ICounterNode node = itr.next(); if (log.isDebugEnabled()) log.debug("considering: " + node.getPath()); if(depth != 0) { final int counterDepth = node.getDepth(); // log.info("counterDepth("+counterDepth+") - rootDepth("+rootDepth+") = "+(counterDepth-rootDepth)); if((counterDepth - ourDepth) > depth) { // prune rendering if (log.isDebugEnabled()) log.debug("skipping: " + node.getPath()); continue; } } final String path = node.getPath(); // if (filter != null) { // // if (!filter.matcher(path).matches()) { // // // skip counter not matching filter. // // continue; // // } // // } w.write(" <tr\n>"); if(node instanceof ICounterSet) { w.write(" <th align=\"left\">");// colspan=\"5\">"); writePath(w, path, ourDepth); w.write(" </th\n>"); w.write(" <td colspan=\"4\"> ...</td>"); } else { final ICounter<?> counter = (ICounter<?>) node; /* * write out values for the counter. */ w.write(" <th align=\"left\">"); writePath(w, path, ourDepth); w.write(" </th\n>"); if (counter.getInstrument() instanceof HistoryInstrument) { /* * Report the average over the last hour, day, and month. * * @todo could report the current value, the weighted * average for the last 5 units, and the weighted average * for the last 10 units. Need a method to compute the * weighted average for a History. Reuse that method in the * LBS. */ final HistoryInstrument<?> inst = (HistoryInstrument<?>) counter .getInstrument(); w.write(" <td>" + value(counter,inst.minutes.getAverage()) + " (" + value(counter,inst.minutes.size()) + ")" + "</td\n>"); w.write(" <td>" + (inst.hours==null?"N/A":(value(counter,inst.hours.getAverage()) + " (" + value(counter,inst.hours.size()) + ")")) + "</td\n>"); w.write(" <td>" + (inst.days==null?"N/A":(value(counter,inst.days.getAverage()) + " (" + value(counter,inst.days.size()) + ")")) + "</td\n>"); // the most recent value. w.write(" <td>" + value(counter,counter.getValue()) + "</td\n>"); } else { /* * Report only the most recent value. */ // w.write(" <th>N/A</th\n>"); // w.write(" <th>N/A</th\n>"); // w.write(" <th>N/A</th\n>"); w.write(" <td colspan=\"4\">" + value(counter,counter.getValue()) + "</td\n>"); } } w.write(" </tr\n>"); } w.write("</table\n>"); } /** * Writes details on a single counter using a {@link HistoryTable} view. * * @param counter * The counter. * * @throws IOException */ protected void writeCounter(final Writer w, @SuppressWarnings("rawtypes") final ICounter counter) throws IOException { if (counter.getInstrument() instanceof HistoryInstrument) { writeHistoryTable(w, new ICounter[] { counter }, model.period, model.timestampFormat); } } // /** // * Writes details on a single counter whose {@link IInstrument} provides a // * history. The goal is to be able to easily copy and paste the data into a // * program for plotting, e.g., as an X-Y graph (values against time). // * // * @param counter // * The counter. // * // * @see HistoryInstrument // */ // protected void writeHistoryCounter(Writer w, ICounter counter) // throws IOException { // // final HistoryInstrument inst = (HistoryInstrument) counter.getInstrument(); // // if (inst.minutes.size() > 0) { // w.write("<p>"); // w.write("</p>"); // writeSamples(w, counter, inst.minutes); // } // // if (inst.hours.size() > 0) { // w.write("<p>"); // w.write("</p>"); // writeSamples(w, counter, inst.hours); // } // // if (inst.days.size() > 0) { // w.write("<p>"); // w.write("</p>"); // writeSamples(w, counter, inst.days); // } // // } // /** // * Writes a table containing the samples for a {@link History} for some // * {@link ICounter}. // * // * @param w // * @param counter // * @param h // * // * @throws IOException // * // * @deprecated replace with // * {@link #writeHistoryTable(Writer, ICounter[], PeriodEnum, TimestampFormatEnum)} // * for the specified counter and units. // */ // protected void writeSamples(Writer w, ICounter counter, History h) throws IOException { // // /* // * Figure out the label for the units of the history. // */ // final String units; // final DateFormat dateFormat; // final long period; // if (h.getPeriod() == 1000 * 60L) { // units = "Minutes"; // period = 1000*60;// 60 seconds (in ms). // dateFormat = DateFormat.getTimeInstance(DateFormat.SHORT); // } else if (h.getPeriod() == 1000 * 60 * 24L) { // units = "Hours"; // period = 1000*60*60;// 60 minutes (in ms). // dateFormat = DateFormat.getTimeInstance(DateFormat.MEDIUM); // } else if (h.getSource() != null // && h.getSource().getPeriod() == 1000 * 60 * 24L) { // units = "Days"; // period = 1000*60*60*24;// 24 hours (in ms). // dateFormat = DateFormat.getDateInstance(DateFormat.MEDIUM); // } else { // throw new AssertionError("period="+h.getPeriod()); //// units = "period=" + h.getPeriod() + "ms"; //// dateFormat = DateFormat.getDateTimeInstance(); // } // // /* // * Iterator will visit the timestamped samples in the history. // * // * Note: the iterator is a snapshot of the history at the time that the // * iterator is requested. // * // * @todo This scans the history first, building up the table rows in a // * buffer. Originally that was required to get the first/last timestamps // * from the history before we format the start of the table but now the // * iterator will report those timestamps directly and this could be // * moved inline. // */ // final SampleIterator itr = h.iterator(); // // final StringBuilder sb = new StringBuilder(); // // final long firstTimestamp = itr.getFirstSampleTime(); // // final long lastTimestamp = itr.getLastSampleTime(); // // while (itr.hasNext()) { // // final IHistoryEntry sample = itr.next(); // // sb.append(" <tr\n>"); // // final long lastModified = sample.lastModified(); // // /* // * The time will be zero for the first row and a delta (expressed in // * the units of the history) for the remaining rows. // * // * Note: The time units are computed using floating point math and // * then converted to a display form using formatting in order to be // * able to accurately convey where a sample falls within the // * granularity of the unit (e.g., early or late in the day). // */ // final String timeStr = model.unitsFormat // .format(((double) lastModified - firstTimestamp) / period); // // sb.append(" <td>" + cdata(timeStr) + "</td\n>"); // // sb.append(" <td>" + value(counter, sample.getValue()) // + "</td\n>"); // // sb.append(" <td>" // + cdata(dateFormat.format(new Date(lastModified))) // + "</td\n>"); // // sb.append(" </tr\n>"); // // } // // /* // * Summary for the table. // * // * @todo add some more substance to the summary? // */ // final String summary = "Showing samples: period=" + units + ", path=" // + counter.getPath(); // // /* // * Format the entire table now that we have all the data on hand. // */ // // w.write("<table border=\"1\" summary=\"" + attrib(summary) + "\"\n>"); // // // // caption : @todo use css to left justify the path. // // w.write(" <caption>"); // // writePath(w, counter.getPath()); // // w.write(" </caption\n>"); // // // header row. // w.write(" <tr\n>"); // w.write(" <th colspan=\"3\">"); // writeFullPath(w, counter.getPath()); // w.write(" </th\n>"); // w.write(" </tr\n>"); // // // header row. // w.write(" <tr\n>"); // w.write(" <th>" + "From: " // + dateFormat.format(new Date(firstTimestamp)) + "</th\n>"); // w.write(" <th>" + "To: " + dateFormat.format(new Date(lastTimestamp)) // + "</th\n>"); // // w.write(" <th></th>"); // w.write(" </tr\n>"); // // // header row. // w.write(" <tr\n>"); // w.write(" <th>" + cdata(units) + "</th\n>"); // w.write(" <th>" + cdata(counter.getName()) + "</th\n>"); // w.write(" <th>Timestamp</th>\n"); // w.write(" </tr\n>"); // // // data rows. // w.write(sb.toString()); // // w.write("</table\n>"); // // } /** * * @author <a href="mailto:thompsonbry@users.sourceforge.net">Bryan Thompson</a> */ public class HTMLValueFormatter extends ValueFormatter { private final URLQueryModel model; /** * * @param model */ public HTMLValueFormatter(final URLQueryModel model) { super(model); this.model = model; } /** * Formats a counter value as a String AND performs any escaping necessary * for inclusion in a CDATA section (we do both operations together so that * we can format {@link IServiceCounters#LOCAL_HTTPD} as a link anchor. */ public String value( @SuppressWarnings("rawtypes") final ICounter counter, final Object val) { return XHTMLRenderer.this.value(counter, val); } /** * A clickable trail of the path from the root. */ @Override public void writeFullPath(final Writer w, final String path) throws IOException { writePath(w, path, 0/* root */); } /** * A clickable trail of the path. * * @param rootDepth * The path components will be shown beginning at this depth - * ZERO (0) is the root. */ @Override public void writePath(final Writer w, final String path, final int rootDepth) throws IOException { final String[] a = path.split(ps); if (rootDepth == 0) { // click through to the root of the counter hierarchy w.write("<a href=\"" + model.getRequestURL(new URLQueryParam[] { new URLQueryParam(URLQueryModel.PATH, ps) }) + "\">"); w.write(ps); w.write("</a>"); } // builds up the path query parameter for each split. final StringBuilder sb = new StringBuilder(ps); for (int n = 1; n < a.length; n++) { final String name = a[n]; if (n > 1) { if ((n+1) > rootDepth) { w.write(" "); w.write(ps); } sb.append(ps); } final String prefix = sb.toString(); sb.append(name); if ((n+1) > rootDepth) { if(rootDepth!=0 && n==rootDepth) { w.write("<a href=\"" + model.getRequestURL(new URLQueryParam[] { new URLQueryParam( URLQueryModel.PATH, prefix) }) + "\">"); w.write("..."); w.write("</a>"); w.write(" "+ps); } w.write(" "); w.write("<a href=\"" + model.getRequestURL(new URLQueryParam[] { new URLQueryParam( URLQueryModel.PATH, sb.toString()) }) + "\">"); // current path component. w.write(cdata(name)); w.write("</a>"); } } } } /** * Writes out a table containing the histories for the selected counters. * * @param a * The selected counters. * @param basePeriod * Identifies the history to be written for each of the selected * counters by its based reporting period. * @param timestampFormat * The format in which to report the timestamp associated with * the row. * * @throws IllegalArgumentException * if <i>w</i> is <code>null</code>. * @throws IllegalArgumentException * if <i>a</i> is <code>null</code>. * @throws IllegalArgumentException * if any element of <i>a</i> <code>null</code>. * @throws IllegalArgumentException * if any element of <i>a</i> does not use a * {@link HistoryInstrument}. * * @todo review use of basePeriod - this is {@link URLQueryModel#period}, * right? */ protected void writeHistoryTable(final Writer w, @SuppressWarnings("rawtypes") final ICounter[] a, final PeriodEnum basePeriod, final TimestampFormatEnum timestampFormat) throws IOException { if (w == null) throw new IllegalArgumentException(); if (a == null) throw new IllegalArgumentException(); if (a.length == 0) { // No data. return; } if (basePeriod == null) throw new IllegalArgumentException(); if (timestampFormat == null) throw new IllegalArgumentException(); final HistoryTable t = new HistoryTable(a, basePeriod); /* * Figure out how we will format the timestamp (From:, To:, and the last * column). */ final DateFormat dateFormat; switch(timestampFormat) { case dateTime: switch (basePeriod) { case Minutes: dateFormat = DateFormat.getTimeInstance(DateFormat.SHORT); break; case Hours: dateFormat = DateFormat.getTimeInstance(DateFormat.MEDIUM); break; case Days: dateFormat = DateFormat.getDateInstance(DateFormat.MEDIUM); break; default: throw new AssertionError(); } break; case epoch: dateFormat = null; break; default: throw new AssertionError(timestampFormat.toString()); } new HTMLHistoryTableRenderer(t, model.pattern, new HTMLValueFormatter( model)).render(w); } /** * * @author <a href="mailto:thompsonbry@users.sourceforge.net">Bryan Thompson</a> */ public static class HTMLPivotTableRenderer extends PivotTableRenderer { public HTMLPivotTableRenderer(final PivotTable pt, final ValueFormatter formatter) { super(pt, formatter); } /** * Generate the table. */ @Override public void render(final Writer w) throws IOException { final HistoryTable t = pt.src; // the table start tag. { // Summary for the table : @todo more substance in summary. final String summary = "Showing samples: period=" + t.units; w.write("<table border=\"1\" summary=\"" + attrib(summary) + "\"\n>"); } // the header rows. { // header row. w.write(" <tr\n>"); // timestamp column headers. w.write(" <th>" + cdata(t.units) + "</th\n>"); w.write(" <th>" + cdata("timestamp") + "</th\n>"); for (String s : pt.cnames) { // category column headers. w.write(" <th>" + cdata(s) + "</th\n>"); } for (String s : pt.vcols) { // performance counter column headers. w.write(" <th>" + cdata(s) + "</th\n>"); } w.write(" </tr\n>"); } /* * FIXME Refactor to use PivotTable and an iterator construct that * visits rows. */ // for each row in the HistoryTable. for (int row = 0; row < t.nrows; row++) { /* * The time will be zero for the first row and a delta (expressed in * the units of the history) for the remaining rows. * * Note: The time units are computed using floating point math and * then converted to a display form using formatting in order to be * able to accurately convey where a sample falls within the * granularity of the unit (e.g., early or late in the day). */ final long timestamp = t.getTimestamp(row); final String unitStr = cdata(formatter.unitsFormat .format(((double) timestamp - t .getTimestamp(0/* row */)) / t.period)); final String timeStr = cdata(formatter.date(timestamp)); /* * The set of distinct ordered matched category values in the * current row of the history table. */ for(CSet cset : pt.csets) { assert cset.cats.length == pt.cnames.length : "cset categories=" + Arrays.toString(cset.cats) + " vs " + "category names: " + Arrays.toString(pt.cnames); /* * Aggregate values for counters in this cset having a value for * each value column in turn. * * If none of the counters in the cset have a value for the row * in the data table then we will not display a row in the * output table for this cset. However, there can still be other * csets which do select counters in the data table for which * there are samples and that would be displayed under the * output for for their cset. */ final Double[] vals = new Double[pt.vcols.size()]; // #of value columns having a value. int ndefined = 0; // index of the current value column. int valueColumnIndex = 0; // for each value column. for (String vcol : pt.vcols) { // #of values aggregated for this value column. int valueCountForColumn = 0; // The aggregated value for this value column. double val = 0d; // consider each counter in the cset for this output row. for (ICounter<?> c : cset.counters) { if (!c.getName().equals(vcol)) { // not for this value column (skip over). continue; } // find the index for that counter in the data table. for (int col = 0; col < t.a.length; col++) { if (c != t.a[col]) continue; // get the sample from the data table. final IHistoryEntry<?> e = t.data[row][col]; if (e == null) { // no sampled value. continue; } // @todo catch class cast problems and ignore // val. @todo protected against overflow of // double. val += ((Number) e.getValue()).doubleValue(); valueCountForColumn++; /* * The counter appears just once in the data table * so we can stop once we find its index. */ break; } } // next counter in CSet. if (valueCountForColumn > 0) { /* * There was at least one sample for the current value * column. */ // save the value. vals[valueColumnIndex] = val; // #of non-empty values in this row. ndefined++; } if (log.isDebugEnabled() && valueCountForColumn > 0) log.debug("vcol=" + vcol + ", vcol#=" + valueColumnIndex + ", #values=" + valueCountForColumn + ", val=" + val); valueColumnIndex++; } // next value column. if (ndefined == 0) { // no data for this row. continue; } w.write(" <tr\n>"); w.write(" <td>" + unitStr + "</td\n>"); w.write(" <td>" + timeStr + "</td\n>"); for (int j = 0; j < pt.cnames.length; j++) { w.write(" <td>" + cset.cats[j] + "</td\n>"); } for (int j = 0; j < vals.length; j++) { final String s = vals[j] == null ? "" : Double .toString(vals[j]); w.write(" <td>" + s + "</td\n>"); } w.write(" </tr\n>"); } } // next row. // the table end tag. w.write("</table\n>"); } } /** * Writes out a pivot table view. * * @param w * Where to write the data. * @param a * The selected counters. * @param basePeriod * @param timestampFormat * * @throws IOException * * @todo review use of basePeriod. is this {@link URLQueryModel#period}? */ protected void writePivotTable(final Writer w, @SuppressWarnings("rawtypes") final ICounter[] a, final PeriodEnum basePeriod, final TimestampFormatEnum timestampFormat) throws IOException { if (w == null) throw new IllegalArgumentException(); if (a == null) throw new IllegalArgumentException(); if (a.length == 0) { // No data. return; } if (basePeriod == null) throw new IllegalArgumentException(); if (timestampFormat == null) throw new IllegalArgumentException(); final HistoryTable t = new HistoryTable(a, basePeriod); final PivotTable pt = new PivotTable(model.pattern, model.category, t); /* * Figure out how we will format the timestamp (From:, To:, and the last * column). */ final DateFormat dateFormat; switch(timestampFormat) { case dateTime: switch (basePeriod) { case Minutes: dateFormat = DateFormat.getTimeInstance(DateFormat.SHORT); break; case Hours: dateFormat = DateFormat.getTimeInstance(DateFormat.MEDIUM); break; case Days: dateFormat = DateFormat.getDateInstance(DateFormat.MEDIUM); break; default: throw new AssertionError(); } break; case epoch: dateFormat = null; break; default: throw new AssertionError(timestampFormat.toString()); } new HTMLPivotTableRenderer(pt, new HTMLValueFormatter(model)).render(w); } /** * Writes data in a format suitable for use in a pivot table. * <p> * The pivot table data are selected in the same manner as the correlated * view and are used to generate a {@link HistoryTable}. There will be one * data row per row in the history table. There will be one category column * for each capturing group in the {@link URLQueryModel#pattern}, one column * for the timestamp associated with the row, and one for the value of each * performance counter selected by the {@link URLQueryModel#pattern}. * <p> * Since the pivot table and the correlated view are both based on the * {@link HistoryTable} you can switch between these views simply by * changing the {@link URLQueryModel#reportType} using the * {@value URLQueryModel#REPORT} URL query parameter. * * @see ReportEnum#pivot */ protected void writePivotTable(final Writer w, @SuppressWarnings("rawtypes") final ICounter[] a) throws IOException { if (model.period == null) { /* * Report for all periods. */ writePivotTable(w, a, PeriodEnum.Minutes, model.timestampFormat); writePivotTable(w, a, PeriodEnum.Hours, model.timestampFormat); writePivotTable(w, a, PeriodEnum.Days, model.timestampFormat); } else { /* * Report only the specified period. */ switch (model.period) { case Minutes: writePivotTable(w, a, PeriodEnum.Minutes, model.timestampFormat); break; case Hours: writePivotTable(w, a, PeriodEnum.Hours, model.timestampFormat); break; case Days: writePivotTable(w, a, PeriodEnum.Days, model.timestampFormat); break; default: throw new AssertionError(model.period.toString()); } } } /** * Write the html to render the Flot-based time-series chart, plotting the * supplied events. */ protected void writeFlot(final Writer w, final IEventReportingService eventReportingService) throws IOException { writeResource(w, "flot-start.txt"); writeEvents(w, eventReportingService); writeResource(w, "flot-end.txt"); } /** * Applies the {@link URLQueryModel#eventFilters} to the event. * * @param e * The event. * @return <code>true</code> if the filters accept the event. */ protected boolean acceptEvent(final Event e) { final Iterator<Map.Entry<Field, Pattern>> itr = model.eventFilters .entrySet().iterator(); while (itr.hasNext()) { final Map.Entry<Field, Pattern> filterEntry = itr.next(); final Field fld = filterEntry.getKey(); final Pattern pattern = filterEntry.getValue(); final String val; try { val = "" + fld.get(e); } catch (Throwable t) { throw new RuntimeException("Could not access field: " + fld); } if (!pattern.matcher(val).matches()) { if (log.isDebugEnabled()) log.debug("Rejected event: fld=" + fld.getName() + " : val=" + val); return false; } } return true; } /** * The key for an event group is formed by combining the String value of the * fields of the event identified by the orderEventBy[] in order and using a * ":" when combining two or more event fields together. */ protected String getEventKey(final Event e) { final StringBuilder sb = new StringBuilder(); int n = 0; for (Field f : model.eventOrderBy) { if (n > 0) sb.append(":"); try { sb.append("" + f.get(e)); } catch (Exception ex) { throw new RuntimeException(ex); } n++; } return sb.toString(); } // FIXME format the event as an HTML table and link into the code. protected String getEventTable(final Event e) { final StringBuilder sb = new StringBuilder(); sb.append(e.toString()); return sb.toString(); } /** * Plots events using <code>flot</code>. * * @see ReportEnum#events * @see URLQueryModel#eventFilters * @see URLQueryModel#eventOrderBy * * FIXME Modify to allow visualization of a performance counter timeseries * data. * * FIXME Modify to allow linked viz of performance counter timeseries w/ an * event timeseries and an overview that controls what is seen in both. * * @todo allow a data label for the start point to be specified as a query * parameter. E.g., the indexName+partitionId or the hostname. * * @todo clicking on the overview should reset to the total view. * * @todo allow aggregation of elapsed time for events and have click through * to the individual events broken out using eventOrderBy=uuid. * * @todo a form to set the various query parameters. * * @todo a dashboard view of the federation as a jsp page incorporating * output from a variety of sources. basically a replacement for the * use of a worksheet to look at views of the data which we know to be * interesting. since I don't want to force the bundling of a servlet * engine with each service, this could be a standoff module. */ protected void writeEvents(final Writer w, final IEventReportingService eventReportingService) throws IOException { /* * Map from the key for the event group to the basename of the variables * for that event group. */ final Map<String,String> seriesByGroup = new HashMap<String, String>(); /* * Map from the key for the event group to the StringBuilder whose * contents are rendered into the page and provide the visualization of * the timeseries for that event group. The events are grouped together * in this manner so that they will be assigned the same color by the * legend. */ final Map<String,StringBuilder> eventsByHost = new HashMap<String,StringBuilder>(); /* * Map from the key for the event group to the StringBuilder whose * contents are rendered into the page and provide the tooltip for each * event in that event group. */ final Map<String,StringBuilder> tooltipsByHost = new HashMap<String,StringBuilder>(); int naccepted = 0; int nvisited = 0; final Iterator<Event> itr = eventReportingService.rangeIterator( model.fromTime, model.toTime); while (itr.hasNext()) { final Event e = itr.next(); nvisited++; // apply the event filters. if (!e.isComplete() || !acceptEvent(e)) { continue; } naccepted++; final String key = getEventKey(e); // basename for the variables for this data series. final String series; // per event-group buffer StringBuilder eventsSB = eventsByHost.get(key); if (eventsSB == null) { eventsSB = new StringBuilder(); series = "series_" + seriesByGroup.size(); seriesByGroup.put(key, series); eventsByHost.put(key, eventsSB); eventsSB.append("var "); eventsSB.append(series); eventsSB.append(" = [\n"); final StringBuilder tooltipsSB = new StringBuilder(); final String tooltipvar = series + "tooltips"; tooltipsByHost.put(key, tooltipsSB); tooltipsSB.append("var "); tooltipsSB.append(tooltipvar); tooltipsSB.append(" = [\n"); } else { series = seriesByGroup.get(key); } eventsSB.append("[ "); eventsSB.append(e.getStartTime()); eventsSB.append(", "); // final double offset = Math.sin(e // .getEndTime()/100d) / 4d; final double offset = (Math.random()*.85d)+.05;// - .5);// / 1d; final String hostyvar = series + "y"; eventsSB.append(hostyvar); eventsSB.append((offset < 0 ? "" : "+") + offset+" ], [ "); eventsSB.append(e.getEndTime()); eventsSB.append(", "); eventsSB.append(hostyvar); eventsSB.append((offset < 0 ? "" : "+") +offset+" ], null,\n"); final StringBuilder tooltipsSB = tooltipsByHost.get(key); final String tooltip; if (false) { /* * FIXME Finish the event flyover formatting support. I need to * validate the HTML table and then validate how it is embedded * inside of the flot data. Since it occurs inline, it probably * needs to be escaped. It may also be impossible to do this in * a manner which validates, but still possible to do it in a * manner which is accepted by at least some browsers. */ StringWriter sw = new StringWriter(); writeEventFlyover(sw, e); tooltip = sw.toString(); } else { /* * use the tab-delimited format, but remove the trailing * newline. */ tooltip = e.toString().replace("\n", ""); } // @todo does this need to escape embedded quotes for javascript? if (tooltip != null && !tooltip.startsWith("\"")) { tooltipsSB.append("\""); } tooltipsSB.append(tooltip); if (tooltip != null && !tooltip.endsWith("\"")) { tooltipsSB.append("\""); } // tooltipsSB.append(", null,\n"); tooltipsSB.append(", null, null,\n"); } if (log.isInfoEnabled()) log.info("accepted: " + naccepted + " out of " + nvisited + " events"); // all series. final String[] keys = eventsByHost.keySet().toArray(new String[0]); // put into lexical order. Arrays.sort(keys); for (int i = 0; i < keys.length; i++) { final int hosty = i; final String key = keys[i]; final StringBuilder eventsSB = new StringBuilder(); final String series = seriesByGroup.get(key); final String hostyvar = series + "y"; eventsSB.append("var "); eventsSB.append(hostyvar); eventsSB.append(" = "); eventsSB.append(hosty); eventsSB.append(";\n"); final StringBuilder sb = eventsSB.append(eventsByHost.get(key)); sb.setLength(sb.length() - ", null,\n".length()); sb.append("\n];"); w.write(sb.toString()); w.write("\n"); } /* * Output the variables containing the tooltips for each series. */ for (int i = 0; i < keys.length; i++) { final StringBuilder sb = tooltipsByHost.get(keys[i]); sb.setLength(sb.length() - 2); sb.append("\n];"); w.write(sb.toString()); w.write("\n"); } /* * data[]. * * Note: The data[] and the tooltips[] MUST be correlated. They are * taken in reverse of the generated series order to put the legend * in the same order (its just a little trick). */ final StringBuilder data = new StringBuilder(); data.append("var data = [\n"); for (int i = keys.length - 1; i >= 0; i--) { final String key = keys[i]; final String series = seriesByGroup.get(key); data.append("{ label: \""); data.append(key); data.append("\", data: "); data.append(series); data.append(" },\n"); } if (data.charAt(data.length() - 2) == ',') { data.setLength(data.length() - 2); } data.append("\n];\n"); w.write(data.toString()); /* * tooltips[] */ final StringBuilder tooltips = new StringBuilder(); tooltips.append("var tooltips = [\n"); for (int i = keys.length - 1; i >= 0; i--) { final String key = keys[i]; final String series = seriesByGroup.get(key); final String tooltipvar = series + "tooltips"; tooltips.append(tooltipvar); tooltips.append(",\n"); } if (tooltips.charAt(tooltips.length() - 2) == ',') { tooltips.setLength(tooltips.length() - 2); } tooltips.append("\n];\n"); w.write(tooltips.toString()); } /** * Pretty up an event by rendering onto the {@link Writer} as an (X)HTML * table. * * @param w * The writer. * @param e * The event. * * @throws IOException */ protected void writeEventFlyover(final Writer w, final Event e) throws IOException { final DateFormat dateFormat = DateFormat.getDateTimeInstance(); final String summary = e.majorEventType + " from " + dateFormat.format(new Date(e.getStartTime())) + " to " + dateFormat.format(new Date(e.getEndTime())) + ", uuid=" + e.eventUUID.toString(); w.write("<table border=\"1\" summary=\"" + attrib(summary) + "\"\n>"); w.write(" <caption>"); w.write(cdata(e.majorEventType + " from " + cdata(dateFormat.format(new Date(e.getStartTime()))) + " to " + e.getEndTime())); w.write("</caption\n>"); // header row. w.write(" <tr\n>"); w.write(" <th>" + "From: " + cdata(dateFormat.format(new Date(e.getStartTime()))) + "</th\n>"); w.write(" <th>" + "To: " + cdata(dateFormat.format(new Date(e.getEndTime()))) + "</th\n>"); w.write(" <th>" + "Duration: " + (e.getEndTime() - e.getStartTime()) + "s</th\n>"); w.write(" </tr\n>"); // attributes. w.write(" <tr\n>"); w.write(" <th align=\"left\">hostname</th>"); w.write(" <td colspan=\"2\">"+cdata(e.hostname.toString())+"</td>"); w.write(" </tr\n>"); w.write(" <tr\n>"); w.write(" <th align=\"left\">serviceIface</th>"); w.write(" <td colspan=\"2\">"+cdata(e.serviceIface.toString())+"</td>"); w.write(" </tr\n>"); w.write(" <tr\n>"); w.write(" <th align=\"left\">serviceName</th>"); w.write(" <td colspan=\"2\">"+cdata(e.serviceName.toString())+"</td>"); w.write(" </tr\n>"); w.write(" <tr\n>"); w.write(" <th align=\"left\">serviceUUID</th>"); w.write(" <td colspan=\"2\">"+cdata(e.serviceUUID.toString())+"</td>"); w.write(" </tr\n>"); w.write(" <tr\n>"); w.write(" <th align=\"left\">resource</th>"); w.write(" <td colspan=\"2\">"+cdata(e.resource.toString())+"</td>"); w.write(" </tr\n>"); w.write(" <tr\n>"); w.write(" <th align=\"left\">minorEventType</th>"); w.write(" <td colspan=\"2\">"+cdata(e.minorEventType.toString())+"</td>"); w.write(" </tr\n>"); w.write(" <tr\n>"); w.write(" <th align=\"left\">majorEventType</th>"); w.write(" <td colspan=\"2\">"+cdata(e.majorEventType.toString())+"</td>"); w.write(" </tr\n>"); if (true && e.getDetails() != null && e.getDetails().size() > 0) { for (Map.Entry<String,Object> entry: e.getDetails().entrySet()) { w.write(" <tr\n>"); w.write(" <th align=\"left\">" + cdata(entry.getKey()) + "</th>"); w.write(" <td colspan=\"4\">" + cdata(""+entry.getValue()) + "</td>"); w.write(" </tr\n>"); } } w.write("</table\n>"); } /** * Write a text file into the html. The supplied resource will be relative * to this class. */ protected void writeResource(final Writer w, final String resource) throws IOException { final InputStream is = getClass().getResourceAsStream(resource); if (is == null) throw new IOException("Resource not on classpath: " + resource); try { final BufferedReader reader = new BufferedReader( new InputStreamReader(is)); try { String s = null; boolean first = true; // read each line (note: chops off newline). while ((s = reader.readLine()) != null) { if (!first) { // write out the chopped off newline. w.write("\n"); } w.write(s); first = false; } } finally { reader.close(); } } finally { is.close(); } } /** * Encode a string for including in a CDATA section. * * @param s * The string. * * @return The encoded string. */ static public String cdata(final String s) { if (s == null) throw new IllegalArgumentException(); return HTMLUtility.escapeForXHTML(s); } /** * Encoding a string for including in an (X)HTML attribute value. * * @param s * The string. * * @return */ static public String attrib(final String s) { return HTMLUtility.escapeForXHTML(s); } /** * Formats a counter value as a String AND performs any escaping necessary * for inclusion in a CDATA section (we do both operations together so that * we can format {@link IServiceCounters#LOCAL_HTTPD} as a link anchor. * * @param counter * The counter. * @param value * The counter value (MAY be <code>null</code>). * @return * * @deprecated Move into formatter objects. */ protected String value(final ICounter<?> counter, final Object val) { if (counter == null) throw new IllegalArgumentException(); if (val == null) return cdata("N/A"); if(val instanceof Double || val instanceof Float) { Format fmt = model.decimalFormat; if (counter.getName().contains("%") || percent_pattern.matcher(counter.getName()).matches()) { fmt = model.percentFormat; } return cdata( fmt.format(((Number)val).doubleValue()) ); } else if(val instanceof Long || val instanceof Integer) { final Format fmt = model.integerFormat; return cdata(fmt.format(((Number) val).longValue())); } if (counter.getName().equals(IServiceCounters.LOCAL_HTTPD)) { return "<a href=" + val + ">" + cdata(val.toString()) + "</a>"; } return cdata(val.toString()); } /** * A pattern matching the occurrence of the word "percent" in a counter * name. Leading and trailing wildcards are used and the match is * case-insensitive. */ static private final Pattern percent_pattern = Pattern.compile( ".*percent.*", Pattern.CASE_INSENSITIVE); }