mirror of
https://github.com/lucaspalomodevelop/core.git
synced 2026-03-14 00:24:40 +00:00
(systemhealth) First commit WIP
This commit is contained in:
parent
38cb88a379
commit
381fc84511
@ -0,0 +1,587 @@
|
||||
<?php
|
||||
/**
|
||||
* Copyright (C) 2015 Deciso B.V. - J. Schellevis
|
||||
*
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
|
||||
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
* AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
||||
* AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
|
||||
* OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
* POSSIBILITY OF SUCH DAMAGE.
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
namespace OPNsense\SystemHealth\Api;
|
||||
|
||||
use \OPNsense\Base\ApiControllerBase;
|
||||
use \OPNsense\Core\Backend;
|
||||
/**
|
||||
* Class ServiceController
|
||||
* @package OPNsense\SystemHealth
|
||||
*/
|
||||
class ServiceController extends ApiControllerBase
|
||||
{
|
||||
|
||||
/**
|
||||
* retrieve Available RRD data
|
||||
* @return array
|
||||
*/
|
||||
public function getRRDlistAction()
|
||||
{
|
||||
# Suurce of data: filelisting of /var/db/rrd/*.rrd
|
||||
$result = array();
|
||||
|
||||
$output = [
|
||||
"system" => ["processor","states", "mbuf"],
|
||||
"traffic" => ["lan", "wan", "ipsec"],
|
||||
"packets" => ["wan", "lan", "ipsec"],
|
||||
"quality" => ["GW_WAN"]
|
||||
|
||||
];
|
||||
$result["result"] = "ok";
|
||||
$result["data"] = $output;
|
||||
|
||||
// Category => Items
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
private function getRRDdetails($rrd = "")
|
||||
{
|
||||
# Source of data: xml fields of corresponding .xml metadata
|
||||
$result = array();
|
||||
if ($rrd == "system-processor") {
|
||||
$backend = new Backend();
|
||||
$response = $backend->configdpRun("systemhealth query details"); #, array(1, 0, "filepos/".$id, $fileid));
|
||||
// $response = $backend->configdRun("configd actions");
|
||||
$xml=simplexml_load_string($response);
|
||||
$json = json_encode($xml);
|
||||
$output= json_decode($json, true);
|
||||
// $output = [
|
||||
// "title" => "System Information - Utilization and Processes",
|
||||
// "x-axis_label" => "[U]tilization , [#]Number",
|
||||
// "field_units" => [
|
||||
// "user" => "[U]",
|
||||
// "nice" => "[U]",
|
||||
// "system" => "[U]",
|
||||
// "interrupt" => "[#]",
|
||||
// "processes" => "[#]"
|
||||
// ]
|
||||
// ];
|
||||
$result["result"] = "ok";
|
||||
} else {
|
||||
$result["result"] = "not found";
|
||||
$output=["title"=>"","x-axis_label"=>"","field_units"=>[]]; // always return a valid (empty) data set
|
||||
}
|
||||
|
||||
$result["data"] = $output;
|
||||
|
||||
return $result;
|
||||
}
|
||||
/**
|
||||
* retrieve SystemHealth Data (previously called RRD Graphs)
|
||||
* @param string $rrd
|
||||
* @param int $from
|
||||
* @param int $to
|
||||
* @param int $max_values
|
||||
* @param bool $inverse
|
||||
* @param int $detail
|
||||
* @return array
|
||||
*/
|
||||
public function getSystemHealthAction(
|
||||
$rrd = "",
|
||||
$from = 0,
|
||||
$to = 0,
|
||||
$max_values = 120,
|
||||
$inverse = false,
|
||||
$detail = -1
|
||||
) {
|
||||
/**
|
||||
* $rrd = rrd filename without extension
|
||||
* $from = from timestamp (0=min)
|
||||
* $to = to timestamp (0=max)
|
||||
* $max_values = limit datapoint as close as possible to this number (or twice if detail (zoom) + overview )
|
||||
* $inverse = Inverse every odd row (multiply by -1)
|
||||
* $detail = limits processing of dataSets to max given (-1 = all ; 1 = 0,1 ; 2 = 0,1,2 ; etc)
|
||||
*/
|
||||
|
||||
$rrd_details=$this->getRRDdetails($rrd)["data"];
|
||||
|
||||
$rrd = $rrd . ".xml"; // Test data
|
||||
$xml = $this->getXMLdata($rrd);
|
||||
|
||||
|
||||
$data_sets_full = $this->getDataSetInfo($xml); // get dataSet information to include in answer
|
||||
|
||||
if ($inverse == 'true') {
|
||||
$inverse = true;
|
||||
} else {
|
||||
$inverse = false;
|
||||
}
|
||||
|
||||
if ((int)$detail >= 0) {
|
||||
for ($count = count($xml->rra); $count > $detail; $count--) {
|
||||
unset($xml->rra[$count]);
|
||||
}
|
||||
}
|
||||
|
||||
// determine available dataSets within range and how to handle them
|
||||
$selected_archives = $this->getSelection($this->getDataSetInfo($xml), $from, $to, $max_values);
|
||||
// get condensed dataSets and translate them to d3 usable data
|
||||
$result = $this->translateD3(
|
||||
$this->getCondensedArchive($xml, $selected_archives),
|
||||
$inverse,
|
||||
$rrd_details["field_units"]
|
||||
);
|
||||
|
||||
return ["sets" => $data_sets_full,
|
||||
"d3" => $result,
|
||||
"title"=>$rrd_details["title"],
|
||||
"x-axis_label"=>$rrd_details["x-axis_label"]
|
||||
]; // return details and d3 data
|
||||
}
|
||||
|
||||
/**
|
||||
* Return XML data dump for given rrd
|
||||
* @param $rrd
|
||||
* @return \SimpleXMLElement
|
||||
*/
|
||||
private function getXMLdata($rrd)
|
||||
{
|
||||
# Source: rrdtool dump filename.rrd
|
||||
$xml = simplexml_load_file(__DIR__ . '/../../../../../../../../../../opnsense_gui/test/conf/' . $rrd);
|
||||
|
||||
return $xml;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return full archive information
|
||||
* @param array $xml
|
||||
* @return array
|
||||
*/
|
||||
private function getDataSetInfo($xml)
|
||||
{
|
||||
$info = array();
|
||||
if (isset($xml)) {
|
||||
$step = intval($xml->step);
|
||||
$lastUpdate = intval($xml->lastupdate);
|
||||
foreach ($xml->rra as $key => $value) {
|
||||
$step_size = (int)$value->pdp_per_row * $step;
|
||||
$first = floor(($lastUpdate / $step_size)) * $step_size -
|
||||
($step_size * (count($value->database->children()) - 1));
|
||||
$last = floor(($lastUpdate / $step_size)) * $step_size;
|
||||
$firstValue_rowNumber = (int)$this->findFirstValue($value);
|
||||
$firstValue_timestamp = (int)$first + ((int)$firstValue_rowNumber * $step_size);
|
||||
array_push($info, [
|
||||
"step" => $step,
|
||||
"pdp_per_row" => (int)$value->pdp_per_row,
|
||||
"rowCount" => $this->countRows($value),
|
||||
"first_timestamp" => (int)$first,
|
||||
"last_timestamp" => (int)$last,
|
||||
"firstValue_rowNumber" => $firstValue_rowNumber,
|
||||
"firstValue_timestamp" => $firstValue_timestamp,
|
||||
"available_rows" => ($this->countRows($value) - $firstValue_rowNumber),
|
||||
"full_step" => ($step * (int)$value->pdp_per_row),
|
||||
"recorded_time" => ($step * (int)$value->pdp_per_row) *
|
||||
($this->countRows($value) - $firstValue_rowNumber)
|
||||
]);
|
||||
|
||||
}
|
||||
}
|
||||
return ($info);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns row number of first row with values other than 'NaN'
|
||||
* @param array $data
|
||||
* @return int
|
||||
*/
|
||||
private function findFirstValue($data = array())
|
||||
{
|
||||
|
||||
$rowNumber = 0;
|
||||
$containsValues = false; // used to break foreach on first row with collected data
|
||||
|
||||
foreach ($data->database->row as $item => $row) {
|
||||
foreach ($row as $rowKey => $rowVal) {
|
||||
if (trim($rowVal) != "NaN") {
|
||||
$containsValues = true;
|
||||
}
|
||||
}
|
||||
if ($containsValues == true) {
|
||||
break;
|
||||
}
|
||||
$rowNumber++;
|
||||
}
|
||||
|
||||
return $rowNumber;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return total number of rows in rra
|
||||
* @param array $data
|
||||
* @return int
|
||||
*/
|
||||
private function countRows($data = array())
|
||||
{
|
||||
$rowCount = 0;
|
||||
foreach ($data->database->row as $item => $row) {
|
||||
$rowCount++;
|
||||
}
|
||||
|
||||
return $rowCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* internal: retrieve selections within range (0-0=full range) and limit number of datapoints (max_values)
|
||||
* @param array $rra_info
|
||||
* @param int $from_timestamp
|
||||
* @param int $to_timestamp
|
||||
* @param $max_values
|
||||
* @return array
|
||||
*/
|
||||
private function getSelection($rra_info = array(), $from_timestamp = 0, $to_timestamp = 0, $max_values = 120)
|
||||
{
|
||||
$full_range = false;
|
||||
if ($from_timestamp == 0 && $to_timestamp == 0) {
|
||||
$full_range = true;
|
||||
$from_timestamp = $this->getMaxRange($rra_info)["oldest_timestamp"];
|
||||
$to_timestamp = $this->getMaxRange($rra_info)["newest_timestamp"];
|
||||
}
|
||||
|
||||
$archives = array();
|
||||
// find archive match
|
||||
foreach ($rra_info as $key => $value) {
|
||||
if ($from_timestamp >= $value['firstValue_timestamp'] && $to_timestamp <= ($value['last_timestamp'] +
|
||||
$value['full_step'])) {
|
||||
// calculate number of rows in set
|
||||
$rowCount = ($to_timestamp - $from_timestamp) / $value['full_step'] + 1;
|
||||
|
||||
// factor to be used to compress the data.
|
||||
// example if 2 then 2 values will be used to calculate one data point.
|
||||
$condense_factor = round($rowCount / $max_values);
|
||||
|
||||
if ($condense_factor == 0) { // if rounded to 0 we will not condense the data
|
||||
$condense_factor = 1; // and thus return the full set of data points
|
||||
}
|
||||
// actual number of rows after compressing/condensing the dataSet
|
||||
$condensed_rowCount = (int)($rowCount / $condense_factor);
|
||||
|
||||
// count the number if rra's (sets), deduct 1 as we need the counter to start at 0
|
||||
$last_rra_key = count($rra_info) - 1;
|
||||
|
||||
// dynamic (condensed) values for full overview to detail level
|
||||
$overview = round($rra_info[$last_rra_key]["available_rows"] / (int)$max_values);
|
||||
|
||||
if ($full_range == false) { // JSC WIP removed: && count($rra_info)==1 // add detail when selected
|
||||
array_push($archives, [
|
||||
"key" => $key,
|
||||
"condensed_rowCount" => $condensed_rowCount,
|
||||
"condense_by" => (int)$condense_factor,
|
||||
"type" => "detail"
|
||||
]);
|
||||
} else { // add condensed detail
|
||||
array_push($archives, [
|
||||
"key" => $key,
|
||||
"condensed_rowCount" => (int)($condensed_rowCount / ($rra_info[$last_rra_key]["pdp_per_row"] /
|
||||
$value["pdp_per_row"])),
|
||||
"condense_by" => (int)$condense_factor * ($rra_info[$last_rra_key]["pdp_per_row"] /
|
||||
$value["pdp_per_row"]),
|
||||
"type" => "detail"
|
||||
]);
|
||||
}
|
||||
// search for last dataSet with actual values, used to exclude sets that do not contain data
|
||||
for ($count = $last_rra_key; $count > 0; $count--) {
|
||||
if ($rra_info[$count]["available_rows"] > 0) {
|
||||
// Found last rra set with values
|
||||
$last_rra_key = $count;
|
||||
break;
|
||||
}
|
||||
}
|
||||
array_push($archives, [
|
||||
"key" => $last_rra_key,
|
||||
"condensed_rowCount" => (int)($rra_info[$last_rra_key]["available_rows"] / $overview),
|
||||
"condense_by" => (int)$overview,
|
||||
"type" => "overview"
|
||||
]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return (["from" => $from_timestamp, "to" => $to_timestamp, "full_range" => $full_range, "data" => $archives]);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* internal: get full available range
|
||||
* @param array $rra_info
|
||||
* @return array
|
||||
*/
|
||||
private function getMaxRange($rra_info = array())
|
||||
{
|
||||
// count the number if rra's (sets), deduct 1 as we need the counter to start at 0
|
||||
$last_rra_key = count($rra_info) - 1;
|
||||
for ($count = $last_rra_key; $count > 0; $count--) {
|
||||
if ($rra_info[$count]["available_rows"] > 0) {
|
||||
// Found last rra set with values
|
||||
$last_rra_key = $count;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (isset($rra_info[0])) {
|
||||
$last = $rra_info[0]["firstValue_timestamp"];
|
||||
$first = $rra_info[$last_rra_key]["firstValue_timestamp"] + $rra_info[$last_rra_key]["recorded_time"] -
|
||||
$rra_info[$last_rra_key]["full_step"];
|
||||
|
||||
} else {
|
||||
$first = 0;
|
||||
$last = 0;
|
||||
}
|
||||
|
||||
return ["newest_timestamp" => $first, "oldest_timestamp" => $last];
|
||||
}
|
||||
|
||||
/**
|
||||
* translate rrd data to usable format for d3 charts
|
||||
* @param array $data
|
||||
* @param boolean $applyInverse
|
||||
* @return array
|
||||
*/
|
||||
private function translateD3($data = array(), $applyInverse = false, $field_units)
|
||||
{
|
||||
$d3_data = array();
|
||||
$from_timestamp = 0;
|
||||
$to_timestamp = 0;
|
||||
|
||||
foreach ($data['archive'] as $row => $rowValues) {
|
||||
$timestamp = $rowValues['timestamp'] * 1000; // javascript works with milliseconds
|
||||
foreach ($data['columns'] as $key => $value) {
|
||||
$name = $value['name'];
|
||||
if ($value['type'] == "GAUGE") {
|
||||
// return values as float
|
||||
$value = $rowValues['condensed_values'][$key];
|
||||
} else {
|
||||
// return values as int
|
||||
if ((string)$rowValues['condensed_values'][$key] != "NaN") {
|
||||
$value = (int)$rowValues['condensed_values'][$key];
|
||||
} else {
|
||||
$value = $rowValues['condensed_values'][$key];
|
||||
}
|
||||
|
||||
}
|
||||
if (!isset($d3_data[$key])) {
|
||||
$d3_data[$key] = [];
|
||||
$d3_data[$key]["area"] = true;
|
||||
if (isset($field_units[$name])) {
|
||||
$d3_data[$key]["key"] = $name . " " . $field_units[$name];
|
||||
} else {
|
||||
$d3_data[$key]["key"] = $name;
|
||||
}
|
||||
$d3_data[$key]["values"] = [];
|
||||
|
||||
}
|
||||
|
||||
if ($value == "NaN") {
|
||||
// If first or the last NaN value in series then add a value of 0 for presentation purposes
|
||||
$nan = false;
|
||||
if (isset($data['archive'][$row - 1]['condensed_values'][$key]) &&
|
||||
(string)$data['archive'][$row - 1]['condensed_values'][$key] != "NaN") {
|
||||
// Translate NaN to 0 as d3chart can't render NaN - (first NaN item before value)
|
||||
$value = 0;
|
||||
} elseif (isset($data['archive'][$row + 1]['condensed_values'][$key]) &&
|
||||
(string)$data['archive'][$row + 1]['condensed_values'][$key] != "NaN") {
|
||||
$value = 0; // Translate NaN to 0 as d3chart can't render NaN - (last NaN item before value)
|
||||
} else {
|
||||
$nan = true; // suppress NaN item as we already drawn a line to 0
|
||||
}
|
||||
} else {
|
||||
$nan = false; // Not a NaN value, so add to list
|
||||
}
|
||||
if ($applyInverse == true) {
|
||||
$check_value = $key / 2; // every odd row gets data inversed (* -1)
|
||||
if ($check_value != (int)$check_value) {
|
||||
$value = $value * -1;
|
||||
}
|
||||
}
|
||||
if ($nan == false) {
|
||||
if ($from_timestamp == 0 || $timestamp < $from_timestamp) {
|
||||
$from_timestamp = $timestamp; // Actual from_timestamp after condensing and cleaning data
|
||||
}
|
||||
if ($to_timestamp == 0 || $timestamp > $to_timestamp) {
|
||||
$to_timestamp = $timestamp; // Actual to_timestamp after condensing and cleaning data
|
||||
}
|
||||
array_push($d3_data[$key]["values"], [$timestamp, $value]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Sort value sets based on timestamp
|
||||
foreach ($d3_data as $key => $value) {
|
||||
usort($value["values"], array($this, "orderByTimestampASC"));
|
||||
$d3_data[$key]["values"] = $value["values"];
|
||||
}
|
||||
|
||||
return [
|
||||
"stepSize" => $data['condensed_step'],
|
||||
"from_timestamp" => $from_timestamp,
|
||||
"to_timestamp" => $to_timestamp,
|
||||
"count" => count($d3_data[0]['values']),
|
||||
"data" => $d3_data
|
||||
];
|
||||
}
|
||||
|
||||
private function getCondensedArchive($xml = array(), $selection = array())
|
||||
{
|
||||
$key_counter = 0;
|
||||
$info = $this->getDataSetInfo($xml);
|
||||
$count_values = 0;
|
||||
$condensed_row_values = array();
|
||||
$condensed_archive = array();
|
||||
$condensed_step = 0;
|
||||
$skip_nan = false;
|
||||
$selected_archives = $selection["data"];
|
||||
|
||||
foreach ($xml->rra as $key => $value) {
|
||||
$calculation_type = trim($value->cf);
|
||||
foreach ($value->database as $db_key => $db_value) {
|
||||
foreach ($selected_archives as $archKey => $archValue) {
|
||||
if ($archValue['key'] == $key_counter) {
|
||||
$rowCount = 0;
|
||||
$condense_counter = 0;
|
||||
$condense = $archValue['condense_by'];
|
||||
|
||||
foreach ($db_value as $rowKey => $rowValues) {
|
||||
if ($rowCount >= $info[$key_counter]['firstValue_rowNumber']) {
|
||||
$timestamp = $info[$key_counter]['first_timestamp'] +
|
||||
($rowCount * $info[$key_counter]['step'] * $info[$key_counter]['pdp_per_row']);
|
||||
if (($timestamp >= $selection["from"] && $timestamp <= $selection["to"] &&
|
||||
$archValue["type"] == "detail") || ($archValue["type"] == "overview" &&
|
||||
$timestamp <= $selection["from"]) || ($archValue["type"] == "overview" &&
|
||||
$timestamp >= $selection["to"])) {
|
||||
$condense_counter++;
|
||||
// Find smallest step in focus area = detail
|
||||
if ($archValue['type'] == "detail" && $selection["full_range"] == false) {
|
||||
// Set new calculated step size
|
||||
$condensed_step = ($info[$key_counter]['full_step'] * $condense);
|
||||
} else {
|
||||
if ($selection["full_range"] == true && $archValue['type'] == "overview") {
|
||||
$condensed_step = ($info[$key_counter]['full_step'] * $condense);
|
||||
}
|
||||
}
|
||||
$column_counter = 0;
|
||||
if (!isset($condensed_row_values[$count_values])) {
|
||||
$condensed_row_values[$count_values] = [];
|
||||
}
|
||||
|
||||
foreach ($rowValues->v as $columnKey => $columnValue) {
|
||||
if (!isset($condensed_row_values[$count_values][$column_counter])) {
|
||||
$condensed_row_values[$count_values][$column_counter] = 0;
|
||||
}
|
||||
if (trim($columnValue) == "NaN") {
|
||||
// skip processing the rest of the values as this set has a NaN value
|
||||
$skip_nan = true;
|
||||
|
||||
$condensed_row_values[$count_values][$column_counter] = "NaN";
|
||||
|
||||
} elseif ($skip_nan == false) {
|
||||
if ($archValue["type"] == "overview") {
|
||||
// overwrite this values and skip averaging, looks better for overview
|
||||
$condensed_row_values[$count_values][$column_counter] =
|
||||
((float)$columnValue);
|
||||
} elseif ($calculation_type == "AVERAGE") {
|
||||
// For AVERAGE always add the values
|
||||
$condensed_row_values[$count_values][$column_counter] +=
|
||||
(float)$columnValue;
|
||||
} elseif ($calculation_type == "MINIMUM" || $condense_counter == 1) {
|
||||
// For MINIMUM update value if smaller one found or first
|
||||
if ($condensed_row_values[$count_values][$column_counter] >
|
||||
(float)$columnValue) {
|
||||
$condensed_row_values[$count_values][$column_counter] =
|
||||
(float)$columnValue;
|
||||
}
|
||||
} elseif ($calculation_type == "MAXIMUM" || $condense_counter == 1) {
|
||||
// For MAXIMUM update value if higher one found or first
|
||||
if ($condensed_row_values[$count_values][$column_counter] <
|
||||
(float)$columnValue) {
|
||||
$condensed_row_values[$count_values][$column_counter] =
|
||||
(float)$columnValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$column_counter++;
|
||||
}
|
||||
|
||||
if ($condense_counter == $condense) {
|
||||
foreach ($condensed_row_values[$count_values] as $crvKey => $crValue) {
|
||||
if ($condensed_row_values[$count_values][$crvKey] != "NaN" &&
|
||||
$calculation_type == "AVERAGE" && $archValue["type"] != "overview") {
|
||||
// For AVERAGE we need to calculate it,
|
||||
// dividing by the total number of values collected
|
||||
$condensed_row_values[$count_values][$crvKey] =
|
||||
(float)$condensed_row_values[$count_values][$crvKey] / $condense;
|
||||
}
|
||||
}
|
||||
$skip_nan = false;
|
||||
if ($info[$key_counter]['available_rows'] > 0) {
|
||||
array_push($condensed_archive, [
|
||||
"timestamp" => $timestamp - ($info[$key_counter]['step'] *
|
||||
$info[$key_counter]['pdp_per_row']),
|
||||
"condensed_values" => $condensed_row_values[$count_values]
|
||||
]);
|
||||
}
|
||||
$count_values++;
|
||||
$condense_counter = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
$rowCount++;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
$key_counter++;
|
||||
}
|
||||
|
||||
// get value information to include in set
|
||||
$column_data = array();
|
||||
foreach ($xml->ds as $key => $value) {
|
||||
array_push($column_data, ["name" => trim($value->name), "type" => trim($value->type)]);
|
||||
}
|
||||
|
||||
return ["condensed_step" => $condensed_step, "columns" => $column_data, "archive" => $condensed_archive];
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom Compare for usort
|
||||
* @param $a
|
||||
* @param $b
|
||||
* @return mixed
|
||||
*/
|
||||
private function orderByTimestampASC($a, $b)
|
||||
{
|
||||
return $a[0] - $b[0];
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,44 @@
|
||||
<?php
|
||||
/**
|
||||
* Copyright (C) 2015 Deciso B.V. - J. Schellevis
|
||||
*
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
|
||||
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
* AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
||||
* AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
|
||||
* OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
* POSSIBILITY OF SUCH DAMAGE.
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
namespace OPNsense\SystemHealth\Api;
|
||||
|
||||
use \OPNsense\Base\ApiControllerBase;
|
||||
|
||||
/**
|
||||
* Class SettingsController
|
||||
* @package OPNsense\SystemHealth
|
||||
*/
|
||||
class SettingsController extends ApiControllerBase
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
?>
|
||||
@ -0,0 +1,43 @@
|
||||
<?php
|
||||
/**
|
||||
* Copyright (C) 2015 Deciso B.V. - J.Schellevis
|
||||
*
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
|
||||
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
* AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
||||
* AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
|
||||
* OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
* POSSIBILITY OF SUCH DAMAGE.
|
||||
*
|
||||
*/
|
||||
namespace OPNsense\SystemHealth;
|
||||
|
||||
/**
|
||||
* Class IndexController
|
||||
* @package OPNsense\Proxy
|
||||
*/
|
||||
class IndexController extends \OPNsense\Base\IndexController
|
||||
{
|
||||
public function indexAction()
|
||||
{
|
||||
$this->view->title = "System Health";
|
||||
$this->view->pick('OPNsense/SystemHealth/index');
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
/**
|
||||
* Copyright (C) 2015 Deciso B.V.
|
||||
*
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
|
||||
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
* AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
||||
* AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
|
||||
* OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
* POSSIBILITY OF SUCH DAMAGE.
|
||||
*
|
||||
*/
|
||||
namespace OPNsense\SystemHealth;
|
||||
|
||||
use OPNsense\Base\BaseModel;
|
||||
|
||||
class Sample extends BaseModel
|
||||
{
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
<model>
|
||||
<mount>//OPNsense/systemhealth</mount>
|
||||
<description>
|
||||
(rrd) System Health Settings
|
||||
</description>
|
||||
<items>
|
||||
</items>
|
||||
</model>
|
||||
736
src/opnsense/mvc/app/views/OPNsense/SystemHealth/index.volt
Normal file
736
src/opnsense/mvc/app/views/OPNsense/SystemHealth/index.volt
Normal file
@ -0,0 +1,736 @@
|
||||
<!--
|
||||
/**
|
||||
* Copyright (C) 2015 Deciso B.V. - J.Schellevis
|
||||
*
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
|
||||
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
* AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
||||
* AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
|
||||
* OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
* POSSIBILITY OF SUCH DAMAGE.
|
||||
*
|
||||
*/
|
||||
-->
|
||||
|
||||
|
||||
<!-- nvd3 -->
|
||||
<link rel="stylesheet" href="/ui/css/nv.d3.css">
|
||||
|
||||
<!-- d3 -->
|
||||
<script type="text/javascript" src="/ui/js/d3.min.js"></script>
|
||||
|
||||
<!-- nvd3 -->
|
||||
<script type="text/javascript" src="/ui/js/nv.d3.min.js"></script>
|
||||
|
||||
<!-- System Health -->
|
||||
<style>
|
||||
|
||||
#chart svg {
|
||||
height: 500px;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
|
||||
<script type="application/javascript">
|
||||
var chart;
|
||||
var data = [];
|
||||
var fetching_data = true;
|
||||
var current_selection_from = 0;
|
||||
var current_selection_to = 0;
|
||||
var disabled = [];
|
||||
var brushendTimer;
|
||||
var resizeTimer;
|
||||
var current_detail = 0;
|
||||
var csvData = [];
|
||||
var zoom_buttons;
|
||||
var rrd="";
|
||||
|
||||
// Load data when document is ready
|
||||
$(document).ready(function () {
|
||||
getRRDlist();
|
||||
});
|
||||
|
||||
// create our chart
|
||||
nv.addGraph(function () {
|
||||
chart = nv.models.lineWithFocusChart()
|
||||
.margin( {left:70})
|
||||
.x(function (d) {
|
||||
return d[0]
|
||||
})
|
||||
.y(function (d) {
|
||||
return d[1]
|
||||
});
|
||||
chart.xAxis
|
||||
.tickFormat(function (d) {
|
||||
return d3.time.format('%b %e %H:%M')(new Date(d))
|
||||
});
|
||||
|
||||
chart.x2Axis
|
||||
.tickFormat(function (d) {
|
||||
return d3.time.format('%x')(new Date(d))
|
||||
});
|
||||
|
||||
chart.yAxis
|
||||
.tickFormat(d3.format(',.2s'));
|
||||
|
||||
chart.y2Axis
|
||||
.tickFormat(d3.format(',.1s'));
|
||||
|
||||
chart.focusHeight(80);
|
||||
chart.interpolate('step-before');
|
||||
|
||||
// dispatch when one of the streams is enabled/disabled
|
||||
chart.dispatch.on('stateChange', function (e) {
|
||||
disabled = e['disabled'];
|
||||
});
|
||||
|
||||
// dispatch when the focus area has changed - delay action with 500ms timer
|
||||
chart.dispatch.on('brush.brushend', function (b) {
|
||||
window.onresize = null; // clear any pending resize events
|
||||
if (fetching_data == false) {
|
||||
if ($('input:radio[name=inverse]:checked').val() == 1) {
|
||||
inverse = true;
|
||||
} else {
|
||||
inverse = false;
|
||||
}
|
||||
|
||||
var detail = $('input:radio[name=detail]:checked').val();
|
||||
var resolution = $('input:radio[name=resolution]:checked').val();
|
||||
|
||||
if ((window.selmin != b.extent[0]) || (window.selmax != b.extent[1])) {
|
||||
|
||||
if (current_selection_from * 1000 != b.extent[0] || current_selection_to != b.extent[1]) {
|
||||
if (brushendTimer) {
|
||||
clearTimeout(brushendTimer);
|
||||
}
|
||||
brushendTimer = setTimeout(function () {
|
||||
if (chart.xAxis.scale().domain()[0] == b.extent[0] && chart.xAxis.scale().domain()[1] == b.extent[1]) {
|
||||
getdata(rrd, 0, 0, resolution, detail);
|
||||
} else {
|
||||
getdata(rrd, Math.floor(b.extent[0] / 1000), Math.floor(b.extent[1] / 1000), resolution, detail);
|
||||
}
|
||||
brushendTimer = null;
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
// dispatch on window resize - delay action with 500ms timer
|
||||
nv.utils.windowResize(function () {
|
||||
if (resizeTimer) {
|
||||
clearTimeout(resizeTimer);
|
||||
}
|
||||
resizeTimer = setTimeout(function () {
|
||||
chart.update();
|
||||
resizeTimer = null;
|
||||
}, 500);
|
||||
});
|
||||
|
||||
return chart;
|
||||
});
|
||||
|
||||
// Some options have changed, check and fetch data
|
||||
function UpdateOptions() {
|
||||
window.onresize = null; // clear any pending resize events
|
||||
var inverse = false;
|
||||
var detail = 0;
|
||||
var resolution = 120;
|
||||
if ($('input:radio[name=inverse]:checked').val() == 1) {
|
||||
inverse = true;
|
||||
}
|
||||
|
||||
detail = $('input:radio[name=detail]:checked').val();
|
||||
resolution = $('input:radio[name=resolution]:checked').val();
|
||||
if (detail != current_detail) {
|
||||
chart.brushExtent([0, 0]);
|
||||
getdata(rrd, 0, 0, resolution, detail);
|
||||
current_detail = detail;
|
||||
} else {
|
||||
getdata(rrd, current_selection_from, current_selection_to, resolution, detail);
|
||||
current_detail = detail;
|
||||
}
|
||||
}
|
||||
|
||||
function getRRDlist() {
|
||||
ajaxGet(url = "/api/systemhealth/service/getRRDlist/", sendData = {}, callback = function (data, status) {
|
||||
if (status == "success") {
|
||||
var category;
|
||||
var tabs="";
|
||||
var subitem="";
|
||||
var active_category=Object.keys(data["data"])[0];
|
||||
var active_subitem=data["data"][active_category][0];
|
||||
var rrd_name="";
|
||||
for ( category in data["data"]) {
|
||||
if (category == active_category) {
|
||||
tabs += '<li role="presentation" class="dropdown active">';
|
||||
} else {
|
||||
tabs += '<li role="presentation" class="dropdown">';
|
||||
}
|
||||
|
||||
subitem=data["data"][category][0]; // first sub item
|
||||
if (category=="system") {
|
||||
rrd_name = category + '-' + subitem;
|
||||
} else {
|
||||
rrd_name = subitem + '-' + category;
|
||||
}
|
||||
|
||||
// create dropdown menu
|
||||
tabs+='<a data-toggle="dropdown" href="#" class="dropdown-toggle pull-right visible-lg-inline-block visible-md-inline-block visible-xs-inline-block visible-sm-inline-block" role="button" style="border-left: 1px dashed lightgray;">';
|
||||
tabs+='<b><span class="caret"></span></b>';
|
||||
tabs+='</a>';
|
||||
tabs+='<a data-toggle="tab" onclick="$(\'#'+rrd_name+'\').click();" class="visible-lg-inline-block visible-md-inline-block visible-xs-inline-block visible-sm-inline-block" style="border-right:0px;"><b>'+category[0].toUpperCase() + category.slice(1)+'</b></a>';
|
||||
tabs+='<ul class="dropdown-menu" role="menu">';
|
||||
rrd_name="";
|
||||
|
||||
// add subtabs
|
||||
for (var count=0; count<data["data"][category].length;++count ) {
|
||||
subitem=data["data"][category][count];
|
||||
if (category=="system") {
|
||||
rrd_name = category + '-' + subitem;
|
||||
} else {
|
||||
rrd_name = subitem + '-' + category;
|
||||
}
|
||||
|
||||
if (subitem==active_subitem) {
|
||||
tabs += '<li class="active"><a data-toggle="tab" onclick="getdata(\''+rrd_name+'\',0,0,120,0);" id="'+rrd_name+'"><i class="fa fa-check-square"></i> ' + subitem[0].toUpperCase() + subitem.slice(1) + '</a></li>';
|
||||
rrd=rrd_name;
|
||||
getdata(rrd_name,0,0,120,false,0); // load initial data
|
||||
} else {
|
||||
tabs += '<li><a data-toggle="tab" onclick="getdata(\''+rrd_name+'\',0,0,120,0);" id="'+rrd_name+'"><i class="fa fa-check-square"></i> ' + subitem[0].toUpperCase() + subitem.slice(1) + '</a></li>';
|
||||
}
|
||||
}
|
||||
tabs+='</ul>';
|
||||
tabs+='</li>';
|
||||
}
|
||||
$('#maintabs').html(tabs);
|
||||
$('#tab_1').toggleClass('active');
|
||||
} else {
|
||||
alert("Error while fetching RRD list : "+status);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getdata(rrd_name, from, to, maxitems, detail) {
|
||||
|
||||
if (zoom_buttons===undefined) {
|
||||
zoom_buttons="";
|
||||
}
|
||||
|
||||
// Set defaults if not specified
|
||||
if (rrd_name === undefined) {
|
||||
rrd_name = rrd;
|
||||
disabled = []; // clear disabled stream data
|
||||
|
||||
} else {
|
||||
if ( rrd_name!=rrd ) {
|
||||
rrd = rrd_name; // set global rrd name to current rrd
|
||||
disabled = []; // clear disabled stream data
|
||||
zoom_buttons=""; // clear zoom_buttons
|
||||
chart.brushExtent([0, 0]); // clear focus area
|
||||
$('#res0').parent().click(); // reset resolution
|
||||
}
|
||||
}
|
||||
|
||||
if (from === undefined) {
|
||||
from = 0;
|
||||
}
|
||||
if (to === undefined) {
|
||||
to = 0;
|
||||
|
||||
}
|
||||
if (maxitems === undefined) {
|
||||
maxitems = 120;
|
||||
|
||||
}
|
||||
|
||||
if ($('input:radio[name=inverse]:checked').val() == 1) {
|
||||
inverse = true;
|
||||
} else {
|
||||
inverse = false;
|
||||
}
|
||||
if (detail === undefined) {
|
||||
detail = 0;
|
||||
}
|
||||
|
||||
// Remember selected area
|
||||
current_selection_from = from;
|
||||
current_selection_to = to;
|
||||
|
||||
// Flag to know when we are fetching data
|
||||
fetching_data = true;
|
||||
|
||||
// Used to set render the zoom/detail buttons
|
||||
//zoom_buttons = "";
|
||||
|
||||
// array used for cvs export option
|
||||
csvData = [];
|
||||
|
||||
// array used for min/max/average table when shown
|
||||
min_max_average = {};
|
||||
|
||||
// info bar - hide averages info bar while refreshing data
|
||||
$('#averages').hide();
|
||||
// info bar - show loading info bar while refreshing data
|
||||
$('#loading').show();
|
||||
// API call to request data
|
||||
ajaxGet(url = "/api/systemhealth/service/getSystemHealth/" + rrd_name + "/" + String(from) + "/" + String(to) + "/" + String(maxitems) + "/" + String(inverse) + "/" + String(detail), sendData = {}, callback = function (data, status) {
|
||||
if (status == "success") {
|
||||
var stepsize = data["d3"]["stepSize"];
|
||||
var scale = "{{ lang._('seconds') }}";
|
||||
var dtformat = '%m/%d %H:%M';
|
||||
var visable_time=to-from;
|
||||
|
||||
// set defaults based on stepsize
|
||||
if (stepsize >= 86400) {
|
||||
stepsize = stepsize / 86400;
|
||||
scale = "{{ lang._('days') }}";
|
||||
dtformat = '\'%y w%U%';
|
||||
} else if (stepsize >= 3600) {
|
||||
stepsize = stepsize / 3600;
|
||||
scale = "{{ lang._('hours') }}";
|
||||
dtformat = '\'%y d%j%';
|
||||
} else if (stepsize >= 60) {
|
||||
stepsize = stepsize / 60;
|
||||
scale = "{{ lang._('minutes') }}";
|
||||
dtformat = '%H:%M';
|
||||
}
|
||||
|
||||
// if we have a focus area then change the x-scale to reflect current view
|
||||
if (visable_time >= (86400*7)) { // one week
|
||||
console.log('a');
|
||||
dtformat = '\'%y w%U%';
|
||||
} else if (visable_time >= (3600*48)) { // 48 hours
|
||||
console.log('b');
|
||||
dtformat = '\'%y d%j%';
|
||||
} else if (visable_time >= (60*maxitems)) { // max minutes
|
||||
console.log('c');
|
||||
dtformat = '%H:%M';
|
||||
}
|
||||
|
||||
// Add zoomlevel buttons/options
|
||||
if ($('input:radio[name=detail]:checked').val() == undefined || zoom_buttons==="") {
|
||||
for (setcount = 0; setcount < data["sets"].length; ++setcount) {
|
||||
recordedtime = data["sets"][setcount]["recorded_time"];
|
||||
// Find out what text matches best
|
||||
if (recordedtime >= 31536000) {
|
||||
detail_text = Math.floor(recordedtime / 31536000).toString() + " {{ lang._('Year(s)') }}";
|
||||
} else if (recordedtime >= 259200) {
|
||||
detail_text = Math.floor(recordedtime / 86400).toString() + " {{ lang._('Days') }}";
|
||||
} else if (recordedtime > 3600) {
|
||||
detail_text = Math.floor(recordedtime / 3600).toString() + " {{ lang._('Hours') }}";
|
||||
} else {
|
||||
detail_text = Math.floor(recordedtime / 60).toString() + " {{ lang._('Minutes') }}";
|
||||
}
|
||||
if (setcount == 0) {
|
||||
zoom_buttons += '<label class="btn btn-default active"> <input type="radio" id="d' + setcount.toString() + '" name="detail" checked="checked" value="' + setcount.toString() + '" /> ' + detail_text + ' </label>';
|
||||
} else {
|
||||
zoom_buttons += '<label class="btn btn-default"> <input type="radio" id="d' + setcount.toString() + '" name="detail" value="' + setcount.toString() + '" /> ' + detail_text + ' </label>';
|
||||
}
|
||||
|
||||
}
|
||||
if (zoom_buttons === "") {
|
||||
zoom_buttons = "<b>No data available</b>";
|
||||
}
|
||||
// insert zoom buttons html code
|
||||
$('#zoom').html(zoom_buttons);
|
||||
}
|
||||
$('#stepsize').text(stepsize + " " + scale);
|
||||
|
||||
// Check for enabled or disabled stream, to make sure that same set stays selected after update
|
||||
for (index = 0; index < disabled.length; ++index) {
|
||||
window.resize = null;
|
||||
data["d3"]["data"][index]["disabled"] = disabled[index]; // disable stream if it was disabled before updating dataset
|
||||
}
|
||||
|
||||
// Create tables (general and detail)
|
||||
if ($('input:radio[name=show_table]:checked').val() == 1) { // check if togle table is on
|
||||
table_head = "<th>#</th>";
|
||||
if ($('input:radio[name=toggle_time]:checked').val() == 1) {
|
||||
table_head += "<th>{{ lang._('full date & time') }}</th>";
|
||||
} else {
|
||||
table_head += "<th>{{ lang._('timestamp') }}</th>";
|
||||
}
|
||||
|
||||
// Setup variables for table data
|
||||
var table_head; // used for table headings in html format
|
||||
var table_row_data = {}; // holds row data for table
|
||||
var table_view_rows = ""; // holds row data in html format
|
||||
|
||||
var keyname = ""; // used for name of key
|
||||
var rowcounter = 0;// general row counter
|
||||
var min; // holds calculated minimum value
|
||||
var max; // holds calculated maximum value
|
||||
var average; // holds calculated average
|
||||
|
||||
var t; // general date/time variable
|
||||
var item; // used for name of key
|
||||
|
||||
var counter = 1; // used for row count
|
||||
|
||||
for (index = 0; index < data["d3"]["data"].length; ++index) {
|
||||
rowcounter = 0;
|
||||
min = 0;
|
||||
max = 0;
|
||||
average = 0;
|
||||
if (data["d3"]["data"][index]["disabled"] != true) {
|
||||
table_head += '<th>' + data["d3"]["data"][index]["key"] + '</th>';
|
||||
keyname = data["d3"]["data"][index]["key"].toString();
|
||||
for (var value_index = 0; value_index < data["d3"]["data"][index]["values"].length; ++value_index) {
|
||||
|
||||
if (data["d3"]["data"][index]["values"][value_index][0] >= (from * 1000) && data["d3"]["data"][index]["values"][value_index][0] <= (to * 1000) || ( from == 0 && to == 0 )) {
|
||||
|
||||
if (table_row_data[data["d3"]["data"][index]["values"][value_index][0]] === undefined) {
|
||||
table_row_data[data["d3"]["data"][index]["values"][value_index][0]] = {};
|
||||
}
|
||||
if (table_row_data[data["d3"]["data"][index]["values"][value_index][0]][data["d3"]["data"][index]["key"]] === undefined) {
|
||||
table_row_data[data["d3"]["data"][index]["values"][value_index][0]][data["d3"]["data"][index]["key"]] = data["d3"]["data"][index]["values"][value_index][1];
|
||||
}
|
||||
if (csvData[rowcounter] === undefined) {
|
||||
csvData[rowcounter] = {};
|
||||
}
|
||||
if (csvData[rowcounter]["timestamp"] === undefined) {
|
||||
t = new Date(parseInt(data["d3"]["data"][index]["values"][value_index][0]));
|
||||
csvData[rowcounter]["timestamp"] = data["d3"]["data"][index]["values"][value_index][0] / 1000;
|
||||
csvData[rowcounter]["date_time"] = t.toString();
|
||||
}
|
||||
csvData[rowcounter][keyname] = data["d3"]["data"][index]["values"][value_index][1];
|
||||
if (data["d3"]["data"][index]["values"][value_index][1] < min) {
|
||||
min = data["d3"]["data"][index]["values"][value_index][1];
|
||||
}
|
||||
if (data["d3"]["data"][index]["values"][value_index][1] > max) {
|
||||
max = data["d3"]["data"][index]["values"][value_index][1];
|
||||
}
|
||||
average += data["d3"]["data"][index]["values"][value_index][1];
|
||||
++rowcounter;
|
||||
}
|
||||
}
|
||||
if (min_max_average[keyname] === undefined) {
|
||||
min_max_average[keyname] = {};
|
||||
min_max_average[keyname]["min"] = min;
|
||||
min_max_average[keyname]["max"] = max;
|
||||
min_max_average[keyname]["average"] = average / rowcounter;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
for ( item in min_max_average) {
|
||||
table_view_rows += "<tr>";
|
||||
table_view_rows += "<td>" + item + "</td>";
|
||||
table_view_rows += "<td>" + min_max_average[item]["min"].toString() + "</td>";
|
||||
table_view_rows += "<td>" + min_max_average[item]["max"].toString() + "</td>";
|
||||
table_view_rows += "<td>" + min_max_average[item]["average"].toString() + "</td>";
|
||||
table_view_rows += "</tr>";
|
||||
}
|
||||
$('#table_view_general_heading').html('<th>item</th><th>min</th><th>max</th><th>average</th>');
|
||||
$('#table_view_general_rows').html(table_view_rows);
|
||||
table_view_rows = "";
|
||||
|
||||
for ( item in table_row_data) {
|
||||
if ($('input:radio[name=toggle_time]:checked').val() == 1) {
|
||||
t = new Date(parseInt(item));
|
||||
table_view_rows += "<tr><td>" + counter.toString() + "</td><td>" + t.toString() + "</td>";
|
||||
} else {
|
||||
table_view_rows += "<tr><td>" + counter.toString() + "</td><td>" + parseInt(item / 1000).toString() + "</td>";
|
||||
}
|
||||
for (var value in table_row_data[item]) {
|
||||
table_view_rows += "<td>" + table_row_data[item][value] + "</td>";
|
||||
}
|
||||
++counter;
|
||||
table_view_rows += "</tr>";
|
||||
}
|
||||
|
||||
$('#table_view_heading').html(table_head);
|
||||
$('#table_view_rows').html(table_view_rows);
|
||||
$('#chart_details_table').show();
|
||||
$('#chart_general_table').show();
|
||||
} else {
|
||||
$('#chart_details_table').hide();
|
||||
$('#chart_general_table').hide();
|
||||
}
|
||||
chart.xAxis
|
||||
.tickFormat(function (d) {
|
||||
return d3.time.format(dtformat)(new Date(d))
|
||||
});
|
||||
chart.yAxis.axisLabel(data["x-axis_label"]);
|
||||
chart.useInteractiveGuideline(true);
|
||||
chart.interactive(true);
|
||||
|
||||
d3.select('#chart svg')
|
||||
.datum(data["d3"]["data"])
|
||||
.transition().duration(0)
|
||||
.call(chart);
|
||||
|
||||
|
||||
chart.update();
|
||||
window.onresize = null; // clear any pending resize events
|
||||
|
||||
|
||||
fetching_data = false;
|
||||
$('#loading').hide(); // Data has been found and chart will be drawn
|
||||
$('#averages').show();
|
||||
if (data["title"]!="") {
|
||||
$('#chart_title').show();
|
||||
$('#chart_title').text(data["title"]);
|
||||
} else
|
||||
{
|
||||
$('#chart_title').hide();
|
||||
}
|
||||
|
||||
} else {
|
||||
alert("Error while fetching data : "+status);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// convert a data Array to CSV format
|
||||
function convertToCSV(args) {
|
||||
var result, ctr, keys, columnDelimiter, lineDelimiter, data;
|
||||
|
||||
data = args.data || null;
|
||||
if (data == null || !data.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
columnDelimiter = args.columnDelimiter || ';';
|
||||
lineDelimiter = args.lineDelimiter || '\n';
|
||||
|
||||
keys = Object.keys(data[0]);
|
||||
|
||||
result = '';
|
||||
result += keys.join(columnDelimiter);
|
||||
result += lineDelimiter;
|
||||
|
||||
data.forEach(function (item) {
|
||||
ctr = 0;
|
||||
keys.forEach(function (key) {
|
||||
if (ctr > 0) result += columnDelimiter;
|
||||
|
||||
result += item[key];
|
||||
ctr++;
|
||||
});
|
||||
result += lineDelimiter;
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// download CVS file
|
||||
function downloadCSV(args) {
|
||||
var data, filename, link;
|
||||
var csv = convertToCSV({
|
||||
data: csvData
|
||||
});
|
||||
if (csv == null) return;
|
||||
console.log(csv);
|
||||
filename = args.filename || 'export.csv';
|
||||
|
||||
if (!csv.match(/^data:text\/csv/i)) {
|
||||
csv = 'data:text/csv;charset=utf-8,' + csv;
|
||||
}
|
||||
data = encodeURI(csv);
|
||||
|
||||
link = document.createElement('a');
|
||||
link.href = data;
|
||||
link.target = '_blank';
|
||||
link.download = filename;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<ul class="nav nav-tabs" role="tablist" id="maintabs">
|
||||
{# Tab Content #}
|
||||
</ul>
|
||||
|
||||
<div class="content-box tab-content">
|
||||
<div id="tab_1" class="tab-pane fade in">
|
||||
<div class="panel panel-primary">
|
||||
<div class="panel-heading"><b>{{ lang._('Options') }}</b></div>
|
||||
<div class="panel-body">
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-md-12"></div>
|
||||
<div class="col-md-4">
|
||||
<b>{{ lang._('Zoom level') }}:</b>
|
||||
|
||||
<form onChange="UpdateOptions()">
|
||||
<div class="btn-group" data-toggle="buttons" id="zoom">
|
||||
<!-- The zoom buttons are generated based upon the current dataset -->
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<b>{{ lang._('Inverse') }}:</b>
|
||||
|
||||
<form onChange="UpdateOptions()">
|
||||
<div class="btn-group" data-toggle="buttons">
|
||||
<label class="btn btn-default active">
|
||||
<input type="radio" id="in0" name="inverse" checked="checked" value="0"/> {{
|
||||
lang._('Off') }}
|
||||
</label>
|
||||
<label class="btn btn-default">
|
||||
<input type="radio" id="in1" name="inverse" value="1"/> {{ lang._('On') }}
|
||||
</label>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<b>{{ lang._('Resolution') }}:</b>
|
||||
|
||||
<form onChange="UpdateOptions()">
|
||||
<div class="btn-group" data-toggle="buttons">
|
||||
<label class="btn btn-default active">
|
||||
<input type="radio" id="res0" name="resolution" checked="checked" value="120"/>
|
||||
{{ lang._('Standard') }}
|
||||
</label>
|
||||
<label class="btn btn-default">
|
||||
<input type="radio" id="res1" name="resolution" value="240"/> {{
|
||||
lang._('Medium') }}
|
||||
</label>
|
||||
<label class="btn btn-default">
|
||||
<input type="radio" id="res2" name="resolution" value="600"/> {{ lang._('High')
|
||||
}}
|
||||
</label>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<b>{{ lang._('Show Tables') }}:</b>
|
||||
|
||||
<form onChange="UpdateOptions()">
|
||||
<div class="btn-group" data-toggle="buttons">
|
||||
<label class="btn btn-default active">
|
||||
<input type="radio" id="tab0" name="show_table" checked="checked" value="0"/> {{
|
||||
lang._('Off') }}
|
||||
</label>
|
||||
<label class="btn btn-default">
|
||||
<input type="radio" id="tab1" name="show_table" value="1"/> {{ lang._('On') }}
|
||||
</label>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div id="averages" class="alert alert-info" role="alert">
|
||||
<b>{{ lang._('Current detail is showing') }} <span id="stepsize"></span> {{ lang._('averages') }}.</b>
|
||||
</div>
|
||||
<div id="loading" class="alert bg-primary"><i class="fa fa-spinner fa-spin"></i>
|
||||
<b>{{ lang._('Please wait while loading data...') }}</b>
|
||||
</div>
|
||||
|
||||
<!--<div id="stepsize"></div>-->
|
||||
|
||||
<!-- place holder for the chart itself -->
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title" id="chart_title">
|
||||
</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div id="chart">
|
||||
<svg></svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- place holder for the general table with min/max/averages, is hidden by default -->
|
||||
<div id="chart_general_table" class="col-md-12" style="display: none;">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title"> {{ lang._('Current View - Overview') }}</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-condensed table-hover table-striped">
|
||||
<thead>
|
||||
<tr id="table_view_general_heading" class="active">
|
||||
<!-- Dynamic data -->
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="table_view_general_rows">
|
||||
<!-- Dynamic data -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div id="chart_details_table" class="col-md-12" style="display: none;">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title"> {{ lang._('Current View - Details') }}</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="btn-toolbar" role="toolbar">
|
||||
<i>{{ lang._('Toggle Timeview') }}:</i>
|
||||
|
||||
<form onChange="UpdateOptions();">
|
||||
<div class="btn-group" data-toggle="buttons">
|
||||
<label class="btn btn-xs btn-default active">
|
||||
<input type="radio" id="time0" name="toggle_time" checked="checked" value="0"/> {{
|
||||
lang._('Timestamp') }}
|
||||
</label>
|
||||
<label class="btn btn-xs btn-default">
|
||||
<input type="radio" id="time1" name="toggle_time" value="1"/> {{ lang._('Full Date &
|
||||
Time') }}
|
||||
</label>
|
||||
</div>
|
||||
</form>
|
||||
<div class="btn btn-xs btn-primary inline"
|
||||
onclick='downloadCSV({ filename: rrd+".csv" });'><i
|
||||
class="glyphicon glyphicon-download-alt"></i>{{ lang._('Download as CSV') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-condensed table-hover table-striped">
|
||||
|
||||
<thead>
|
||||
<tr id="table_view_heading" class="active">
|
||||
<!-- Dynamic data -->
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="table_view_rows">
|
||||
<!-- Dynamic data -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
@ -0,0 +1,12 @@
|
||||
<?xml version="1.0"?>
|
||||
<details>
|
||||
<title> System Information - Utilization and Processes</title>
|
||||
<x-axis_label>[U]tilization , [#]Number</x-axis_label>
|
||||
<field_units>
|
||||
<user>[U]</user>
|
||||
<nice>[U]</nice>
|
||||
<system>[U]</system>
|
||||
<interrupt>[#]</interrupt>
|
||||
<processes>[#]</processes>
|
||||
</field_units>
|
||||
</details>
|
||||
35
src/opnsense/scripts/systemhealth/queryDetails.py
Executable file
35
src/opnsense/scripts/systemhealth/queryDetails.py
Executable file
@ -0,0 +1,35 @@
|
||||
#!/usr/local/bin/python2.7
|
||||
|
||||
"""
|
||||
Copyright (c) 2015 Deciso B.V. - J. Schellevis
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice,
|
||||
this list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
|
||||
INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
||||
AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
|
||||
OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
--------------------------------------------------------------------------------------
|
||||
|
||||
"""
|
||||
|
||||
file = open('/Users/josschellevis/Development/opnsense/scripts/systemhealth/metadata/system-processor.xml', 'r')
|
||||
file_contents = file.read()
|
||||
print (file_contents)
|
||||
file.close()
|
||||
@ -0,0 +1,5 @@
|
||||
[query.details]
|
||||
command:/Users/josschellevis/Development/opnsense/scripts/systemhealth/queryDetails.py
|
||||
parameters:
|
||||
type:script_output
|
||||
message:request rrd graph details
|
||||
Loading…
x
Reference in New Issue
Block a user