diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-14 13:21:16 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-14 13:21:16 +0000 |
commit | 2e582fe0b8b6a8e67982ddb84935db1bd3b401fe (patch) | |
tree | dd511b321f308264952cffb005a4288ea4e478e6 /library/Graphite/Web | |
parent | Initial commit. (diff) | |
download | icingaweb2-module-graphite-2e582fe0b8b6a8e67982ddb84935db1bd3b401fe.tar.xz icingaweb2-module-graphite-2e582fe0b8b6a8e67982ddb84935db1bd3b401fe.zip |
Adding upstream version 1.2.2.upstream/1.2.2
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'library/Graphite/Web')
16 files changed, 1783 insertions, 0 deletions
diff --git a/library/Graphite/Web/Controller/IcingadbGraphiteController.php b/library/Graphite/Web/Controller/IcingadbGraphiteController.php new file mode 100644 index 0000000..36bc026 --- /dev/null +++ b/library/Graphite/Web/Controller/IcingadbGraphiteController.php @@ -0,0 +1,110 @@ +<?php + +/* Icinga Graphite Web | (c) 2022 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Graphite\Web\Controller; + +use Icinga\Application\Modules\Module; +use Icinga\Module\Graphite\ProvidedHook\Icingadb\IcingadbSupport; +use Icinga\Module\Icingadb\Common\Auth; +use Icinga\Module\Icingadb\Common\Database; +use Icinga\Module\Icingadb\Common\SearchControls; +use ipl\Orm\Query; +use ipl\Stdlib\Contract\Paginatable; +use ipl\Web\Compat\CompatController; +use ipl\Web\Control\LimitControl; +use ipl\Web\Control\PaginationControl; +use ipl\Web\Control\SortControl; +use ipl\Web\Filter\QueryString; +use ipl\Stdlib\Filter; +use ipl\Web\Url; + +class IcingadbGraphiteController extends CompatController +{ + use Auth; + use Database; + use SearchControls; + + /** @var bool Whether to use icingadb as the backend */ + protected $useIcingadbAsBackend; + + /** @var string[] Graph parameters */ + protected $graphParams = ['graphs_limit', 'graph_range', 'graph_start', 'graph_end', 'legacyParams']; + + /** @var Filter\Rule Filter from query string parameters */ + private $filter; + + protected function moduleInit() + { + $this->useIcingadbAsBackend = Module::exists('icingadb') && IcingadbSupport::useIcingaDbAsBackend(); + } + + /** + * Get the filter created from query string parameters + * + * @return Filter\Rule + */ + public function getFilter(): Filter\Rule + { + if ($this->filter === null) { + $this->filter = QueryString::parse((string) $this->params); + } + + return $this->filter; + } + + /** + * Create and return the LimitControl + * + * This automatically shifts the limit URL parameter from {@link $params}. + * + * @return LimitControl + */ + public function createLimitControl(): LimitControl + { + $limitControl = new LimitControl(Url::fromRequest()); + $limitControl->setDefaultLimit($this->getPageSize(null)); + + $this->params->shift($limitControl->getLimitParam()); + + return $limitControl; + } + + /** + * Create and return the PaginationControl + * + * This automatically shifts the pagination URL parameters from {@link $params}. + * + * @return PaginationControl + */ + public function createPaginationControl(Paginatable $paginatable): PaginationControl + { + $paginationControl = new PaginationControl($paginatable, Url::fromRequest()); + $paginationControl->setDefaultPageSize($this->getPageSize(null)); + $paginationControl->setAttribute('id', $this->getRequest()->protectId('pagination-control')); + + $this->params->shift($paginationControl->getPageParam()); + $this->params->shift($paginationControl->getPageSizeParam()); + + return $paginationControl->apply(); + } + + /** + * Create and return the SortControl + * + * This automatically shifts the sort URL parameter from {@link $params}. + * + * @param Query $query + * @param array $columns Possible sort columns as sort string-label pairs + * + * @return SortControl + */ + public function createSortControl(Query $query, array $columns): SortControl + { + $sortControl = SortControl::create($columns); + + $this->params->shift($sortControl->getSortParam()); + + return $sortControl->apply($query); + } +} diff --git a/library/Graphite/Web/Controller/MonitoringAwareController.php b/library/Graphite/Web/Controller/MonitoringAwareController.php new file mode 100644 index 0000000..dca2ebd --- /dev/null +++ b/library/Graphite/Web/Controller/MonitoringAwareController.php @@ -0,0 +1,175 @@ +<?php + +namespace Icinga\Module\Graphite\Web\Controller; + +use ArrayIterator; +use Icinga\Application\Modules\Module; +use Icinga\Data\Filter\Filter; +use Icinga\Data\Filterable; +use Icinga\Exception\ConfigurationError; +use Icinga\Exception\QueryException; +use Icinga\Module\Graphite\ProvidedHook\Icingadb\IcingadbSupport; +use Icinga\Module\Monitoring\Backend\MonitoringBackend; +use Icinga\Module\Monitoring\Data\CustomvarProtectionIterator; +use Icinga\Module\Monitoring\DataView\DataView; +use Icinga\Util\Json; +use Icinga\File\Csv; +use Icinga\Web\Controller; +use Icinga\Web\Url; + +abstract class MonitoringAwareController extends Controller +{ + /** @var bool Whether to use icingadb as the backend */ + protected $useIcingadbAsBackend = false; + + /** + * Restrict the given monitored object query for the currently authenticated user + * + * @param DataView $dataView + * + * @return DataView The given data view + */ + protected function applyMonitoringRestriction(DataView $dataView) + { + $this->applyRestriction('monitoring/filter/objects', $dataView); + + return $dataView; + } + + protected function moduleInit() + { + if (Module::exists('icingadb') && IcingadbSupport::useIcingaDbAsBackend()) { + $this->useIcingadbAsBackend = true; + + return; + } + + $this->backend = MonitoringBackend::instance($this->_getParam('backend')); + $this->view->url = Url::fromRequest(); + } + + + protected function handleFormatRequest($query) + { + $desiredContentType = $this->getRequest()->getHeader('Accept'); + if ($desiredContentType === 'application/json') { + $desiredFormat = 'json'; + } elseif ($desiredContentType === 'text/csv') { + $desiredFormat = 'csv'; + } else { + $desiredFormat = strtolower($this->params->get('format', 'html')); + } + + if ($desiredFormat !== 'html' && ! $this->params->has('limit')) { + $query->limit(); // Resets any default limit and offset + } + + switch ($desiredFormat) { + case 'sql': + echo '<pre>' + . htmlspecialchars(wordwrap($query->dump())) + . '</pre>'; + exit; + case 'json': + $response = $this->getResponse(); + $response + ->setHeader('Content-Type', 'application/json') + ->setHeader('Cache-Control', 'no-store') + ->setHeader( + 'Content-Disposition', + 'inline; filename=' . $this->getRequest()->getActionName() . '.json' + ) + ->appendBody( + Json::sanitize( + iterator_to_array( + new CustomvarProtectionIterator( + new ArrayIterator($query->fetchAll()) + ) + ) + ) + ) + ->sendResponse(); + exit; + case 'csv': + $response = $this->getResponse(); + $response + ->setHeader('Content-Type', 'text/csv') + ->setHeader('Cache-Control', 'no-store') + ->setHeader( + 'Content-Disposition', + 'attachment; filename=' . $this->getRequest()->getActionName() . '.csv' + ) + ->appendBody((string) Csv::fromQuery(new CustomvarProtectionIterator($query))) + ->sendResponse(); + exit; + } + } + + /** + * Apply a restriction of the authenticated on the given filterable + * + * @param string $name Name of the restriction + * @param Filterable $filterable Filterable to restrict + * + * @return Filterable The filterable having the restriction applied + */ + protected function applyRestriction($name, Filterable $filterable) + { + $filterable->applyFilter($this->getRestriction($name)); + return $filterable; + } + + /** + * Get a restriction of the authenticated + * + * @param string $name Name of the restriction + * + * @return Filter Filter object + * @throws ConfigurationError If the restriction contains invalid filter columns + */ + protected function getRestriction($name) + { + $restriction = Filter::matchAny(); + $restriction->setAllowedFilterColumns(array( + 'host_name', + 'hostgroup_name', + 'instance_name', + 'service_description', + 'servicegroup_name', + function ($c) { + return preg_match('/^_(?:host|service)_/i', $c); + } + )); + foreach ($this->getRestrictions($name) as $filter) { + if ($filter === '*') { + return Filter::matchAll(); + } + try { + $restriction->addFilter(Filter::fromQueryString($filter)); + } catch (QueryException $e) { + throw new ConfigurationError( + $this->translate( + 'Cannot apply restriction %s using the filter %s. You can only use the following columns: %s' + ), + $name, + $filter, + implode(', ', array( + 'instance_name', + 'host_name', + 'hostgroup_name', + 'service_description', + 'servicegroup_name', + '_(host|service)_<customvar-name>' + )), + $e + ); + } + } + + if ($restriction->isEmpty()) { + return Filter::matchAll(); + } + + return $restriction; + } +} diff --git a/library/Graphite/Web/Controller/TimeRangePickerTrait.php b/library/Graphite/Web/Controller/TimeRangePickerTrait.php new file mode 100644 index 0000000..7352b1b --- /dev/null +++ b/library/Graphite/Web/Controller/TimeRangePickerTrait.php @@ -0,0 +1,115 @@ +<?php + +namespace Icinga\Module\Graphite\Web\Controller; + +use Icinga\Module\Graphite\Forms\TimeRangePicker\CommonForm; +use Icinga\Module\Graphite\Forms\TimeRangePicker\CustomForm; +use Icinga\Web\Request; +use Icinga\Web\Url; +use Icinga\Web\View; + +trait TimeRangePickerTrait +{ + /** + * @var CommonForm + */ + protected $timeRangePickerCommonForm; + + /** + * @var CustomForm + */ + protected $timeRangePickerCustomForm; + + /** + * Process the given request using the forms + * + * @param Request $request The request to be processed + * + * @return Request The request supposed to be processed + */ + protected function handleTimeRangePickerRequest(Request $request = null) + { + $this->getTimeRangePickerCommonForm()->handleRequest($request); + return $this->getTimeRangePickerCustomForm()->handleRequest($request); + } + + /** + * Render all needed forms and links + * + * @param View $view + * + * @return string + */ + protected function renderTimeRangePicker(View $view) + { + $url = Url::fromRequest()->getAbsoluteUrl(); + + return '<div class="timerangepicker-container">' + . $this->getTimeRangePickerCommonForm() + . '<div class="flyover flyover-arrow-top" data-flyover-suspends-auto-refresh id="' + . $view->protectId('graphite-customrange') + . '">' + . $view->qlink(null, '#', null, [ + 'title' => $view->translate('Specify custom time range'), + 'class' => 'button-link flyover-toggle', + 'icon' => 'service' + ]) + . '<div class="flyover-content">' . $this->getTimeRangePickerCustomForm() . '</div>' + . '</div>' + . '</div>'; + } + + /** + * Get {@link timeRangePickerCommonForm} + * + * @return CommonForm + */ + public function getTimeRangePickerCommonForm() + { + if ($this->timeRangePickerCommonForm === null) { + $this->timeRangePickerCommonForm = new CommonForm(); + } + + return $this->timeRangePickerCommonForm; + } + + /** + * Set {@link timeRangePickerCommonForm} + * + * @param CommonForm $timeRangePickerCommonForm + * + * @return $this + */ + public function setTimeRangePickerCommonForm(CommonForm $timeRangePickerCommonForm) + { + $this->timeRangePickerCommonForm = $timeRangePickerCommonForm; + return $this; + } + + /** + * Get {@link timeRangePickerCustomForm} + * + * @return CustomForm + */ + public function getTimeRangePickerCustomForm() + { + if ($this->timeRangePickerCustomForm === null) { + $this->timeRangePickerCustomForm = new CustomForm(); + } + + return $this->timeRangePickerCustomForm; + } + + /** + * Set {@link timeRangePickerCustomForm} + * + * @param CustomForm $timeRangePickerCustomForm + * + * @return $this + */ + public function setTimeRangePickerCustomForm(CustomForm $timeRangePickerCustomForm) + { + $this->timeRangePickerCustomForm = $timeRangePickerCustomForm; + return $this; + } +} diff --git a/library/Graphite/Web/FakeSchemeRequest.php b/library/Graphite/Web/FakeSchemeRequest.php new file mode 100644 index 0000000..dc415cd --- /dev/null +++ b/library/Graphite/Web/FakeSchemeRequest.php @@ -0,0 +1,18 @@ +<?php + +namespace Icinga\Module\Graphite\Web; + +use Icinga\Web\Request; + +/** + * Rationale: + * + * {@link Url::fromPath()} doesn't preserve URLs which seem to be internal as they are. + */ +class FakeSchemeRequest extends Request +{ + public function getScheme() + { + return 'a random url scheme which always differs from the current request\'s one'; + } +} diff --git a/library/Graphite/Web/Form/Decorator/Proxy.php b/library/Graphite/Web/Form/Decorator/Proxy.php new file mode 100644 index 0000000..63d339c --- /dev/null +++ b/library/Graphite/Web/Form/Decorator/Proxy.php @@ -0,0 +1,47 @@ +<?php + +namespace Icinga\Module\Graphite\Web\Form\Decorator; + +use Zend_Form_Decorator_Abstract; +use Zend_Form_Decorator_Interface; + +/** + * Wrap a decorator and use it only for rendering + */ +class Proxy extends Zend_Form_Decorator_Abstract +{ + /** + * The actual decorator being proxied + * + * @var Zend_Form_Decorator_Interface + */ + protected $actualDecorator; + + public function render($content) + { + return $this->actualDecorator->render($content); + } + + /** + * Get {@link actualDecorator} + * + * @return Zend_Form_Decorator_Interface + */ + public function getActualDecorator() + { + return $this->actualDecorator; + } + + /** + * Set {@link actualDecorator} + * + * @param Zend_Form_Decorator_Interface $actualDecorator + * + * @return $this + */ + public function setActualDecorator($actualDecorator) + { + $this->actualDecorator = $actualDecorator; + return $this; + } +} diff --git a/library/Graphite/Web/Form/Validator/CustomErrorMessagesValidator.php b/library/Graphite/Web/Form/Validator/CustomErrorMessagesValidator.php new file mode 100644 index 0000000..893a5b7 --- /dev/null +++ b/library/Graphite/Web/Form/Validator/CustomErrorMessagesValidator.php @@ -0,0 +1,42 @@ +<?php + +namespace Icinga\Module\Graphite\Web\Form\Validator; + +use Zend_Validate_Abstract; + +/** + * Provides an easy way to implement validators with custom error messages + * + * TODO(ak): move to framework(?) + */ +abstract class CustomErrorMessagesValidator extends Zend_Validate_Abstract +{ + /** + * Constructor + */ + public function __construct() + { + $this->_messageTemplates = ['CUSTOM_ERROR' => '']; + } + + public function isValid($value) + { + $errorMessage = $this->validate($value); + if ($errorMessage === null) { + return true; + } + + $this->setMessage($errorMessage, 'CUSTOM_ERROR'); + $this->_error('CUSTOM_ERROR'); + return false; + } + + /** + * Validate the given value and return an error message if it's invalid + * + * @param string $value + * + * @return string|null + */ + abstract protected function validate($value); +} diff --git a/library/Graphite/Web/Form/Validator/HttpUserValidator.php b/library/Graphite/Web/Form/Validator/HttpUserValidator.php new file mode 100644 index 0000000..d5f4a86 --- /dev/null +++ b/library/Graphite/Web/Form/Validator/HttpUserValidator.php @@ -0,0 +1,30 @@ +<?php + +namespace Icinga\Module\Graphite\Web\Form\Validator; + +use Zend_Validate_Abstract; + +/** + * Validates http basic authn user names + * + * TODO(ak): move to Icinga Web 2 + */ +class HttpUserValidator extends Zend_Validate_Abstract +{ + /** + * Constructor + */ + public function __construct() + { + $this->_messageTemplates = ['HAS_COLON' => mt('graphite', 'The username must not contain colons.')]; + } + + public function isValid($value) + { + $hasColon = false !== strpos($value, ':'); + if ($hasColon) { + $this->_error('HAS_COLON'); + } + return ! $hasColon; + } +} diff --git a/library/Graphite/Web/Form/Validator/MacroTemplateValidator.php b/library/Graphite/Web/Form/Validator/MacroTemplateValidator.php new file mode 100644 index 0000000..8ff4e3c --- /dev/null +++ b/library/Graphite/Web/Form/Validator/MacroTemplateValidator.php @@ -0,0 +1,21 @@ +<?php + +namespace Icinga\Module\Graphite\Web\Form\Validator; + +use Icinga\Module\Graphite\Util\MacroTemplate; +use InvalidArgumentException; + +/** + * Validates Icinga-style macro templates + */ +class MacroTemplateValidator extends CustomErrorMessagesValidator +{ + protected function validate($value) + { + try { + new MacroTemplate($value); + } catch (InvalidArgumentException $e) { + return $e->getMessage(); + } + } +} diff --git a/library/Graphite/Web/Widget/GraphImage.php b/library/Graphite/Web/Widget/GraphImage.php new file mode 100644 index 0000000..af64e69 --- /dev/null +++ b/library/Graphite/Web/Widget/GraphImage.php @@ -0,0 +1,143 @@ +<?php + +namespace Icinga\Module\Graphite\Web\Widget; + +use Icinga\Module\Graphite\Graphing\Chart; +use Icinga\Web\Url; +use Icinga\Web\UrlParams; +use Icinga\Web\Widget\AbstractWidget; +use RuntimeException; + +class GraphImage extends AbstractWidget +{ + /** + * The chart to be rendered + * + * @var Chart + */ + protected $chart; + + /** + * The rendered PNG image + * + * @var string|null + */ + protected $rendered; + + /** + * Constructor + * + * @param Chart $chart The chart to be rendered + */ + public function __construct(Chart $chart) + { + $this->chart = $chart; + } + + /** + * Render the graph lazily + * + * @return string + */ + public function render() + { + if ($this->rendered === null) { + $now = time(); + + $from = (int) $this->chart->getFrom(); + if ($from < 0) { + $from += $now; + } + + $until = (string) $this->chart->getUntil(); + + if ($until === '') { + $until = $now; + } else { + $until = (int) $until; + if ($until < 0) { + $until += $now; + } + } + + $variables = $this->chart->getMetricVariables(); + $template = $this->chart->getTemplate(); + $graphiteWebClient = $this->chart->getGraphiteWebClient(); + $params = (new UrlParams())->addValues([ + 'from' => $from, + 'until' => $until, + 'bgcolor' => $this->chart->getBackgroundColor() ?? 'black', + 'fgcolor' => $this->chart->getForegroundColor() ?? 'white', + 'majorGridLineColor' => $this->chart->getMajorGridLineColor() ?? '0000003F', + 'minorGridLineColor' => $this->chart->getMinorGridLineColor() ?? 'black', + 'width' => $this->chart->getWidth(), + 'height' => $this->chart->getHeight(), + 'hideLegend' => (string) ! $this->chart->getShowLegend(), + 'tz' => date_default_timezone_get(), + '_salt' => "$now.000", + 'vTitle' => 'Percent', + 'lineMode' => 'connected', + 'drawNullAsZero' => 'false', + 'graphType' => 'line', + '_ext' => 'whatever.svg' + ]); + + foreach ($template->getUrlParams() as $key => $value) { + $params->set($key, $value->resolve($variables)); + } + + $metrics = $this->chart->getMetrics(); + $allVars = []; + + foreach ($template->getCurves() as $curveName => $curve) { + if (!isset($metrics[$curveName])) { + continue; + } + + $vars = $curve[0]->reverseResolve($metrics[$curveName]); + + if ($vars !== false) { + $allVars = array_merge($allVars, $vars); + } + } + + foreach ($metrics as $curveName => $metric) { + $allVars['metric'] = $metric; + $params->add('target', $template->getCurves()[$curveName][1]->resolve($allVars)); + } + + $url = Url::fromPath('/render')->setParams($params); + $headers = [ + 'Accept-language' => 'en', + 'Content-type' => 'application/x-www-form-urlencoded' + ]; + + for (;;) { + try { + $this->rendered = $graphiteWebClient->request($url, 'GET', $headers); + } catch (RuntimeException $e) { + if (preg_match('/\b500\b/', $e->getMessage())) { + // A 500 Internal Server Error, probably because of + // a division by zero because of a too low time range to render. + + $until = (int) $url->getParam('until'); + $diff = $until - (int) $url->getParam('from'); + + // Try to render a higher time range, but give up + // once our default (1h) has been reached (non successfully). + if ($diff < 3600) { + $url->setParam('from', $until - $diff * 2); + continue; + } + } + + throw $e; + } + + break; + } + } + + return $this->rendered; + } +} diff --git a/library/Graphite/Web/Widget/Graphs.php b/library/Graphite/Web/Widget/Graphs.php new file mode 100644 index 0000000..e18b8da --- /dev/null +++ b/library/Graphite/Web/Widget/Graphs.php @@ -0,0 +1,688 @@ +<?php + +namespace Icinga\Module\Graphite\Web\Widget; + +use Icinga\Application\Config; +use Icinga\Application\Icinga; +use Icinga\Authentication\Auth; +use Icinga\Exception\ConfigurationError; +use Icinga\Module\Graphite\Graphing\Chart; +use Icinga\Module\Graphite\Graphing\GraphingTrait; +use Icinga\Module\Graphite\Graphing\Template; +use Icinga\Module\Graphite\Util\InternalProcessTracker as IPT; +use Icinga\Module\Graphite\Util\TimeRangePickerTools; +use Icinga\Module\Graphite\Web\Widget\Graphs\Host as HostGraphs; +use Icinga\Module\Graphite\Web\Widget\Graphs\Service as ServiceGraphs; +use Icinga\Module\Monitoring\Object\Host; +use Icinga\Module\Monitoring\Object\MonitoredObject; +use Icinga\Module\Monitoring\Object\Service; +use Icinga\Web\Request; +use Icinga\Web\Url; +use Icinga\Web\View; +use Icinga\Web\Widget\AbstractWidget; +use ipl\Orm\Model; +use Icinga\Module\Icingadb\Model\Host as IcingadbHost; +use Icinga\Module\Graphite\Web\Widget\Graphs\Icingadb\IcingadbHost as IcingadbHostGraphs; +use Icinga\Module\Graphite\Web\Widget\Graphs\Icingadb\IcingadbService as IcingadbServiceGraphs; + +abstract class Graphs extends AbstractWidget +{ + use GraphingTrait; + + /** + * The Icinga custom variable with the "real" check command (if any) of objects we display graphs for + * + * @var string + */ + protected static $obscuredCheckCommandCustomVar; + + /** + * The type of the object to render the graphs for + * + * @var string + */ + protected $objectType; + + /** + * The object to render the graphs for + * + * @var MonitoredObject|Model + */ + protected $object; + + /** + * Graph image width + * + * @var string + */ + protected $width = '350'; + + /** + * Graph image height + * + * @var string + */ + protected $height = '200'; + + /** + * Graph range start + * + * @var string + */ + protected $start; + + /** + * Graph range end + * + * @var string + */ + protected $end; + + /** + * Whether to render as compact as possible + * + * @var bool + */ + protected $compact = false; + + /** + * The check command of the monitored object we display graphs for + * + * @var string + */ + protected $checkCommand; + + /** + * The "real" check command (if any) of the monitored object we display graphs for + * + * E.g. the command executed remotely via check_by_ssh + * + * @var string|null + */ + protected $obscuredCheckCommand; + + /** + * Additional CSS classes for the <div/>s around the images + * + * @var string[] + */ + protected $classes = []; + + /** + * Whether to serve a transparent dummy image first and let the JS code load the actual graph + * + * @var bool + */ + protected $preloadDummy = false; + + /** + * Whether to render the graphs inline + * + * @var bool + */ + protected $renderInline; + + /** + * Whether to explicitly display that no graphs were found + * + * @var bool|null + */ + protected $showNoGraphsFound; + + /** + * Factory, based on the given monitoring object + * + * @param MonitoredObject $object + * + * @return static + */ + public static function forMonitoredObject(MonitoredObject $object) + { + switch ($object->getType()) { + case 'host': + /** @var Host $object */ + return new HostGraphs($object); + + case 'service': + /** @var Service $object */ + return new ServiceGraphs($object); + } + } + + /** + * Factory, based on the given icingadb object + * + * @param Model $object + * + * @return static + */ + public static function forIcingadbObject(Model $object) + { + if ($object instanceof IcingadbHost) { + return new IcingadbHostGraphs($object); + } + + return new IcingadbServiceGraphs($object); + } + + /** + * Get the Icinga custom variable with the "real" check command (if any) of monitored objects we display graphs for + * + * @return string + */ + public static function getObscuredCheckCommandCustomVar() + { + if (static::$obscuredCheckCommandCustomVar === null) { + static::$obscuredCheckCommandCustomVar = Config::module('graphite') + ->get('icinga', 'customvar_obscured_check_command', 'check_command'); + } + + return static::$obscuredCheckCommandCustomVar; + } + + /** + * Constructor + * + * @param MonitoredObject|Model $object The object to render the graphs for + */ + public function __construct($object) + { + $this->object = $object; + $this->renderInline = Url::fromRequest()->getParam('format') === 'pdf'; + + if ($object instanceof Model) { + $this->checkCommand = $object->checkcommand_name; + $this->obscuredCheckCommand = $object->vars[Graphs::getObscuredCheckCommandCustomVar()] ?? null; + } else { + $this->checkCommand = $object->{"{$this->objectType}_check_command"}; + $this->obscuredCheckCommand = $object->{ + "_{$this->objectType}_" . Graphs::getObscuredCheckCommandCustomVar() + }; + } + } + + /** + * Process the given request using this widget + * + * @param Request $request The request to be processed + * + * @return $this + */ + public function handleRequest(Request $request = null) + { + if ($request === null) { + $request = Icinga::app()->getRequest(); + } + + $params = $request->getUrl()->getParams(); + list($this->start, $this->end) = $this->getRangeFromTimeRangePicker($request); + $this->width = $params->shift('width', $this->width); + $this->height = $params->shift('height', $this->height); + + return $this; + } + + /** + * Render the graphs list + * + * @return string + */ + protected function getGraphsList() + { + $result = []; // kind of string builder + $imageBaseUrl = $this->getImageBaseUrl(); + $allTemplates = $this->getAllTemplates(); + $actualCheckCommand = $this->obscuredCheckCommand === null ? $this->checkCommand : $this->obscuredCheckCommand; + $concreteTemplates = $allTemplates->getTemplates($actualCheckCommand); + + $excludedMetrics = []; + + foreach ($concreteTemplates as $concreteTemplate) { + foreach ($concreteTemplate->getCurves() as $curve) { + $excludedMetrics[] = $curve[0]; + } + } + + IPT::recordf("Icinga check command: %s", $this->checkCommand); + IPT::recordf("Obscured check command: %s", $this->obscuredCheckCommand); + + foreach ( + [ + ['template', $concreteTemplates, []], + ['default_template', $allTemplates->getDefaultTemplates(), $excludedMetrics], + ] as $templateSet + ) { + list($urlParam, $templates, $excludeMetrics) = $templateSet; + + if ($urlParam === 'template') { + IPT::recordf('Applying templates for check command %s', $actualCheckCommand); + } else { + IPT::recordf('Applying default templates, excluding previously used metrics'); + } + + IPT::indent(); + + foreach ($templates as $templateName => $template) { + if ($this->designedForObjectType($template)) { + IPT::recordf('Applying template %s', $templateName); + IPT::indent(); + + $charts = $template->getCharts( + static::getMetricsDataSource(), + $this->object, + [], + $excludeMetrics + ); + + if (! empty($charts)) { + $currentGraphs = []; + + foreach ($charts as $chart) { + /** @var Chart $chart */ + + $metricVariables = $chart->getMetricVariables(); + $bestIntersect = -1; + $bestPos = count($result); + + foreach ($result as $graphPos => & $graph) { + $currentIntersect = count(array_intersect_assoc($graph[1], $metricVariables)); + + if ($currentIntersect >= $bestIntersect) { + $bestIntersect = $currentIntersect; + $bestPos = $graphPos + 1; + } + } + unset($graph); + + $urlParams = $template->getUrlParams(); + if (array_key_exists("height", $urlParams)) { + $actheight = $urlParams["height"]->resolve(['height']); + if ($actheight < $this->height) { + $actheight = $this->height; + } + } else { + $actheight = $this->height; + } + $actwidth = $this->width; + $actwidthfix = ""; + if (array_key_exists("width", $urlParams)) { + $actwidth = $urlParams["width"]->resolve(['width']); + $actwidthfix = "width: {$actwidth}px; "; + } + + if ($this->renderInline) { + $chart->setFrom($this->start) + ->setUntil($this->end) + ->setWidth($actwidth) + ->setHeight($actheight) + ->setBackgroundColor('white') + ->setForegroundColor('black') + ->setMajorGridLineColor('grey') + ->setMinorGridLineColor('white') + ->setShowLegend(! $this->compact); + + $img = new InlineGraphImage($chart); + } else { + $imageUrl = $this->filterImageUrl($imageBaseUrl->with($metricVariables)) + ->setParam($urlParam, $templateName) + ->setParam('start', $this->start) + ->setParam('end', $this->end) + ->setParam('width', $actwidth) + ->setParam('height', $actheight); + + if (! $this->compact) { + $imageUrl->setParam('legend', 1); + } + + if ($this->preloadDummy) { + $src = 'data:image/png;base64,' // 1x1 dummy + . 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUAAACnej3aAAAAAXRS' + . 'TlMAQObYZgAAAApJREFUeAFjZAAAAAQAAhq+CAMAAAAASUVORK5CYII='; + } else { + $src = $imageUrl; + } + + $img = '<img id="graphiteImg-' . md5((string) $imageUrl) . '"' + . " src=\"$src\" data-actualimageurl=\"$imageUrl\" class=\"detach graphiteImg\"" + . " alt=\"\" width=\"$actwidth\" height=\"$actheight\"" + . " style=\"min-width: {$actwidth}px; $actwidthfix min-height: {$actheight}px;\">"; + } + + $currentGraphs[] = [$img, $metricVariables, $bestPos]; + } + + foreach (array_reverse($currentGraphs) as $graph) { + list($img, $metricVariables, $bestPos) = $graph; + array_splice($result, $bestPos, 0, [[$img, $metricVariables]]); + } + } + + IPT::unindent(); + } else { + IPT::recordf('Not applying template %s', $templateName); + } + } + + IPT::unindent(); + } + + if (! empty($result)) { + foreach ($result as & $graph) { + $graph = $graph[0]; + } + unset($graph); + + $currentUrl = Icinga::app()->getRequest()->getUrl(); + $limit = (int) $currentUrl->getParam('graphs_limit', 2); + $total = count($result); + + if ($limit < 1) { + $limit = -1; + } + + if ($limit !== -1 && $total > $limit) { + $result = array_slice($result, 0, $limit); + + if (! $this->compact) { + /** @var View $view */ + $view = $this->view(); + + $url = $this->getGraphsListBaseUrl(); + TimeRangePickerTools::copyAllRangeParameters($url->getParams(), $currentUrl->getParams()); + + $result[] = "<p class='load-more'>{$view->qlink( + sprintf($view->translate('Load all %d graphs'), $total), + $url->setParam('graphs_limit', '-1'), + null, + ['class' => 'action-link'] + )}</p>"; + } + } + + $classes = $this->classes; + $classes[] = 'images'; + + array_unshift($result, '<div class="' . implode(' ', $classes) . '">'); + $result[] = '</div>'; + } + + if ($this->renderInline) { + foreach ($result as $html) { + if ($html instanceof InlineGraphImage) { + // Errors should occur now or not at all + $html->render(); + } + } + } + + return implode($result); + } + + public function render() + { + IPT::clear(); + + try { + $result = $this->getGraphsList(); + } catch (ConfigurationError $e) { + $view = $this->view(); + + return "<p>{$view->escape($e->getMessage())}</p>" + . '<p>' . vsprintf( + $view->escape($view->translate('Please %scorrect%s the configuration of the Graphite module.')), + Auth::getInstance()->hasPermission('config/modules') + ? explode( + '$LINK_TEXT$', + $view->qlink('$LINK_TEXT$', 'graphite/config/backend', null, ['class' => 'action-link']) + ) + : ['', ''] + ) . '</p>'; + } + + $view = $this->view(); + + if ($result === '' && $this->getShowNoGraphsFound()) { + $result = "<p>{$view->escape($view->translate('No graphs found'))}</p>"; + } + + if (IPT::enabled()) { + $result .= "<h3>{$view->escape($view->translate('Graphs assembling process record'))}</h3>" + . "<pre>{$view->escape(IPT::dump())}</pre>"; + } + + return $result; + } + + /** + * Get time range parameters for Graphite from the URL + * + * @param Request $request The request to be used + * + * @return string[] + */ + protected function getRangeFromTimeRangePicker(Request $request) + { + $params = $request->getUrl()->getParams(); + $relative = $params->get(TimeRangePickerTools::getRelativeRangeParameter()); + if ($relative !== null) { + return ["-$relative", null]; + } + + $absolute = TimeRangePickerTools::getAbsoluteRangeParameters(); + $start = $params->get($absolute['start']); + return [ + $start === null ? -TimeRangePickerTools::getDefaultRelativeTimeRange() : $start, + $params->get($absolute['end']) + ]; + } + + /** + * Return a identifier specifying the monitored object we display graphs for + * + * @return string + */ + abstract protected function getMonitoredObjectIdentifier(); + + /** + * Get the base URL to a graph specifying just the monitored object kind + * + * @return Url + */ + abstract protected function getImageBaseUrl(); + + /** + * Get the base URL to the monitored object's graphs list + * + * @return Url + */ + abstract protected function getGraphsListBaseUrl(); + + /** + * Extend the {@link getImageBaseUrl()}'s result's parameters with the concrete monitored object + * + * @param Url $url The URL to extend + * + * @return Url The given URL + */ + abstract protected function filterImageUrl(Url $url); + + /** + * Return whether the given template is designed for the type of the object we display graphs for + * + * @param Template $template + * + * @return bool + */ + abstract protected function designedForObjectType(Template $template); + + /** + * Get {@link compact} + * + * @return bool + */ + public function getCompact() + { + return $this->compact; + } + + /** + * Set {@link compact} + * + * @param bool $compact + * + * @return $this + */ + public function setCompact($compact = true) + { + $this->compact = $compact; + return $this; + } + + /** + * Get the graph image width + * + * @return string + */ + public function getWidth() + { + return $this->width; + } + + /** + * Set the graph image width + * + * @param string $width + * + * @return $this + */ + public function setWidth($width) + { + $this->width = $width; + + return $this; + } + + /** + * Get the graph image height + * + * @return string + */ + public function getHeight() + { + return $this->height; + } + + /** + * Set the graph image height + * + * @param string $height + * + * @return $this + */ + public function setHeight($height) + { + $this->height = $height; + + return $this; + } + + /** + * Get additional CSS classes for the <div/>s around the images + * + * @return string[] + */ + public function getClasses() + { + return $this->classes; + } + + /** + * Set additional CSS classes for the <div/>s around the images + * + * @param string[] $classes + * + * @return $this + */ + public function setClasses($classes) + { + $this->classes = $classes; + + return $this; + } + + /** + * Get whether to serve a transparent dummy image first and let the JS code load the actual graph + * + * @return bool + */ + public function getPreloadDummy() + { + return $this->preloadDummy; + } + + /** + * Set whether to serve a transparent dummy image first and let the JS code load the actual graph + * + * @param bool $preloadDummy + * + * @return $this + */ + public function setPreloadDummy($preloadDummy = true) + { + $this->preloadDummy = $preloadDummy; + + return $this; + } + + /** + * Get whether to render the graphs inline + * + * @return bool + */ + public function getRenderInline() + { + return $this->renderInline; + } + + /** + * Set whether to render the graphs inline + * + * @param bool $renderInline + * + * @return $this + */ + public function setRenderInline($renderInline = true) + { + $this->renderInline = $renderInline; + + return $this; + } + + /** + * Get whether to explicitly display that no graphs were found + * + * @return bool + */ + public function getShowNoGraphsFound() + { + if ($this->showNoGraphsFound === null) { + $this->showNoGraphsFound = ! Config::module('graphite')->get('ui', 'disable_no_graphs_found'); + } + + return $this->showNoGraphsFound; + } + + /** + * Set whether to explicitly display that no graphs were found + * + * @param bool $showNoGraphsFound + * + * @return $this + */ + public function setShowNoGraphsFound($showNoGraphsFound = true) + { + $this->showNoGraphsFound = $showNoGraphsFound; + + return $this; + } +} diff --git a/library/Graphite/Web/Widget/Graphs/Host.php b/library/Graphite/Web/Widget/Graphs/Host.php new file mode 100644 index 0000000..2247bcc --- /dev/null +++ b/library/Graphite/Web/Widget/Graphs/Host.php @@ -0,0 +1,51 @@ +<?php + +namespace Icinga\Module\Graphite\Web\Widget\Graphs; + +use Icinga\Module\Graphite\Graphing\Template; +use Icinga\Module\Graphite\Web\Widget\Graphs; +use Icinga\Module\Monitoring\Object\Host as MonitoredHost; +use Icinga\Web\Url; + +class Host extends Graphs +{ + protected $objectType = 'host'; + + /** + * The host to render the graphs of + * + * @var MonitoredHost + */ + protected $object; + + protected function getImageBaseUrl() + { + return Url::fromPath('graphite/graph/host'); + } + + protected function getGraphsListBaseUrl() + { + return Url::fromPath('graphite/list/hosts', ['host' => $this->object->getName()]); + } + + protected function filterImageUrl(Url $url) + { + return $url->setParam('host.name', $this->object->getName()); + } + + protected function getMonitoredObjectIdentifier() + { + return $this->object->getName(); + } + + protected function designedForObjectType(Template $template) + { + foreach ($template->getCurves() as $curve) { + if (in_array('host_name_template', $curve[0]->getMacros())) { + return true; + } + } + + return false; + } +} diff --git a/library/Graphite/Web/Widget/Graphs/Icingadb/IcingadbHost.php b/library/Graphite/Web/Widget/Graphs/Icingadb/IcingadbHost.php new file mode 100644 index 0000000..2b0a614 --- /dev/null +++ b/library/Graphite/Web/Widget/Graphs/Icingadb/IcingadbHost.php @@ -0,0 +1,61 @@ +<?php + +namespace Icinga\Module\Graphite\Web\Widget\Graphs\Icingadb; + +use Icinga\Module\Graphite\Graphing\Template; +use Icinga\Module\Graphite\Web\Widget\Graphs; +use Icinga\Web\Url; +use Icinga\Module\Icingadb\Model\Host; + +class IcingadbHost extends Graphs +{ + protected $objectType = 'host'; + + /** + * The Icingadb host to render the graphs for + * + * @var Host + */ + protected $object; + + protected function getGraphsListBaseUrl() + { + return Url::fromPath('graphite/hosts', ['host.name' => $this->object->name]); + } + + protected function filterImageUrl(Url $url) + { + return $url->setParam('host.name', $this->object->name); + } + + public function createHostTitle() + { + return $this->object->name; + } + + public function getObjectType() + { + return $this->objectType; + } + + protected function getMonitoredObjectIdentifier() + { + return $this->object->name; + } + + protected function getImageBaseUrl() + { + return Url::fromPath('graphite/graph/host'); + } + + protected function designedForObjectType(Template $template) + { + foreach ($template->getCurves() as $curve) { + if (in_array('host_name_template', $curve[0]->getMacros())) { + return true; + } + } + + return false; + } +} diff --git a/library/Graphite/Web/Widget/Graphs/Icingadb/IcingadbService.php b/library/Graphite/Web/Widget/Graphs/Icingadb/IcingadbService.php new file mode 100644 index 0000000..7827e86 --- /dev/null +++ b/library/Graphite/Web/Widget/Graphs/Icingadb/IcingadbService.php @@ -0,0 +1,71 @@ +<?php + +namespace Icinga\Module\Graphite\Web\Widget\Graphs\Icingadb; + +use Icinga\Module\Graphite\Graphing\Template; +use Icinga\Module\Graphite\Web\Widget\Graphs; +use Icinga\Web\Url; +use Icinga\Module\Icingadb\Model\Service; + +class IcingadbService extends Graphs +{ + protected $objectType = 'service'; + + /** + * The icingadb service to render the graphs for + * + * @var Service + */ + protected $object; + + protected function getGraphsListBaseUrl() + { + return Url::fromPath( + 'graphite/services', + ['service.name' => $this->object->name, 'host.name' => $this->object->host->name] + ); + } + + protected function filterImageUrl(Url $url) + { + return $url + ->setParam('host.name', $this->object->host->name) + ->setParam('service.name', $this->object->name); + } + + public function createHostTitle() + { + return $this->object->host->name; + } + + public function createServiceTitle() + { + return ' : ' . $this->object->name; + } + + public function getObjectType() + { + return $this->objectType; + } + + protected function getMonitoredObjectIdentifier() + { + return $this->object->host->name . ':' . $this->object->name; + } + + protected function getImageBaseUrl() + { + return Url::fromPath('graphite/graph/service'); + } + + protected function designedforObjectType(Template $template) + { + foreach ($template->getCurves() as $curve) { + if (in_array('service_name_template', $curve[0]->getMacros())) { + return true; + } + } + + return false; + } +} diff --git a/library/Graphite/Web/Widget/Graphs/Service.php b/library/Graphite/Web/Widget/Graphs/Service.php new file mode 100644 index 0000000..5fc0143 --- /dev/null +++ b/library/Graphite/Web/Widget/Graphs/Service.php @@ -0,0 +1,56 @@ +<?php + +namespace Icinga\Module\Graphite\Web\Widget\Graphs; + +use Icinga\Module\Graphite\Graphing\Template; +use Icinga\Module\Graphite\Web\Widget\Graphs; +use Icinga\Module\Monitoring\Object\Service as MonitoredService; +use Icinga\Web\Url; + +class Service extends Graphs +{ + protected $objectType = 'service'; + + /** + * The service to render the graphs for + * + * @var MonitoredService + */ + protected $object; + + protected function getImageBaseUrl() + { + return Url::fromPath('graphite/graph/service'); + } + + protected function getGraphsListBaseUrl() + { + return Url::fromPath( + 'graphite/list/services', + ['host' => $this->object->getHost()->getName(), 'service' => $this->object->getName()] + ); + } + + protected function filterImageUrl(Url $url) + { + return $url + ->setParam('host.name', $this->object->getHost()->getName()) + ->setParam('service.name', $this->object->getName()); + } + + protected function getMonitoredObjectIdentifier() + { + return $this->object->getHost()->getName() . ':' . $this->object->getName(); + } + + protected function designedForObjectType(Template $template) + { + foreach ($template->getCurves() as $curve) { + if (in_array('service_name_template', $curve[0]->getMacros())) { + return true; + } + } + + return false; + } +} diff --git a/library/Graphite/Web/Widget/IcingadbGraphs.php b/library/Graphite/Web/Widget/IcingadbGraphs.php new file mode 100644 index 0000000..e038e92 --- /dev/null +++ b/library/Graphite/Web/Widget/IcingadbGraphs.php @@ -0,0 +1,106 @@ +<?php + +/* Icinga Graphite Web | (c) 2022 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Graphite\Web\Widget; + +use Icinga\Data\Filter\Filter; +use Icinga\Module\Graphite\Web\Widget\Graphs\Icingadb\IcingadbHost; +use Icinga\Module\Graphite\Web\Widget\Graphs\Icingadb\IcingadbService; +use Icinga\Module\Icingadb\Common\Links; +use Icinga\Module\Icingadb\Widget\EmptyState; +use Icinga\Module\Icingadb\Model\Host; +use ipl\Html\BaseHtmlElement; +use ipl\Html\Html; +use ipl\Html\HtmlDocument; +use ipl\Html\HtmlString; +use ipl\Orm\ResultSet; +use ipl\Stdlib\BaseFilter; +use ipl\Web\Filter\QueryString; +use ipl\Web\Widget\Link; + +/** +* Class for creating graphs of icingadb objects +*/ +class IcingadbGraphs extends BaseHtmlElement +{ + use BaseFilter; + + protected $defaultAttributes = ['class' => 'grid']; + + /** @var Iterable */ + protected $objects; + + protected $tag = 'div'; + + /** + * Create a new Graph item + * + * @param ResultSet $objects + */ + public function __construct(ResultSet $objects) + { + $this->objects = $objects; + } + + protected function assemble() + { + if (! $this->objects->hasResult()) { + $this->add(new EmptyState(t('No items found.'))); + } + + foreach ($this->objects as $object) { + $this->add($this->createGridItem($object)); + } + + $document = new HtmlDocument(); + $document->addHtml(Html::tag('div', ['class' => 'graphite-graph-color-registry']), $this); + $this->prependWrapper($document); + } + + protected function createGridItem($object) + { + if ($object instanceof Host) { + $graph = new IcingadbHost($object); + $hostObj = $object; + } else { + $graph = new IcingadbService($object); + $hostObj = $object->host; + } + + $hostUrl = Links::host($hostObj); + + if ($this->hasBaseFilter()) { + $hostUrl->addFilter(Filter::fromQueryString(QueryString::render($this->getBaseFilter()))); + } + + $hostLink = new Link( + $graph->createHostTitle(), + $hostUrl, + ['data-base-target' => '_next'] + ); + + $serviceLink = null; + if ($graph->getObjectType() === 'service') { + $serviceUrl = Links::service($object, $hostObj); + + if ($this->hasBaseFilter()) { + $serviceUrl->addFilter(Filter::fromQueryString(QueryString::render($this->getBaseFilter()))); + } + + $serviceLink = new Link( + $graph->createServiceTitle(), + $serviceUrl, + ['data-base-target' => '_next'] + ); + } + + $gridItem = Html::tag('div', ['class' => 'grid-item']); + $header = Html::tag('h2'); + + $header->add([$hostLink, $serviceLink]); + $gridItem->add($header); + + return $gridItem->add(HtmlString::create($graph->setPreloadDummy()->handleRequest())); + } +} diff --git a/library/Graphite/Web/Widget/InlineGraphImage.php b/library/Graphite/Web/Widget/InlineGraphImage.php new file mode 100644 index 0000000..881384d --- /dev/null +++ b/library/Graphite/Web/Widget/InlineGraphImage.php @@ -0,0 +1,49 @@ +<?php + +namespace Icinga\Module\Graphite\Web\Widget; + +use Icinga\Module\Graphite\Graphing\Chart; +use Icinga\Web\Widget\AbstractWidget; + +class InlineGraphImage extends AbstractWidget +{ + /** + * The image to be rendered + * + * @var GraphImage + */ + protected $image; + + /** + * The rendered <img> + * + * @var string|null + */ + protected $rendered; + + /** + * Constructor + * + * @param Chart $chart The chart to be rendered + */ + public function __construct(Chart $chart) + { + $this->image = new GraphImage($chart); + } + + /** + * Render the graph lazily + * + * @return string + */ + public function render() + { + if ($this->rendered === null) { + $this->rendered = '<img src="data:image/png;base64,' + . implode("\n", str_split(base64_encode($this->image->render()), 76)) + . '">'; + } + + return $this->rendered; + } +} |