Java tutorial
// This file is part of Droopy. // Copyright (C) 2011 Benoit Sigoure. // // This program is free software: you can redistribute it and/or modify it // under the terms of the GNU Lesser General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your // option) any later version. This program is distributed in the hope that it // will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty // of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser // General Public License for more details. You should have received a copy // of the GNU Lesser General Public License along with this program. If not, // see <http://www.gnu.org/licenses/>. package viewer; import java.util.Collections; import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Map; import com.google.gwt.core.client.EntryPoint; import com.google.gwt.core.client.JavaScriptObject; import com.google.gwt.core.client.JsArray; import com.google.gwt.event.dom.client.ClickEvent; import com.google.gwt.event.dom.client.ClickHandler; import com.google.gwt.event.dom.client.DomEvent; import com.google.gwt.event.logical.shared.ValueChangeEvent; import com.google.gwt.event.logical.shared.ValueChangeHandler; import com.google.gwt.event.shared.EventHandler; import com.google.gwt.http.client.Request; import com.google.gwt.http.client.RequestBuilder; import com.google.gwt.http.client.RequestCallback; import com.google.gwt.http.client.RequestException; import com.google.gwt.http.client.Response; import com.google.gwt.http.client.URL; import com.google.gwt.i18n.client.DateTimeFormat; import com.google.gwt.json.client.JSONArray; import com.google.gwt.json.client.JSONException; import com.google.gwt.json.client.JSONNumber; import com.google.gwt.json.client.JSONObject; import com.google.gwt.json.client.JSONParser; import com.google.gwt.json.client.JSONString; import com.google.gwt.json.client.JSONValue; import com.google.gwt.user.client.DOM; import com.google.gwt.user.client.Element; import com.google.gwt.user.client.History; import com.google.gwt.user.client.Window; import com.google.gwt.user.client.rpc.AsyncCallback; import com.google.gwt.user.client.ui.Button; import com.google.gwt.user.client.ui.CheckBox; import com.google.gwt.user.client.ui.HorizontalPanel; import com.google.gwt.user.client.ui.InlineLabel; import com.google.gwt.user.client.ui.ListBox; import com.google.gwt.user.client.ui.RootPanel; import com.google.gwt.user.client.ui.TextBox; import com.google.gwt.user.client.ui.Tree; import com.google.gwt.user.client.ui.TreeItem; import com.google.gwt.user.client.ui.VerticalPanel; import com.google.gwt.visualization.client.DataTable; import com.google.gwt.visualization.client.Selection; import com.google.gwt.visualization.client.VisualizationUtils; import com.google.gwt.visualization.client.events.SelectHandler; import com.google.gwt.visualization.client.visualizations.corechart.AxisOptions; import com.google.gwt.visualization.client.visualizations.corechart.ColumnChart; import com.google.gwt.visualization.client.visualizations.corechart.CoreChart; import com.google.gwt.visualization.client.visualizations.corechart.Options; import com.google.gwt.visualization.client.visualizations.corechart.PieChart; import static viewer.Json.object; /** * Main class for the Droopy UI. */ final class Main implements EntryPoint { static final DateTimeFormat FULLDATE = DateTimeFormat.getFormat("yyyy/MM/dd-HH:mm:ss"); private static final DateTimeFormat INDEXDATE = DateTimeFormat.getFormat("yyyyMM"); /** Max number of results we'll fetch from ES. */ private static final short MAX_RESULTS = 200; private static final short DEFAULT_RESULTS = 20; /** ip:port of the ES server to talk to. */ private String server; /** Base name of the ES index to use, typically an alias name. */ private String indexname; private final VerticalPanel root = new VerticalPanel(); private final InlineLabel status = new InlineLabel(); private final HorizontalPanel charts = new HorizontalPanel(); private final AlignedTree traces = new AlignedTree(); private short nresults = DEFAULT_RESULTS; // How many traces we want. private final DateTimeBox start_datebox = new DateTimeBox(); private final DateTimeBox end_datebox = new DateTimeBox(); private final TextBox esquery = new TextBox(); private final ListBox sortby = new ListBox(); private abstract class AjaxCallback implements RequestCallback/*AsyncCallback<JavaScriptObject>*/ { public void onError(final Request req, final Throwable e) { status.setText("AJAX query failed: " + e.getMessage()); } public final void onResponseReceived(final Request request, final Response response) { final String text = response.getText(); if (text.isEmpty()) { final int code = response.getStatusCode(); final String errmsg; if (code == 0) { // Happens when a cross-domain request fails to connect. errmsg = ("Failed to connect to " + server + ", check that the server" + " is up and that you can connect to it."); } else { errmsg = ("Empty response from server: code=" + code + " status=" + response.getStatusText()); } onError(request, new RuntimeException(errmsg)); } else { JSONValue value; try { value = JSONParser.parseStrict(text); } catch (JSONException e) { onError(request, e); return; } onSuccess(value); } } protected abstract void onSuccess(final JSONValue response); } /** * This is the entry point method. */ public void onModuleLoad() { final class Start implements Runnable { public void run() { onModuleLoadReal(); } } VisualizationUtils.loadVisualizationApi(new Start(), "corechart"); } /** Returns the name of the index for this month. */ private String currentIndexName() { return indexname + "-" + INDEXDATE.format(new Date()); } private void onModuleLoadReal() { server = getServer(); indexname = getIndexName(); if (server == null) { promptForServerUi(); return; } status.setText("Checking server health..."); root.add(status); root.add(charts); removeLoadingMessage(); RootPanel.get().add(root); ajax("/" + indexname + "/_status", new AjaxCallback() { public void onSuccess(final JSONValue response) { final JSONObject resp = response.isObject(); if (resp.containsKey("ok") && resp.get("ok").isBoolean().booleanValue()) { status.setText("Server is ready, " + (resp.get("indices").isObject().get(currentIndexName()) .isObject().get("docs").isObject().get("num_docs")) + " traces available." + " Loading ..."); setupUi(); setupHistory(); } else if (resp.containsKey("error") && resp.containsKey("status")) { status.setText( "Server health check failed: error " + resp.get("status") + ": " + resp.get("error")); } else { status.setText("Incomprehensible response from server: " + resp); } } }); } private static void removeLoadingMessage() { // Remove the static "Loading..." message. final Element loading = DOM.getElementById("loading"); DOM.removeChild(DOM.getParent(loading), loading); } private static String getServer() { final String server = Window.Location.getParameter("srv"); if (server == null || server.isEmpty()) { return null; } return "http://" + server; } private static String getIndexName() { final String index = Window.Location.getParameter("index"); if (index == null || index.isEmpty()) { return "droopy"; // Default. } return index; } private void promptForServerUi() { final VerticalPanel vbox = new VerticalPanel(); vbox.add(new InlineLabel( "I need to know the address of the ElasticSearch" + " server where Droopy traces are stored.")); final HorizontalPanel hbox = new HorizontalPanel(); hbox.setSpacing(5); hbox.add(new InlineLabel("Address (host:port):")); final ValidatedTextBox textbox = new ValidatedTextBox(); textbox.setValidationRegexp("^[-_.a-zA-Z0-9]+:[1-9][0-9]*$"); hbox.add(textbox); hbox.add(new InlineLabel("Index name:")); final ValidatedTextBox indexbox = new ValidatedTextBox(); indexbox.setValue(getIndexName()); indexbox.setValidationRegexp("^[-_.a-zA-Z0-9]+$"); hbox.add(indexbox); hbox.add(new Button("Go!", new ClickHandler() { public void onClick(final ClickEvent event) { final String srv = textbox.getValue(); if (srv == null || srv.isEmpty()) { return; } final String index = indexbox.getValue(); if (index == null || index.isEmpty()) { return; } final StringBuilder url = new StringBuilder(); url.append(Window.Location.getPath()); String tmp = Window.Location.getQueryString(); if (tmp != null && !tmp.isEmpty()) { url.append(tmp).append("&srv="); } else { url.append("?srv="); } url.append(srv); url.append("&index=").append(index); tmp = Window.Location.getHash(); if (tmp != null && !tmp.isEmpty()) { url.append(tmp); } Window.Location.assign(url.toString()); } })); vbox.add(hbox); root.add(vbox); removeLoadingMessage(); RootPanel.get().add(root); } private void ajax(final String resource, final AjaxCallback callback) { ajax(resource, null, callback); } private void ajax(final String resource, final String body, final AjaxCallback callback) { final boolean has_body = body != null; final RequestBuilder builder = new RequestBuilder(has_body ? RequestBuilder.POST : RequestBuilder.GET, server + resource); // Doesn't work on Chrome due to ES bug #828. //if (has_body) { // builder.setHeader("Content-Type", "application/json"); //} try { builder.sendRequest(body, callback); } catch (RequestException e) { status.setText("Failed to setup AJAX call to " + server + resource + ": " + e); } } private void setupUi() { setupChangeHandlers(); final HorizontalPanel hbox = new HorizontalPanel(); hbox.setSpacing(5); hbox.add(new InlineLabel("From")); hbox.add(start_datebox); // If we're not trying to see anything already, look at some recent // traces by default. if (History.getToken().isEmpty()) { final long now = System.currentTimeMillis(); start_datebox.setValue(new Date(now - 600000), false); end_datebox.setValue(new Date(now), true); } hbox.add(new InlineLabel("To")); hbox.add(end_datebox); hbox.add(new InlineLabel("Query")); hbox.add(esquery); hbox.add(new InlineLabel("Sort by")); sortby.addItem("Request timestamp", "request_ts"); // 1st is default. sortby.addItem("End-to-end latency", "end_to_end"); sortby.addItem("Slowest system call", "slowest_syscall.duration"); sortby.addItem("Number of system calls", "num_syscalls"); hbox.add(sortby); root.add(hbox); traces.setAnimationEnabled(true); root.add(traces); } private void setupChangeHandlers() { final EventsHandler refresh = new EventsHandler() { protected <H extends EventHandler> void onEvent(final DomEvent<H> event) { refresh(); } }; { final ValueChangeHandler<Date> vch = new ValueChangeHandler<Date>() { public void onValueChange(final ValueChangeEvent<Date> event) { refresh(); } }; TextBox tb = start_datebox.getTextBox(); tb.addKeyPressHandler(refresh); start_datebox.addValueChangeHandler(vch); tb = end_datebox.getTextBox(); tb.addKeyPressHandler(refresh); end_datebox.addValueChangeHandler(vch); } esquery.addKeyPressHandler(refresh); sortby.addChangeHandler(refresh); } private void setupHistory() { final ValueChangeHandler<String> handler = new ValueChangeHandler<String>() { public void onValueChange(final ValueChangeEvent<String> event) { final Map<String, List<String>> params = parseQueryString(event.getValue()); setTextBox(start_datebox.getTextBox(), params.get("start")); setTextBox(end_datebox.getTextBox(), params.get("end")); setTextBox(esquery, params.get("q")); List<String> value = params.get("sort"); if (value != null) { final String sort = value.get(0); final int n = sortby.getItemCount(); for (int i = 0; i < n; i++) { if (sortby.getValue(i).equals(sort)) { sortby.setSelectedIndex(i); break; } } } else { sortby.setSelectedIndex(0); // Pick the 1st item as the default. } if ((value = params.get("results")) != null) { short n; try { n = Short.valueOf(value.get(0)); } catch (NumberFormatException e) { n = -1; } if (0 < n && n < MAX_RESULTS) { nresults = n; } } else { nresults = DEFAULT_RESULTS; } loadTraces(); } private void setTextBox(final TextBox box, final List<String> values) { if (values != null) { box.setValue(values.get(0)); } else { box.setValue(""); } } }; History.addValueChangeHandler(handler); History.fireCurrentHistoryState(); } private void refresh() { final Date start = start_datebox.getValue(); if (start == null) { status.setText("Please specify a start time."); return; } final Date end = end_datebox.getValue(); if (end != null && end.getTime() <= start.getTime()) { end_datebox.addStyleName("dateBoxFormatError"); status.setText("End time must be after start time!"); return; } final StringBuilder token = new StringBuilder(); token.append("start=").append(FULLDATE.format(start)); if (end != null) { token.append("&end=").append(FULLDATE.format(end)); } // The 1st item is the default, so only put it in the URL if we picked the // non-default (index > 0). If index is < 0, then nothing is selected, so // use the default too. if (sortby.getSelectedIndex() > 0) { token.append("&sort=").append(sortby.getValue(sortby.getSelectedIndex())); } if (!esquery.getValue().isEmpty()) { token.append("&q=").append(esquery.getValue()); } History.newItem(token.toString()); } /** * Returns the indices to search for the current date range. * For instance "/droopy201106," */ private String indicesToSearch() { final Date from = start_datebox.getValue(); final Date to = end_datebox.getValue() != null ? end_datebox.getValue() : new Date(); final int y1 = 1900 + from.getYear(); // Start year final int m1 = 1 + from.getMonth(); // Start month final int y2 = 1900 + to.getYear(); // End year final int m2 = 1 + to.getMonth(); // End month final StringBuilder buf = new StringBuilder(8 * (y2 * 12 + m2 - y1 * 12 - m1)); buf.append("/"); int start_month = m1; for (int y = y1; y <= y2; y++) { // Use `<=' to loop at least once. // If we're on the last year, stop at the end month, otherwise go // through all the months until the end of the year. final int end_month = y == y2 ? m2 : 12; for (int m = start_month; m <= end_month; m++) { buf.append(indexname).append('-').append(y); if (m < 10) { buf.append('0'); // Padding for values less than 10. } buf.append(m).append(','); } start_month = 1; } buf.setLength(buf.length() - 1); // Remove the last `,' return buf.toString(); } private void loadTraces() { status.setText("Loading..."); final Json request_ts = object().add("from", toMillis(start_datebox)); if (end_datebox.getValue() != null) { request_ts.add("to", toMillis(end_datebox)); } final String json = object().add("size", nresults) .add("sort", Json.array().add(sortby.getValue(sortby.getSelectedIndex()), object("order", "desc"))) .add("query", object("filtered", object("query", getESQuery()).add("filter", object("numeric_range", object("request_ts", request_ts))))) .add("facets", object().add("slowbe", object("terms", object("field", "prev_connect.host"))) .add("betype", object("terms", object("field", "prev_connect.type"))) //.add("lathisto", object("histogram", // object("field", "end_to_end").add("interval", 30))) ).toString(); ajax(indicesToSearch() + "/summary/_search", json, new AjaxCallback() { public void onSuccess(final JSONValue response) { final ESResponse<Summary> resp = ESResponse.fromJson(response.isObject()); status.setText("Found " + resp.hits().total() + " traces in " + resp.took() + "ms"); charts.clear(); renderChart(resp.<ESResponse.TermFacet>facets("slowbe"), "Slowest Backend Hosts", "host"); renderChart(resp.<ESResponse.TermFacet>facets("betype"), "Slowest Backend Types", "type"); renderLatencyHistogram(resp.<ESResponse.HistoFacet>facets("lathisto")); renderTraces(resp.hits()); } }); } private Json getESQuery() { final String q = esquery.getValue(); if (q.isEmpty()) { return object("match_all", object()); } return object("query_string", object("query", q).add("default_operator", "AND")); } private void renderChart(final ESResponse.Facets<ESResponse.TermFacet> facets, final String title, final String tag) { if (facets == null) { return; } final DataTable data = DataTable.create(); data.addColumn(DataTable.ColumnType.STRING, "Backend"); data.addColumn(DataTable.ColumnType.NUMBER, "Number of times slowest"); final JsArray<ESResponse.TermFacet> terms = facets.terms(); final int nterms = terms.length(); data.addRows(nterms); for (int i = 0; i < nterms; i++) { final ESResponse.TermFacet facet = terms.get(i); final String backend = facet.term(); if ("unknown".equals(backend)) { continue; } data.setValue(i, 0, backend); data.setValue(i, 1, facet.count()); } final PieChart.PieOptions options = PieChart.createPieOptions(); options.setWidth(400); options.setHeight(240); options.setTitle(title); final PieChart chart = new PieChart(data, options); final SearchOnSelectHandler handler = new SearchOnSelectHandler(chart, data, tag); chart.addSelectHandler(handler); charts.add(chart); } private final class SearchOnSelectHandler extends SelectHandler { /* * Just wanna say: the select handler API is ridiculously bad. * The SelectEvent we receive contains nothing, so we have to retain a * reference to the chart manually in the handler. But that's not enough * because the chart doesn't have an API to access its data, so you also * need to manually retain a reference to the data in the chart. The data * doesn't speak in Selection so you have to manually translate that into * a row/column request. Sigh. */ private final CoreChart chart; private final DataTable data; private final String tag; private SearchOnSelectHandler(final CoreChart chart, final DataTable data, final String tag) { this.chart = chart; this.data = data; this.tag = tag + ":"; } public void onSelect(final SelectEvent event) { String qs = esquery.getValue(); for (final Selection selection : JsArrayIterator.iter(chart.getSelections())) { if (!selection.isRow()) { continue; } qs += ' ' + tag + data.getValueString(selection.getRow(), 0); } esquery.setValue(qs.trim()); refresh(); } } private void renderLatencyHistogram(final ESResponse.Facets<ESResponse.HistoFacet> facets) { // Disabled because it triggers a JavaScript error in corechart. See: // http://groups.google.com/group/gwt-google-apis/browse_thread/thread/332a644b2e7e66fc //if (facets == null) { // return; //} //final DataTable data = DataTable.create(); //data.addColumn(DataTable.ColumnType.NUMBER, "Latency"); //data.addColumn(DataTable.ColumnType.NUMBER, "Number of hits"); //final JsArray<ESResponse.HistoFacet> buckets = facets.terms(); //final int nbuckets = buckets.length(); //data.addRows(nbuckets); //for (int i = 0; i < nbuckets; i++) { // final ESResponse.HistoFacet facet = buckets.get(i); // data.setValue(i, 0, facet.key()); // data.setValue(i, 1, facet.count()); //} //final Options options = ColumnChart.createOptions(); //options.setWidth(400); //options.setHeight(240); //options.setTitle("Response Latency"); //final AxisOptions axis = AxisOptions.create(); //axis.setTitle("Latency"); //options.setHAxisOptions(axis); //charts.add(new ColumnChart(data, options)); } private void renderTraces(final ESResponse.Hits<Summary> summaries) { traces.clear(); final HashSet<String> expanded = new HashSet(getHistoryTokens("trace")); for (final ESResponse.Hit<Summary> hit : summaries.iterator()) { final String index = hit.index(); final String id = hit.id(); final Summary summary = hit.source(); final TreeItem trace = new LazyTreeItem(summary.widget()) { protected void onFirstOpen() { expandTrace(this, index, id, summary); onOpen(); } protected void onOpen() { appendHistoryToken("trace", id); } protected void onClose() { removeHistoryToken("trace", id); } }; traces.addItem(trace); if (expanded.remove(id)) { // If this trace ID should be expanded... trace.setState(true); // then expand it now. } } if (nresults < MAX_RESULTS && summaries.total() > nresults) { final Button more = new Button("Load more traces", new ClickHandler() { public void onClick(final ClickEvent event) { nresults += 20; traces.getItem(traces.getItemCount() - 1).setText("Loading..."); replaceHistoryTokens("results", Short.toString(nresults)); loadTraces(); } }); traces.addItem(more); } traces.align(); // IDs that haven't been expanded are no longer displayed, // so remove them from the URL. for (final String id : expanded) { removeHistoryToken("trace", id); } } private void expandTrace(final TreeItem parent, final String index, final String traceid, final Summary summary) { ajax('/' + index + "/trace/" + traceid, new AjaxCallback() { public void onSuccess(final JSONValue response) { final ESResponse.Hit<Trace> hit = ESResponse.Hit.fromJson(response.isObject()); parent.removeItems(); parent.addItem(hit.source().widget(summary)); } }); } // ---------------- // // History helpers. // // ---------------- // private static void appendHistoryToken(final String key, final String value) { appendHistoryToken(key, value, false); } private static void appendHistoryToken(final String key, final String value, final boolean fire_event) { final String current = History.getToken(); if (current.isEmpty()) { History.newItem(key + '=' + value, fire_event); return; } final Map<String, List<String>> params = parseQueryString(current); final List<String> values = params.get(key); if (values != null) { for (final String existing : values) { if (value.equals(existing)) { return; } } } History.newItem(current + '&' + key + '=' + value, fire_event); } private static void removeHistoryToken(final String key, final String value) { removeHistoryToken(key, value, false); } private static void removeHistoryToken(final String key, final String value, final boolean fire_event) { final String current = History.getToken(); if (current.isEmpty()) { return; } final String search = key + '=' + value; if (current.startsWith(search)) { History.newItem(current.substring(search.length())); return; } final Map<String, List<String>> params = parseQueryString(current); final List<String> values = params.get(key); if (values != null) { for (final String existing : values) { if (value.equals(existing)) { final int index = current.indexOf('&' + search); if (index < 0) { // Should never happen. Be loud. Window.alert("WTF? Failed to find history token " + search + " in URL"); } else { History.newItem( current.substring(0, index) + current.substring(index + search.length() + 1), fire_event); } return; } } } } private static final List<String> getHistoryTokens(final String key) { final String token = History.getToken(); if (token.isEmpty()) { return Collections.emptyList(); } final Map<String, List<String>> params = parseQueryString(token); final List<String> values = params.get(key); // OK, this is an interesting peculiarity of the Java language. // I originally wrote the following line, but it doesn't compile: //return values == null ? Collections.emptyList() : values; // Because of this somewhat cryptic error: // Type mismatch: cannot convert from // List<capture#1-of ? extends Object> to List<String // This is because the type of the conditional expression is the "lub" // (lower upper bound) of the two operands. So instead we have to help // the compiler and disambiguate the code with this C++-like syntax: return values == null ? Collections.<String>emptyList() : values; } private static final Map<String, List<String>> parseQueryString(final String querystring) { return QueryStringDecoder.getParameters(querystring); } private static void replaceHistoryTokens(final String key, final String value) { for (final String remove : getHistoryTokens(key)) { removeHistoryToken(key, remove); } appendHistoryToken(key, value); } // ------------- // // Misc helpers. // // ------------- // private static final long toMillis(final DateTimeBox box) { return box.getValue().getTime(); } }