Console templating library, including graphs.

This provides the basic js, css and console template
templates required to build dashboards.
Included as an example are consoles for the node_exporter.

Change-Id: I4cfeea5e9691a9413f74ae98ca32a908df8e4a59
This commit is contained in:
Brian Brazil 2014-11-20 18:41:25 +00:00
parent 3560ece5fb
commit 43b105d32d
9 changed files with 1211 additions and 0 deletions

View file

@ -0,0 +1,56 @@
{{/* vim: set ft=html: */}}
{{/* Navbar, should be passed . */}}
{{ define "navbar" }}
<div class="navbar navbar-inverse navbar-static-top">
<div class="navbar-inner">
<ul class="navbar-nav nav">
<a class="brand" href="/">Prometheus</a>
<li><a href="/alerts">Alerts</a></li>
<!-- Add in navbar links here -->
<li><a href="https://pagerduty.com/">PagerDuty</a></li>
</ul>
</div>
</div>
{{ end }}
{{/* LHS menu, should be passed . */}}
{{ define "menu" }}
<div class="prom_lhs_menu">
<ul>
{{ $dot := . }}
{{ template "_menuItem" (args . "index.html.example" "Overview") }}
{{ template "_menuItem" (args . "node.html" "Node") }}
{{ if match "^node" .Path }}
<ul>
{{ if .Params.instance }}
<li {{ if eq .Path "node-overview.html" }}class="prom_lhs_menu_selected"{{end}}>
<a href="node-overview.html?instance={{ .Params.instance }}">{{ reReplaceAll "(.*?://)([^:/]+?)(:\\d+)?/.*" "$2" .Params.instance }}</a>
</li>
<ul>
<li {{ if eq .Path "node-cpu.html" }}class="prom_lhs_menu_selected"{{end}}>
<a href="node-cpu.html?instance={{ .Params.instance }}">CPU</a>
</li>
<li {{ if eq .Path "node-disk.html" }}class="prom_lhs_menu_selected"{{end}}>
<a href="node-disk.html?instance={{ .Params.instance }}">Disk</a>
</li>
</ul>
{{ else }}
{{ range query "up{job='node'}" | sortByLabel "instance" }}
<li><a href="node-overview.html?instance={{ .Labels.instance }}">{{ reReplaceAll "(.*?://)([^:/]+?)(:\\d+)?/.*" "$2" .Labels.instance }}</a></li>
{{ end }}
{{ end }}
</ul>
{{ end }}
</ul>
</div>
{{ end }}
{{/* Helper, pass (args . path name) */}}
{{ define "_menuItem" }}
<li {{ if eq .arg0.Path .arg1 }} class="prom_lhs_menu_selected" {{ end }}><a href="{{ .arg1 }}">{{ .arg2 }}</a></li>
{{ end }}

112
console_libraries/prom.lib Normal file
View file

@ -0,0 +1,112 @@
{{/* vim: set ft=html: */}}
{{/* Load Prometheus console library JS/CSS. Should go in <head> */}}
{{define "prom_console_head"}}
<link type="text/css" rel="stylesheet" href="/static/vendor/rickshaw/rickshaw.min.css">
<link type="text/css" rel="stylesheet" href="/static/vendor/bootstrap/css/bootstrap.css">
<link type="text/css" rel="stylesheet" href="/static/css/prom_console.css">
<script src="/static/vendor/rickshaw/vendor/d3.min.js"></script>
<script src="/static/vendor/rickshaw/vendor/d3.layout.min.js"></script>
<script src="/static/vendor/rickshaw/rickshaw.min.js"></script>
<script src="/static/vendor/js/jquery.min.js"></script>
<script src="/static/vendor/bootstrap/js/bootstrap.min.js"></script>
<script src="/static/js/prom_console.js"></script>
{{end}}
{{/* Top of all pages. */}}
{{define "head"}}
<html>
<head>
{{template "prom_console_head"}}
</head>
<body>
{{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")}}
<a class="prom_query_drilldown" href="{{ graphLink $expr }}">{{ with query $expr }}{{tmpl $renderTemplate ( . | first | value )}}{{ $suffix }}{{else}}-{{ end }}</a>
{{ end }}
{{ define "prom_path" }}/consoles/{{.Path}}?{{range $param, $value := .Params}}{{$param}}={{$value}}&amp;{{end}}{{ end }}"
{{/* Top and bottom of table on RHS */}}
{{define "prom_right_table_head"}}
<div class="prom_console_rhs">
<table class="table table-bordered table-hover table-condensed">
{{end}}
{{define "prom_right_table_tail"}}
</table>
</div>
{{end}}
{{/* Top and bottom of main content */}}
{{define "prom_content_head"}}
<div class="prom_console_content">
{{template "prom_graph_timecontrol" .}}
{{end}}
{{define "prom_content_tail"}}
</div>
{{end}}
{{define "prom_graph_timecontrol"}}
<div class="prom_graph_timecontrol">
<div class="prom_graph_timecontrol_inner">
<label for="prom_graph_duration">Range:</label>
<div class="input-prepend input-append">
<button class="btn btn-mini" type="button" id="prom_graph_duration_shrink" title="Shrink the time range.">
<i class="icon-minus"></i>
</button>
<input class="input-mini" title="Time range of graph" type="text" id="prom_graph_duration">
<button class="btn btn-mini" type="button" id="prom_graph_duration_grow" title="Grow the time range.">
<i class="icon-plus"></i>
</button>
</div>
<label for="prom_graph_duration">End:</label>
<div class="input-prepend input-append">
<button class="btn btn-mini" type="button" id="prom_graph_time_back" title="Rewind the end time.">
<i class="icon-backward"></i>
</button>
<input class="input-medium" title="End time of graph" placeholder="Until" type="text" id="prom_graph_time_end" size="16" value="">
<button class="btn btn-mini" type="button" id="prom_graph_time_forward" title="Advance the end time.">
<i class="icon-forward"></i>
</button>
</div>
<div class="input-prepend input-append">
<div class="btn-group dropup prom_graph_timecontrol_refresh">
<button type="button" class="btn btn-mini" id="prom_graph_refresh_button">
<span class="icon-repeat"></span>
(<span id="prom_graph_refresh_button_value">Off</span>)
</button>
<button type="button" class="btn btn-mini dropdown-toggle" data-toggle="dropdown">
<span class="caret"></span>&nbsp;
</button>
<ul class="dropdown-menu" id="prom_graph_refresh_intervals" role="menu">
</ul>
</div>
</div>
</div>
<script>
new PromConsole.TimeControl();
</script>
</div>
{{end}}
{{/* Bottom of all pages. */}}
{{define "tail"}}
</body>
</html>
{{end}}

View file

@ -0,0 +1,11 @@
{{ template "head" . }}
{{template "prom_right_table_head"}}
{{template "prom_right_table_tail"}}
{{template "prom_content_head" .}}
<h1>Overview</h1>
<p>These are example consoles for Prometheus, they are still under development.</p>
{{template "prom_content_tail" .}}
{{template "tail"}}

58
consoles/node-cpu.html Normal file
View file

@ -0,0 +1,58 @@
{{template "head" .}}
{{template "prom_right_table_head"}}
<tr><th colspan="2">CPU</th></tr>
{{ 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"}}
<tr>
<td>{{ .Labels.mode | title }} CPU</td>
<td>{{ .Value | printf "%.3g" }}%</td>
</tr>
{{ end }}
<tr><th colspan="2">Misc</th></tr>
<tr>
<td>Processes Running</td>
<td>{{ template "prom_query_drilldown" (args (printf "node_procs_running{job='node',instance='%s'}" .Params.instance) "" "humanize") }}</td>
</tr>
<tr>
<td>Processes Blocked</td>
<td>{{ template "prom_query_drilldown" (args (printf "node_procs_blocked{job='node',instance='%s'}" .Params.instance) "" "humanize") }}</td>
</tr>
<tr>
<td>Forks</td>
<td>{{ template "prom_query_drilldown" (args (printf "rate(node_forks{job='node',instance='%s'}[5m])" .Params.instance) "/s" "humanize") }}</td>
</tr>
<tr>
<td>Context Switches</td>
<td>{{ template "prom_query_drilldown" (args (printf "rate(node_context_switches{job='node',instance='%s'}[5m])" .Params.instance) "/s" "humanize") }}</td>
</tr>
<tr>
<td>Interrupts</td>
<td>{{ template "prom_query_drilldown" (args (printf "rate(node_intr{job='node',instance='%s'}[5m])" .Params.instance) "/s" "humanize") }}</td>
</tr>
<tr>
<td>1m Loadavg</td>
<td>{{ template "prom_query_drilldown" (args (printf "node_load1{job='node',instance='%s'}" .Params.instance)) }}</td>
</tr>
<tr>
</tr>
{{template "prom_right_table_tail"}}
{{template "prom_content_head" .}}
<h1>Node CPU - {{ reReplaceAll "(.*?://)([^:/]+?)(:\\d+)?/.*" "$2" .Params.instance }}</h1>
<h3>CPU Usage</h3>
<div id="cpuGraph"></div>
<script>
new PromConsole.Graph({
node: document.querySelector("#cpuGraph"),
expr: "sum by (mode)(rate(node_cpu{job='node',instance='{{.Params.instance}}',mode!='idle'}[5m]))",
renderer: 'area',
max: {{ with printf "count(count by (cpu)(node_cpu{job='node',instance='%s'}))" .Params.instance | query }}{{ . | first | value}}{{else}}undefined{{end}},
yAxisFormatter: PromConsole.NumberFormatter.humanizeNoSmallPrefix,
yHoverFormatter: PromConsole.NumberFormatter.humanizeNoSmallPrefix,
yTitle: 'Cores'
})
</script>
{{template "prom_content_tail" .}}
{{template "tail"}}

76
consoles/node-disk.html Normal file
View file

@ -0,0 +1,76 @@
{{template "head" .}}
{{template "prom_right_table_head"}}
<th colspan="2">Disks</th>
</tr>
{{ range printf "node_disk_io_time_ms{job='node',instance='%s'}" .Params.instance | query | sortByLabel "device"}}
<th colspan="2">{{ .Labels.device }}</th>
<tr>
<td>Utilization</td>
<td>{{ 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") }}</td>
</tr>
<tr>
<td>Throughput</td>
<td>{{ 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") }}</td>
</tr>
<tr>
<td>Avg Read Time</td>
<td>{{ 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") }}</td>
</tr>
<tr>
<td>Avg Write Time</td>
<td>{{ 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") }}</td>
</tr>
{{ end }}
<th colspan="2">Filesystem Fullness</th>
</tr>
{{ 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"}}
<tr>
<td>{{.Labels.filesystem}}</td>
<td>{{ 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") }}</td>
</tr>
{{ end }}
<tr>
</tr>
{{template "prom_right_table_tail"}}
{{template "prom_content_head" .}}
<h1>Node Disk - {{ reReplaceAll "(.*?://)([^:/]+?)(:\\d+)?/.*" "$2" .Params.instance }}</h1>
<h3>Disk I/O Utilization</h3>
<div id="diskioGraph"></div>
<script>
new PromConsole.Graph({
node: document.querySelector("#diskioGraph"),
expr: [
"rate(node_disk_io_time_ms{job='node',instance='{{.Params.instance}}',device!~'^(md\\d+$|dm-)'}[5m]) / 1000 * 100",
],
min: 0,
name: '[[ device ]]',
yUnits: "%",
yAxisFormatter: PromConsole.NumberFormatter.humanizeNoSmallPrefix,
yHoverFormatter: PromConsole.NumberFormatter.humanizeNoSmallPrefix,
yTitle: 'Disk I/O Utilization'
})
</script>
<h3>Filesystem Usage</h3>
<div id="fsGraph"></div>
<script>
new PromConsole.Graph({
node: document.querySelector("#fsGraph"),
expr: "100 - node_filesystem_free{job='node',instance='{{ .Params.instance }}'} / node_filesystem_size{job='node'} * 100",
min: 0,
max: 100,
name: '[[ filesystem ]]',
yUnits: "%",
yAxisFormatter: PromConsole.NumberFormatter.humanizeNoSmallPrefix,
yHoverFormatter: PromConsole.NumberFormatter.humanizeNoSmallPrefix,
yTitle: 'Filesystem Fullness'
})
</script>
{{template "prom_content_tail" .}}
{{template "tail"}}

122
consoles/node-overview.html Normal file
View file

@ -0,0 +1,122 @@
{{template "head" .}}
{{template "prom_right_table_head"}}
<tr><th colspan="2">Overview</th></tr>
<tr>
<td>User CPU</td>
<td>{{ 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") }}</td>
</tr>
<tr>
<td>System CPU</td>
<td>{{ 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") }}</td>
</tr>
<tr>
<td>Memory Total</td>
<td>{{ template "prom_query_drilldown" (args (printf "node_memory_MemTotal{job='node',instance='%s'}" .Params.instance) "B" "humanize1024") }}</td>
</tr>
<tr>
<td>Memory Free</td>
<td>{{ template "prom_query_drilldown" (args (printf "node_memory_MemFree{job='node',instance='%s'}" .Params.instance) "B" "humanize1024") }}</td>
</tr>
<tr>
<th colspan="2">Network</th>
</tr>
{{ range printf "node_network_receive_bytes{job='node',instance='%s',device!='lo'}" .Params.instance | query | sortByLabel "device"}}
<tr>
<td>{{ .Labels.device }} Received</td>
<td>{{ 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") }}</td>
</tr>
<tr>
<td>{{ .Labels.device }} Transmitted</td>
<td>{{ 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") }}</td>
</tr>
{{ end }}
</tr>
<tr>
<th colspan="2">Disks</th>
</tr>
{{ range printf "node_disk_io_time_ms{job='node',instance='%s',device!~'^(md\\d+$|dm-)'}" .Params.instance | query | sortByLabel "device"}}
<tr>
<td>{{ .Labels.device }} Utilization</td>
<td>{{ 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") }}</td>
</tr>
{{ end }}
{{ range printf "node_disk_io_time_ms{job='node',instance='%s'}" .Params.instance | query | sortByLabel "device"}}
<tr>
<td>{{ .Labels.device }} Throughput</td>
<td>{{ 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") }}</td>
</tr>
{{ end }}
<tr>
<th colspan="2">Filesystem Fullness</th>
</tr>
{{ 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"}}
<tr>
<td>{{.Labels.filesystem}}</td>
<td>{{ 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") }}</td>
</tr>
{{ end }}
</tr>
{{template "prom_right_table_tail"}}
{{template "prom_content_head" .}}
<h1>Node Overview - {{ reReplaceAll "(.*?://)([^:/]+?)(:\\d+)?/.*" "$2" .Params.instance }}</h1>
<h3>CPU Usage</h3>
<div id="cpuGraph"></div>
<script>
new PromConsole.Graph({
node: document.querySelector("#cpuGraph"),
expr: "sum by (mode)(rate(node_cpu{job='node',instance='{{.Params.instance}}',mode!='idle'}[5m]))",
renderer: 'area',
max: {{ with printf "count(count by (cpu)(node_cpu{job='node',instance='%s'}))" .Params.instance | query }}{{ . | first | value}}{{else}}undefined{{end}},
yAxisFormatter: PromConsole.NumberFormatter.humanizeNoSmallPrefix,
yHoverFormatter: PromConsole.NumberFormatter.humanizeNoSmallPrefix,
yTitle: 'Cores'
})
</script>
<h3>Disk I/O Utilization</h3>
<div id="diskioGraph"></div>
<script>
new PromConsole.Graph({
node: document.querySelector("#diskioGraph"),
expr: [
"rate(node_disk_io_time_ms{job='node',instance='{{.Params.instance}}',device!~'^(md\\d+$|dm-)'}[5m]) / 1000 * 100",
],
min: 0,
name: '[[ device ]]',
yAxisFormatter: PromConsole.NumberFormatter.humanizeNoSmallPrefix,
yHoverFormatter: PromConsole.NumberFormatter.humanizeNoSmallPrefix,
yUnits: "%",
yTitle: 'Disk I/O Utilization'
})
</script>
<h3>Memory</h3>
<div id="memoryGraph"></div>
<script>
new PromConsole.Graph({
node: document.querySelector("#memoryGraph"),
renderer: 'area',
expr: [
"node_memory_Cached{job='node',instance='{{.Params.instance}}'}",
"node_memory_Buffers{job='node',instance='{{.Params.instance}}'}",
"node_memory_MemTotal{job='node',instance='{{.Params.instance}}'} - node_memory_MemFree{job='node',instance='{{.Params.instance}}'} - node_memory_Buffers{job='node',instance='{{.Params.instance}}'} - node_memory_Cached{job='node',instance='{{.Params.instance}}'}",
"node_memory_MemFree{job='node',instance='{{.Params.instance}}'}",
],
name: function(metric) {
return !metric.__name__ ? 'Used' : metric.__name__.split('_', 3)[2] },
min: 0,
yUnits: "B",
yAxisFormatter: PromConsole.NumberFormatter.humanize1024,
yHoverFormatter: PromConsole.NumberFormatter.humanize1024,
yTitle: 'Memory'
})
</script>
{{template "prom_content_tail" .}}
{{template "tail"}}

16
consoles/node.html Normal file
View file

@ -0,0 +1,16 @@
{{template "head" .}}
{{template "prom_right_table_head"}}
<tr>
<th>Node</th>
<th>{{ template "prom_query_drilldown" (args "sum(up{job='node'})") }} / {{ template "prom_query_drilldown" (args "count(up{job='node'})") }}</th>
</tr>
{{template "prom_right_table_tail"}}
{{template "prom_content_head" .}}
<h1>Node</h1>
Choose an instance on the left.
{{template "prom_content_tail" .}}
{{template "tail"}}

View file

@ -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;
}

View file

@ -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));
};