nagios4/html/js/trends-graph.js

663 lines
18 KiB
JavaScript

angular.module("trendsApp")
.directive("nagiosTrends", function() {
return {
templateUrl: "trends-graph.html",
restrict: "AE",
scope: {
cgiurl: "@cgiurl",
reporttype: "@reporttype",
host: "@host",
service: "@service",
timeperiod: "@timeperiod",
t1: "@t1",
t2: "@t2",
assumeinitialstates: "@assumeinitialstates",
assumestateretention: "@assumestateretention",
assumestatesduringnotrunning: "@assumestatesduringnotrunning",
includesoftstates: "@includesoftstates",
initialassumedhoststate: "@initialassumedhoststate",
initialassumedservicestate: "@initialassumedservicestate",
backtrack: "@backtrack",
nopopups: "@nopopups",
nomap: "@nomap",
lastUpdate: "=lastUpdate",
reload: "@reload",
build: "&build"
},
controller: function($scope, $element, $attrs, $http,
spinnerOpts) {
// Global variables
$scope.fontSize = 11;
$scope.states = null;
$scope.stateChangeMargin = {
top: 53,
right: 280,
bottom: 156,
left: 110
};
$scope.svgHeight = $scope.svgHostHeight;
$scope.svgWidth = 900;
$scope.dateFormat = d3.time.format("%a %b %d %H:%M:%S %Y");
$scope.showPopups = $scope.nopopups == "false";
// State information for services
$scope.serviceStates = {
"ok": {
"label": "Ok",
"popupText": "OK",
"y": 13,
"color": { "r": 0, "g": 210, "b": 0 }
},
"warning": {
"label": "Warning",
"popupText": "WARNING",
"y": 36,
"color": { "r": 176, "g": 178, "b": 20 }
},
"unknown": {
"label": "Unknown",
"popupText": "UNKNOWN",
"y": 59,
"color": { "r": 255, "g": 100, "b": 25 }
},
"critical": {
"label": "Critical",
"popupText": "CRITICAL",
"y": 82,
"color": { "r": 255, "g": 0, "b": 0 }
},
"nodata": {
"label": "Indeterminate",
"popupText": "?",
"y": 107,
"color": { "r": 0, "g": 0, "b": 0, "a": 0 }
}
};
// State information for hosts
$scope.hostStates = {
"up": {
"label": "Up",
"popupText": "UP",
"y": 13,
"color": { "r": 0, "g": 210, "b": 0 }
},
"down": {
"label": "Down",
"popupText": "DOWN",
"y": 38,
"color": { "r": 255, "g": 0, "b": 0 }
},
"unreachable": {
"label": "Unreachable",
"popupText": "UNREACHABLE",
"y": 63,
"color": { "r": 128, "g": 0, "b": 0 }
},
"nodata": {
"label": "Indeterminate",
"popupText": "?",
"y": 88,
"color": { "r": 0, "g": 0, "b": 0, "a": 0 }
}
};
// Application state variables
$scope.fetchingStateChanges = false;
$scope.fetchingAvailability = false;
$scope.popupDisplayed = false;
$scope.popupDisplayedOnZoomStart = false;
$scope.$watch("reload", function(newValue) {
// Set the graph dimensions
switch ($scope.reporttype) {
case "hosts":
$scope.svgHeight = 300;
break;
case "services":
$scope.svgHeight = 320;
break;
default:
$scope.svgHeight = 0;
break;
}
// Set the start and end times
$scope.startTime = $scope.t1 * 1000;
$scope.endTime = $scope.t2 * 1000;
// Show the trend
if($scope.build()) {
displayStateChanges();
getAvailability($scope.t1, $scope.t2);
}
});
$scope.getYAxisTemplate = function() {
switch ($scope.reporttype) {
case "hosts":
return "trends-host-yaxis.html";
break;
case "services":
return "trends-service-yaxis.html";
break;
default:
return null;
break;
}
};
// Create a color from a color object
var genColor = function(color) {
if(color.hasOwnProperty("a")) {
return "rgba(" + color.r + "," + color.g + "," +
color.b + "," + color.a + ")";
}
else {
return "rgb(" + color.r + "," + color.g + "," +
color.b + ")";
}
};
var setPopupContents = function(popup, previousState,
currentState) {
$scope.popupContents = {
state: $scope.states[previousState.state].popupText,
start: previousState.timestamp,
end: currentState.timestamp,
duration: currentState.timestamp -
previousState.timestamp,
info: previousState.plugin_output
};
$scope.$apply($scope.popupContents);
};
var getTickValues = function(stateChangeList) {
// Set up tick values, skipping those that are too
// close together
var tickValues = [];
tickValues.push(stateChangeList[0].timestamp);
for(var i = 1; i < stateChangeList.length; i++) {
var ts = stateChangeList[i].timestamp;
if($scope.xScale(ts) >
$scope.xScale(tickValues[tickValues.length - 1]) +
$scope.fontSize) {
tickValues.push(ts);
}
}
return tickValues;
};
var displayGrid = function(zoom, sidePadding) {
// Local layout variables
var bottomPadding = 3;
var gridWidth = $scope.svgWidth -
($scope.stateChangeMargin.left +
$scope.stateChangeMargin.right);
var gridHeight = $scope.svgHeight -
($scope.stateChangeMargin.top +
$scope.stateChangeMargin.bottom);
// Adjust the grid group attributes
d3.select("g#grid").call(zoom);
// Generate rectangles for state durations
var rects = d3.select("g#groupRects")
.selectAll("rect.state")
.data($scope.stateChangeJSON.data.statechangelist,
function(d) {
return d.timestamp;
})
.enter()
.append("rect")
.attr({
x: function(d, i) {
if(i == 0) { return 0; }
var ts = d.previous.timestamp;
return $scope.xScale(ts);
},
y: function(d, i) {
if(i == 0) { return 0; }
var state = d.previous.state;
return $scope.states[state].y - 1;
},
width: function(d, i) {
if(i == 0) { return 0; }
var ts = d.previous.timestamp;
return $scope.xScale(d.timestamp) -
$scope.xScale(ts);
},
height: function(d, i) {
if(i == 0) { return 0; }
var state = d.previous.state;
return gridHeight - $scope.states[state].y -
bottomPadding;
},
fill: function(d, i) {
if(i == 0) { return "white"; }
var state = d.previous.state;
return genColor($scope.states[state].color);
},
class: "state"
});
if($scope.showPopups) {
rects.on("mouseover", function(d, i) {
var mouse = d3.mouse(this);
var popup = d3.select("#popup")
.style({
left: (mouse[0] +
$scope.stateChangeMargin.left) +
"px",
top: (mouse[1] +
$scope.stateChangeMargin.top) +
"px"
});
setPopupContents(popup, d.previous, d);
$scope.popupDisplayed = true;
$scope.$apply("popupDisplayed");
})
.on("mouseout", function(d, i) {
$scope.popupDisplayed = false;
$scope.$apply("popupDisplayed");
});
}
// Display the x-axis
var xTranslate = -gridHeight;
var yTranslate = -($scope.fontSize / 2) - gridHeight;
var x = d3.select("g#groupXAxis").call($scope.xAxis);
x.selectAll("text")
.attr({
class: "xaxis",
transform: "rotate(-90) translate(" +
xTranslate + "," + yTranslate + ")"
})
.style({
"text-anchor": "end"
});
x.selectAll("line")
.attr({
class: "vLine",
});
d3.select("g#groupXAxis").select("path").remove();
};
// Handle a successful availability response
var onAvailabilitySuccess = function(json) {
// Local layout variables
var dataPosition = 100;
// Attach the json to the current scope
$scope.availabilityJSON = json;
var totalTime = (json.data.selectors.endtime -
json.data.selectors.starttime) / 1000;
switch ($scope.reporttype) {
case "services":
$scope.states = $scope.serviceStates;
$scope.availabilityStates =
calculateAvailability(json.data.service,
$scope.serviceStates, totalTime);
break;
case "hosts":
$scope.states = $scope.hostStates;
$scope.availabilityStates =
calculateAvailability(json.data.host,
$scope.hostStates, totalTime);
break;
}
};
// Display the availability information
var getAvailability = function(start, end) {
// Where to place the spinner
var spinnerdiv = d3.select("div#availabilityspinner");
var spinner = null;
var parameters = {
query: "availability",
formatoptions: "enumerate bitmask",
availabilityobjecttype: $scope.reporttype,
hostname: $scope.host,
statetypes: $scope.includesoftstates ?
"hard soft" : "hard",
starttime: start,
endtime: end,
};
if($scope.reporttype == "services") {
parameters.servicedescription = $scope.service;
}
var getConfig = {
params: parameters,
withCredentials: true
};
// Send the query
$http.get($scope.cgiurl + "/archivejson.cgi?",
getConfig)
.error(function(err) {
// Stop the spinner
spinner.stop();
$scope.fetchingAvailability = false;
console.warn(err);
})
.success(function(json) {
// Stop the spinner
spinner.stop();
$scope.fetchingAvailability = false;
onAvailabilitySuccess(json);
});
// Start the spinner
$scope.fetchingAvailability = true;
spinner = new Spinner(spinnerOpts)
.spin(spinnerdiv[0][0]);
};
var onStateChangeSuccess = function(json) {
// Local layout variables
var sidePadding = 3;
// Attach the json to the current scope
$scope.stateChangeJSON = json;
// Record the time of the query
$scope.lastUpdate = new Date(json.result.query_time);
// Add a pseudo end state so drawing will be correct
var last = $scope.stateChangeJSON.data.statechangelist[json.data.statechangelist.length - 1];
if(last.timestamp < $scope.stateChangeJSON.data.selectors.endtime) {
var final = {
timestamp: $scope.stateChangeJSON.data.selectors.endtime
};
$scope.stateChangeJSON.data.statechangelist.push(final);
}
// For all but the first entry create a previous
// entry "pointer"
for(var i = 1;
i < $scope.stateChangeJSON.data.statechangelist.length;
i++) {
$scope.stateChangeJSON.data.statechangelist[i].previous =
$scope.stateChangeJSON.data.statechangelist[i-1];
}
// Determine whether the state change list is for a
// host or service
switch ($scope.reporttype) {
case "services":
$scope.states = $scope.serviceStates;
break;
case "hosts":
$scope.states = $scope.hostStates;
break;
}
var gridWidth = $scope.svgWidth -
($scope.stateChangeMargin.left +
$scope.stateChangeMargin.right);
var gridHeight = $scope.svgHeight -
($scope.stateChangeMargin.top +
$scope.stateChangeMargin.bottom);
// Set up the x-axis scale
$scope.xScale = d3.time.scale()
.domain([$scope.stateChangeJSON.data.selectors.starttime,
$scope.stateChangeJSON.data.selectors.endtime])
.rangeRound([sidePadding,
gridWidth - sidePadding * 2])
.clamp(true);
// Set up x axis
$scope.xAxis = d3.svg.axis()
.scale($scope.xScale)
.orient("bottom")
.tickSize(gridHeight)
.tickValues(function() {
return getTickValues($scope.stateChangeJSON.data.statechangelist);
})
.tickFormat(function(d) {
var ts = new Date(d);
return $scope.dateFormat(ts);
});
// Create the zoom
var maxExtent = Math.round(($scope.stateChangeJSON.data.selectors.endtime -
$scope.stateChangeJSON.data.selectors.starttime) / 2000);
var zoom = d3.behavior.zoom()
.x($scope.xScale)
.scaleExtent([1, maxExtent])
.on("zoomstart", onZoomStart)
.on("zoom", onZoom)
.on("zoomend", onZoomEnd);
// Display the grid
displayGrid(zoom, sidePadding);
};
var displayStateChanges = function() {
// Where to place the spinner
var spinnerdiv = d3.select("div#gridspinner");
var spinner = null;
var parameters = {
query: "statechangelist",
formatoptions: "enumerate bitmask",
objecttype: ($scope.reporttype == "hosts" ?
"host" : "service"),
hostname: $scope.host,
starttime: $scope.t1,
endtime: $scope.t2,
statetypes: $scope.includesoftstates ?
"hard soft" : "hard",
backtrackedarchives: $scope.backtrack
};
if($scope.reporttype == "services") {
parameters.servicedescription = $scope.service;
}
var getConfig = {
params: parameters,
withCredentials: true
};
// Send the query
$http.get($scope.cgiurl + "/archivejson.cgi?",
getConfig)
.error(function(err) {
// Stop the spinner
spinner.stop();
$scope.fetchingStateChanges = false;
console.warn(err);
})
.success(function(json) {
// Stop the spinner
spinner.stop();
$scope.fetchingStateChanges = false;
onStateChangeSuccess(json);
});
// Clear the current grid
d3.select("g#groupRects")
.selectAll("rect.state")
.data([])
.exit()
.remove();
// Clear the current x-axis
if($scope.xAxis != undefined) {
$scope.xAxis.tickValues([]);
var x = d3.select("g#groupXAxis")
.call($scope.xAxis);
x.selectAll("text").remove();
x.selectAll("line").remove();
x.selectAll("path").remove();
}
// Start the spinner
$scope.fetchingStateChanges = true;
spinner = new Spinner(spinnerOpts)
.spin(spinnerdiv[0][0]);
};
var onZoomStart = function() {
$scope.popupDisplayedOnZoomStart = $scope.popupDisplayed;
$scope.popupDisplayed = false;
$scope.$apply("popupDisplayed");
};
var onZoom = function() {
var gridHeight = $scope.svgHeight -
($scope.stateChangeMargin.top +
$scope.stateChangeMargin.bottom);
var xTranslate = -gridHeight;
var yTranslate = -($scope.fontSize / 2) - gridHeight;
var domain = $scope.xScale.domain();
// Update the x-axis
var x = d3.select("g#groupXAxis");
x.call($scope.xAxis);
x.selectAll("text")
.attr({
class: "xaxis",
transform: "rotate(-90) translate(" +
xTranslate + "," + yTranslate + ")"
})
.style({
"text-anchor": "end"
})
.text(function(d, i) {
// This is a kludge to get the endpoints
// to update
if(i == 0) {
return $scope.dateFormat(new Date(domain[0]));
}
else if(d > domain[1]) {
return $scope.dateFormat(new Date(domain[1]));
}
else {
return $scope.dateFormat(new Date(d));
}
});
x.selectAll("line")
.attr({
class: "vLine",
});
x.select("path").remove();
// Update the times for the header
$scope.startTime = domain[0];
$scope.$apply("startTime");
$scope.endTime = domain[1];
$scope.$apply("endTime");
// Update the rectangles
d3.select("g#groupRects")
.selectAll("rect")
.attr({
x: function(d, i) {
if(i == 0) { return 0; }
var ts = d.previous.timestamp;
return $scope.xScale(ts);
},
width: function(d, i) {
if(i == 0) { return 0; }
var ts = d.previous.timestamp;
return $scope.xScale(d.timestamp) -
$scope.xScale(ts);
}
});
}
var onZoomEnd = function() {
// Get the new domain
var domain = $scope.xScale.domain();
// Determine the new start and end times
var start = Math.round(domain[0].getTime() / 1000);
var end = Math.round(domain[1].getTime() / 1000);
if(start == end) {
start--;
}
// Update the availability display
getAvailability(start, end);
// Re-display the popup if it was shown before zooming
$scope.popupDisplayed = $scope.popupDisplayedOnZoomStart;
$scope.$apply("popupDisplayed");
$scope.popupDisplayedOnZoomStart = false;
};
var calculateAvailability = function(availability,
layoutStates, totalTime) {
var states = {};
for(var s in layoutStates) {
if(s == "up") {
layoutStates[s].totalTime =
availability.time_up +
availability.scheduled_time_up;
}
else if(s == "down") {
layoutStates[s].totalTime =
availability.time_down +
availability.scheduled_time_down;
}
else if(s == "unreachable") {
layoutStates[s].totalTime =
availability.time_unreachable +
availability.scheduled_time_unreachable;
}
else if(s == "ok") {
layoutStates[s].totalTime =
availability.time_ok +
availability.scheduled_time_ok;
}
else if(s == "warning") {
layoutStates[s].totalTime =
availability.time_warning +
availability.scheduled_time_warning;
}
else if(s == "unknown") {
layoutStates[s].totalTime =
availability.time_unknown +
availability.scheduled_time_unknown;
}
else if(s == "critical") {
layoutStates[s].totalTime =
availability.time_critical +
availability.scheduled_time_critical;
}
else if(s == "nodata") {
layoutStates[s].totalTime =
availability.time_indeterminate_nodata +
availability.time_indeterminate_notrunning;
}
layoutStates[s].percentageTime =
layoutStates[s].totalTime / totalTime;
states[s] = layoutStates[s];
}
return states;
};
}
};
});