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" }} +
  • {{ .arg2 }}
  • +{{ 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"}} +
    +
    + +
    + + + +
    + +
    + + + +
    +
    +
    + + + +
    +
    +
    + +
    +{{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)); + +};