mirror of
https://github.com/simon987/checkup-statuspage.git
synced 2025-12-14 13:09:02 +00:00
Initial commit
This commit is contained in:
231
js/checkup.js
Normal file
231
js/checkup.js
Normal file
@@ -0,0 +1,231 @@
|
||||
// checkup is the global namespace for all checkup variables (except time).
|
||||
var checkup = checkup || {};
|
||||
|
||||
// time provides simple nanosecond-based unit measurements.
|
||||
var time = (function() {
|
||||
// now gets the current time with millisecond accuracy,
|
||||
// but as a unit of nanoseconds.
|
||||
var now = function() {
|
||||
return new Date().getTime() * 1e6;
|
||||
};
|
||||
var ns = 1,
|
||||
us = 1000 * ns,
|
||||
ms = 1000 * us,
|
||||
second = 1000 * ms,
|
||||
minute = 60 * second,
|
||||
hour = 60 * minute,
|
||||
day = 24 * hour,
|
||||
week = 7 * day;
|
||||
|
||||
return {
|
||||
Now: now,
|
||||
Nanosecond: ns,
|
||||
Microsecond: us,
|
||||
Millisecond: ms,
|
||||
Second: second,
|
||||
Minute: minute,
|
||||
Hour: hour,
|
||||
Day: day,
|
||||
Week: week
|
||||
};
|
||||
})();
|
||||
|
||||
// formatDuration formats d (in nanoseconds) with
|
||||
// a proper unit suffix based on its value.
|
||||
checkup.formatDuration = function(d) {
|
||||
if (d == 0)
|
||||
return d+"ms";
|
||||
else if (d < time.Millisecond)
|
||||
return Math.round(d*1e-3)+"µs";
|
||||
else if (d < 10 * time.Second)
|
||||
return Math.round(d*1e-6)+"ms";
|
||||
else if (d < 90 * time.Second)
|
||||
return Math.round(d*1e-9)+"s";
|
||||
else if (d < 90 * time.Minute)
|
||||
return Math.round(d*1e-9/60)+" minutes";
|
||||
else if (d < 48 * time.Hour)
|
||||
return Math.round(d*1e-9/60/60)+" hours";
|
||||
else
|
||||
return Math.round(d*1e-9 / 60/60/24)+" days";
|
||||
};
|
||||
|
||||
// I'm not even joking
|
||||
checkup.leftpad = function(str, len, ch) {
|
||||
str = String(str);
|
||||
var i = -1;
|
||||
if (!ch && ch !== 0) ch = ' ';
|
||||
len = len - str.length;
|
||||
while (++i < len) str = ch + str;
|
||||
return str;
|
||||
}
|
||||
|
||||
// timeSince renders the duration ms (in milliseconds) in human-friendly form.
|
||||
checkup.timeSince = function(ms) {
|
||||
var seconds = Math.floor((new Date() - ms) / 1000);
|
||||
var interval = Math.floor(seconds / 31536000);
|
||||
if (interval > 1) return interval + " years";
|
||||
interval = Math.floor(seconds / 2592000);
|
||||
if (interval > 1) return interval + " months";
|
||||
interval = Math.floor(seconds / 86400);
|
||||
if (interval > 1) return interval + " days";
|
||||
interval = Math.floor(seconds / 3600);
|
||||
if (interval > 1) return interval + " hours";
|
||||
interval = Math.floor(seconds / 60);
|
||||
if (interval > 1) return interval + " minutes";
|
||||
return Math.floor(seconds) + " seconds";
|
||||
};
|
||||
|
||||
// makeTimeTag returns a <time> tag (as a string) that
|
||||
// has the time since the timestamp, ms (in milliseconds).
|
||||
checkup.makeTimeTag = function(ms) {
|
||||
// dateTimeString converts ms (in milliseconds) into
|
||||
// a value usable in a <time> tag's datetime attribute.
|
||||
function dateTimeString(ms) {
|
||||
return (new Date(ms)).toUTCString();
|
||||
}
|
||||
|
||||
return '<time class="dynamic" datetime="'+dateTimeString(ms)+'">'
|
||||
+ checkup.timeSince(ms)
|
||||
+ '</time>';
|
||||
}
|
||||
|
||||
// All check files must have this suffix.
|
||||
checkup.checkFileSuffix = "-check.json";
|
||||
|
||||
// Width and height of chart viewport scale
|
||||
checkup.CHART_WIDTH = 600;
|
||||
checkup.CHART_HEIGHT = 200;
|
||||
|
||||
// A couple bits of state to coordinate rendering the page
|
||||
checkup.domReady = false; // whether DOM is loaded
|
||||
checkup.graphsMade = false; // whether graphs have been rendered at least once
|
||||
checkup.placeholdersRemoved = false; // whether chart placeholders have been removed
|
||||
|
||||
checkup.unixNanoToD3Timestamp = function(unixNanoTimestamp) {
|
||||
return new Date(unixNanoTimestamp * 1e-6);
|
||||
};
|
||||
|
||||
// Maps status names to their associated color class.
|
||||
checkup.color = {healthy: "green", degraded: "yellow", down: "red"};
|
||||
|
||||
// Stores the checks that are downloaded (1:1 ratio with check files)
|
||||
checkup.checks = [];
|
||||
|
||||
// Stores all the results, keyed by endpoint
|
||||
checkup.results = {};
|
||||
|
||||
// Stores all the results, keyed by timestamp indicated in the JSON
|
||||
// of the check file (may be multiple results with same timestamp)
|
||||
checkup.groupedResults = {};
|
||||
|
||||
// Stores the results in ascending timestamp order; order may not be
|
||||
// guaranteed until all results are loaded
|
||||
checkup.orderedResults = [];
|
||||
|
||||
// Stores the charts (keyed by endpoint) and all their data/info/elements
|
||||
checkup.charts = {};
|
||||
|
||||
// ID counter for the charts, always incremented
|
||||
checkup.chartCounter = 0;
|
||||
|
||||
// ID counter for events generated by checks, always incremented
|
||||
checkup.eventCounter = 0;
|
||||
|
||||
// Events that get rendered to the timeline
|
||||
checkup.events = [];
|
||||
|
||||
// Duration of chart animations in ms
|
||||
checkup.animDuration = 0;
|
||||
|
||||
// Quick, reusable access to DOM elements; populated after DOM loads
|
||||
checkup.dom = {};
|
||||
|
||||
// Timestamp of the last result, (taken from the 'timestamp' field
|
||||
// of JSON) as a Date() object.
|
||||
checkup.lastResultTs = null;
|
||||
|
||||
// Timestamp of the last check (taken from the first part of the
|
||||
// check file name).
|
||||
checkup.lastCheckTs = null;
|
||||
|
||||
checkup.makeChart = function(title) {
|
||||
var chart = {
|
||||
id: "chart"+(checkup.chartCounter++),
|
||||
title: title,
|
||||
results: [],
|
||||
series: {
|
||||
min: [],
|
||||
med: [],
|
||||
max: [],
|
||||
threshold: [],
|
||||
events: [],
|
||||
},
|
||||
data: []
|
||||
};
|
||||
|
||||
// add series here to add more lines to a single chart; layered
|
||||
// in order that they appear here (last series appears on top)
|
||||
chart.data = [chart.series.threshold, chart.series.med];
|
||||
|
||||
return chart;
|
||||
}
|
||||
|
||||
// getJSON downloads the file at url and executes callback
|
||||
// with the parsed JSON and the url as arguments.
|
||||
checkup.getJSON = function(url, callback) {
|
||||
var request = new XMLHttpRequest();
|
||||
request.open('GET', url, true);
|
||||
request.onload = function() {
|
||||
if (request.status >= 200 && request.status < 400) {
|
||||
var json = JSON.parse(request.responseText);
|
||||
callback(json, url);
|
||||
} else {
|
||||
console.error("GET "+url+":", request);
|
||||
}
|
||||
};
|
||||
request.onerror = function() {
|
||||
console.error("Network error (GET "+url+"):", request.error);
|
||||
};
|
||||
request.send();
|
||||
};
|
||||
|
||||
checkup.loadScript = function(url, callback) {
|
||||
var head = document.getElementsByTagName("head")[0];
|
||||
var script = document.createElement("script");
|
||||
script.type = "text/javascript";
|
||||
script.src = url;
|
||||
|
||||
script.onreadystatechange = callback;
|
||||
script.onload = callback;
|
||||
|
||||
head.appendChild(script);
|
||||
};
|
||||
|
||||
// computeStats computes basic stats about a result.
|
||||
checkup.computeStats = function(result) {
|
||||
function median(values) {
|
||||
values.sort(function(a, b) { return a.rtt - b.rtt; });
|
||||
var half = Math.floor(values.length / 2);
|
||||
if (values.length % 2 == 0)
|
||||
return Math.round((values[half-1].rtt + values[half].rtt) / 2);
|
||||
else
|
||||
return values[half].rtt;
|
||||
}
|
||||
var sum = 0, min, max;
|
||||
for (var i = 0; i < result.times.length; i++) {
|
||||
var attempt = result.times[i];
|
||||
if (!attempt.rtt) continue;
|
||||
sum += attempt.rtt;
|
||||
if (attempt.rtt < min || (typeof min === 'undefined'))
|
||||
min = attempt.rtt;
|
||||
if (attempt.rtt > max || (typeof max === 'undefined'))
|
||||
max = attempt.rtt;
|
||||
}
|
||||
return {
|
||||
total: sum,
|
||||
average: sum / result.times.length,
|
||||
median: median(result.times),
|
||||
min: min,
|
||||
max: max
|
||||
};
|
||||
};
|
||||
30
js/config.js
Normal file
30
js/config.js
Normal file
@@ -0,0 +1,30 @@
|
||||
checkup.config = {
|
||||
// How much history to show on the status page. Long durations and
|
||||
// frequent checks make for slow loading, so be conservative.
|
||||
// This value is in NANOSECONDS to mirror Go's time package.
|
||||
"timeframe": 12 * time.Hour,
|
||||
|
||||
// How often, in seconds, to pull new checks and update the page.
|
||||
"refresh_interval": 30,
|
||||
|
||||
// Configure read-only access to stored checks. This configuration
|
||||
// depends on your storage provider. Any credentials and other values
|
||||
// here will be visible to everyone, so use keys with ONLY read access!
|
||||
"storage": {
|
||||
// Amazon S3 - if using, ensure these are public, READ-ONLY credentials!
|
||||
"AccessKeyID": "<key id here>",
|
||||
"SecretAccessKey": "<not-so-secret key here>",
|
||||
"Region": "<bucket region name here if you specified one>",
|
||||
"BucketName": "<bucket name here>",
|
||||
|
||||
// Local file system (Caddy recommended: https://caddyserver.com)
|
||||
"url": "https://status.simon987.net/data/"
|
||||
},
|
||||
|
||||
// The text to display along the top bar depending on overall status.
|
||||
"status_text": {
|
||||
"healthy": "All services online",
|
||||
"degraded": "Degraded Service",
|
||||
"down": "Service Disruption"
|
||||
}
|
||||
};
|
||||
5
js/d3.v3.min.js
vendored
Normal file
5
js/d3.v3.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
68
js/fs.js
Normal file
68
js/fs.js
Normal file
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
|
||||
FS Storage Adapter for Checkup.js
|
||||
|
||||
**/
|
||||
|
||||
var checkup = checkup || {};
|
||||
|
||||
checkup.storage = (function() {
|
||||
var url;
|
||||
|
||||
// getCheckFileList gets the list of check files within
|
||||
// the given timeframe (as a unit of nanoseconds) to
|
||||
// download.
|
||||
function getCheckFileList(timeframe, callback) {
|
||||
var after = time.Now() - timeframe;
|
||||
checkup.getJSON(url+'/index.json', function(index) {
|
||||
var names = [];
|
||||
for (var name in index) {
|
||||
if (index[name] >= after) {
|
||||
names.push(name);
|
||||
}
|
||||
}
|
||||
callback(names);
|
||||
});
|
||||
};
|
||||
|
||||
// setup prepares this storage unit to operate.
|
||||
this.setup = function(cfg) {
|
||||
url = cfg.url;
|
||||
};
|
||||
|
||||
// getChecksWithin gets all the checks within timeframe as a unit
|
||||
// of nanoseconds, and executes callback for each check file.
|
||||
this.getChecksWithin = function(timeframe, fileCallback, doneCallback) {
|
||||
var checksLoaded = 0, resultsLoaded = 0;
|
||||
getCheckFileList(timeframe, function(list) {
|
||||
if (list.length == 0 && (typeof doneCallback === 'function')) {
|
||||
doneCallback(checksLoaded);
|
||||
} else {
|
||||
for (var i = 0; i < list.length; i++) {
|
||||
checkup.getJSON(url+'/'+list[i], function(filename) {
|
||||
return function(json, url) {
|
||||
checksLoaded++;
|
||||
resultsLoaded += json.length;
|
||||
if (typeof fileCallback === 'function')
|
||||
fileCallback(json, filename);
|
||||
if (checksLoaded >= list.length && (typeof doneCallback === 'function'))
|
||||
doneCallback(checksLoaded, resultsLoaded);
|
||||
};
|
||||
}(list[i]));
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// getNewChecks gets any checks since the timestamp on the file name
|
||||
// of the youngest check file that has been downloaded. If no check
|
||||
// files have been downloaded, no new check files will be loaded.
|
||||
this.getNewChecks = function(fileCallback, doneCallback) {
|
||||
if (!checkup.lastCheckTs == null)
|
||||
return;
|
||||
var timeframe = time.Now() - checkup.lastCheckTs;
|
||||
return this.getChecksWithin(timeframe, fileCallback, doneCallback);
|
||||
};
|
||||
|
||||
return this;
|
||||
})();
|
||||
513
js/statuspage.js
Normal file
513
js/statuspage.js
Normal file
@@ -0,0 +1,513 @@
|
||||
// config.js must be included BEFORE this file!
|
||||
|
||||
// IE 11 Polyfill for .remove()
|
||||
if (!('remove' in Element.prototype)) {
|
||||
Element.prototype.remove = function() {
|
||||
if (this.parentNode) {
|
||||
this.parentNode.removeChild(this);
|
||||
}
|
||||
};
|
||||
}
|
||||
// Configure access to storage
|
||||
checkup.storage.setup(checkup.config.storage);
|
||||
|
||||
// Once the DOM is loaded, go ahead and render the graphs
|
||||
// (if it hasn't been done already).
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
checkup.domReady = true;
|
||||
|
||||
checkup.dom.favicon = document.getElementById("favicon");
|
||||
checkup.dom.status = document.getElementById("overall-status");
|
||||
checkup.dom.statustext = document.getElementById("overall-status-text");
|
||||
checkup.dom.timeframe = document.getElementById("info-timeframe");
|
||||
checkup.dom.checkcount = document.getElementById("info-checkcount");
|
||||
checkup.dom.lastcheck = document.getElementById("info-lastcheck");
|
||||
checkup.dom.timeline = document.getElementById("timeline");
|
||||
// Immediately begin downloading check files, and keep page updated
|
||||
checkup.storage.getChecksWithin(checkup.config.timeframe, processNewCheckFile, allCheckFilesLoaded);
|
||||
|
||||
if (!checkup.graphsMade) makeGraphs();
|
||||
}, false);
|
||||
|
||||
setInterval(function() {
|
||||
checkup.storage.getNewChecks(processNewCheckFile, allCheckFilesLoaded);
|
||||
}, checkup.config.refresh_interval * 1000);
|
||||
|
||||
// Update "time ago" tags every so often
|
||||
setInterval(function() {
|
||||
var times = document.querySelectorAll("time.dynamic");
|
||||
for (var i = 0; i < times.length; i++) {
|
||||
var timeEl = times[i];
|
||||
var ms = Date.parse(timeEl.getAttribute("datetime"));
|
||||
timeEl.innerHTML = checkup.timeSince(ms);
|
||||
}
|
||||
}, 5000);
|
||||
|
||||
|
||||
function processNewCheckFile(json, filename) {
|
||||
checkup.checks.push(json);
|
||||
|
||||
// update the timestamp of the last check file's timestamp
|
||||
var dashLoc = filename.indexOf("-");
|
||||
if (dashLoc > 0) {
|
||||
var checkTs = Number(filename.substr(0, dashLoc));
|
||||
if (checkTs > checkup.lastCheckTs) {
|
||||
checkup.lastCheckTs = checkTs;
|
||||
}
|
||||
}
|
||||
|
||||
var process = function(result) {
|
||||
checkup.orderedResults.push(result); // will sort later, more efficient that way
|
||||
|
||||
if (!checkup.groupedResults[result.timestamp])
|
||||
checkup.groupedResults[result.timestamp] = [result];
|
||||
else
|
||||
checkup.groupedResults[result.timestamp].push(result);
|
||||
|
||||
if (!checkup.results[result.endpoint])
|
||||
checkup.results[result.endpoint] = [result];
|
||||
else
|
||||
checkup.results[result.endpoint].push(result);
|
||||
|
||||
var chart = checkup.charts[result.endpoint] || checkup.makeChart(result.title);
|
||||
chart.results.push(result);
|
||||
|
||||
var ts = checkup.unixNanoToD3Timestamp(result.timestamp);
|
||||
chart.series.min.push({ timestamp: ts, rtt: Math.round(result.stats.min) });
|
||||
chart.series.med.push({ timestamp: ts, rtt: Math.round(result.stats.median) });
|
||||
chart.series.max.push({ timestamp: ts, rtt: Math.round(result.stats.max) });
|
||||
if (result.threshold)
|
||||
chart.series.threshold.push({ timestamp: ts, rtt: result.threshold });
|
||||
|
||||
if (!checkup.lastResultTs || ts > checkup.lastResultTs) {
|
||||
checkup.lastResultTs = ts;
|
||||
checkup.dom.lastcheck.innerHTML = checkup.makeTimeTag(checkup.lastResultTs)+" ago";
|
||||
}
|
||||
return chart;
|
||||
};
|
||||
|
||||
// iterate each result and store/process it
|
||||
json.forEach(function(result) {
|
||||
// Save stats with the result so we don't have to recompute them later
|
||||
result.stats = checkup.computeStats(result);
|
||||
|
||||
var chart = process(result);
|
||||
checkup.charts[result.endpoint] = chart;
|
||||
checkup.charts[result.endpoint].endpoint = result.endpoint;
|
||||
});
|
||||
|
||||
var byTimestamp = function(a, b) {
|
||||
return a.timestamp - b.timestamp;
|
||||
};
|
||||
var values = function(obj) {
|
||||
return Object.keys(obj)
|
||||
.map(function(key) { return obj[key]; });
|
||||
};
|
||||
|
||||
values(checkup.charts)
|
||||
.forEach(function(chart) {
|
||||
values(chart.series)
|
||||
.forEach(function(series) { series.sort(byTimestamp); })
|
||||
});
|
||||
|
||||
if (checkup.domReady)
|
||||
makeGraphs();
|
||||
}
|
||||
|
||||
function allCheckFilesLoaded(numChecksLoaded, numResultsLoaded) {
|
||||
// Sort the result lists
|
||||
checkup.orderedResults.sort(function(a, b) { return a.timestamp - b.timestamp; });
|
||||
for (var endpoint in checkup.results)
|
||||
checkup.results[endpoint].sort(function(a, b) { return a.timestamp - b.timestamp; });
|
||||
|
||||
// Create events for the timeline
|
||||
|
||||
var newEvents = [];
|
||||
var statuses = {}; // keyed by endpoint
|
||||
|
||||
// First load the last known status of each endpoint
|
||||
for (var i = checkup.events.length-1; i >= 0; i--) {
|
||||
var result = checkup.events[i].result;
|
||||
if (!statuses[result.endpoint])
|
||||
statuses[result.endpoint] = checkup.events[i].status;
|
||||
}
|
||||
|
||||
// Then go through the new results and look for new events
|
||||
for (var i = checkup.orderedResults.length-numResultsLoaded; i < checkup.orderedResults.length; i++) {
|
||||
var result = checkup.orderedResults[i];
|
||||
|
||||
var status = "healthy";
|
||||
if (result.degraded) status = "degraded";
|
||||
else if (result.down) status = "down";
|
||||
|
||||
if (status != statuses[result.endpoint]) {
|
||||
// New event because status changed
|
||||
newEvents.push({
|
||||
id: checkup.eventCounter++,
|
||||
result: result,
|
||||
status: status
|
||||
});
|
||||
}
|
||||
if (result.message) {
|
||||
// New event because message posted
|
||||
newEvents.push({
|
||||
id: checkup.eventCounter++,
|
||||
result: result,
|
||||
status: status,
|
||||
message: result.message
|
||||
});
|
||||
}
|
||||
|
||||
statuses[result.endpoint] = status;
|
||||
}
|
||||
|
||||
checkup.events = checkup.events.concat(newEvents);
|
||||
|
||||
function renderTime(ns) {
|
||||
var d = new Date(ns * 1e-6);
|
||||
var hours = d.getHours();
|
||||
var ampm = "AM";
|
||||
if (hours > 12) {
|
||||
hours -= 12;
|
||||
ampm = "PM";
|
||||
}
|
||||
return hours+":"+checkup.leftpad(d.getMinutes(), 2, "0")+" "+ampm;
|
||||
}
|
||||
|
||||
// Render events
|
||||
for (var i = 0; i < newEvents.length; i++) {
|
||||
var e = newEvents[i];
|
||||
|
||||
// Save this event to the chart's event series so it will render on the graph
|
||||
var imgFile = "ok.png", imgWidth = 15, imgHeight = 15; // the different icons look smaller/larger because of their shape
|
||||
if (e.status == "down") { imgFile = "incident.png"; imgWidth = 20; imgHeight = 20; }
|
||||
else if (e.status == "degraded") { imgFile = "degraded.png"; imgWidth = 25; imgHeight = 25; }
|
||||
var chart = checkup.charts[e.result.endpoint];
|
||||
chart.series.events.push({
|
||||
timestamp: checkup.unixNanoToD3Timestamp(e.result.timestamp),
|
||||
rtt: e.result.stats.median,
|
||||
eventid: e.id,
|
||||
imgFile: imgFile,
|
||||
imgWidth: imgWidth,
|
||||
imgHeight: imgHeight
|
||||
});
|
||||
|
||||
// Render event to timeline
|
||||
var evtElem = document.createElement("div");
|
||||
evtElem.setAttribute("data-eventid", e.id);
|
||||
evtElem.classList.add("event-item");
|
||||
evtElem.classList.add("event-id-"+e.id);
|
||||
evtElem.classList.add(checkup.color[e.status]);
|
||||
if (e.message) {
|
||||
evtElem.classList.add("message");
|
||||
evtElem.innerHTML = '<div class="message-head">'+checkup.makeTimeTag(e.result.timestamp*1e-6)+' ago</div>';
|
||||
evtElem.innerHTML += '<div class="message-body">'+e.message+'</div>';
|
||||
} else {
|
||||
evtElem.classList.add("event");
|
||||
evtElem.innerHTML = '<span class="time">'+renderTime(e.result.timestamp)+'</span> '+e.result.title+" "+e.status;
|
||||
}
|
||||
checkup.dom.timeline.insertBefore(evtElem, checkup.dom.timeline.childNodes[0]);
|
||||
}
|
||||
|
||||
// Update DOM now that we have the whole picture
|
||||
|
||||
// Update overall status
|
||||
var overall = "healthy";
|
||||
for (var endpoint in checkup.results) {
|
||||
if (overall == "down") break;
|
||||
var lastResult = checkup.results[endpoint][checkup.results[endpoint].length-1];
|
||||
if (lastResult) {
|
||||
if (lastResult.down)
|
||||
overall = "down";
|
||||
else if (lastResult.degraded)
|
||||
overall = "degraded";
|
||||
}
|
||||
}
|
||||
|
||||
if (overall == "healthy") {
|
||||
checkup.dom.favicon.href = "images/status-green.png";
|
||||
checkup.dom.status.className = "green";
|
||||
checkup.dom.statustext.innerHTML = checkup.config.status_text.healthy || "System Nominal";
|
||||
} else if (overall == "degraded") {
|
||||
checkup.dom.favicon.href = "images/status-yellow.png";
|
||||
checkup.dom.status.className = "yellow";
|
||||
checkup.dom.statustext.innerHTML = checkup.config.status_text.degraded || "Sub-Optimal";
|
||||
} else if (overall == "down") {
|
||||
checkup.dom.favicon.href = "images/status-red.png";
|
||||
checkup.dom.status.className = "red";
|
||||
checkup.dom.statustext.innerHTML = checkup.config.status_text.down || "Outage";
|
||||
} else {
|
||||
checkup.dom.favicon.href = "images/status-gray.png";
|
||||
checkup.dom.status.className = "gray";
|
||||
checkup.dom.statustext.innerHTML = checkup.config.status_text.unknown || "Status Unknown";
|
||||
}
|
||||
|
||||
|
||||
// Detect big gaps in any of the charts, and if there is one, show an explanation.
|
||||
var bigGap = false;
|
||||
var lastTimeDiff;
|
||||
for (var key in checkup.charts) {
|
||||
// We expect results to be chronologically ordered, but since they are downloaded
|
||||
// in an arbitrary order due to network conditions, we have to sort to be sure.
|
||||
checkup.charts[key].results.sort(function(a, b) {
|
||||
return a.timestamp - b.timestamp;
|
||||
});
|
||||
for (var k = 1; k < checkup.charts[key].results.length; k++) {
|
||||
var timeDiff = Math.abs(checkup.charts[key].results[k].timestamp - checkup.charts[key].results[k-1].timestamp);
|
||||
bigGap = lastTimeDiff && timeDiff > lastTimeDiff * 10;
|
||||
lastTimeDiff = timeDiff;
|
||||
if (bigGap) {
|
||||
document.getElementById("big-gap").style.display = 'block';
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (bigGap) break;
|
||||
}
|
||||
if (!bigGap) {
|
||||
document.getElementById("big-gap").style.display = 'none';
|
||||
}
|
||||
|
||||
makeGraphs(); // must render graphs again after we've filled in the event series
|
||||
}
|
||||
|
||||
function makeGraphs() {
|
||||
checkup.dom.timeframe.innerHTML = checkup.formatDuration(checkup.config.timeframe);
|
||||
checkup.dom.checkcount.innerHTML = checkup.checks.length;
|
||||
|
||||
if (!checkup.placeholdersRemoved && checkup.checks.length > 0) {
|
||||
// Remove placeholder to make way for the charts;
|
||||
// placeholder necessary to give space in absense of charts.
|
||||
if (phElem = document.getElementById("chart-placeholder"))
|
||||
phElem.remove();
|
||||
checkup.placeholdersRemoved = true;
|
||||
}
|
||||
|
||||
for (var endpoint in checkup.charts) {
|
||||
makeGraph(checkup.charts[endpoint], endpoint);
|
||||
}
|
||||
|
||||
checkup.graphsMade = true;
|
||||
}
|
||||
|
||||
function makeGraph(chart, endpoint) {
|
||||
// Render chart to page if first time seeing this endpoint
|
||||
if (!chart.elem) {
|
||||
renderChart(chart);
|
||||
}
|
||||
|
||||
chart.xScale.domain([
|
||||
d3.min(chart.data, function(c) { return d3.min(c, function(d) { return d.timestamp; }); }),
|
||||
d3.max(chart.data, function(c) { return d3.max(c, function(d) { return d.timestamp; }); })
|
||||
]);
|
||||
chart.yScale.domain([
|
||||
0,
|
||||
d3.max(chart.data, function(c) { return d3.max(c, function(d) { return d.rtt; }); })
|
||||
]);
|
||||
|
||||
chart.xAxis = d3.svg.axis()
|
||||
.scale(chart.xScale)
|
||||
.ticks(5)
|
||||
.outerTickSize(0)
|
||||
.orient("bottom");
|
||||
|
||||
chart.yAxis = d3.svg.axis()
|
||||
.scale(chart.yScale)
|
||||
.tickFormat(checkup.formatDuration)
|
||||
.outerTickSize(0)
|
||||
.ticks(2)
|
||||
.orient("left");
|
||||
|
||||
if (chart.svg.selectAll(".x.axis")[0].length == 0) {
|
||||
chart.svg.insert("g", ":first-child")
|
||||
.attr("class", "x axis")
|
||||
.attr("transform", "translate(0," + chart.height + ")")
|
||||
.call(chart.xAxis);
|
||||
} else {
|
||||
chart.svg.selectAll(".x.axis")
|
||||
//.transition()
|
||||
//.duration(checkup.animDuration)
|
||||
.call(chart.xAxis);
|
||||
}
|
||||
if (chart.svg.selectAll(".y.axis")[0].length == 0) {
|
||||
chart.svg.insert("g", ":first-child")
|
||||
.attr("class", "y axis")
|
||||
.call(chart.yAxis);
|
||||
} else {
|
||||
chart.svg.selectAll(".y.axis")
|
||||
//.transition()
|
||||
//.duration(checkup.animDuration)
|
||||
.call(chart.yAxis);
|
||||
}
|
||||
|
||||
chart.lines = chart.lineGroup.selectAll(".line")
|
||||
.data(chart.data);
|
||||
chart.events = chart.eventGroup.selectAll("image")
|
||||
.data(chart.series.events);
|
||||
|
||||
// transition from old paths to new paths
|
||||
chart.lines
|
||||
//.transition()
|
||||
//.duration(checkup.animDuration)
|
||||
.attr("d", chart.line);
|
||||
chart.events
|
||||
//.transition()
|
||||
//.duration(checkup.animDuration);
|
||||
|
||||
// enter any new data (lines)
|
||||
chart.lines.enter()
|
||||
.append("path")
|
||||
.attr("class", function(d) {
|
||||
if (d == chart.series.min) return "min line";
|
||||
else if (d == chart.series.med) return "main line";
|
||||
else if (d == chart.series.max) return "max line";
|
||||
else if (d == chart.series.threshold) return "tolerance line";
|
||||
else return "line";
|
||||
})
|
||||
.attr("d", chart.line);
|
||||
|
||||
// enter any new data (events)
|
||||
chart.events.enter().append("svg:image")
|
||||
.attr("width", function(d, i) { return d.imgWidth || 0; })
|
||||
.attr("height", function(d, i) { return d.imgHeight || 0; })
|
||||
.attr("xlink:href", function(d, i) { return "images/"+d.imgFile; })
|
||||
.attr("x", function(d, i) { return chart.xScale(d.timestamp) - (d.imgWidth/2); })
|
||||
.attr("y", function(d, i) { return chart.yScale(d.rtt) - (d.imgHeight/2); })
|
||||
.attr("data-eventid", function(d, i) { return d.eventid; })
|
||||
.attr("class", function(d, i) { return "event-item event-id-"+d.eventid; })
|
||||
.on("mouseover", highlightSameEvent)
|
||||
.on("mouseout", unhighlightSameEvent);
|
||||
|
||||
// exit any old data
|
||||
chart.lines
|
||||
.exit()
|
||||
.remove();
|
||||
}
|
||||
|
||||
|
||||
function renderChart(chart) {
|
||||
// Outer div is a wrapper that we use for layout
|
||||
var el = document.createElement('div');
|
||||
var containerSize = "chart-50";
|
||||
if (document.getElementsByClassName('chart-container').length == 0) {
|
||||
containerSize = "chart-100";
|
||||
} else {
|
||||
// It's possible that a chart was created that, at the time,
|
||||
// was the only one, but now it is too wide, since there are
|
||||
// at least two charts. Resize the wide one to be smaller.
|
||||
var tooWide = document.querySelector('.chart-container.chart-100');
|
||||
if (tooWide)
|
||||
tooWide.className = "chart-container chart-50";
|
||||
}
|
||||
el.className = "chart-container "+containerSize;
|
||||
|
||||
// Div to contain the endpoint / title
|
||||
var el2 = document.createElement('div');
|
||||
el2.className = "chart-title";
|
||||
var el2b = document.createElement('a'); el2b.setAttribute("href", chart.endpoint);
|
||||
el2b.appendChild(document.createTextNode(chart.title));
|
||||
el2.appendChild(el2b);
|
||||
el.appendChild(el2);
|
||||
|
||||
// Inner div is used to contain the actual svg tag
|
||||
var el3 = document.createElement('div');
|
||||
el3.className = "chart";
|
||||
el.appendChild(el3);
|
||||
|
||||
// Inject elements into DOM
|
||||
document.getElementById('chart-grid').appendChild(el);
|
||||
|
||||
// Save it with the chart and use D3 to set up its svg element.
|
||||
chart.elem = el3;
|
||||
chart.svgTag = d3.select(chart.elem)
|
||||
.append("svg")
|
||||
.attr("id", chart.id)
|
||||
.attr("preserveAspectRatio", "xMinYMin meet")
|
||||
.attr("viewBox", "0 0 "+checkup.CHART_WIDTH+" "+checkup.CHART_HEIGHT);
|
||||
|
||||
chart.margin = {top: 20, right: 20, bottom: 40, left: 75};
|
||||
chart.width = checkup.CHART_WIDTH - chart.margin.left - chart.margin.right;
|
||||
chart.height = checkup.CHART_HEIGHT - chart.margin.top - chart.margin.bottom;
|
||||
|
||||
// chart.svgTag is the svg tag, but chart.svg
|
||||
// is the group where we actually put the lines.
|
||||
chart.svg = chart.svgTag
|
||||
.append("g")
|
||||
.attr("class", "chart-data")
|
||||
.attr("transform", "translate(" + chart.margin.left + "," + chart.margin.top + ")");
|
||||
|
||||
chart.xScale = d3.time.scale()
|
||||
.range([0, chart.width]);
|
||||
|
||||
chart.yScale = d3.scale.linear()
|
||||
.range([chart.height, 0]);
|
||||
|
||||
chart.line = d3.svg.line()
|
||||
.x(function(d) { return chart.xScale(d.timestamp); })
|
||||
.y(function(d) { return chart.yScale(d.rtt); })
|
||||
.interpolate("step-after"); // linear, monotone, or basis
|
||||
|
||||
|
||||
chart.lineGroup = chart.svg
|
||||
.append("g")
|
||||
.attr("class", "lines");
|
||||
|
||||
var focus = chart.svg
|
||||
.append("g")
|
||||
.attr("class", "focus")
|
||||
.style("display", "none");
|
||||
|
||||
focus.append("circle")
|
||||
.attr("r", 6);
|
||||
|
||||
var text = focus.append("text")
|
||||
.attr("x", 9)
|
||||
.attr("dy", ".35em")
|
||||
.attr("class", "focus-text");
|
||||
|
||||
|
||||
// Next we build an overlay to cover the data area,
|
||||
// so when the mouse hovers it we can show the point.
|
||||
var bisectDate = d3.bisector(function(d) { return d.timestamp; }).left;
|
||||
var overlay;
|
||||
var mousemove = function() {
|
||||
var x0 = chart.xScale.invert(d3.mouse(this)[0]),
|
||||
i = bisectDate(chart.series.med, x0, 1),
|
||||
d0 = chart.series.med[i - 1],
|
||||
d1 = chart.series.med[i],
|
||||
d = (d0 && d1) ? (x0 - d0.timestamp > d1.timestamp - x0 ? d1 : d0) : (d0 || d1);
|
||||
var xloc = chart.xScale(d.timestamp);
|
||||
focus.attr("transform", "translate(" + xloc + "," + chart.yScale(d.rtt) + ")");
|
||||
if (xloc > overlay.width.animVal.value - 50)
|
||||
text.attr("transform", "translate(-60, 10)");
|
||||
else
|
||||
text.attr("transform", "translate(0, 10)");
|
||||
focus.select("text").text(checkup.formatDuration(d.rtt));
|
||||
};
|
||||
chart.svg.append("rect")
|
||||
.attr("class", "overlay")
|
||||
.attr("width", chart.width)
|
||||
.attr("height", chart.height)
|
||||
.on("mouseover", function() { focus.style("display", null); })
|
||||
.on("mouseout", function() { focus.style("display", "none"); })
|
||||
.on("mousemove", mousemove);
|
||||
overlay = document.querySelector("#"+chart.id+" .overlay");
|
||||
|
||||
chart.eventGroup = chart.svg
|
||||
.append("g")
|
||||
.attr("class", "events");
|
||||
}
|
||||
|
||||
|
||||
function highlightSameEvent() {
|
||||
var elems = document.querySelectorAll(".event-item:not(.event-id-"+this.getAttribute("data-eventid")+")");
|
||||
for (var i = 0; i < elems.length; i++) {
|
||||
elems[i].style.opacity = ".25";
|
||||
}
|
||||
}
|
||||
|
||||
function unhighlightSameEvent() {
|
||||
var elems = document.querySelectorAll(".event-item:not(.event-id-"+this.getAttribute("data-eventid")+")");
|
||||
for (var i = 0; i < elems.length; i++) {
|
||||
elems[i].style.opacity = "";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user