// Graph options we might want: // Grid off/on // Stacked off/on // Area off/on // Legend position // Short link // Graph title // Palette // Background // Enable tooltips // width/height // Axis options // Y-Range min/max // (X-Range min/max) // X-Axis format // Y-Axis format // Y-Axis title // X-Axis title // Log scale var Prometheus = Prometheus || {}; var graphs = []; var graphTemplate; var SECOND = 1000; Prometheus.Graph = function(element, options) { this.el = element; this.options = options; this.changeHandler = null; this.rickshawGraph = null; this.data = []; this.initialize(); }; Prometheus.Graph.timeFactors = { "y": 60 * 60 * 24 * 365, "w": 60 * 60 * 24 * 7, "d": 60 * 60 * 24, "h": 60 * 60, "m": 60, "s": 1 }; Prometheus.Graph.stepValues = [ "1s", "10s", "1m", "5m", "15m", "30m", "1h", "2h", "6h", "12h", "1d", "2d", "1w", "2w", "4w", "8w", "1y", "2y" ]; Prometheus.Graph.numGraphs = 0; Prometheus.Graph.prototype.initialize = function() { var self = this; self.id = Prometheus.Graph.numGraphs++; // Set default options. self.options["id"] = self.id; self.options["range_input"] = self.options["range_input"] || "1h"; self.options["stacked_checked"] = self.options["stacked"] ? "checked" : ""; self.options["tab"] = self.options["tab"] || 1; // Draw graph controls and container from Handlebars template. var graphHtml = graphTemplate(self.options); self.el.append(graphHtml); // Get references to all the interesting elements in the graph container and // bind event handlers. var graphWrapper = self.el.find("#graph_wrapper" + self.id); self.queryForm = graphWrapper.find(".query_form"); self.expr = graphWrapper.find("input[name=expr]"); self.rangeInput = self.queryForm.find("input[name=range_input]"); self.stacked = self.queryForm.find("input[name=stacked]"); self.insertMetric = self.queryForm.find("select[name=insert_metric]"); self.refreshInterval = self.queryForm.find("select[name=refresh]"); self.consoleTab = graphWrapper.find(".console"); self.graphTab = graphWrapper.find(".graph_container"); self.tabs = graphWrapper.find(".tabs"); self.tab = $(self.tabs.find("> div")[self.options["tab"]]); // active tab self.tabs.tabs({ active: self.options["tab"], activate: function(e, ui) { storeGraphOptionsInUrl(); self.tab = ui.newPanel if (self.tab.hasClass("reload")) { // reload if flagged with class "reload" self.submitQuery(); } } }); // Return moves focus back to expr instead of submitting. self.insertMetric.bind("keydown", "return", function(e) { self.expr.focus(); self.expr.val(self.expr.val()); return e.preventDefault(); }) self.error = graphWrapper.find(".error"); self.graph = graphWrapper.find(".graph"); self.yAxis = graphWrapper.find(".y_axis"); self.legend = graphWrapper.find(".legend"); self.spinner = graphWrapper.find(".spinner"); self.evalStats = graphWrapper.find(".eval_stats"); self.endDate = graphWrapper.find("input[name=end_input]"); if (self.options["end_input"]) { self.endDate.appendDtpicker({"current": self.options["end_input"]}); } else { self.endDate.appendDtpicker(); self.endDate.val(""); } self.endDate.change(function() { self.submitQuery() }); self.refreshInterval.change(function() { self.updateRefresh() }); self.stacked.change(function() { self.updateGraph(); }); self.queryForm.submit(function() { self.consoleTab.addClass("reload"); self.graphTab.addClass("reload"); self.submitQuery(); return false; }); self.spinner.hide(); self.queryForm.find("button[name=inc_range]").click(function() { self.increaseRange(); }); self.queryForm.find("button[name=dec_range]").click(function() { self.decreaseRange(); }); self.queryForm.find("button[name=inc_end]").click(function() { self.increaseEnd(); }); self.queryForm.find("button[name=dec_end]").click(function() { self.decreaseEnd(); }); self.insertMetric.change(function() { self.expr.selection("replace", {text: self.insertMetric.val(), mode: "before"}); self.expr.focus(); // refocusing }); self.expr.focus(); // TODO: move to external Graph method. self.populateInsertableMetrics(); if (self.expr.val()) { self.submitQuery(); } }; Prometheus.Graph.prototype.populateInsertableMetrics = function() { var self = this; $.ajax({ method: "GET", url: "/api/metrics", dataType: "json", success: function(json, textStatus) { var availableMetrics = []; for (var i = 0; i < json.length; i++) { self.insertMetric[0].options.add(new Option(json[i], json[i])); availableMetrics.push(json[i]); } self.expr.autocomplete({source: availableMetrics}); }, error: function() { self.showError("Error loading available metrics!"); }, }); }; Prometheus.Graph.prototype.onChange = function(handler) { this.changeHandler = handler; }; Prometheus.Graph.prototype.getOptions = function() { var self = this; var options = {}; var optionInputs = [ "expr", "range_input", "end_input", "step_input", "stacked" ]; self.queryForm.find("input").each(function(index, element) { var name = element.name; if ($.inArray(name, optionInputs) >= 0) { if (name == "stacked") { options[name] = element.checked; } else { options[name] = element.value; } } }); options["tab"] = self.tabs.tabs("option", "active"); return options; }; Prometheus.Graph.prototype.parseDuration = function(rangeText) { var rangeRE = new RegExp("^([0-9]+)([ywdhms]+)$"); var matches = rangeText.match(rangeRE); if (!matches) { return }; if (matches.length != 3) { return 60; } var value = parseInt(matches[1]); var unit = matches[2]; return value * Prometheus.Graph.timeFactors[unit]; }; Prometheus.Graph.prototype.increaseRange = function() { var self = this; var rangeSeconds = self.parseDuration(self.rangeInput.val()); for (var i = 0; i < Prometheus.Graph.stepValues.length; i++) { if (rangeSeconds < self.parseDuration(Prometheus.Graph.stepValues[i])) { self.rangeInput.val(Prometheus.Graph.stepValues[i]); if (self.expr.val()) { self.submitQuery(); } return; } } }; Prometheus.Graph.prototype.decreaseRange = function() { var self = this; var rangeSeconds = self.parseDuration(self.rangeInput.val()); for (var i = Prometheus.Graph.stepValues.length - 1; i >= 0; i--) { if (rangeSeconds > self.parseDuration(Prometheus.Graph.stepValues[i])) { self.rangeInput.val(Prometheus.Graph.stepValues[i]); if (self.expr.val()) { self.submitQuery(); } return; } } }; Prometheus.Graph.prototype.getEndDate = function() { var self = this; if (!self.endDate || !self.endDate.val()) { return null; } return new Date(self.endDate.val()).getTime(); }; Prometheus.Graph.prototype.getOrSetEndDate = function() { var self = this; var date = self.getEndDate(); if (date) { return date; } date = new Date(); self.setEndDate(date); return date; } Prometheus.Graph.prototype.setEndDate = function(date) { var self = this; dateString = date.getFullYear() + "-" + (date.getMonth()+1) + "-" + date.getDate() + " " + date.getHours() + ":" + date.getMinutes(); self.endDate.val(""); self.endDate.appendDtpicker({"current": dateString}); }; Prometheus.Graph.prototype.increaseEnd = function() { var self = this; self.setEndDate(new Date(self.getOrSetEndDate() + self.parseDuration(self.rangeInput.val()) * 1000/2 )) // increase by 1/2 range & convert ms in s self.submitQuery(); }; Prometheus.Graph.prototype.decreaseEnd = function() { var self = this; self.setEndDate(new Date(self.getOrSetEndDate() - self.parseDuration(self.rangeInput.val()) * 1000/2 )) self.submitQuery(); }; Prometheus.Graph.prototype.submitQuery = function() { var self = this; self.clearError(); if (!self.expr.val()) { return; } self.spinner.show(); self.evalStats.empty(); var startTime = new Date().getTime(); var rangeSeconds = self.parseDuration(self.rangeInput.val()); self.queryForm.find("input[name=range]").val(rangeSeconds); var resolution = self.queryForm.find("input[name=step_input]").val() || Math.max(Math.floor(rangeSeconds / 250), 1); self.queryForm.find("input[name=step]").val(resolution); var endDate = self.getEndDate() / 1000; self.queryForm.find("input[name=end]").val(endDate); if (self.queryXhr) { self.queryXhr.abort(); } var url; var success; if (self.tab[0] == self.graphTab[0]) { url = self.queryForm.attr("action"); success = function(json, textStatus) { self.handleGraphResponse(json, textStatus); }; } else { url = "/api/query"; success = function(text, textStatus) { self.handleConsoleResponse(text, textStatus); }; } self.queryXhr = $.ajax({ method: self.queryForm.attr("method"), url: url, dataType: "json", data: self.queryForm.serialize(), success: success, error: function(xhr, resp) { if (resp != "abort") { self.showError("Error executing query: " + resp); } }, complete: function() { var duration = new Date().getTime() - startTime; self.evalStats.html("Load time: " + duration + "ms, resolution: " + resolution + "s"); self.spinner.hide(); } }); }; Prometheus.Graph.prototype.showError = function(msg) { var self = this; self.error.text(msg); self.error.show(); } Prometheus.Graph.prototype.clearError = function(msg) { var self = this; self.error.text(''); self.error.hide(); } Prometheus.Graph.prototype.updateRefresh = function() { var self = this; if (self.timeoutID) { window.clearTimeout(self.timeoutID); } interval = self.parseDuration(self.refreshInterval.val()); if (!interval) { return }; self.timeoutID = window.setTimeout(function() { self.submitQuery(); self.updateRefresh(); }, interval * SECOND); } Prometheus.Graph.prototype.renderLabels = function(labels) { var labelStrings = []; for (label in labels) { if (label != "__name__") { labelStrings.push("" + label + ": " + labels[label]); } } return labels = "