Compare commits

...

3 commits

Author SHA1 Message Date
Milan Pässler c66aeb2f8a Merge branch 'master' of https://github.com/plumpudding/hopglass into prometheus-charts 2016-04-01 00:53:19 +02:00
Milan Pässler e685fe385f Merge branch 'master' into prometheus-charts 2016-03-29 02:57:28 +02:00
Michael Rüttgers 2a58bcf5f1 infobox/node: render statistic charts based on prometheus backend
history:

Frontend generated node charts with graphite backend
Resolution for bower conflict
Let the user configure the metrics in config.json
Fix grunt task for deploying c3 css
Fix tooltip layout issue
2016-03-29 02:55:36 +02:00
10 changed files with 347 additions and 11 deletions

3
app.js
View file

@ -14,7 +14,8 @@ require.config({
"virtual-dom": "../bower_components/virtual-dom/dist/virtual-dom", "virtual-dom": "../bower_components/virtual-dom/dist/virtual-dom",
"rbush": "../bower_components/rbush/rbush", "rbush": "../bower_components/rbush/rbush",
"helper": "../helper", "helper": "../helper",
"jshashes": "../bower_components/jshashes/hashes" "jshashes": "../bower_components/jshashes/hashes",
"c3": "../bower_components/c3/c3.min"
}, },
shim: { shim: {
"leaflet.label": ["leaflet"], "leaflet.label": ["leaflet"],

View file

@ -25,12 +25,16 @@
"virtual-dom": "~2.0.1", "virtual-dom": "~2.0.1",
"leaflet-providers": "~1.0.27", "leaflet-providers": "~1.0.27",
"rbush": "https://github.com/mourner/rbush.git#~1.3.5", "rbush": "https://github.com/mourner/rbush.git#~1.3.5",
"jshashes": "~1.0.5" "jshashes": "~1.0.5",
"c3": "~0.4.10"
}, },
"authors": [ "authors": [
"Milan Pässler <me@petabyteboy.de>", "Milan Pässler <me@petabyteboy.de>",
"Nils Schneider <nils@nilsschneider.net>" "Nils Schneider <nils@nilsschneider.net>"
], ],
"license": "AGPL3", "license": "AGPL3",
"private": true "private": true,
"resolutions": {
"d3": "~3.5.5"
}
} }

View file

@ -1,6 +1,10 @@
{ {
"dataPath": "https://map.luebeck.freifunk.net/data/", "dataPath": [
"siteName": "Freifunk Lübeck", "http://map.ffgl.eu/data/",
"http://karte.ffdus.de/data/",
"http://karte.freifunk-iserlohn.de/data/"
],
"siteName": "Freifunk Fluss",
"mapSigmaScale": 0.5, "mapSigmaScale": 0.5,
"showContact": true, "showContact": true,
"maxAge": 14, "maxAge": 14,
@ -19,9 +23,40 @@
} }
], ],
"siteNames": [ "siteNames": [
{ "site": "ffhl", "name": "Lübeck" }, { "site": "ffgl-bcd", "name": "Burscheid" },
{ "site": "ffeh", "name": "Entenhausen" }, { "site": "ffgl-bgl", "name": "Bergisch Gladbach" },
{ "site": "ffgt", "name": "Gothamcity" }, { "site": "ffgl-lln", "name": "Leichlingen" },
{ "site": "ffal", "name": "Atlantis" } { "site": "ffgl-ode", "name": "Odenthal" },
{ "site": "ffgl-ovr", "name": "Overath" },
{ "site": "ffgl-rrh", "name": "Rösrath" },
{ "site": "ffdus", "name": "Flingern" }
{ "site": "ffis", "name": "Iserlohn" }
],
"nodeCharts": [
{
"name": "Statistik",
"metrics": [
{
"id": "clients",
"color": "#1566A9",
"label": "Clients",
"query": "sum(statistics_clients_total{nodeid=%22{{NODE_ID}}%22})"
},
{
"id": "load",
"color": "1566A9",
"label": "Systemlast",
"query": "sum(statistics_loadavg{nodeid=%22{{NODE_ID}}%22})"
},
{
"id": "rx",
"color": "#1566A9",
"label": "Traffic (RX, kbps)",
"query": "sum(rate(statistics_traffic_rx_bytes{nodeid=%22{{NODE_ID}}%22}[30m])/125)"
}
],
"defaultMetric": "clients",
"url": "https://prometheus.map.eulenfunk.de/api/v1/query_range"
}
] ]
} }

View file

@ -6,6 +6,7 @@
<link rel="stylesheet" href="css/ionicons.min.css"> <link rel="stylesheet" href="css/ionicons.min.css">
<link rel="stylesheet" href="roboto-slab-fontface.css"> <link rel="stylesheet" href="roboto-slab-fontface.css">
<link rel="stylesheet" href="roboto-fontface.css"> <link rel="stylesheet" href="roboto-fontface.css">
<link rel="stylesheet" href="c3.min.css">
<link rel="stylesheet" href="style.css"> <link rel="stylesheet" href="style.css">
<script src="vendor/es6-shim/es6-shim.min.js"></script> <script src="vendor/es6-shim/es6-shim.min.js"></script>
<script src="app.js"></script> <script src="app.js"></script>

View file

@ -8,6 +8,7 @@
<link rel="stylesheet" href="bower_components/leaflet/dist/leaflet.css"> <link rel="stylesheet" href="bower_components/leaflet/dist/leaflet.css">
<link rel="stylesheet" href="bower_components/Leaflet.label/dist/leaflet.label.css"> <link rel="stylesheet" href="bower_components/Leaflet.label/dist/leaflet.label.css">
<link rel="stylesheet" href="bower_components/ionicons/css/ionicons.min.css"> <link rel="stylesheet" href="bower_components/ionicons/css/ionicons.min.css">
<link rel="stylesheet" href="bower_components/c3/c3.min.css">
<link rel="stylesheet" href="style.css"> <link rel="stylesheet" href="style.css">
<script src="bower_components/es6-shim/es6-shim.min.js"></script> <script src="bower_components/es6-shim/es6-shim.min.js"></script>
<script src="bower_components/requirejs/require.js" data-main="app"></script> <script src="bower_components/requirejs/require.js" data-main="app"></script>

255
lib/infobox/charts.js Normal file
View file

@ -0,0 +1,255 @@
define(["c3", "d3"], function (c3, d3) {
var charts = function (node, config) {
this.node = node
this.chartConfig = config
this.chart = null
this.zoomConfig = {
"levels": [
{"label": "8h", "from": 8 * 3600, "interval": 15 * 60}
/*{"label": "24h", "from": "26h", "interval": "1h"},
{"label": "1m", "from": "1mon", "interval": "1d"},
{"label": "1y", "from": 31536000, "interval": 2628000}*/
]
}
this.zoomLevel = 0
this.c3Config = {
"size": {
"height": 240
},
padding: {
bottom: 30
},
"legend": {
"item": {
"onclick": function (id) {
this.api.hide()
this.api.show(id)
}
}
},
"tooltip": {
"format": {
"value": this.c3FormatToolTip.bind(this)
}
},
"axis": {
"x": {
"type": "timeseries",
"tick": {
"format": this.c3FormatXAxis.bind(this),
"rotate": -45
}
},
"y": {
"min": 0,
"padding": {
"bottom": 0
}
}
}
}
this.cache = []
this.init()
}
charts.prototype = {
init: function () {
// Workaround for endless loop bug
if (this.c3Config.axis.x.tick && this.c3Config.axis.x.tick.format && typeof this.c3Config.axis.x.tick.format === "function") {
if (this.c3Config.axis.x.tick.format && !this.c3Config.axis.x.tick._format)
this.c3Config.axis.x.tick._format = this.c3Config.axis.x.tick.format
this.c3Config.axis.x.tick.format = function (val) {
return this.c3Config.axis.x.tick._format(val)
}.bind(this)
}
// Configure metrics
this.c3Config.data = {
"keys": {
"x": "time",
"value": this.chartConfig.metrics.map(function (metric) {
return metric.id
})
},
"colors": this.chartConfig.metrics.reduce(function (collector, metric) {
collector[metric.id] = metric.color
return collector
}, {}),
"names": this.chartConfig.metrics.reduce(function (collector, metric) {
collector[metric.id] = metric.label
return collector
}, {}),
"hide": this.chartConfig.metrics.map(function (metric) {
return metric.id
}).filter(function (id) {
return id !== this.chartConfig.defaultMetric
}.bind(this))
}
},
render: function () {
var div = document.createElement("div")
div.classList.add("chart")
var h4 = document.createElement("h4")
h4.textContent = this.chartConfig.name
div.appendChild(h4)
// Render chart
this.load(function (data) {
div.appendChild(this.renderChart(data))
// Render zoom controls
if (this.zoomConfig.levels.length > 0)
div.appendChild(this.renderZoomControls())
}.bind(this))
return div
},
renderChart: function (data) {
this.c3Config.data.json = data
this.chart = c3.generate(this.c3Config)
return this.chart.element
},
updateChart: function (data) {
this.c3Config.data.json = data
this.chart.load(this.c3Config.data)
},
renderZoomControls: function () {
// Draw zoom controls
var zoomDiv = document.createElement("div")
zoomDiv.classList.add("zoom-buttons")
var zoomButtons = []
this.zoomConfig.levels.forEach(function (v, level) {
var btn = document.createElement("button")
btn.classList.add("zoom-button")
btn.setAttribute("data-zoom-level", level)
if (level === this.zoomLevel)
btn.classList.add("active")
btn.onclick = function () {
if (level !== this.zoomLevel) {
zoomButtons.forEach(function (v, k) {
if (level !== k)
v.classList.remove("active")
else
v.classList.add("active")
})
this.setZoomLevel(level)
}
}.bind(this)
btn.textContent = v.label
zoomButtons[level] = btn
zoomDiv.appendChild(btn)
}.bind(this))
return zoomDiv
},
setZoomLevel: function (level) {
if (level !== this.zoomLevel) {
this.zoomLevel = level
this.load(this.updateChart.bind(this))
}
},
load: function (callback) {
if (this.cache[this.zoomLevel])
callback(this.cache[this.zoomLevel])
else {
var urls = []
var id = this.node.nodeinfo.node_id
var unixStamp = Math.floor(Date.now() / 1000)
var zoomConfig = this.zoomConfig.levels[this.zoomLevel]
this.chartConfig.metrics.forEach(function(metric) {
var parameters = [
"start=" + (unixStamp - zoomConfig.from),
"end=" + unixStamp,
"step=" + zoomConfig.interval,
"query=" + metric.query.replace("{{NODE_ID}}", id)
]
var url = this.chartConfig.url + "?" + parameters.join("&")
urls.push(url)
}.bind(this))
Promise.all(urls.map(getJSON)).then(function (data) {
this.cache[this.zoomLevel] = this.parse(data)
callback(this.cache[this.zoomLevel])
}.bind(this))
}
},
parse: function (results) {
var data = []
results[0].data.result[0].values.forEach(function (tp) {
var time = {"time": new Date(tp[0] * 1000)}
results.forEach(function(result) {
var metric = this.chartConfig.metrics[results.indexOf(result)]
var index = results[0].data.result[0].values.indexOf(tp)
time[metric.id] = this.formatValue(metric.id, result.data.result[0].values[index][1])
}.bind(this))
data.push(time)
}.bind(this))
return data
},
c3FormatToolTip: function (d, ratio, id) {
switch (id) {
case "uptime":
return d.toFixed(1) + " Tage"
default:
return d
}
},
c3FormatXAxis: function (d) {
var pad = function (number, pad) {
var N = Math.pow(10, pad)
return number < N ? ("" + (N + number)).slice(1) : "" + number
}
switch (this.zoomLevel) {
case 0: // 8h
case 1: // 24h
return pad(d.getHours(), 2) + ":" + pad(d.getMinutes(), 2)
case 2: // 1m
return pad(d.getDate(), 2) + "." + pad(d.getMonth() + 1, 2)
case 3: // 1y
return ["Jan", "Feb", "Mrz", "Apr", "Mai", "Jun", "Jul", "Aug", "Sep", "Okt", "Nov", "Dez"][d.getMonth()]
default:
break
}
},
formatValue: function (id, value) {
switch (id) {
case "loadavg":
return (d3.format(".2r")(value))
case "clientcount":
return (Math.ceil(value))
case "uptime":
return (value / 86400)
default:
return value
}
}
}
return charts
})

View file

@ -1,5 +1,5 @@
define(["moment", "numeral", "tablesort", "tablesort.numeric"], define(["moment", "numeral", "tablesort", "infobox/charts", "tablesort.numeric"],
function (moment, numeral, Tablesort) { function (moment, numeral, Tablesort, Charts) {
function showGeoURI(d) { function showGeoURI(d) {
function showLatitude(d) { function showLatitude(d) {
var suffix = Math.sign(d) > -1 ? "'N" : "'S" var suffix = Math.sign(d) > -1 ? "'N" : "'S"
@ -258,6 +258,10 @@ define(["moment", "numeral", "tablesort", "tablesort.numeric"],
el.appendChild(attributes) el.appendChild(attributes)
if (!d.flags.gateway && config.nodeCharts)
config.nodeCharts.forEach( function (config) {
el.appendChild((new Charts(d, config)).render())
})
if (config.nodeInfos) if (config.nodeInfos)
config.nodeInfos.forEach( function (nodeInfo) { config.nodeInfos.forEach( function (nodeInfo) {

28
scss/_chart.scss Normal file
View file

@ -0,0 +1,28 @@
.infobox .chart {
position: relative;
& > .c3 {
margin-right: 20px;
}
.zoom-buttons {
position: absolute;
top: 0px;
right: 20px;
button {
font-size: 10pt;
width: 3em;
height: 3em;
border-radius: 1.5em;
margin-left: 6px;
}
}
.c3-tooltip-container {
width: 150px;
.c3-tooltip {
border-collapse: collapse;
}
}
}

View file

@ -14,6 +14,7 @@ $buttondistance: 12pt;
@import '_map'; @import '_map';
@import '_forcegraph'; @import '_forcegraph';
@import '_legend'; @import '_legend';
@import '_chart';
.content { .content {
position: fixed; position: fixed;

View file

@ -53,6 +53,12 @@ module.exports = function(grunt) {
expand: true, expand: true,
dest: "build/", dest: "build/",
cwd: "bower_components/leaflet/dist/" cwd: "bower_components/leaflet/dist/"
},
c3: {
src: ["c3.min.css"],
expand: true,
dest: "build/",
cwd: "bower_components/c3/"
} }
}, },
sass: { sass: {