From c9197b9bfd5cd3ab49ee7dfdc81d207715ccd5d8 Mon Sep 17 00:00:00 2001 From: Robert Günzler Date: Mon, 23 Feb 2026 14:19:55 +0100 Subject: prometheus: modernize console + alerts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Robert Günzler --- .../files/console_assets/ajax-loader.gif | Bin 0 -> 847 bytes bundles/prometheus/files/console_assets/menu.lib | 51 ++ bundles/prometheus/files/console_assets/prom.lib | 150 +++++ .../files/console_assets/prom_console.css | 190 ++++++ .../files/console_assets/prom_console.js | 687 +++++++++++++++++++++ 5 files changed, 1078 insertions(+) create mode 100644 bundles/prometheus/files/console_assets/ajax-loader.gif create mode 100644 bundles/prometheus/files/console_assets/menu.lib create mode 100644 bundles/prometheus/files/console_assets/prom.lib create mode 100644 bundles/prometheus/files/console_assets/prom_console.css create mode 100644 bundles/prometheus/files/console_assets/prom_console.js (limited to 'bundles/prometheus/files/console_assets') diff --git a/bundles/prometheus/files/console_assets/ajax-loader.gif b/bundles/prometheus/files/console_assets/ajax-loader.gif new file mode 100644 index 0000000..e192ca8 Binary files /dev/null and b/bundles/prometheus/files/console_assets/ajax-loader.gif differ diff --git a/bundles/prometheus/files/console_assets/menu.lib b/bundles/prometheus/files/console_assets/menu.lib new file mode 100644 index 0000000..e5cf31f --- /dev/null +++ b/bundles/prometheus/files/console_assets/menu.lib @@ -0,0 +1,51 @@ +{{/* vim: set ft=gotmpl: */}} + +{{/* Navbar, should be passed . */}} +{{ define "navbar" }} + +{{ end }} + +{{/* LHS menu, should be passed . */}} +{{ define "menu" }} + +{{ end }} + +{{/* Helper, pass (args . path name) */}} +{{ define "_menuItem" }} +{{ $pagelink := .arg0.Path }} +{{ if .arg0.Params.instance }}{{ $pagelink = (printf "%v?instance=%v" .arg0.Path .arg0.Params.instance) }}{{ end }} + +{{ end }} diff --git a/bundles/prometheus/files/console_assets/prom.lib b/bundles/prometheus/files/console_assets/prom.lib new file mode 100644 index 0000000..fbc39ff --- /dev/null +++ b/bundles/prometheus/files/console_assets/prom.lib @@ -0,0 +1,150 @@ +{{/* vim: set ft=gotmpl: */}} +{{/* Load Prometheus console library JS/CSS. Should go in head */}} + +{{ define "prom_console_head" }} + + +Prometheus Console + + + + + + + + + + + + + +{{ end }} + +{{/* Top of all pages. */}} +{{ define "head" -}} + + + + {{ template "prom_console_head" }} + + + {{ template "navbar" . }} + {{ template "menu" . }} +{{ end }} + +{{ define "__prom_query_drilldown_noop" }}{{ . }}{{ end }} +{{ define "humanize" }}{{ humanize . }}{{ end }} +{{ define "humanizeNoSmallPrefix" }}{{ if and (lt . 1.0) (gt . -1.0) }}{{ printf "%.3g" . }}{{ else }}{{ humanize . }}{{ end }}{{ end }} +{{ define "humanize1024" }}{{ humanize1024 . }}{{ end }} +{{ define "humanizeDuration" }}{{ humanizeDuration . }}{{ end }} +{{ define "humanizePercentage" }}{{ humanizePercentage . }}{{ end }} +{{ define "humanizeTimestamp" }}{{ humanizeTimestamp . }}{{ end }} +{{ define "printf.1f" }}{{ printf "%.1f" . }}{{ end }} +{{ define "printf.3g" }}{{ printf "%.3g" . }}{{ end }} + +{{/* prom_query_drilldown (args expr suffix? renderTemplate?) +Displays the result of the expression, with a link to /graph for it. + +renderTemplate is the name of the template to use to render the value. +*/}} +{{ define "prom_query_drilldown" }} +{{ $expr := .arg0 }}{{ $suffix := (or .arg1 "") }}{{ $renderTemplate := (or .arg2 "__prom_query_drilldown_noop") }} + {{ with query $expr }}{{tmpl $renderTemplate ( . | first | value )}}{{ $suffix }}{{ else }}-{{ end }} +{{ end }} + +{{ define "prom_path" }}/consoles/{{ .Path }}?{{ range $param, $value := .Params }}{{ $param }}={{ $value }}&{{ end }}{{ end }}" + +{{ define "prom_content_head" }} +
+ {{ template "prom_graph_timecontrol" . }} +{{ end }} +{{ define "prom_content_tail" }} +
+{{ end }} + +{{ define "prom_graph_timecontrol" }} +
+
+
+ +
+
+ +
+
+
+ + + +
+
+
+ +
+{{ end }} + +{{/* Bottom of all pages. */}} + +{{ define "tail" }} + + +{{ end }} diff --git a/bundles/prometheus/files/console_assets/prom_console.css b/bundles/prometheus/files/console_assets/prom_console.css new file mode 100644 index 0000000..e5b8be7 --- /dev/null +++ b/bundles/prometheus/files/console_assets/prom_console.css @@ -0,0 +1,190 @@ +.navbar { + margin-bottom: 20px; +} + +div.prom_lhs_menu { + float: left; + margin-right: 2ex; + background: #222; + min-height: 100%; + overflow: auto; + width: auto; + margin-left: -40px; + margin-top: 55px; + height: 100%; +} +.prom_lhs_menu_nav { + padding-right: 0; +} +.prom_lhs_menu ul { + list-style: none; + padding-left: .5ex; + margin-left: .5ex; +} +.prom_lhs_menu li { + padding-right: 1ex; + padding-left: 1ex; + font-size: 0.9rem; +} +.prom_lhs_menu a, +.prom_lhs_menu li { + color: #999999; + display: block; +} +.prom_lhs_menu a { + text-decoration: none; + padding-top: 0; + padding-bottom: 0; +} +.prom_lhs_menu_selected a { + pointer-events: none; + cursor: default; +} +li.prom_lhs_menu_selected { + background: #555555; + background-clip: padding-box; +} +.prom_lhs_menu li:hover { + background: #666666; +} + +.prom_console_rhs { + float: right; + margin-left: 1ex; + height: 100%; + padding-top: 55px; +} +.prom_console_rhs table { + margin-top: 1ex; + margin-bottom: 32px; /* Space for time control. */ +} +.prom_console_rhs th { + text-align: center; +} +.prom_console_rhs td:nth-child(2) { + text-align: right; +} + +.prom_console_content { + overflow: visible; + margin-bottom: 32px; /* Space for time control. */ + padding-top: 70px; /* Space for navbar */ +} + +a.prom_query_drilldown { + text-decoration: none; + color: black; +} +a.prom_query_drilldown:hover, a.prom_query_drilldown:active { + text-decoration: underline; +} + +#samplesGraph { + width: 100%; +} + +.prom_content_div { + width: 80%; +} + +.prom_graph_table { +} +.rickshaw_legend { + padding: 2px; + margin-top: 1px; +} +.rickshaw_legend li { + min-width: 0; +} +.rickshaw_legend ul li { + list-style-type: none; + display: inline-block; +} +.rickshaw_graph { + width: 100%; + padding: 0; +} +.prom_graph_hover_flipped.x_label { + right: 0; +} +.prom_graph_hover_flipped.item { + right: 10px; +} +.rickshaw_graph .detail .prom_graph_hover_flipped.item:before { + content: "\25b8"; + left: auto; + right: 1px; + font-size: 0.8em; +} +.prom_graph_ytitle { + -webkit-transform: rotate(-90deg); + -moz-transform: rotate(-90deg); + font-size: 11px; + font-family: Arial; + max-width: 13px; + white-space: nowrap; +} +.prom_graph_xtitle { + text-align: center; + font-size: 11px; + font-family: Arial; +} +.prom_graph_loading { + position: absolute; + top: 0px; + left: 0px; +} +.prom_graph_error { + font-family: Arial; + text-align: center; +} +a.prom_graph_link { + text-decoration: none; + color: black; +} + + +.prom_graph_timecontrol { + background: #222; + position: fixed; + padding: 2px; + bottom: 0; + left: 0; + z-index: 2; + width: 100%; +} + +.prom_graph_timecontrol_inner { + text-align: center; + height: 31px; +} + +.prom_graph_timecontrol_group { + display: inline-block; + margin: 1px 5px; +} +.prom_graph_timecontrol_group .btn { + font-size: 0.8em; + border-radius: 0; + height: 29px; + border-color: #ccc; +} + +.prom_graph_timecontrol_group .input { + height: 29px; + padding: 6px 12px; + font-size: 14px; + line-height: 1.42857143; + color: #555; + background-color: #fff; + background-image: none; + border: 1px solid #ccc; + text-align: center; +} + +.prom_graph_timecontrol_group .dropdown-menu { + min-width: 90px; +} +.prom_graph_timecontrol_group .dropdown-menu li { + text-align: center; +} diff --git a/bundles/prometheus/files/console_assets/prom_console.js b/bundles/prometheus/files/console_assets/prom_console.js new file mode 100644 index 0000000..cb3501c --- /dev/null +++ b/bundles/prometheus/files/console_assets/prom_console.js @@ -0,0 +1,687 @@ +/* + * Functions to make it easier to write prometheus consoles, such + * as graphs. + * + */ +PromConsole = {}; + +PromConsole.NumberFormatter = {}; +PromConsole.NumberFormatter.prefixesBig = ["k", "M", "G", "T", "P", "E", "Z", "Y"]; +PromConsole.NumberFormatter.prefixesBig1024 = ["ki", "Mi", "Gi", "Ti", "Pi", "Ei", "Zi", "Yi"]; +PromConsole.NumberFormatter.prefixesSmall = ["m", "u", "n", "p", "f", "a", "z", "y"]; + +PromConsole._stripTrailingZero = function(x) { + if (x.indexOf("e") == -1) { + // It's not safe to strip if it's scientific notation. + return x.replace(/\.?0*$/, ''); + } + return x; +}; + +// Humanize a number. +PromConsole.NumberFormatter.humanize = function(x) { + if (x === null) { + return "null"; + } + var ret = PromConsole.NumberFormatter._humanize( + x, PromConsole.NumberFormatter.prefixesBig, + PromConsole.NumberFormatter.prefixesSmall, 1000); + x = ret[0]; + var prefix = ret[1]; + if (Math.abs(x) < 1) { + return x.toExponential(3) + prefix; + } + return PromConsole._stripTrailingZero(x.toFixed(3)) + prefix; +}; + +// Humanize a number, don't use milli/micro/etc. prefixes. +PromConsole.NumberFormatter.humanizeNoSmallPrefix = function(x) { + if (x === null) { + return "null"; + } + if (Math.abs(x) < 1) { + return PromConsole._stripTrailingZero(x.toPrecision(3)); + } + var ret = PromConsole.NumberFormatter._humanize( + x, PromConsole.NumberFormatter.prefixesBig, + [], 1000); + x = ret[0]; + var prefix = ret[1]; + return PromConsole._stripTrailingZero(x.toFixed(3)) + prefix; +}; + +// Humanize a number with 1024 as the base, rather than 1000. +PromConsole.NumberFormatter.humanize1024 = function(x) { + if (x === null) { + return "null"; + } + var ret = PromConsole.NumberFormatter._humanize( + x, PromConsole.NumberFormatter.prefixesBig1024, + [], 1024); + x = ret[0]; + var prefix = ret[1]; + if (Math.abs(x) < 1) { + return x.toExponential(3) + prefix; + } + return PromConsole._stripTrailingZero(x.toFixed(3)) + prefix; +}; + +// Humanize a number, returning an exact representation. +PromConsole.NumberFormatter.humanizeExact = function(x) { + if (x === null) { + return "null"; + } + var ret = PromConsole.NumberFormatter._humanize( + x, PromConsole.NumberFormatter.prefixesBig, + PromConsole.NumberFormatter.prefixesSmall, 1000); + return ret[0] + ret[1]; +}; + +PromConsole.NumberFormatter._humanize = function(x, prefixesBig, prefixesSmall, factor) { + var prefix = ""; + if (x === 0) { + /* Do nothing. */ + } else if (Math.abs(x) >= 1) { + for (var i = 0; i < prefixesBig.length && Math.abs(x) >= factor; ++i) { + x /= factor; + prefix = prefixesBig[i]; + } + } else { + for (var i = 0; i < prefixesSmall.length && Math.abs(x) < 1; ++i) { + x *= factor; + prefix = prefixesSmall[i]; + } + } + return [x, prefix]; +}; + + +PromConsole.TimeControl = function() { + document.getElementById("prom_graph_duration_shrink").onclick = this.decreaseDuration.bind(this); + document.getElementById("prom_graph_duration_grow").onclick = this.increaseDuration.bind(this); + document.getElementById("prom_graph_time_back").onclick = this.decreaseEnd.bind(this); + document.getElementById("prom_graph_time_forward").onclick = this.increaseEnd.bind(this); + document.getElementById("prom_graph_refresh_button").onclick = this.refresh.bind(this); + this.durationElement = document.getElementById("prom_graph_duration"); + this.endElement = document.getElementById("prom_graph_time_end"); + this.durationElement.oninput = this.dispatch.bind(this); + this.endElement.oninput = this.dispatch.bind(this); + this.endElement.oninput = this.dispatch.bind(this); + this.refreshValueElement = document.getElementById("prom_graph_refresh_button_value"); + + var refreshList = document.getElementById("prom_graph_refresh_intervals"); + var refreshIntervals = ["Off", "1m", "5m", "15m", "1h"]; + for (var i = 0; i < refreshIntervals.length; ++i) { + var li = document.createElement("li"); + li.onclick = this.setRefresh.bind(this, refreshIntervals[i]); + li.textContent = refreshIntervals[i]; + refreshList.appendChild(li); + } + + this.durationElement.value = PromConsole.TimeControl.prototype.getHumanDuration( + PromConsole.TimeControl._initialValues.duration); + if (!PromConsole.TimeControl._initialValues.endTimeNow) { + this.endElement.value = PromConsole.TimeControl.prototype.getHumanDate( + new Date(PromConsole.TimeControl._initialValues.endTime * 1000)); + } +}; + +PromConsole.TimeControl.timeFactors = { + "y": 60 * 60 * 24 * 365, + "w": 60 * 60 * 24 * 7, + "d": 60 * 60 * 24, + "h": 60 * 60, + "m": 60, + "s": 1 +}; + +PromConsole.TimeControl.stepValues = [ + "10s", "1m", "5m", "15m", "30m", "1h", "2h", "6h", "12h", "1d", "2d", + "1w", "2w", "4w", "8w", "1y", "2y" +]; + +PromConsole.TimeControl.prototype._setHash = function() { + var duration = this.parseDuration(this.durationElement.value); + var endTime = this.getEndDate() / 1000; + window.location.hash = "#pctc" + encodeURIComponent(JSON.stringify( + { duration: duration, endTime: endTime })); +}; + +PromConsole.TimeControl._initialValues = function() { + var hash = window.location.hash; + var values; + if (hash.indexOf('#pctc') === 0) { + values = JSON.parse(decodeURIComponent(hash.substring(5))); + } else { + values = { duration: 3600, endTime: 0 }; + } + if (values.endTime == 0) { + values.endTime = new Date().getTime() / 1000; + values.endTimeNow = true; + } + return values; +}(); + +PromConsole.TimeControl.prototype.parseDuration = function(durationText) { + var durationRE = new RegExp("^([0-9]+)([ywdhms]?)$"); + var matches = durationText.match(durationRE); + if (!matches) { return 3600; } + var value = parseInt(matches[1]); + var unit = matches[2] || 's'; + return value * PromConsole.TimeControl.timeFactors[unit]; +}; + +PromConsole.TimeControl.prototype.getHumanDuration = function(duration) { + var units = []; + for (var key in PromConsole.TimeControl.timeFactors) { + units.push([PromConsole.TimeControl.timeFactors[key], key]); + } + units.sort(function(a, b) { return b[0] - a[0]; }); + for (var i = 0; i < units.length; ++i) { + if (duration % units[i][0] === 0) { + return (duration / units[i][0]) + units[i][1]; + } + } + return duration; +}; + +PromConsole.TimeControl.prototype.increaseDuration = function() { + var durationSeconds = this.parseDuration(this.durationElement.value); + for (var i = 0; i < PromConsole.TimeControl.stepValues.length; i++) { + if (durationSeconds < this.parseDuration(PromConsole.TimeControl.stepValues[i])) { + this.setDuration(PromConsole.TimeControl.stepValues[i]); + this.dispatch(); + return; + } + } +}; + +PromConsole.TimeControl.prototype.decreaseDuration = function() { + var durationSeconds = this.parseDuration(this.durationElement.value); + for (var i = PromConsole.TimeControl.stepValues.length - 1; i >= 0; i--) { + if (durationSeconds > this.parseDuration(PromConsole.TimeControl.stepValues[i])) { + this.setDuration(PromConsole.TimeControl.stepValues[i]); + this.dispatch(); + return; + } + } +}; + +PromConsole.TimeControl.prototype.setDuration = function(duration) { + this.durationElement.value = duration; + this._setHash(); +}; + +PromConsole.TimeControl.prototype.getEndDate = function() { + if (this.endElement.value === '') { + return null; + } + var dateParts = this.endElement.value.split(/[- :]/) + return Date.UTC(dateParts[0], dateParts[1] - 1, dateParts[2], dateParts[3], dateParts[4]); +}; + +PromConsole.TimeControl.prototype.getOrSetEndDate = function() { + var date = this.getEndDate(); + if (date) { + return date; + } + date = new Date(); + this.setEndDate(date); + return date.getTime(); +}; + +PromConsole.TimeControl.prototype.getHumanDate = function(date) { + var hours = date.getUTCHours() < 10 ? '0' + date.getUTCHours() : date.getUTCHours(); + var minutes = date.getUTCMinutes() < 10 ? '0' + date.getUTCMinutes() : date.getUTCMinutes(); + return date.getUTCFullYear() + "-" + (date.getUTCMonth() + 1) + "-" + date.getUTCDate() + " " + + hours + ":" + minutes; +}; + +PromConsole.TimeControl.prototype.setEndDate = function(date) { + this.setRefresh("Off"); + this.endElement.value = this.getHumanDate(date); + this._setHash(); +}; + +PromConsole.TimeControl.prototype.increaseEnd = function() { + // Increase duration 25% range & convert ms to s. + this.setEndDate(new Date(this.getOrSetEndDate() + this.parseDuration(this.durationElement.value) * 1000 / 4)); + this.dispatch(); +}; + +PromConsole.TimeControl.prototype.decreaseEnd = function() { + this.setEndDate(new Date(this.getOrSetEndDate() - this.parseDuration(this.durationElement.value) * 1000 / 4)); + this.dispatch(); +}; + +PromConsole.TimeControl.prototype.refresh = function() { + this.endElement.value = ''; + this._setHash(); + this.dispatch(); +}; + +PromConsole.TimeControl.prototype.dispatch = function() { + var durationSeconds = this.parseDuration(this.durationElement.value); + var end = this.getEndDate(); + if (end === null) { + end = new Date().getTime(); + } + for (var i = 0; i < PromConsole._graph_registry.length; i++) { + var graph = PromConsole._graph_registry[i]; + graph.params.duration = durationSeconds; + graph.params.endTime = end / 1000; + graph.dispatch(); + } +}; + +PromConsole.TimeControl.prototype._refreshInterval = null; + +PromConsole.TimeControl.prototype.setRefresh = function(duration) { + if (this._refreshInterval !== null) { + window.clearInterval(this._refreshInterval); + this._refreshInterval = null; + } + if (duration != "Off") { + if (this.endElement.value !== '') { + this.refresh(); + } + var durationSeconds = this.parseDuration(duration); + this._refreshInterval = window.setInterval(this.dispatch.bind(this), durationSeconds * 1000); + } + this.refreshValueElement.textContent = duration; +}; + +// List of all graphs, used by time controls. +PromConsole._graph_registry = []; + +PromConsole.graphDefaults = { + expr: null, // Expression to graph. Can be a list of strings. + node: null, // DOM node to place graph under. + // How long the graph is over, in seconds. + duration: PromConsole.TimeControl._initialValues.duration, + // The unixtime the graph ends at. + endTime: PromConsole.TimeControl._initialValues.endTime, + width: null, // Height of the graph div, excluding titles and legends. + // Defaults to auto-detection. + height: 200, // Height of the graph div, excluding titles and legends. + min: "auto", // Minimum Y-axis value, defaults to lowest data value. + max: undefined, // Maximum Y-axis value, defaults to highest data value. + renderer: 'line', // Type of graphs, options are 'line' and 'area'. + name: null, // What to call plots, defaults to trying to do + // something reasonable. + // If a string, it'll use that. [[ label ]] will be substituted. + // If a function it'll be called with a map of keys to values, + // and should return the name to use. + // Can be a list of strings/functions, each element + // will be applied to the plots from the corresponding + // element of the expr list. + xTitle: "Time", // The title of the x axis. + yUnits: "", // The units of the y axis. + yTitle: "", // The title of the y axis. + // Number formatter for y axis. + yAxisFormatter: PromConsole.NumberFormatter.humanize, + // Number of ticks (horizontal lines) for the y axis + yAxisTicks: null, + // Number formatter for y values hover detail. + yHoverFormatter: PromConsole.NumberFormatter.humanizeExact, + // Color scheme to be used by the plots. Can be either a list of hex color + // codes or one of the color scheme names supported by Rickshaw. + colorScheme: null, +}; + +PromConsole.Graph = function(params) { + for (var k in PromConsole.graphDefaults) { + if (!(k in params)) { + params[k] = PromConsole.graphDefaults[k]; + } + } + if (typeof params.expr == "string") { + params.expr = [params.expr]; + } + if (typeof params.name == "string" || typeof params.name == "function") { + var name = []; + for (var i = 0; i < params.expr.length; i++) { + name.push(params.name); + } + params.name = name; + } + + this.params = params; + this.rendered_data = null; + // Keep a reference so that further updates (e.g. annotations) can be made + // by the user in their templates. + this.rickshawGraph = null; + PromConsole._graph_registry.push(this); + + /* + * Table layout: + * | yTitle | Graph | + * | | xTitle | + * | /graph | Legend | + */ + var table = document.createElement("table"); + table.className = "prom_graph_table"; + params.node.appendChild(table); + var tr = document.createElement("tr"); + table.appendChild(tr); + var yTitleTd = document.createElement("td"); + tr.appendChild(yTitleTd); + var yTitleDiv = document.createElement("td"); + yTitleTd.appendChild(yTitleDiv); + yTitleDiv.className = "prom_graph_ytitle"; + yTitleDiv.textContent = params.yTitle + (params.yUnits ? " (" + params.yUnits.trim() + ")" : ""); + + this.graphTd = document.createElement("td"); + tr.appendChild(this.graphTd); + this.graphTd.className = "rickshaw_graph"; + this.graphTd.width = params.width; + this.graphTd.height = params.height; + + tr = document.createElement("tr"); + table.appendChild(tr); + tr.appendChild(document.createElement("td")); + var xTitleTd = document.createElement("td"); + tr.appendChild(xTitleTd); + xTitleTd.className = "prom_graph_xtitle"; + xTitleTd.textContent = params.xTitle; + + tr = document.createElement("tr"); + table.appendChild(tr); + var graphLinkTd = document.createElement("td"); + tr.appendChild(graphLinkTd); + var graphLinkA = document.createElement("a"); + graphLinkTd.appendChild(graphLinkA); + graphLinkA.className = "prom_graph_link"; + graphLinkA.textContent = "+"; + graphLinkA.href = PromConsole._graphsToSlashGraphURL(params.expr); + var legendTd = document.createElement("td"); + tr.appendChild(legendTd); + this.legendDiv = document.createElement("div"); + legendTd.width = params.width; + legendTd.appendChild(this.legendDiv); + + window.addEventListener('resize', function() { + if (this.rendered_data !== null) { + this._render(this.rendered_data); + } + }.bind(this)); + + this.dispatch(); +}; + +PromConsole.Graph.prototype._parseValue = function(value) { + var val = parseFloat(value); + if (isNaN(val)) { + // "+Inf", "-Inf", "+Inf" will be parsed into NaN by parseFloat(). The + // can't be graphed, so show them as gaps (null). + return null; + } + return val; +}; + +PromConsole.Graph.prototype._escapeHTML = function(string) { + var entityMap = { + "&": "&", + "<": "<", + ">": ">", + '"': '"', + "'": ''', + "/": '/' + }; + + return string.replace(/[&<>"'\/]/g, function(s) { + return entityMap[s]; + }); +}; + +PromConsole.Graph.prototype._render = function(data) { + var self = this; + var palette = new Rickshaw.Color.Palette({ scheme: this.params.colorScheme }); + var series = []; + + // This will be used on resize. + this.rendered_data = data; + + + var nameFuncs = []; + if (this.params.name === null) { + var chooser = PromConsole._chooseNameFunction(data); + for (var i = 0; i < this.params.expr.length; i++) { + nameFuncs.push(chooser); + } + } else { + for (var i = 0; i < this.params.name.length; i++) { + if (typeof this.params.name[i] == "string") { + nameFuncs.push(function(i, metric) { + return PromConsole._interpolateName(this.params.name[i], metric); + }.bind(this, i)); + } else { + nameFuncs.push(this.params.name[i]); + } + } + } + + // Get the data into the right format. + var seriesLen = 0; + + for (var e = 0; e < data.length; e++) { + for (var i = 0; i < data[e].data.result.length; i++) { + series[seriesLen] = { + data: data[e].data.result[i].values.map(function(s) { return { x: s[0], y: self._parseValue(s[1]) }; }), + color: palette.color(), + name: self._escapeHTML(nameFuncs[e](data[e].data.result[i].metric)), + }; + // Insert nulls for all missing steps. + var newSeries = []; + var pos = 0; + var start = self.params.endTime - self.params.duration; + var step = Math.floor(self.params.duration / this.graphTd.offsetWidth * 1000) / 1000; + for (var t = start; t <= self.params.endTime; t += step) { + // Allow for floating point inaccuracy. + if (series[seriesLen].data.length > pos && series[seriesLen].data[pos].x < t + step / 100) { + newSeries.push(series[seriesLen].data[pos]); + pos++; + } else { + newSeries.push({ x: t, y: null }); + } + } + series[seriesLen].data = newSeries; + seriesLen++; + } + } + this._clearGraph(); + if (!series.length) { + var errorText = document.createElement("div"); + errorText.className = 'prom_graph_error'; + errorText.textContent = 'No timeseries returned'; + this.graphTd.appendChild(errorText); + return; + } + // Render. + var graph = new Rickshaw.Graph({ + interpolation: "linear", + width: this.graphTd.offsetWidth, + height: this.params.height, + element: this.graphTd, + renderer: this.params.renderer, + max: this.params.max, + min: this.params.min, + series: series + }); + var hoverDetail = new Rickshaw.Graph.HoverDetail({ + graph: graph, + onRender: function() { + var xLabel = this.element.getElementsByClassName("x_label")[0]; + var item = this.element.getElementsByClassName("item")[0]; + if (xLabel.offsetWidth + xLabel.offsetLeft + this.element.offsetLeft > graph.element.offsetWidth || + item.offsetWidth + item.offsetLeft + this.element.offsetLeft > graph.element.offsetWidth) { + xLabel.classList.add("prom_graph_hover_flipped"); + item.classList.add("prom_graph_hover_flipped"); + } else { + xLabel.classList.remove("prom_graph_hover_flipped"); + item.classList.remove("prom_graph_hover_flipped"); + } + }, + yFormatter: function(y) { + if (y === null) { + return ""; + } + return this.params.yHoverFormatter(y) + this.params.yUnits; + }.bind(this) + }); + var yAxis = new Rickshaw.Graph.Axis.Y({ + graph: graph, + tickFormat: this.params.yAxisFormatter, + ticks: this.params.yAxisTicks + }); + var xAxis = new Rickshaw.Graph.Axis.Time({ + graph: graph, + }); + var legend = new Rickshaw.Graph.Legend({ + graph: graph, + element: this.legendDiv + }); + xAxis.render(); + yAxis.render(); + graph.render(); + + this.rickshawGraph = graph; +}; + +PromConsole.Graph.prototype._clearGraph = function() { + while (this.graphTd.lastChild) { + this.graphTd.removeChild(this.graphTd.lastChild); + } + while (this.legendDiv.lastChild) { + this.legendDiv.removeChild(this.legendDiv.lastChild); + } + this.rickshawGraph = null; +}; + +PromConsole.Graph.prototype._xhrs = []; + +PromConsole.Graph.prototype.buildQueryUrl = function(expr) { + var p = this.params; + return PATH_PREFIX + "/api/v1/query_range?query=" + + encodeURIComponent(expr) + + "&step=" + Math.floor(p.duration / this.graphTd.offsetWidth * 1000) / 1000 + + "&start=" + (p.endTime - p.duration) + "&end=" + p.endTime; +}; + +PromConsole.Graph.prototype.dispatch = function() { + for (var j = 0; j < this._xhrs.length; j++) { + this._xhrs[j].abort(); + } + var all_data = new Array(this.params.expr.length); + this._xhrs = new Array(this.params.expr.length); + var pending_requests = this.params.expr.length; + for (var i = 0; i < this.params.expr.length; ++i) { + var endTime = this.params.endTime; + var url = this.buildQueryUrl(this.params.expr[i]); + var xhr = new XMLHttpRequest(); + xhr.open('get', url, true); + xhr.responseType = 'json'; + xhr.onerror = function(xhr, i) { + this._clearGraph(); + var errorText = document.createElement("div"); + errorText.className = 'prom_graph_error'; + errorText.textContent = 'Error loading data'; + this.graphTd.appendChild(errorText); + console.log('Error loading data for ' + this.params.expr[i]); + pending_requests = 0; + // onabort gets any aborts. + for (var j = 0; j < pending_requests; j++) { + this._xhrs[j].abort(); + } + }.bind(this, xhr, i); + xhr.onload = function(xhr, i) { + if (pending_requests === 0) { + // Got an error before this success. + return; + } + var data = xhr.response; + if (typeof data !== "object") { + data = JSON.parse(xhr.responseText); + } + pending_requests -= 1; + all_data[i] = data; + if (pending_requests === 0) { + this._xhrs = []; + this._render(all_data); + } + }.bind(this, xhr, i); + xhr.send(); + this._xhrs[i] = xhr; + } + + var loadingImg = document.createElement("img"); + loadingImg.src = PATH_PREFIX + '/user/ajax-loader.gif'; + loadingImg.alt = 'Loading...'; + loadingImg.className = 'prom_graph_loading'; + this.graphTd.appendChild(loadingImg); +}; + +// Substitute the value of 'label' for [[ label ]]. +PromConsole._interpolateName = function(name, metric) { + var re = /(.*?)\[\[\s*(\w+)+\s*\]\](.*?)/g; + var result = ''; + while (match = re.exec(name)) { + result = result + match[1] + metric[match[2]] + match[3]; + } + if (!result) { + return name; + } + return result; +}; + +// Given the data returned by the API, return an appropriate function +// to return plot names. +PromConsole._chooseNameFunction = function(data) { + // By default, use the full metric name. + var nameFunc = function(metric) { + var name = metric.__name__ + "{"; + for (var label in metric) { + if (label.substring(0, 2) == "__") { + continue; + } + name += label + "='" + metric[label] + "',"; + } + return name + "}"; + }; + + // If only one label varies, use that value. + var labelValues = {}; + for (var e = 0; e < data.length; e++) { + for (var i = 0; i < data[e].data.result.length; i++) { + for (var label in data[e].data.result[i].metric) { + if (!(label in labelValues)) { + labelValues[label] = {}; + } + labelValues[label][data[e].data.result[i].metric[label]] = 1; + } + } + } + + var multiValueLabels = []; + for (var label in labelValues) { + if (Object.keys(labelValues[label]).length > 1) { + multiValueLabels.push(label); + } + } + if (multiValueLabels.length == 1) { + nameFunc = function(metric) { + return metric[multiValueLabels[0]]; + }; + } + return nameFunc; +}; + +// Given a list of expressions, produce the /graph url for them. +PromConsole._graphsToSlashGraphURL = function(exprs) { + var data = []; + for (var i = 0; i < exprs.length; ++i) { + data.push({ 'expr': exprs[i], 'tab': 0 }); + } + return PATH_PREFIX + '/graph#' + encodeURIComponent(JSON.stringify(data)); + +}; -- cgit 1.4.1