RRD Graph optimization (#12735)

* RRD Graph optimization
Do not use temporary files to generate graphs
Don't start up a process to pipe commands to, just run the command
Error image improvements

* fix style issues
This commit is contained in:
Tony Murray
2021-04-28 05:28:07 -05:00
committed by GitHub
parent 33c7151763
commit 2833d935e0
7 changed files with 165 additions and 105 deletions

View File

@@ -28,9 +28,11 @@ use Illuminate\Support\Str;
use LibreNMS\Config;
use LibreNMS\Data\Measure\Measurement;
use LibreNMS\Exceptions\FileExistsException;
use LibreNMS\Exceptions\RrdGraphException;
use LibreNMS\Proc;
use LibreNMS\Util\Rewrite;
use Log;
use Symfony\Component\Process\Process;
class Rrd extends BaseDatastore
{
@@ -545,25 +547,26 @@ class Rrd extends BaseDatastore
/**
* Generates a graph file at $graph_file using $options
* Opens its own rrdtool pipe.
* Graphs are a single command per run, so this just runs rrdtool
*
* @param string $graph_file
* @param string $options
* @return string|int
* @param string $options
* @return string
* @throws \LibreNMS\Exceptions\FileExistsException
* @throws \LibreNMS\Exceptions\RrdGraphException
*/
public function graph($graph_file, $options)
public function graph(string $options): string
{
if ($this->init(false)) {
$cmd = $this->buildCommand('graph', $graph_file, $options);
$cmd = $this->buildCommand('graph', '-', $options);
$process = Process::fromShellCommandline(Config::get('rrdtool') . ' ' . $cmd);
$process->setTimeout(300);
$process->setIdleTimeout(300);
$process->run();
$output = implode($this->sync_process->sendCommand($cmd));
d_echo("<p>$cmd</p>\n<p>command returned ($output)</p>");
return $output;
} else {
return 0;
if (! $process->isSuccessful()) {
throw new RrdGraphException($process->getErrorOutput(), $process->getExitCode(), $process->getOutput());
}
return $process->getOutput();
}
public function __destruct()

View File

@@ -0,0 +1,44 @@
<?php
/*
* RrdGraphException.php
*
* Exception generated when there is an error creating the graph image
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @package LibreNMS
* @link http://librenms.org
* @copyright 2021 Tony Murray
* @author Tony Murray <murraytony@gmail.com>
*/
namespace LibreNMS\Exceptions;
use Exception;
class RrdGraphException extends Exception
{
protected $image_output;
public function __construct($error, $exit_code, $image_output)
{
parent::__construct($error, $exit_code);
$this->image_output = $image_output;
}
public function getImage()
{
return $this->image_output;
}
}

View File

@@ -81,8 +81,7 @@ function api_get_graph(array $vars)
{
global $dur; // Needed for callback within graph code
$auth = '1';
$base64_output = '';
$auth = true;
// prevent ugly error for undefined graphs from being passed to the user
[$type, $subtype] = extract_graph_type($vars['type']);
@@ -99,10 +98,10 @@ function api_get_graph(array $vars)
ob_end_clean();
if ($vars['output'] === 'base64') {
return api_success(['image' => $base64_output, 'content-type' => get_image_type()], 'image');
return api_success(['image' => $image, 'content-type' => get_image_type(Config::get('webui.graph_type'))], 'image');
}
return response($image, 200, ['Content-Type' => get_image_type()]);
return response($image, 200, ['Content-Type' => get_image_type(Config::get('webui.graph_type'))]);
}
function check_bill_permission($bill_id, $callback)

View File

@@ -46,14 +46,6 @@ function var_get($v)
return false;
}
function data_uri($file, $mime)
{
$contents = file_get_contents($file);
$base64 = base64_encode($contents);
return 'data:' . $mime . ';base64,' . $base64;
}//end data_uri()
function toner2colour($descr, $percent)
{
$colour = \LibreNMS\Util\Colors::percentage(100 - $percent, null);
@@ -464,19 +456,30 @@ function graph_error($text, $color = [128, 0, 0])
{
global $vars, $debug;
$type = Config::get('webui.graph_type');
if (! $debug) {
set_image_type();
header('Content-type: ' . get_image_type($type));
}
$width = $vars['width'] ?? 150;
$height = $vars['height'] ?? 60;
$width = (int) ($vars['width'] ?? 150);
$height = (int) ($vars['height'] ?? 60);
if (Config::get('webui.graph_type') === 'svg') {
if ($type === 'svg') {
$rgb = implode(', ', $color);
$font_size = 20;
$svg_x = 100;
$svg_y = min($font_size, $width ? (($height / $width) * $svg_x) : 1);
echo "<svg viewBox=\"0 0 $svg_x $svg_y\" xmlns=\"http://www.w3.org/2000/svg\"><text x=\"50%\" y=\"50%\" dominant-baseline=\"middle\" text-anchor=\"middle\" style=\"font-family: sans-serif; fill: rgb($rgb);\">$text</text></svg>";
echo <<<SVG
<svg xmlns="http://www.w3.org/2000/svg"
xmlns:xhtml="http://www.w3.org/1999/xhtml"
viewBox="0 0 $width $height"
preserveAspectRatio="xMinYMin">
<foreignObject x="0" y="0" width="$width" height="$height" transform="translate(0,0)">
<xhtml:div style="display:table; width:{$width}px; height:{$height}px; overflow:hidden;">
<xhtml:div style="display:table-cell; vertical-align:middle;">
<xhtml:div style="color:rgb($rgb); text-align:center; font-family:sans-serif; font-size:0.6em;">$text</xhtml:div>
</xhtml:div>
</xhtml:div>
</foreignObject>
</svg>
SVG;
} else {
$img = imagecreate($width, $height);
imagecolorallocatealpha($img, 255, 255, 255, 127); // transparent background
@@ -1019,18 +1022,14 @@ function eventlog_severity($eventlog_severity)
}
} // end eventlog_severity
function set_image_type()
/**
* Get the http content type of the image
* @param string $type svg or png
* @return string
*/
function get_image_type(string $type)
{
header('Content-type: ' . get_image_type());
}
function get_image_type()
{
if (Config::get('webui.graph_type') === 'svg') {
return 'image/svg+xml';
} else {
return 'image/png';
}
return $type === 'svg' ? 'image/svg+xml' : 'image/png';
}
function get_oxidized_nodes_list()

View File

@@ -38,8 +38,8 @@ foreach (dbFetchRows('SELECT * FROM `sensors` WHERE `sensor_class` = ? AND `devi
}//end switch
$sensor_descr_fixed = \LibreNMS\Data\Store\Rrd::fixedSafeDescr($sensor['sensor_descr'], 12);
$rrd_file = get_sensor_rrd($device, $sensor);
$rrd_options .= " DEF:sensor{$sensor['sensor_id']}=$rrd_file:sensor:AVERAGE";
$rrd_filename = get_sensor_rrd($device, $sensor);
$rrd_options .= " DEF:sensor{$sensor['sensor_id']}=$rrd_filename:sensor:AVERAGE";
$rrd_options .= " LINE1:sensor{$sensor['sensor_id']}#$colour:'$sensor_descr_fixed'";
$rrd_options .= " GPRINT:sensor{$sensor['sensor_id']}:LAST:%5.1lf$unit";
$rrd_options .= " GPRINT:sensor{$sensor['sensor_id']}:MIN:%5.1lf$unit";

View File

@@ -2,6 +2,8 @@
use LibreNMS\Config;
global $debug;
// Push $_GET into $vars to be compatible with web interface naming
foreach ($_GET as $name => $value) {
$vars[$name] = $value;
@@ -24,13 +26,12 @@ $legend = $vars['legend'];
$output = (! empty($vars['output']) ? $vars['output'] : 'default');
$from = parse_at_time($_GET['from']) ?: Config::get('time.day');
$to = parse_at_time($_GET['to']) ?: Config::get('time.now');
$graph_type = (isset($vars['graph_type']) ? $vars['graph_type'] : Config::get('webui.graph_type'));
$period = ($to - $from);
$base64_output = '';
$prev_from = ($from - $period);
$graphfile = Config::get('temp_dir') . '/' . strgen();
$graph_image_type = $vars['graph_type'] ?? Config::get('webui.graph_type');
$rrd_options = '';
$auth = false;
require Config::get('install_dir') . "/includes/html/graphs/$type/auth.inc.php";
@@ -47,61 +48,75 @@ if ($auth && is_customoid_graph($type, $subtype)) {
// Graph Template Missing");
}
if ($error_msg) {
if (! empty($error_msg)) {
// We have an error :(
graph_error($graph_error);
} elseif ($auth === null) {
graph_error($error_msg);
return;
}
if ($auth === null) {
// We are unauthenticated :(
graph_error($width < 200 ? 'No Auth' : 'No Authorization');
} else {
// $rrd_options .= " HRULE:0#999999";
if ($graph_type === 'svg') {
$rrd_options .= ' --imgformat=SVG';
if ($width < 350) {
$rrd_options .= ' -m 0.75 -R light';
}
}
if ($command_only) {
echo "<div class='infobox'>";
echo "<p style='font-size: 16px; font-weight: bold;'>RRDTool Command</p>";
echo "<pre class='rrd-pre'>";
echo 'rrdtool ' . Rrd::buildCommand('graph', $graphfile, $rrd_options);
echo '</pre>';
$return = Rrd::graph($graphfile, $rrd_options);
echo "<p style='font-size: 16px; font-weight: bold;'>RRDTool Output</p>";
echo "<pre class='rrd-pre'>";
echo "$return";
echo '</pre>';
unlink($graphfile);
echo '</div>';
} elseif ($no_file) {
graph_error($width < 200 ? 'No Data' : 'No Data file');
} elseif ($rrd_options) {
Rrd::graph($graphfile, $rrd_options);
d_echo($rrd_cmd);
if (is_file($graphfile)) {
if (! $debug) {
set_image_type();
if ($output === 'base64') {
$imagedata = file_get_contents($graphfile);
$base64_output = base64_encode($imagedata);
} else {
$fd = fopen($graphfile, 'r');
fpassthru($fd);
fclose($fd);
}
} else {
echo `ls -l $graphfile`;
echo '<img src="' . data_uri($graphfile, 'image/svg+xml') . '" alt="graph" />';
}
unlink($graphfile);
} elseif (isset($rrd_filename) && ! Rrd::checkRrdExists($rrd_filename)) {
graph_error($width < 200 ? 'No Data' : 'No Data file');
} else {
graph_error($width < 200 ? 'Draw Error' : 'Error Drawing Graph');
}
} else {
graph_error($width < 200 ? 'Def Error' : 'Graph Definition Error');
return;
}
if ($graph_image_type === 'svg') {
$rrd_options .= ' --imgformat=SVG';
if ($width < 350) {
$rrd_options .= ' -m 0.75 -R light';
}
}
// command output requested
if (! empty($command_only)) {
echo "<div class='infobox'>";
echo "<p style='font-size: 16px; font-weight: bold;'>RRDTool Command</p>";
echo "<pre class='rrd-pre'>";
echo 'rrdtool ' . Rrd::buildCommand('graph', '-', $rrd_options);
echo '</pre>';
try {
Rrd::graph($rrd_options);
} catch (\LibreNMS\Exceptions\RrdGraphException $e) {
echo "<p style='font-size: 16px; font-weight: bold;'>RRDTool Output</p>";
echo "<pre class='rrd-pre'>";
echo $e->getMessage();
echo '</pre>';
}
echo '</div>';
return;
}
// graph sent file not found flag
if (! empty($no_file)) {
graph_error($width < 200 ? 'No Data' : 'No Data file ' . $no_file);
return;
}
if (empty($rrd_options)) {
graph_error($width < 200 ? 'Def Error' : 'Graph Definition Error');
return;
}
// Generating the graph!
try {
$image_data = Rrd::graph($rrd_options);
// output the graph
if ($debug) {
echo '<img src="data:' . get_image_type($graph_image_type) . ';base64,' . base64_encode($image_data) . '" alt="graph" />';
} else {
header('Content-type: ' . get_image_type(Config::get('webui.graph_type')));
echo $output === 'base64' ? base64_encode($image_data) : $image_data;
}
} catch (\LibreNMS\Exceptions\RrdGraphException $e) {
if (isset($rrd_filename) && ! Rrd::checkRrdExists($rrd_filename)) {
graph_error($width < 200 ? 'No Data' : 'No Data file ' . basename($rrd_filename));
} else {
graph_error($width < 200 ? 'Draw Error' : 'Error Drawing Graph: ' . $e->getMessage());
}
}

View File

@@ -92,8 +92,8 @@ if (! $auth) {
$link_array['page'] = 'graphs';
$link = \LibreNMS\Util\Url::generate($link_array);
echo '<td align=center>';
echo '<b>' . $text . '</b><br>';
echo '<td style="text-align: center;">';
echo '<b>' . $text . '</b>';
echo '<a href="' . $link . '">';
echo \LibreNMS\Util\Url::lazyGraphTag($graph_array);
echo '</a>';