Device monitoring/v2
This commit is contained in:
@@ -30,9 +30,6 @@ class DeviceMonitoringController extends mfBaseController
|
||||
$this->postData = json_decode(file_get_contents('php://input'), true) ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a list of all available interfaces, grouping Sent/Received items.
|
||||
*/
|
||||
protected function listInterfacesAction()
|
||||
{
|
||||
$hostId = $this->request->hostId;
|
||||
@@ -54,9 +51,6 @@ class DeviceMonitoringController extends mfBaseController
|
||||
self::returnJson($sortedInterfaces);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets historical data for a specific list of item IDs.
|
||||
*/
|
||||
protected function interfaceDataAction()
|
||||
{
|
||||
$itemIds = $this->postData['itemIds'] ?? [];
|
||||
@@ -71,7 +65,7 @@ class DeviceMonitoringController extends mfBaseController
|
||||
$params = [
|
||||
'itemids' => $itemIds,
|
||||
'output' => 'extend',
|
||||
'history' => 3, // Numeric (unsigned)
|
||||
'history' => 3, // Type of history: float
|
||||
'sortfield' => 'clock',
|
||||
'sortorder' => 'ASC',
|
||||
'time_from' => $time_from,
|
||||
@@ -82,66 +76,246 @@ class DeviceMonitoringController extends mfBaseController
|
||||
foreach ($history as $point) {
|
||||
$historyByItemId[$point['itemid']][] = [
|
||||
'x' => intval($point['clock']) * 1000,
|
||||
'y' => round(floatval($point['value']) / 1000000, 2)
|
||||
'y' => round(floatval($point['value']) / 1000000, 2) // Mbps
|
||||
];
|
||||
}
|
||||
|
||||
self::returnJson($historyByItemId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets general monitoring data (Uptime, Ping, Temp).
|
||||
*/
|
||||
protected function generalDataAction() {
|
||||
$hostId = $this->request->hostId;
|
||||
|
||||
$itemsToFetch = [
|
||||
'ping' => $this->zabbix->getICMPItems($hostId),
|
||||
'uptime' => $this->zabbix->getUptimeItems($hostId),
|
||||
];
|
||||
|
||||
$itemIds = [];
|
||||
$itemMap = [];
|
||||
foreach ($itemsToFetch as $type => $items) {
|
||||
if (!empty($items)) {
|
||||
foreach($items as $item) {
|
||||
$itemIds[] = $item['itemid'];
|
||||
$itemMap[$item['itemid']] = ['type' => $type, 'name' => $item['name'], 'units' => $item['units']];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$values = [];
|
||||
if(!empty($itemIds)) {
|
||||
$history = $this->zabbix->getItemValues($itemIds, 1);
|
||||
foreach($history as $h) {
|
||||
$info = $itemMap[$h['itemid']];
|
||||
$values[$info['type']][] = ['name' => $info['name'], 'value' => $h['value'], 'clock' => $h['clock'], 'units' => $info['units']];
|
||||
}
|
||||
}
|
||||
|
||||
self::returnJson($values);
|
||||
$data = $this->zabbix->getOverviewData($hostId);
|
||||
self::returnJson($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets Zabbix problems (triggers) for the host.
|
||||
*/
|
||||
protected function getProblemsAction() {
|
||||
$hostId = $this->request->hostId;
|
||||
$problems = $this->zabbix->zabbixRequest('problem.get', [
|
||||
$currentProblems = $this->zabbix->zabbixRequest('problem.get', [
|
||||
'hostids' => $hostId,
|
||||
'output' => 'extend',
|
||||
'recent' => true, // Use boolean true
|
||||
'recent' => true,
|
||||
'sortfield' => ['eventid'],
|
||||
'sortorder' => 'DESC'
|
||||
])['result'] ?? [];
|
||||
|
||||
self::returnJson($problems);
|
||||
$resolvedProblems = $this->zabbix->getResolvedProblems($hostId, strtotime('-7 days'));
|
||||
|
||||
self::returnJson([
|
||||
'current' => $currentProblems,
|
||||
'resolved' => $resolvedProblems
|
||||
]);
|
||||
}
|
||||
|
||||
protected function getConfigurationDataAction() {
|
||||
$hostId = $this->request->hostId;
|
||||
|
||||
$host = $this->zabbix->getHostWithInterfaces($hostId);
|
||||
if (!$host) {
|
||||
self::returnJson(['error' => 'Host not found.']);
|
||||
return;
|
||||
}
|
||||
|
||||
$snmpInterface = null;
|
||||
foreach ($host['interfaces'] as $iface) {
|
||||
if ($iface['type'] == '2') { // SNMP type
|
||||
$snmpInterface = $iface;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$opStatusItems = $this->zabbix->getInterfaceOperationalStatusItems($hostId);
|
||||
$allTriggers = $this->zabbix->getTriggersForHostByDescription($hostId, "Interface ");
|
||||
$triggerMap = [];
|
||||
foreach ($allTriggers as $trigger) {
|
||||
$triggerMap[$trigger['description']] = $trigger;
|
||||
}
|
||||
|
||||
$interfaceAlarms = [];
|
||||
foreach ($opStatusItems as $item) {
|
||||
$expectedDescription = "Interface " . $item['name'] . " is down on " . $host['name'];
|
||||
$trigger = $triggerMap[$expectedDescription] ?? null;
|
||||
|
||||
$interfaceAlarms[] = [
|
||||
'itemid' => $item['itemid'],
|
||||
'name' => $item['name'],
|
||||
'key' => $item['key_'],
|
||||
'isAlarmed' => !is_null($trigger),
|
||||
'triggerId' => $trigger['triggerid'] ?? null
|
||||
];
|
||||
}
|
||||
|
||||
self::returnJson([
|
||||
'snmp' => $snmpInterface,
|
||||
'interfaces' => $interfaceAlarms
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
protected function updateSnmpAction() {
|
||||
$interfaceId = $this->postData['interfaceId'] ?? null;
|
||||
$details = $this->postData['details'] ?? null;
|
||||
if (!$interfaceId || !$details) {
|
||||
http_response_code(400);
|
||||
self::returnJson(['error' => 'Missing required parameters.']);
|
||||
return;
|
||||
}
|
||||
|
||||
$result = $this->zabbix->updateHostInterface($interfaceId, $details);
|
||||
self::returnJson($result);
|
||||
}
|
||||
|
||||
protected function updateInterfaceAlarmAction() {
|
||||
$hostId = $this->postData['hostId'];
|
||||
$item = $this->postData['item'];
|
||||
$enabled = $this->postData['enabled'];
|
||||
$host = $this->zabbix->getHostById($hostId)[0] ?? null;
|
||||
|
||||
if (!$host) {
|
||||
self::returnJson(['error' => 'Host not found.']);
|
||||
return;
|
||||
}
|
||||
|
||||
$description = "Interface " . $item['name'] . " is down on " . $host['name'];
|
||||
|
||||
if ($enabled) {
|
||||
$expression = "last(/".$host['host']."/".$item['key'].")=2";
|
||||
$result = $this->zabbix->createInterfaceLinkDownTrigger($expression, $description);
|
||||
} else {
|
||||
$triggers = $this->zabbix->getTriggersForHostByDescription($hostId, $description);
|
||||
$triggerIds = array_column($triggers, 'triggerid');
|
||||
$result = $this->zabbix->deleteTriggers($triggerIds);
|
||||
}
|
||||
|
||||
self::returnJson($result);
|
||||
}
|
||||
|
||||
protected function getReportDataAction() {
|
||||
$hostId = $this->request->hostId;
|
||||
$timeRange = $this->request->timeRange ?? '7d';
|
||||
$time_from = strtotime('-' . str_replace(['d'], [' days'], $timeRange));
|
||||
|
||||
// Step 1: Fetch all interface-related items (traffic and speed) in a single API call.
|
||||
// We include 'value_type' to handle different history types correctly.
|
||||
$items = $this->zabbix->zabbixRequest('item.get', [
|
||||
'hostids' => $hostId,
|
||||
'output' => ['itemid', 'name', 'key_', 'value_type'],
|
||||
'search' => ['key_' => ['net.if.in', 'net.if.out', 'net.if.speed']],
|
||||
'searchByAny' => true,
|
||||
'sortfield' => 'name'
|
||||
])['result'] ?? [];
|
||||
|
||||
// Step 2: Organize items and group them for efficient processing.
|
||||
$interfaces = [];
|
||||
$trafficItems = []; // Will hold item info for both rx and tx.
|
||||
$speedItemsByType = [];
|
||||
$speedItemMap = [];
|
||||
|
||||
foreach ($items as $item) {
|
||||
$key = $item['key_'];
|
||||
if (str_contains($key, 'net.if.in') || str_contains($key, 'net.if.out')) {
|
||||
$baseName = preg_replace('/:\s*Bits\s*(sent|received)$/i', '', $item['name']);
|
||||
$direction = str_contains($key, 'net.if.in') ? 'rx' : 'tx';
|
||||
if (!isset($interfaces[$baseName])) {
|
||||
$interfaces[$baseName] = ['name' => $baseName, 'rx_item' => null, 'tx_item' => null, 'speed' => null];
|
||||
}
|
||||
$interfaces[$baseName][$direction . '_item'] = $item;
|
||||
$trafficItems[$item['itemid']] = $item;
|
||||
} elseif (str_contains($key, 'net.if.speed')) {
|
||||
$baseName = preg_replace('/:\s*Interface\s*|\s*speed$/i', '', $item['name']);
|
||||
$value_type = (int)$item['value_type'];
|
||||
$speedItemsByType[$value_type][] = $item['itemid'];
|
||||
$speedItemMap[$item['itemid']] = $baseName;
|
||||
}
|
||||
}
|
||||
|
||||
// Step 3: Aggressively fetch the last known speed for all interfaces.
|
||||
// We query history with a larger limit to find the value even if it's not recent.
|
||||
foreach ($speedItemsByType as $type => $itemIds) {
|
||||
$historyResult = $this->zabbix->zabbixRequest('history.get', [
|
||||
'itemids' => $itemIds,
|
||||
'history' => $type,
|
||||
'output' => ['itemid', 'value'],
|
||||
'sortfield' => 'clock',
|
||||
'sortorder' => 'DESC',
|
||||
'limit' => count($itemIds) * 5 // Increase limit to better ensure finding a value for each item
|
||||
])['result'] ?? [];
|
||||
|
||||
$latestForType = [];
|
||||
foreach ($historyResult as $point) {
|
||||
if (!isset($latestForType[$point['itemid']])) {
|
||||
$latestForType[$point['itemid']] = $point;
|
||||
$baseName = $speedItemMap[$point['itemid']] ?? null;
|
||||
if ($baseName && isset($interfaces[$baseName])) {
|
||||
$interfaces[$baseName]['speed'] = (float)$point['value'];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Step 4: Attempt to fetch trend data for all traffic items at once.
|
||||
$trafficItemIds = array_keys($trafficItems);
|
||||
$trends = $this->zabbix->getTrends($trafficItemIds, $time_from);
|
||||
$trendsByItemId = [];
|
||||
foreach ($trends as $trend) {
|
||||
$trendsByItemId[$trend['itemid']][] = $trend;
|
||||
}
|
||||
|
||||
// Step 5: Build the report, using trends first and falling back to raw history if trends are unavailable.
|
||||
$report = [];
|
||||
foreach ($interfaces as $iface) {
|
||||
$rx_item = $iface['rx_item'];
|
||||
$tx_item = $iface['tx_item'];
|
||||
$speed = $iface['speed'];
|
||||
|
||||
// This function calculates statistics from either trend data or raw history data.
|
||||
$calcStats = function($item, $speed, $trendData) use ($time_from) {
|
||||
if (!$item) return ['avg' => 0, 'max' => 0, 'usage' => 0];
|
||||
|
||||
$values = [];
|
||||
$avg = 0;
|
||||
$max = 0;
|
||||
|
||||
if (!empty($trendData)) {
|
||||
// Method 1: Use efficient trend data if available.
|
||||
$avg = array_sum(array_column($trendData, 'value_avg')) / count($trendData);
|
||||
$max = max(array_column($trendData, 'value_max'));
|
||||
} else {
|
||||
// Method 2 (Fallback): Fetch raw history if trends are missing.
|
||||
$history = $this->zabbix->zabbixRequest('history.get', [
|
||||
'itemids' => [$item['itemid']],
|
||||
'history' => (int)$item['value_type'],
|
||||
'time_from' => $time_from,
|
||||
'output' => ['value']
|
||||
])['result'] ?? [];
|
||||
|
||||
if (!empty($history)) {
|
||||
$values = array_column($history, 'value');
|
||||
$avg = array_sum($values) / count($values);
|
||||
$max = max($values);
|
||||
}
|
||||
}
|
||||
|
||||
$usage = ($speed > 0) ? ($avg / $speed) * 100 : 0;
|
||||
return [
|
||||
'avg' => round($avg / 1000000, 2), // bps to Mbps
|
||||
'max' => round($max / 1000000, 2), // bps to Mbps
|
||||
'usage' => round($usage, 2)
|
||||
];
|
||||
};
|
||||
|
||||
$report[] = [
|
||||
'name' => $iface['name'],
|
||||
'speed' => $speed !== null ? round($speed / 1000000) : 'N/A', // bps to Mbps
|
||||
'rx' => $calcStats($rx_item, $speed, $trendsByItemId[$rx_item['itemid']] ?? []),
|
||||
'tx' => $calcStats($tx_item, $speed, $trendsByItemId[$tx_item['itemid']] ?? [])
|
||||
];
|
||||
}
|
||||
|
||||
usort($report, fn($a, $b) => strnatcmp($a['name'], $b['name']));
|
||||
self::returnJson($report);
|
||||
}
|
||||
|
||||
/**
|
||||
* Forces a Zabbix item check and returns the latest value for live graphs.
|
||||
*/
|
||||
protected function liveDataAction() {
|
||||
$itemId = $this->request->itemId;
|
||||
if(empty($itemId)) {
|
||||
@@ -168,9 +342,6 @@ class DeviceMonitoringController extends mfBaseController
|
||||
self::returnJson($formattedPoint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a dedicated HTML page for the live graph popup.
|
||||
*/
|
||||
public function liveGraphPageAction() {
|
||||
$this->layout(false);
|
||||
$this->layout()->set('API_BASE_URL', self::getUrl("DeviceMonitoring"));
|
||||
|
||||
Reference in New Issue
Block a user