diff --git a/console_libraries/menu.lib b/console_libraries/menu.lib
new file mode 100644
index 000000000..9842f1fcc
--- /dev/null
+++ b/console_libraries/menu.lib
@@ -0,0 +1,56 @@
+{{/* vim: set ft=html: */}}
+
+{{/* Navbar, should be passed . */}}
+{{ define "navbar" }}
+
+{{ end }}
+
+{{/* LHS menu, should be passed . */}}
+{{ define "menu" }}
+
+{{ end }}
+
+{{/* Helper, pass (args . path name) */}}
+{{ define "_menuItem" }}
+
+{{ end }}
+
diff --git a/console_libraries/prom.lib b/console_libraries/prom.lib
new file mode 100644
index 000000000..fd21a3034
--- /dev/null
+++ b/console_libraries/prom.lib
@@ -0,0 +1,112 @@
+{{/* vim: set ft=html: */}}
+{{/* Load Prometheus console library JS/CSS. Should go in */}}
+{{define "prom_console_head"}}
+
+
+
+
+
+
+
+
+
+{{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 "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 }}"
+
+{{/* Top and bottom of table on RHS */}}
+{{define "prom_right_table_head"}}
+
+
+{{end}}
+{{define "prom_right_table_tail"}}
+
+
+
+{{end}}
+
+{{/* Top and bottom of main content */}}
+{{define "prom_content_head"}}
+
+{{template "prom_graph_timecontrol" .}}
+{{end}}
+{{define "prom_content_tail"}}
+
+{{end}}
+
+{{define "prom_graph_timecontrol"}}
+
+
+
Range:
+
+
+
+
+
+
+
+
+
+
End:
+
+
+
+
+
+
+
+
+
+
+
+
+
+{{end}}
+
+{{/* Bottom of all pages. */}}
+{{define "tail"}}
+
+
+{{end}}
diff --git a/consoles/index.html.example b/consoles/index.html.example
new file mode 100644
index 000000000..4279b4b6f
--- /dev/null
+++ b/consoles/index.html.example
@@ -0,0 +1,11 @@
+{{ template "head" . }}
+
+{{template "prom_right_table_head"}}
+{{template "prom_right_table_tail"}}
+
+{{template "prom_content_head" .}}
+Overview
+These are example consoles for Prometheus, they are still under development.
+{{template "prom_content_tail" .}}
+
+{{template "tail"}}
diff --git a/consoles/node-cpu.html b/consoles/node-cpu.html
new file mode 100644
index 000000000..678b27c35
--- /dev/null
+++ b/consoles/node-cpu.html
@@ -0,0 +1,58 @@
+{{template "head" .}}
+
+{{template "prom_right_table_head"}}
+ CPU
+{{ range printf "sum by (mode)(rate(node_cpu{job='node',instance='%s'}[5m])) * 100 / scalar(count(count by (cpu)(node_cpu{job='node',instance='%s'})))" .Params.instance .Params.instance | query | sortByLabel "mode"}}
+
+ {{ .Labels.mode | title }} CPU
+ {{ .Value | printf "%.3g" }}%
+
+{{ end }}
+ Misc
+
+ Processes Running
+ {{ template "prom_query_drilldown" (args (printf "node_procs_running{job='node',instance='%s'}" .Params.instance) "" "humanize") }}
+
+
+ Processes Blocked
+ {{ template "prom_query_drilldown" (args (printf "node_procs_blocked{job='node',instance='%s'}" .Params.instance) "" "humanize") }}
+
+
+ Forks
+ {{ template "prom_query_drilldown" (args (printf "rate(node_forks{job='node',instance='%s'}[5m])" .Params.instance) "/s" "humanize") }}
+
+
+ Context Switches
+ {{ template "prom_query_drilldown" (args (printf "rate(node_context_switches{job='node',instance='%s'}[5m])" .Params.instance) "/s" "humanize") }}
+
+
+ Interrupts
+ {{ template "prom_query_drilldown" (args (printf "rate(node_intr{job='node',instance='%s'}[5m])" .Params.instance) "/s" "humanize") }}
+
+
+ 1m Loadavg
+ {{ template "prom_query_drilldown" (args (printf "node_load1{job='node',instance='%s'}" .Params.instance)) }}
+
+
+
+{{template "prom_right_table_tail"}}
+
+{{template "prom_content_head" .}}
+ Node CPU - {{ reReplaceAll "(.*?://)([^:/]+?)(:\\d+)?/.*" "$2" .Params.instance }}
+
+ CPU Usage
+
+
+{{template "prom_content_tail" .}}
+
+{{template "tail"}}
diff --git a/consoles/node-disk.html b/consoles/node-disk.html
new file mode 100644
index 000000000..c304d894e
--- /dev/null
+++ b/consoles/node-disk.html
@@ -0,0 +1,76 @@
+{{template "head" .}}
+
+{{template "prom_right_table_head"}}
+ Disks
+
+{{ range printf "node_disk_io_time_ms{job='node',instance='%s'}" .Params.instance | query | sortByLabel "device"}}
+ {{ .Labels.device }}
+
+ Utilization
+ {{ template "prom_query_drilldown" (args (printf "rate(node_disk_io_time_ms{job='node',instance='%s',device='%s'}[5m]) / 1000 * 100" .Labels.instance .Labels.device) "%" "printf.3g") }}
+
+
+ Throughput
+ {{ template "prom_query_drilldown" (args (printf "rate(node_disk_sectors_read{job='node',instance='%s',device='%s'}[5m]) * 512 + rate(node_disk_sectors_written{job='node',instance='%s',device='%s'}[5m]) * 512" .Labels.instance .Labels.device .Labels.instance .Labels.device) "B/s" "humanize") }}
+
+
+ Avg Read Time
+ {{ template "prom_query_drilldown" (args (printf "rate(node_disk_read_time_ms{job='node',instance='%s',device='%s'}[5m]) / 1000 / rate(node_disk_reads_completed{job='node',instance='%s',device='%s'}[5m])" .Labels.instance .Labels.device .Labels.instance .Labels.device) "s" "humanize") }}
+
+
+ Avg Write Time
+ {{ template "prom_query_drilldown" (args (printf "rate(node_disk_write_time_ms{job='node',instance='%s',device='%s'}[5m]) / 1000 / rate(node_disk_writes_completed{job='node',instance='%s',device='%s'}[5m])" .Labels.instance .Labels.device .Labels.instance .Labels.device) "s" "humanize") }}
+
+{{ end }}
+ Filesystem Fullness
+
+{{ define "roughlyNearZero"}}
+{{ if gt .1 . }}~0{{ else }}{{ printf "%.3g" . }}{{ end }}
+{{ end }}
+{{ range printf "node_filesystem_size{job='node',instance='%s'}" .Params.instance | query | sortByLabel "filesystem"}}
+
+ {{.Labels.filesystem}}
+ {{ template "prom_query_drilldown" (args (printf "100 - node_filesystem_free{job='node',instance='%s',filesystem='%s'} / node_filesystem_size{job='node'} * 100" .Labels.instance .Labels.filesystem) "%" "roughlyNearZero") }}
+
+{{ end }}
+
+
+{{template "prom_right_table_tail"}}
+
+{{template "prom_content_head" .}}
+ Node Disk - {{ reReplaceAll "(.*?://)([^:/]+?)(:\\d+)?/.*" "$2" .Params.instance }}
+
+ Disk I/O Utilization
+
+
+ Filesystem Usage
+
+
+{{template "prom_content_tail" .}}
+
+{{template "tail"}}
diff --git a/consoles/node-overview.html b/consoles/node-overview.html
new file mode 100644
index 000000000..6bbc1d4f8
--- /dev/null
+++ b/consoles/node-overview.html
@@ -0,0 +1,122 @@
+{{template "head" .}}
+
+{{template "prom_right_table_head"}}
+ Overview
+
+ User CPU
+ {{ template "prom_query_drilldown" (args (printf "sum(rate(node_cpu{job='node',instance='%s',mode='user'}[5m])) * 100 / count(count by (cpu)(node_cpu{job='node',instance='%s'}))" .Params.instance .Params.instance) "%" "printf.3g") }}
+
+
+ System CPU
+ {{ template "prom_query_drilldown" (args (printf "sum(rate(node_cpu{job='node',instance='%s',mode='system'}[5m])) * 100 / count(count by (cpu)(node_cpu{job='node',instance='%s'}))" .Params.instance .Params.instance) "%" "printf.3g") }}
+
+
+ Memory Total
+ {{ template "prom_query_drilldown" (args (printf "node_memory_MemTotal{job='node',instance='%s'}" .Params.instance) "B" "humanize1024") }}
+
+
+ Memory Free
+ {{ template "prom_query_drilldown" (args (printf "node_memory_MemFree{job='node',instance='%s'}" .Params.instance) "B" "humanize1024") }}
+
+
+ Network
+
+{{ range printf "node_network_receive_bytes{job='node',instance='%s',device!='lo'}" .Params.instance | query | sortByLabel "device"}}
+
+ {{ .Labels.device }} Received
+ {{ template "prom_query_drilldown" (args (printf "rate(node_network_receive_bytes{job='node',instance='%s',device='%s'}[5m])" .Labels.instance .Labels.device) "B/s" "humanize") }}
+
+
+ {{ .Labels.device }} Transmitted
+ {{ template "prom_query_drilldown" (args (printf "rate(node_network_transmit_bytes{job='node',instance='%s',device='%s'}[5m])" .Labels.instance .Labels.device) "B/s" "humanize") }}
+
+{{ end }}
+
+
+ Disks
+
+{{ range printf "node_disk_io_time_ms{job='node',instance='%s',device!~'^(md\\d+$|dm-)'}" .Params.instance | query | sortByLabel "device"}}
+
+ {{ .Labels.device }} Utilization
+ {{ template "prom_query_drilldown" (args (printf "rate(node_disk_io_time_ms{job='node',instance='%s',device='%s'}[5m]) / 1000 * 100" .Labels.instance .Labels.device) "%" "printf.3g") }}
+
+{{ end }}
+{{ range printf "node_disk_io_time_ms{job='node',instance='%s'}" .Params.instance | query | sortByLabel "device"}}
+
+ {{ .Labels.device }} Throughput
+ {{ template "prom_query_drilldown" (args (printf "rate(node_disk_sectors_read{job='node',instance='%s',device='%s'}[5m]) * 512 + rate(node_disk_sectors_written{job='node',instance='%s',device='%s'}[5m]) * 512" .Labels.instance .Labels.device .Labels.instance .Labels.device) "B/s" "humanize") }}
+
+{{ end }}
+
+ Filesystem Fullness
+
+{{ define "roughlyNearZero"}}
+{{ if gt .1 . }}~0{{ else }}{{ printf "%.3g" . }}{{ end }}
+{{ end }}
+{{ range printf "node_filesystem_size{job='node',instance='%s'}" .Params.instance | query | sortByLabel "filesystem"}}
+
+ {{.Labels.filesystem}}
+ {{ template "prom_query_drilldown" (args (printf "100 - node_filesystem_free{job='node',instance='%s',filesystem='%s'} / node_filesystem_size{job='node'} * 100" .Labels.instance .Labels.filesystem) "%" "roughlyNearZero") }}
+
+{{ end }}
+
+{{template "prom_right_table_tail"}}
+
+{{template "prom_content_head" .}}
+ Node Overview - {{ reReplaceAll "(.*?://)([^:/]+?)(:\\d+)?/.*" "$2" .Params.instance }}
+
+ CPU Usage
+
+
+
+ Disk I/O Utilization
+
+
+
+ Memory
+
+
+{{template "prom_content_tail" .}}
+
+{{template "tail"}}
diff --git a/consoles/node.html b/consoles/node.html
new file mode 100644
index 000000000..6dea84fcc
--- /dev/null
+++ b/consoles/node.html
@@ -0,0 +1,16 @@
+{{template "head" .}}
+
+{{template "prom_right_table_head"}}
+
+ Node
+ {{ template "prom_query_drilldown" (args "sum(up{job='node'})") }} / {{ template "prom_query_drilldown" (args "count(up{job='node'})") }}
+
+{{template "prom_right_table_tail"}}
+
+{{template "prom_content_head" .}}
+Node
+
+Choose an instance on the left.
+{{template "prom_content_tail" .}}
+
+{{template "tail"}}
diff --git a/web/static/css/prom_console.css b/web/static/css/prom_console.css
new file mode 100644
index 000000000..7d24224b4
--- /dev/null
+++ b/web/static/css/prom_console.css
@@ -0,0 +1,176 @@
+.prom_lhs_menu {
+ float: left;
+ margin-right: 2ex;
+ background: #000000;
+ min-height: 100%;
+ overflow: auto;
+}
+.prom_lhs_menu ul {
+ list-style: none;
+ padding-left: .5ex;
+ margin-left: .5ex;
+}
+.prom_lhs_menu li {
+ padding-right: 1ex;
+ padding-left: 1ex;
+}
+.prom_lhs_menu a,
+.prom_lhs_menu li {
+ color: #999999;
+ display: block;
+}
+.prom_lhs_menu a {
+ text-decoration: none;
+}
+.prom_lhs_menu_selected a {
+ pointer-events: none;
+ cursor: default;
+}
+.prom_lhs_menu_selected {
+ background: #555555;
+ background-clip: padding-box;
+}
+.prom_lhs_menu span:hover,
+.prom_lhs_menu a:hover {
+ background: #666666;
+}
+
+.prom_console_rhs {
+ float: right;
+ margin-left: 1ex;
+ height: 100%;
+}
+.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. */
+}
+
+.prom_query_drilldown {
+ text-decoration: none;
+ color: black;
+}
+.prom_query_drilldown:hover, a.prom_query_drilldown:active {
+ text-decoration: underline;
+}
+
+
+.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: #000000;
+ position: fixed;
+ padding: 2px;
+ bottom: 0;
+ left: 0;
+ width: 100%;
+ text-align: center;
+ z-index: 2;
+}
+.prom_graph_timecontrol_inner {
+ display: inline-block;
+}
+.prom_graph_timecontrol button {
+ font-size: 10pt;
+ margin-right: .3em;
+ padding-top: 2px;
+ padding-bottom: 2px;
+}
+.prom_graph_timecontrol_refresh button {
+ padding-top: 0;
+ padding-bottom: 0;
+ border-bottom: 0;
+ border-top: 0;
+}
+.prom_graph_timecontrol_refresh .dropdown-menu {
+ min-width: 70px;
+ padding-top: 0;
+ padding-bottom: 0;
+}
+.prom_graph_timecontrol input {
+ font-size: 10pt;
+ margin-left: -4px;
+ margin-right: -4px;
+ text-align: center;
+}
+.prom_graph_timecontrol label {
+ display: inline;
+ color: #999999;
+}
+.prom_graph_timecontrol .input-append {
+ margin: 2px;
+}
+.prom_graph_timecontrol .input-append .btn:first-child i {
+ margin-right: 4px;
+ margin-left: 2px;
+}
+.prom_graph_timecontrol .input-append .btn:last-child i {
+ margin-left: 6px;
+}
+.prom_graph_timecontrol .input-append i {
+ padding-top: -4px;
+ margin-top: 0;
+}
diff --git a/web/static/js/prom_console.js b/web/static/js/prom_console.js
new file mode 100644
index 000000000..154df9e2d
--- /dev/null
+++ b/web/static/js/prom_console.js
@@ -0,0 +1,584 @@
+/*
+ * 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) {
+ 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 (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) {
+ 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) {
+ 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 === undefined) {
+ 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;
+ if (hash.indexOf('#pctc') == 0) {
+ return JSON.parse(decodeURIComponent(hash.substring(5)));
+ }
+ return {duration: 3600, endTime: new Date().getTime() / 1000, endTimeNow: true};
+}();
+
+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;
+ }
+ return new Date(this.endElement.value).getTime();
+};
+
+PromConsole.TimeControl.prototype.getOrSetEndDate = function() {
+ var date = this.getEndDate();
+ if (date) {
+ return date;
+ }
+ date = new Date();
+ this.setEndDate(date);
+ return date;
+}
+
+PromConsole.TimeControl.prototype.getHumanDate = function(date) {
+ var hours = date.getHours() < 10 ? '0' + date.getHours() : date.getHours();
+ var minutes = date.getMinutes() < 10 ? '0' + date.getMinutes() : date.getMinutes();
+ return date.getFullYear() + "-" + (date.getMonth()+1) + "-" + date.getDate() + " " +
+ 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.
+ 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 formatter for y values hover detail.
+ yHoverFormatter: PromConsole.NumberFormatter.humanizeExact,
+};
+
+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]
+ }
+
+ this.params = params;
+ this.rendered_data = 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._render = function(data) {
+ var palette = new Rickshaw.Color.Palette();
+ var series = [];
+
+ // This will be used on resize.
+ this.rendered_data = data;
+
+ var nameFunc;
+ if (this.params.name === null) {
+ nameFunc = PromConsole._chooseNameFunction(data);
+ } else if (typeof this.params.name == "string") {
+ nameFunc = function(metric) {
+ return PromConsole._interpolateName(this.params.name, metric);
+ }.bind(this);
+ } else {
+ nameFunc = this.params.name;
+ }
+
+ // Get the data into the right format.
+ for (var e = 0; e < data.length; e++) {
+ var len = 0;
+ for (var i = 0; i < data[e].Value.length; i++) {
+ series[len++] = {
+ data: data[e].Value[i].Values.map(function(s) {return {x: s.Timestamp, y: parseFloat(s.Value)} }),
+ color: palette.color(),
+ name: nameFunc(data[e].Value[i].Metric),
+ };
+ }
+ }
+ 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) {return this.params.yHoverFormatter(y) + this.params.yUnits}.bind(this)
+ });
+ var yAxis = new Rickshaw.Graph.Axis.Y({
+ graph: graph,
+ tickFormat: this.params.yAxisFormatter
+ });
+ 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();
+};
+
+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);
+ }
+}
+
+PromConsole.Graph.prototype._xhrs = []
+
+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 = "/api/query_range?expr=" + encodeURIComponent(this.params.expr[i])
+ + "&step=" + this.params.duration / this.graphTd.offsetWidth
+ + "&range=" + this.params.duration + "&end=" + endTime;
+ 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;
+ 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 = '/static/img/ajax-loader.gif';
+ loadingImg.alt = 'Loading...';
+ loadingImg.className = 'prom_graph_loading';
+ this.graphTd.appendChild(loadingImg);
+};
+
+// Substitue 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) {
+ 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].Value.length; i++) {
+ for (var label in data[e].Value[i].Metric) {
+ if (!(label in labelValues)) {
+ labelValues[label] = {};
+ }
+ labelValues[label][data[e].Value[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]});
+ }
+ return '/graph#' + encodeURIComponent(JSON.stringify(data));
+
+};