var Prometheus = Prometheus || {};
var graphs = [];
var graphTemplate;
var SECOND = 1000;
Handlebars.registerHelper('pathPrefix', function() { return PATH_PREFIX; });
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";
if (self.options["tab"] === undefined) {
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("textarea[name=expr]");
self.expr.keypress(function(e) {
// Enter was pressed without the shift key.
if (e.which == 13 && !e.shiftKey) {
self.queryForm.submit();
e.preventDefault();
}
// Auto-resize the text area on input.
var offset = this.offsetHeight - this.clientHeight;
var resizeTextarea = function(el) {
$(el).css('height', 'auto').css('height', el.scrollHeight + offset);
};
$(this).on('keyup input', function() { resizeTextarea(this); });
});
self.expr.change(storeGraphOptionsInURL);
self.rangeInput = self.queryForm.find("input[name=range_input]");
self.stackedBtn = self.queryForm.find(".stacked_btn");
self.stacked = self.queryForm.find("input[name=stacked]");
self.refreshInterval = self.queryForm.find("select[name=refresh]");
self.consoleTab = graphWrapper.find(".console");
self.graphTab = graphWrapper.find(".graph_container");
self.tabs = graphWrapper.find("a[data-toggle='tab']");
self.tabs.eq(self.options["tab"]).tab("show");
self.tabs.on("shown.bs.tab", function(e) {
var target = $(e.target);
self.options["tab"] = target.parent().index();
storeGraphOptionsInURL();
if ($("#" + target.attr("aria-controls")).hasClass("reload")) {
self.submitQuery();
}
});
self.error = graphWrapper.find(".error").hide();
self.graphArea = graphWrapper.find(".graph_area");
self.graph = self.graphArea.find(".graph");
self.yAxis = self.graphArea.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]");
self.endDate.datetimepicker({
language: 'en',
pickSeconds: false,
});
if (self.options["end_input"]) {
self.endDate.data('datetimepicker').setValue(self.options["end_input"]);
}
self.endDate.change(function() { self.submitQuery() });
self.refreshInterval.change(function() { self.updateRefresh() });
self.isStacked = function() {
return self.stacked.val() === '1';
};
var styleStackBtn = function() {
var icon = self.stackedBtn.find('.glyphicon');
if (self.isStacked()) {
self.stackedBtn.addClass("btn-primary");
icon.addClass("glyphicon-check");
icon.removeClass("glyphicon-unchecked");
} else {
self.stackedBtn.removeClass("btn-primary");
icon.addClass("glyphicon-unchecked");
icon.removeClass("glyphicon-check");
}
};
styleStackBtn();
self.stackedBtn.click(function() {
if (self.isStacked() && self.graphJSON) {
// If the graph was stacked, the original series data got mutated
// (scaled) and we need to reconstruct it from the original JSON data.
self.data = self.transformData(self.graphJSON);
}
self.stacked.val(self.isStacked() ? '0' : '1');
styleStackBtn();
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.populateAutocompleteMetrics();
if (self.expr.val()) {
self.submitQuery();
}
};
// Returns true if the character at "pos" in the expression string "str" looks
// like it could be a metric name (if it's not in a string, a label matchers
// section, or a range specification).
function isPotentialMetric(str, pos) {
var quote = null;
var inMatchersOrRange = false;
for (var i = 0; i < pos; i++) {
var ch = str[i];
// Skip over escaped characters (quotes or otherwise) in strings.
if (quote !== null && ch === "\\") {
i += 1;
continue;
}
// Track if we are entering or leaving a string.
switch (ch) {
case quote:
quote = null;
break;
case '"':
case "'":
quote = ch;
break;
}
// Ignore curly braces and square brackets in strings.
if (quote) {
continue;
}
// Track whether we are in curly braces (label matchers).
switch (ch) {
case "{":
case "[":
inMatchersOrRange = true;
break;
case "}":
case "]":
inMatchersOrRange = false;
break;
}
}
return !inMatchersOrRange && quote === null;
}
// Returns the current word under the cursor position in $input.
function currentWord($input) {
var wordRE = new RegExp("[a-zA-Z0-9:_]");
var pos = $input.prop("selectionStart");
var str = $input.val();
var len = str.length;
var start = pos;
var end = pos;
while (start > 0 && str[start-1].match(wordRE)) {
start--;
}
while (end < len && str[end].match(wordRE)) {
end++;
}
return {
start: start,
end: end,
word: $input.val().substring(start, end)
};
}
Prometheus.Graph.prototype.populateAutocompleteMetrics = function() {
var self = this;
$.ajax({
method: "GET",
url: PATH_PREFIX + "/api/v1/label/__name__/values",
dataType: "json",
success: function(json, textStatus) {
if (json.status !== "success") {
self.showError("Error loading available metrics!")
return;
}
// For the typeahead autocompletion, we need to remember where to put
// the cursor after inserting an autocompleted word (we want to put it
// after that word, not at the end of the entire input string).
var afterUpdatePos = null;
self.expr.typeahead({
// Needs to return true for autocomplete items that should be matched
// by the current input.
matcher: function(item) {
var cw = currentWord(self.expr);
if (cw.word.length !== 0 &&
item.toLowerCase().indexOf(cw.word.toLowerCase()) > -1 &&
isPotentialMetric(self.expr.val(), cw.start)) {
return true;
}
return false;
},
// Returns the entire string to which the input field should be set
// upon selecting an item from the autocomplete list.
updater: function(item) {
var str = self.expr.val();
var cw = currentWord(self.expr);
afterUpdatePos = cw.start + item.length;
return str.substring(0, cw.start) + item + str.substring(cw.end, str.length);
},
// Is called *after* the input field has been set to the string
// returned by the "updater" callback. We want to move the cursor to
// the end of the actually inserted word here.
afterSelect: function(item) {
self.expr.prop("selectionStart", afterUpdatePos);
self.expr.prop("selectionEnd", afterUpdatePos);
},
source: json.data,
items: "all"
});
// This needs to happen after attaching the typeahead plugin, as it
// otherwise breaks the typeahead functionality.
self.expr.focus();
},
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 = [
"range_input",
"end_input",
"step_input",
"stacked"
];
self.queryForm.find("input").each(function(index, element) {
var name = element.name;
if ($.inArray(name, optionInputs) >= 0) {
options[name] = element.value;
}
});
options["expr"] = self.expr.val();
options["tab"] = self.options["tab"];
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 new Date();
}
return self.endDate.data('datetimepicker').getDate().getTime();
};
Prometheus.Graph.prototype.getOrSetEndDate = function() {
var self = this;
var date = self.getEndDate();
self.setEndDate(date);
return date;
}
Prometheus.Graph.prototype.setEndDate = function(date) {
var self = this;
self.endDate.data('datetimepicker').setValue(date);
};
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());
var resolution = self.queryForm.find("input[name=step_input]").val() || Math.max(Math.floor(rangeSeconds / 250), 1);
var endDate = self.getEndDate() / 1000;
if (self.queryXhr) {
self.queryXhr.abort();
}
var url;
var success;
var params = {
"query": self.expr.val()
};
if (self.options["tab"] === 0) {
params['start'] = endDate - rangeSeconds;
params['end'] = endDate;
params['step'] = resolution;
url = PATH_PREFIX + "/api/v1/query_range";
success = function(json, textStatus) { self.handleGraphResponse(json, textStatus); };
} else {
params['time'] = startTime / 1000;
url = PATH_PREFIX + "/api/v1/query";
success = function(json, textStatus) { self.handleConsoleResponse(json, textStatus); };
}
self.queryXhr = $.ajax({
method: self.queryForm.attr("method"),
url: url,
dataType: "json",
data: params,
success: function(json, textStatus) {
if (json.status !== "success") {
self.showError(json.error);
return;
}
success(json.data, textStatus);
},
error: function(xhr, resp) {
if (resp != "abort") {
var err;
if (xhr.responseJSON !== undefined) {
err = xhr.responseJSON.error;
} else {
err = xhr.statusText;
}
self.showError("Error executing query: " + err);
}
},
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 + ": " + escapeHTML(labels[label]));
}
}
return labels = "