663 lines
18 KiB
JavaScript
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;
|
|
};
|
|
}
|
|
};
|
|
});
|