getAllTemplates() as $checkCommand => $templates) {
$perfdata = [];
foreach ($templates as $templateName => $template) {
/** @var Template $template */
$urlParams = $template->getUrlParams();
switch (isset($urlParams['yUnitSystem']) ? $urlParams['yUnitSystem']->resolve([]) : 'none') {
case 'si':
case 'binary':
$max = 42000000;
break;
case 'sec':
case 'msec':
$max = 82800;
break;
default:
$max = 100;
}
foreach ($template->getCurves() as $curveName => $curve) {
/** @var MacroTemplate $metricFilter */
$metricFilter = $curve[0];
$macros = array_flip($metricFilter->getMacros());
$service = isset($macros['service_name_template']);
foreach ($macros as & $macro) {
$macro = ['dummy1', 'dummy2', 'dummy3', 'dummy4'];
}
$macros['host_name_template'] = [''];
$macros['service_name_template'] = [''];
foreach ($this->cartesianProduct($macros) as $macroValues) {
if (
preg_match(
'/\A\.[^.]+\.(.+)\.[^.]+\z/',
$metricFilter->resolve($macroValues),
$match
)
) {
$perfdata[$match[1]] = $max;
}
}
}
}
assert(isset($service), '$service not initialized in the loop');
$monObj = $service
? [
"apply Service \"{$icinga2CfgObjPrefix}_{$checkCommand}\" {",
" assign where host.vars.$icinga2CfgObjPrefix"
]
: ["object Host \"{$icinga2CfgObjPrefix}_{$checkCommand}\" {"];
$monObj[] = " check_command = \"$icinga2CfgObjPrefix\"";
$monObj[] = ' check_interval = 30s';
$monObj[] = " vars.$obscuredCheckCommandCustomVar = \"$checkCommand\"";
$monObj[] = " vars.$icinga2CfgObjPrefix = {";
foreach ($perfdata as $label => $max) {
$monObj[] = " \"$label\" = $max";
}
$monObj[] = ' }';
$monObj[] = '}';
$result[] = implode("\n", $monObj);
}
echo implode("\n\n", $result) . "\n";
}
/**
* Generate the cartesian product of the given array
*
* [
* 'a' => ['b', 'c'],
* 'd' => ['e', 'f']
* ]
*
* [
* ['a' => 'b', 'd' => 'e'],
* ['a' => 'b', 'd' => 'f'],
* ['a' => 'c', 'd' => 'e'],
* ['a' => 'c', 'd' => 'f']
* ]
*
* @param array[] $input
*
* @return array[]
*/
protected function cartesianProduct(array &$input)
{
$results = [[]];
foreach ($input as $key => & $values) {
$nextStep = [];
foreach ($results as & $result) {
foreach ($values as $value) {
$nextStep[] = array_merge($result, [$key => $value]);
}
}
unset($result);
$results = & $nextStep;
unset($nextStep);
}
unset($values);
return $results;
}
}
icingaweb2-module-graphite-1.3.0/application/controllers/ 0000775 0000000 0000000 00000000000 15163212266 0023466 5 ustar 00root root 0000000 0000000 icingaweb2-module-graphite-1.3.0/application/controllers/ConfigController.php 0000664 0000000 0000000 00000001703 15163212266 0027451 0 ustar 00root root 0000000 0000000
// SPDX-License-Identifier: GPL-3.0-or-later
namespace Icinga\Module\Graphite\Controllers;
use Icinga\Module\Graphite\Forms\Config\AdvancedForm;
use Icinga\Module\Graphite\Forms\Config\BackendForm;
use Icinga\Web\Controller;
class ConfigController extends Controller
{
public function init()
{
$this->assertPermission('config/modules');
parent::init();
}
public function backendAction()
{
$this->view->form = $form = new BackendForm();
$form->setIniConfig($this->Config())->handleRequest();
$this->view->tabs = $this->Module()->getConfigTabs()->activate('backend');
}
public function advancedAction()
{
$this->view->form = $form = new AdvancedForm();
$form->setIniConfig($this->Config())->handleRequest();
$this->view->tabs = $this->Module()->getConfigTabs()->activate('advanced');
}
}
icingaweb2-module-graphite-1.3.0/application/controllers/GraphController.php 0000664 0000000 0000000 00000013303 15163212266 0027304 0 ustar 00root root 0000000 0000000
// SPDX-License-Identifier: GPL-3.0-or-later
namespace Icinga\Module\Graphite\Controllers;
use Icinga\Exception\Http\HttpBadRequestException;
use Icinga\Exception\Http\HttpNotFoundException;
use Icinga\Module\Graphite\Graphing\GraphingTrait;
use Icinga\Module\Graphite\Util\IcingadbUtils;
use Icinga\Module\Graphite\Web\Widget\Graphs;
use Icinga\Module\Icingadb\Model\Host;
use Icinga\Module\Icingadb\Model\Service;
use Icinga\Web\Controller;
use Icinga\Web\UrlParams;
use ipl\Orm\Model;
use ipl\Stdlib\Filter;
class GraphController extends Controller
{
use GraphingTrait;
/**
* The URL parameters for the graph
*
* @var string[]
*/
protected $graphParamsNames = [
'start', 'end',
'width', 'height',
'legend',
'template', 'default_template',
'bgcolor', 'fgcolor',
'majorGridLineColor', 'minorGridLineColor'
];
/**
* The URL parameters for metrics filtering
*
* @var UrlParams
*/
protected $filterParams;
/**
* The URL parameters for the graph
*
* @var string[]
*/
protected $graphParams = [];
public function init()
{
parent::init();
$this->filterParams = clone $this->getRequest()->getUrl()->getParams();
foreach ($this->graphParamsNames as $paramName) {
$this->graphParams[$paramName] = $this->filterParams->shift($paramName);
}
}
public function serviceAction()
{
$hostName = $this->filterParams->getRequired('host.name');
$serviceName = $this->filterParams->getRequired('service.name');
$icingadbUtils = IcingadbUtils::getInstance();
$query = Service::on($icingadbUtils->getDb())
->with('state')
->with('host');
$query->filter(Filter::all(
Filter::equal('service.name', $serviceName),
Filter::equal('service.host.name', $hostName)
));
$icingadbUtils->applyRestrictions($query);
/** @var Service $service */
$service = $query->first();
if ($service === null) {
throw new HttpNotFoundException($this->translate('No such service'));
}
$checkCommandColumn = $service->vars[Graphs::getObscuredCheckCommandCustomVar()] ?? null;
$this->supplyImage(
$service,
$service->checkcommand_name,
$checkCommandColumn
);
}
public function hostAction()
{
$hostName = $this->filterParams->getRequired('host.name');
$icingadbUtils = IcingadbUtils::getInstance();
$query = Host::on($icingadbUtils->getDb())->with('state');
$query->filter(Filter::equal('host.name', $hostName));
$icingadbUtils->applyRestrictions($query);
/** @var Host $host */
$host = $query->first();
if ($host === null) {
throw new HttpNotFoundException($this->translate('No such host'));
}
$checkCommandColumn = $host->vars[Graphs::getObscuredCheckCommandCustomVar()] ?? null;
$this->supplyImage(
$host,
$host->checkcommand_name,
$checkCommandColumn
);
}
/**
* Do all monitored object type independent actions
*
* @param Model $object The object to render the graphs for
* @param string $checkCommand The check command of the object we supply an image for
* @param string|null $obscuredCheckCommand The "real" check command (if any) of the object we
* display graphs for
*/
protected function supplyImage($object, $checkCommand, $obscuredCheckCommand)
{
if (isset($this->graphParams['default_template'])) {
$urlParam = 'default_template';
$templates = $this->getAllTemplates()->getDefaultTemplates();
} else {
$urlParam = 'template';
$templates = $this->getAllTemplates()->getTemplates(
$obscuredCheckCommand === null ? $checkCommand : $obscuredCheckCommand
);
}
if (! isset($templates[$this->graphParams[$urlParam]])) {
throw new HttpNotFoundException($this->translate('No such template'));
}
$charts = $templates[$this->graphParams[$urlParam]]->getCharts(
static::getMetricsDataSource(),
$object,
array_map('rawurldecode', $this->filterParams->toArray(false))
);
switch (count($charts)) {
case 0:
throw new HttpNotFoundException($this->translate('No such graph'));
case 1:
$charts[0]
->setFrom($this->graphParams['start'])
->setUntil($this->graphParams['end'])
->setWidth((int) $this->graphParams['width'])
->setHeight((int) $this->graphParams['height'])
->setBackgroundColor($this->graphParams['bgcolor'])
->setForegroundColor($this->graphParams['fgcolor'])
->setMajorGridLineColor($this->graphParams['majorGridLineColor'])
->setMinorGridLineColor($this->graphParams['minorGridLineColor'])
->setShowLegend((bool) $this->graphParams['legend'])
->serveImage($this->getResponse());
// not falling through, serveImage exits
default:
throw new HttpBadRequestException('%s', $this->translate(
'Graphite Web yields more than one metric for the given filter.'
. ' Please specify a more precise filter.'
));
}
}
}
icingaweb2-module-graphite-1.3.0/application/controllers/HostsController.php 0000664 0000000 0000000 00000007427 15163212266 0027355 0 ustar 00root root 0000000 0000000
// SPDX-License-Identifier: GPL-3.0-or-later
namespace Icinga\Module\Graphite\Controllers;
use GuzzleHttp\Psr7\ServerRequest;
use Icinga\Module\Graphite\Util\InternalProcessTracker as IPT;
use Icinga\Module\Graphite\Web\Controller\IcingadbGraphiteController;
use Icinga\Module\Graphite\Web\Controller\TimeRangePickerTrait;
use Icinga\Module\Graphite\Web\Widget\IcingadbGraphs;
use Icinga\Module\Icingadb\Model\Host;
use Icinga\Module\Icingadb\Web\Control\SearchBar\ObjectSuggestions;
use Icinga\Web\Url;
use ipl\Html\HtmlString;
use ipl\Stdlib\Filter;
use ipl\Web\Control\LimitControl;
use ipl\Web\Control\SortControl;
class HostsController extends IcingadbGraphiteController
{
use TimeRangePickerTrait;
public function indexAction()
{
if (! $this->useIcingadbAsBackend) {
$params = urldecode($this->params->get('legacyParams'));
$this->redirectNow(Url::fromPath('graphite/list/hosts')->setQueryString($params));
}
// shift graph params to avoid exception
$graphDebug = $this->params->shift('graph_debug');
$graphRange = $this->params->shift('graph_range');
if ($graphDebug) {
IPT::enable();
}
$baseFilter = $graphRange ? Filter::equal('graph_range', $graphRange) : null;
foreach ($this->preservedParams as $param) {
$this->params->shift($param);
}
$this->addTitleTab(t('Hosts'));
$db = $this->getDb();
$hosts = Host::on($db)->with('state');
$hosts->filter(Filter::like('state.performance_data', '*'));
$this->applyRestrictions($hosts);
$limitControl = $this->createLimitControl();
$paginationControl = $this->createPaginationControl($hosts);
$sortControl = $this->createSortControl($hosts, ['host.display_name' => t('Hostname')]);
$searchBar = $this->createSearchBar(
$hosts,
array_merge(
[$limitControl->getLimitParam(), $sortControl->getSortParam()],
$this->preservedParams
)
);
if ($searchBar->hasBeenSent() && ! $searchBar->isValid()) {
if ($searchBar->hasBeenSubmitted()) {
$filter = $this->getFilter();
} else {
$this->addControl($searchBar);
$this->sendMultipartUpdate();
return;
}
} else {
$filter = $searchBar->getFilter();
}
$hosts->filter($filter);
$this->addControl($paginationControl);
$this->addControl($sortControl);
$this->addControl($limitControl);
$this->addControl($searchBar);
$this->handleTimeRangePickerRequest();
$this->addControl(HtmlString::create($this->renderTimeRangePicker($this->view)));
$this->addContent(
(new IcingadbGraphs($hosts->execute()))
->setBaseFilter($baseFilter)
);
if (! $searchBar->hasBeenSubmitted() && $searchBar->hasBeenSent()) {
$this->sendMultipartUpdate();
}
$this->setAutorefreshInterval(30);
}
public function completeAction()
{
$suggestions = new ObjectSuggestions();
$suggestions->setModel(Host::class);
$suggestions->forRequest(ServerRequest::fromGlobals());
$this->getDocument()->add($suggestions);
}
public function searchEditorAction()
{
$editor = $this->createSearchEditor(
Host::on($this->getDb()),
array_merge(
[LimitControl::DEFAULT_LIMIT_PARAM, SortControl::DEFAULT_SORT_PARAM],
$this->preservedParams
)
);
$this->getDocument()->add($editor);
$this->setTitle(t('Adjust Filter'));
}
}
icingaweb2-module-graphite-1.3.0/application/controllers/ListController.php 0000664 0000000 0000000 00000014517 15163212266 0027166 0 ustar 00root root 0000000 0000000
// SPDX-License-Identifier: GPL-3.0-or-later
namespace Icinga\Module\Graphite\Controllers;
use Icinga\Data\Filter\Filter;
use Icinga\Module\Graphite\Util\TimeRangePickerTools;
use Icinga\Module\Graphite\Web\Controller\MonitoringAwareController;
use Icinga\Module\Graphite\Web\Controller\TimeRangePickerTrait;
use Icinga\Module\Icingadb\Compat\UrlMigrator;
use Icinga\Module\Monitoring\DataView\DataView;
use Icinga\Module\Monitoring\Object\Host;
use Icinga\Module\Monitoring\Object\Service;
use Icinga\Web\Url;
use Icinga\Web\Widget\Tabextension\DashboardAction;
use Icinga\Web\Widget\Tabextension\MenuAction;
use Icinga\Web\Widget\Tabextension\OutputFormat;
use ipl\Web\Filter\QueryString;
class ListController extends MonitoringAwareController
{
use TimeRangePickerTrait;
public function init()
{
parent::init();
$this->getTabs()
->extend(new OutputFormat([OutputFormat::TYPE_CSV, OutputFormat::TYPE_JSON]))
->extend(new DashboardAction())
->extend(new MenuAction());
}
public function hostsAction()
{
if ($this->useIcingadbAsBackend) {
$legacyParams = urlencode($this->params->toString());
$params = QueryString::render(
UrlMigrator::transformFilter(
QueryString::parse($this->params->toString())
)
);
$url = Url::fromPath('graphite/hosts')
->setQueryString($params);
if ($legacyParams) {
$url->setParam('legacyParams', $legacyParams);
}
$this->redirectNow($url);
}
$this->addTitleTab(
'hosts',
mt('monitoring', 'Hosts'),
mt('monitoring', 'List hosts')
);
$hostsQuery = $this->applyMonitoringRestriction(
$this->backend->select()->from('hoststatus', ['host_name'])
);
$hostsQuery->applyFilter(Filter::expression('host_perfdata', '!=', ''));
$this->view->baseUrl = $baseUrl = Url::fromPath('monitoring/host/show');
TimeRangePickerTools::copyAllRangeParameters(
$baseUrl->getParams(),
$this->getRequest()->getUrl()->getParams()
);
$this->filterQuery($hostsQuery);
$this->setupPaginationControl($hostsQuery);
$this->setupLimitControl();
$this->setupSortControl(['host_display_name' => mt('monitoring', 'Hostname')], $hostsQuery);
$hosts = [];
foreach ($hostsQuery->peekAhead($this->view->compact) as $host) {
$host = new Host($this->backend, $host->host_name);
$host->fetch();
$hosts[] = $host;
}
$this->handleTimeRangePickerRequest();
$this->view->timeRangePicker = $this->renderTimeRangePicker($this->view);
$this->view->hosts = $hosts;
$this->view->hasMoreHosts = ! $this->view->compact && $hostsQuery->hasMore();
$this->setAutorefreshInterval(30);
}
public function servicesAction()
{
if ($this->useIcingadbAsBackend) {
$legacyParams = urlencode($this->params->toString());
$params = QueryString::render(
UrlMigrator::transformFilter(
QueryString::parse($this->params->toString())
)
);
$url = Url::fromPath('graphite/services')
->setQueryString($params);
if ($legacyParams) {
$url->setParam('legacyParams', $legacyParams);
}
$this->redirectNow($url);
}
$this->addTitleTab(
'services',
mt('monitoring', 'Services'),
mt('monitoring', 'List services')
);
$servicesQuery = $this->applyMonitoringRestriction(
$this->backend->select()->from('servicestatus', ['host_name', 'service_description'])
);
$servicesQuery->applyFilter(Filter::expression('service_perfdata', '!=', ''));
$this->view->hostBaseUrl = $hostBaseUrl = Url::fromPath('monitoring/host/show');
TimeRangePickerTools::copyAllRangeParameters(
$hostBaseUrl->getParams(),
$this->getRequest()->getUrl()->getParams()
);
$this->view->serviceBaseUrl = $serviceBaseUrl = Url::fromPath('monitoring/service/show');
TimeRangePickerTools::copyAllRangeParameters(
$serviceBaseUrl->getParams(),
$this->getRequest()->getUrl()->getParams()
);
$this->filterQuery($servicesQuery);
$this->setupPaginationControl($servicesQuery);
$this->setupLimitControl();
$this->setupSortControl([
'service_display_name' => mt('monitoring', 'Service Name'),
'host_display_name' => mt('monitoring', 'Hostname')
], $servicesQuery);
$services = [];
foreach ($servicesQuery->peekAhead($this->view->compact) as $service) {
$service = new Service($this->backend, $service->host_name, $service->service_description);
$service->fetch();
$services[] = $service;
}
$this->handleTimeRangePickerRequest();
$this->view->timeRangePicker = $this->renderTimeRangePicker($this->view);
$this->view->services = $services;
$this->view->hasMoreServices = ! $this->view->compact && $servicesQuery->hasMore();
$this->setAutorefreshInterval(30);
}
/**
* Apply filters on a DataView
*
* @param DataView $dataView The DataView to apply filters on
*/
protected function filterQuery(DataView $dataView)
{
$this->setupFilterControl(
$dataView,
null,
null,
array_merge(
['format', 'stateType', 'addColumns', 'problems', 'graphs_limit'],
TimeRangePickerTools::getAllRangeParameters()
)
);
$this->handleFormatRequest($dataView);
}
/**
* Add title tab
*
* @param string $action
* @param string $title
* @param string $tip
*/
protected function addTitleTab($action, $title, $tip)
{
$this->getTabs()->add($action, [
'title' => $tip,
'label' => $title,
'url' => Url::fromRequest(),
'active' => true
]);
}
}
icingaweb2-module-graphite-1.3.0/application/controllers/MonitoringGraphController.php 0000664 0000000 0000000 00000013261 15163212266 0031355 0 ustar 00root root 0000000 0000000
// SPDX-License-Identifier: GPL-3.0-or-later
namespace Icinga\Module\Graphite\Controllers;
use Icinga\Exception\Http\HttpBadRequestException;
use Icinga\Exception\Http\HttpNotFoundException;
use Icinga\Module\Graphite\Graphing\GraphingTrait;
use Icinga\Module\Graphite\Web\Controller\MonitoringAwareController;
use Icinga\Module\Graphite\Web\Widget\Graphs;
use Icinga\Module\Monitoring\Object\Host;
use Icinga\Module\Monitoring\Object\MonitoredObject;
use Icinga\Module\Monitoring\Object\Service;
use Icinga\Web\UrlParams;
class MonitoringGraphController extends MonitoringAwareController
{
use GraphingTrait;
/**
* The URL parameters for the graph
*
* @var string[]
*/
protected $graphParamsNames = [
'start', 'end',
'width', 'height',
'legend',
'template', 'default_template',
'bgcolor', 'fgcolor',
'majorGridLineColor', 'minorGridLineColor'
];
/**
* The URL parameters for metrics filtering
*
* @var UrlParams
*/
protected $filterParams;
/**
* The URL parameters for the graph
*
* @var string[]
*/
protected $graphParams = [];
public function init()
{
parent::init();
$this->filterParams = clone $this->getRequest()->getUrl()->getParams();
foreach ($this->graphParamsNames as $paramName) {
$this->graphParams[$paramName] = $this->filterParams->shift($paramName);
}
}
public function hostAction()
{
$hostName = $this->filterParams->getRequired('host.name');
$checkCommandColumn = '_host_' . Graphs::getObscuredCheckCommandCustomVar();
$host = $this->applyMonitoringRestriction(
$this->backend->select()->from('hoststatus', ['host_check_command', $checkCommandColumn])
)
->where('host_name', $hostName)
->limit(1) // just to be sure to save a few CPU cycles
->fetchRow();
if ($host === false) {
throw new HttpNotFoundException($this->translate('No such host'));
}
$this->supplyImage(new Host($this->backend, $hostName), $host->host_check_command, $host->$checkCommandColumn);
}
public function serviceAction()
{
$hostName = $this->filterParams->getRequired('host.name');
$serviceName = $this->filterParams->getRequired('service.name');
$checkCommandColumn = '_service_' . Graphs::getObscuredCheckCommandCustomVar();
$service = $this->applyMonitoringRestriction(
$this->backend->select()->from('servicestatus', ['service_check_command', $checkCommandColumn])
)
->where('host_name', $hostName)
->where('service_description', $serviceName)
->limit(1) // just to be sure to save a few CPU cycles
->fetchRow();
if ($service === false) {
throw new HttpNotFoundException($this->translate('No such service'));
}
$this->supplyImage(
new Service($this->backend, $hostName, $serviceName),
$service->service_check_command,
$service->$checkCommandColumn
);
}
/**
* Do all monitored object type independend actions
*
* @param MonitoredObject $object The object to render the graphs for
* @param string $checkCommand The check command of the object we supply an image for
* @param string|null $obscuredCheckCommand The "real" check command (if any) of the object we
* display graphs for
*/
protected function supplyImage($object, $checkCommand, $obscuredCheckCommand)
{
if (isset($this->graphParams['default_template'])) {
$urlParam = 'default_template';
$templates = $this->getAllTemplates()->getDefaultTemplates();
} else {
$urlParam = 'template';
$templates = $this->getAllTemplates()->getTemplates(
$obscuredCheckCommand === null ? $checkCommand : $obscuredCheckCommand
);
}
if (! isset($templates[$this->graphParams[$urlParam]])) {
throw new HttpNotFoundException($this->translate('No such template'));
}
$charts = $templates[$this->graphParams[$urlParam]]->getCharts(
static::getMetricsDataSource(),
$object,
array_map('rawurldecode', $this->filterParams->toArray(false))
);
switch (count($charts)) {
case 0:
throw new HttpNotFoundException($this->translate('No such graph'));
case 1:
$charts[0]
->setFrom($this->graphParams['start'])
->setUntil($this->graphParams['end'])
->setWidth((int) $this->graphParams['width'])
->setHeight((int) $this->graphParams['height'])
->setBackgroundColor($this->graphParams['bgcolor'])
->setForegroundColor($this->graphParams['fgcolor'])
->setMajorGridLineColor($this->graphParams['majorGridLineColor'])
->setMinorGridLineColor($this->graphParams['minorGridLineColor'])
->setShowLegend((bool) $this->graphParams['legend'])
->serveImage($this->getResponse());
// not falling through, serveImage exits
default:
throw new HttpBadRequestException($this->translate(
'Graphite Web yields more than one metric for the given filter.'
. ' Please specify a more precise filter.'
));
}
}
}
icingaweb2-module-graphite-1.3.0/application/controllers/ServicesController.php 0000664 0000000 0000000 00000007672 15163212266 0030042 0 ustar 00root root 0000000 0000000
// SPDX-License-Identifier: GPL-3.0-or-later
namespace Icinga\Module\Graphite\Controllers;
use GuzzleHttp\Psr7\ServerRequest;
use Icinga\Module\Graphite\Util\InternalProcessTracker as IPT;
use Icinga\Module\Graphite\Web\Controller\IcingadbGraphiteController;
use Icinga\Module\Graphite\Web\Controller\TimeRangePickerTrait;
use Icinga\Module\Graphite\Web\Widget\IcingadbGraphs;
use Icinga\Module\Icingadb\Model\Service;
use Icinga\Module\Icingadb\Web\Control\SearchBar\ObjectSuggestions;
use Icinga\Web\Url;
use ipl\Html\HtmlString;
use ipl\Stdlib\Filter;
use ipl\Web\Control\LimitControl;
use ipl\Web\Control\SortControl;
class ServicesController extends IcingadbGraphiteController
{
use TimeRangePickerTrait;
public function indexAction()
{
if (! $this->useIcingadbAsBackend) {
$params = urldecode($this->params->get('legacyParams'));
$this->redirectNow(Url::fromPath('graphite/list/services')->setQueryString($params));
}
// shift graph params to avoid exception
$graphDebug = $this->params->shift('graph_debug');
$graphRange = $this->params->shift('graph_range');
if ($graphDebug) {
IPT::enable();
}
$baseFilter = $graphRange ? Filter::equal('graph_range', $graphRange) : null;
foreach ($this->preservedParams as $param) {
$this->params->shift($param);
}
$this->addTitleTab(t('Services'));
$db = $this->getDb();
$services = Service::on($db)
->with('state')
->with('host');
$services->filter(Filter::like('state.performance_data', '*'));
$this->applyRestrictions($services);
$limitControl = $this->createLimitControl();
$paginationControl = $this->createPaginationControl($services);
$sortControl = $this->createSortControl($services, [
'service.display_name' => t('Servicename'),
'host.display_name' => t('Hostname')
]);
$searchBar = $this->createSearchBar(
$services,
array_merge(
[$limitControl->getLimitParam(), $sortControl->getSortParam()],
$this->preservedParams
)
);
if ($searchBar->hasBeenSent() && ! $searchBar->isValid()) {
if ($searchBar->hasBeenSubmitted()) {
$filter = $this->getFilter();
} else {
$this->addControl($searchBar);
$this->sendMultipartUpdate();
return;
}
} else {
$filter = $searchBar->getFilter();
}
$services->filter($filter);
$this->addControl($paginationControl);
$this->addControl($sortControl);
$this->addControl($limitControl);
$this->addControl($searchBar);
$this->handleTimeRangePickerRequest();
$this->addControl(HtmlString::create($this->renderTimeRangePicker($this->view)));
$this->addContent(
(new IcingadbGraphs($services->execute()))
->setBaseFilter($baseFilter)
);
if (! $searchBar->hasBeenSubmitted() && $searchBar->hasBeenSent()) {
$this->sendMultipartUpdate();
}
$this->setAutorefreshInterval(30);
}
public function completeAction()
{
$suggestions = new ObjectSuggestions();
$suggestions->setModel(Service::class);
$suggestions->forRequest(ServerRequest::fromGlobals());
$this->getDocument()->add($suggestions);
}
public function searchEditorAction()
{
$editor = $this->createSearchEditor(
Service::on($this->getDb()),
array_merge(
[LimitControl::DEFAULT_LIMIT_PARAM, SortControl::DEFAULT_SORT_PARAM],
$this->preservedParams
)
);
$this->getDocument()->add($editor);
$this->setTitle(t('Adjust Filter'));
}
}
icingaweb2-module-graphite-1.3.0/application/forms/ 0000775 0000000 0000000 00000000000 15163212266 0022246 5 ustar 00root root 0000000 0000000 icingaweb2-module-graphite-1.3.0/application/forms/Config/ 0000775 0000000 0000000 00000000000 15163212266 0023453 5 ustar 00root root 0000000 0000000 icingaweb2-module-graphite-1.3.0/application/forms/Config/AdvancedForm.php 0000664 0000000 0000000 00000007403 15163212266 0026521 0 ustar 00root root 0000000 0000000
// SPDX-License-Identifier: GPL-3.0-or-later
namespace Icinga\Module\Graphite\Forms\Config;
use Icinga\Forms\ConfigForm;
use Icinga\Module\Graphite\Web\Form\Validator\MacroTemplateValidator;
use Zend_Validate_Regex;
class AdvancedForm extends ConfigForm
{
public function init()
{
$this->setName('form_config_graphite_advanced');
$this->setSubmitLabel($this->translate('Save Changes'));
}
public function createElements(array $formData)
{
$this->addElements([
[
'number',
'ui_default_time_range',
[
'label' => $this->translate('Default time range'),
'description' => $this->translate('The default time range for graphs'),
'min' => 0,
'value' => 1
]
],
[
'select',
'ui_default_time_range_unit',
[
'label' => $this->translate('Default time range unit'),
'description' => $this->translate('The above range\'s unit'),
'multiOptions' => [
'minutes' => $this->translate('Minutes'),
'hours' => $this->translate('Hours'),
'days' => $this->translate('Days'),
'weeks' => $this->translate('Weeks'),
'months' => $this->translate('Months'),
'years' => $this->translate('Years')
],
'value' => 'hours'
]
],
[
'checkbox',
'ui_disable_no_graphs_found',
[
'label' => $this->translate('Disable "no graphs found"'),
'description' => $this->translate(
'If no graphs were found for a monitored object, just display nothing at all'
),
]
],
[
'text',
'icinga_graphite_writer_host_name_template',
[
'label' => $this->translate('Host name template'),
'description' => $this->translate(
'The value of your Icinga 2 GraphiteWriter\'s'
. ' attribute host_name_template (if specified)'
),
'validators' => [new MacroTemplateValidator()]
]
],
[
'text',
'icinga_graphite_writer_service_name_template',
[
'label' => $this->translate('Service name template'),
'description' => $this->translate(
'The value of your Icinga 2 GraphiteWriter\'s'
. ' attribute service_name_template (if specified)'
),
'validators' => [new MacroTemplateValidator()]
]
],
[
'text',
'icinga_customvar_obscured_check_command',
[
'label' => $this->translate('Obscured check command custom variable'),
'description' => $this->translate(
'The Icinga custom variable with the "actual" check command obscured'
. ' by e.g. check_by_ssh (defaults to check_command)'
),
'validators' => [new Zend_Validate_Regex('/\A\w*\z/')]
]
]
]);
}
}
icingaweb2-module-graphite-1.3.0/application/forms/Config/BackendForm.php 0000664 0000000 0000000 00000004637 15163212266 0026351 0 ustar 00root root 0000000 0000000
// SPDX-License-Identifier: GPL-3.0-or-later
namespace Icinga\Module\Graphite\Forms\Config;
use Icinga\Forms\ConfigForm;
use Icinga\Module\Graphite\Web\Form\Validator\HttpUserValidator;
class BackendForm extends ConfigForm
{
public function init()
{
$this->setName('form_config_graphite_backend');
$this->setSubmitLabel($this->translate('Save Changes'));
}
public function createElements(array $formData)
{
$this->addElements([
[
'text',
'graphite_url',
[
'required' => true,
'label' => $this->translate('Graphite Web URL'),
'description' => $this->translate('URL to your Graphite Web'),
'validators' => ['UrlValidator']
]
],
[
'text',
'graphite_user',
[
'label' => $this->translate('Graphite Web user'),
'description' => $this->translate(
'A user with access to your Graphite Web via HTTP basic authentication'
),
'validators' => [new HttpUserValidator()]
]
],
[
'password',
'graphite_password',
[
'renderPassword' => true,
'label' => $this->translate('Graphite Web password'),
'description' => $this->translate('The above user\'s password')
]
],
[
'checkbox',
'graphite_insecure',
[
'label' => $this->translate('Connect insecurely'),
'description' => $this->translate('Check this to not verify the remote\'s TLS certificate')
]
],
[
'number',
'graphite_timeout',
[
'label' => $this->translate('Request timeout'),
'description' => $this->translate('The timeout for HTTP requests to Graphite Web'),
'min' => 0,
'placeholder' => 10
]
]
]);
}
}
icingaweb2-module-graphite-1.3.0/application/forms/TimeRangePicker/ 0000775 0000000 0000000 00000000000 15163212266 0025257 5 ustar 00root root 0000000 0000000 icingaweb2-module-graphite-1.3.0/application/forms/TimeRangePicker/CommonForm.php 0000664 0000000 0000000 00000014310 15163212266 0030043 0 ustar 00root root 0000000 0000000
// SPDX-License-Identifier: GPL-3.0-or-later
namespace Icinga\Module\Graphite\Forms\TimeRangePicker;
use Icinga\Module\Graphite\Util\TimeRangePickerTools;
use Icinga\Web\Form;
use Zend_Form_Element_Select;
class CommonForm extends Form
{
/**
* The selectable units with themselves in seconds
*
* One month equals 30 days and one year equals 365.25 days. This should cover enough cases.
*
* @var int[]
*/
protected $rangeFactors = [
'minutes' => 60,
'hours' => 3600,
'days' => 86400,
'weeks' => 604800,
'months' => 2592000,
'years' => 31557600
];
/**
* The elements' default values
*
* @var string[]|null
*/
protected $defaultFormData;
public function init()
{
$this->setName('form_timerangepickercommon_graphite');
$this->setAttrib('data-base-target', '_self');
$this->setAttrib('class', 'icinga-form icinga-controls inline');
}
public function createElements(array $formData)
{
$this->addElements([
$this->createSelect(
'minutes',
$this->translate('Minutes'),
$this->translate('Show the last … minutes'),
[5, 10, 15, 30, 45],
$this->translate('%d minute'),
$this->translate('%d minutes')
),
$this->createSelect(
'hours',
$this->translate('Hours'),
$this->translate('Show the last … hours'),
[1, 2, 3, 6, 12, 18],
$this->translate('%d hour'),
$this->translate('%d hours')
),
$this->createSelect(
'days',
$this->translate('Days'),
$this->translate('Show the last … days'),
range(1, 6),
$this->translate('%d day'),
$this->translate('%d days')
),
$this->createSelect(
'weeks',
$this->translate('Weeks'),
$this->translate('Show the last … weeks'),
range(1, 4),
$this->translate('%d week'),
$this->translate('%d weeks')
),
$this->createSelect(
'months',
$this->translate('Months'),
$this->translate('Show the last … months'),
[1, 2, 3, 6, 9],
$this->translate('%d month'),
$this->translate('%d months')
),
$this->createSelect(
'years',
$this->translate('Years'),
$this->translate('Show the last … years'),
range(1, 3),
$this->translate('%d year'),
$this->translate('%d years')
)
]);
$this->urlToForm();
$this->defaultFormData = $this->getValues();
}
public function onSuccess()
{
$this->formToUrl();
$this->getRedirectUrl()->remove(array_values(TimeRangePickerTools::getAbsoluteRangeParameters()));
}
/**
* Create a common range picker for a specific time unit
*
* @param string $name
* @param string $label
* @param string $description
* @param int[] $options
* @param string $singular
* @param string $plural
*
* @return Zend_Form_Element_Select
*/
protected function createSelect($name, $label, $description, array $options, $singular, $plural)
{
$multiOptions = ['' => $label];
foreach ($options as $option) {
$multiOptions[$option] = sprintf($option === 1 ? $singular : $plural, $option);
}
$element = $this->createElement('select', $name, [
'label' => $label,
'description' => $description,
'multiOptions' => $multiOptions,
'title' => $description,
'autosubmit' => true
]);
$decorators = $element->getDecorators();
$element->setDecorators([
'Zend_Form_Decorator_ViewHelper' => $decorators['Zend_Form_Decorator_ViewHelper']
]);
return $element;
}
/**
* Set this form's elements' default values based on the redirect URL's parameters
*/
protected function urlToForm()
{
$params = $this->getRedirectUrl()->getParams();
$seconds = TimeRangePickerTools::getRelativeSeconds($params);
if (
$seconds === null
&& count(array_intersect_key(
$params->toArray(false),
array_keys(TimeRangePickerTools::getAllRangeParameters())
)) === 0
) {
$seconds = TimeRangePickerTools::getDefaultRelativeTimeRange();
}
if ($seconds !== null) {
if ($seconds !== false) {
foreach ($this->rangeFactors as $unit => $factor) {
/** @var Zend_Form_Element_Select $element */
$element = $this->getElement($unit);
$options = $element->getMultiOptions();
unset($options['']);
foreach ($options as $option => $_) {
if ($seconds === (int) $option * $factor) {
$element->setValue((string) $option);
return;
}
}
}
}
$params->remove(TimeRangePickerTools::getRelativeRangeParameter());
}
}
/**
* Change the redirect URL's parameters based on this form's elements' values
*/
protected function formToUrl()
{
$formData = $this->getValues();
foreach ($this->rangeFactors as $unit => $factor) {
if ($formData[$unit] !== '' && $formData[$unit] !== $this->defaultFormData[$unit]) {
$this->getRedirectUrl()->setParam(
TimeRangePickerTools::getRelativeRangeParameter(),
(string) ((int) $formData[$unit] * $factor)
);
return;
}
}
}
}
icingaweb2-module-graphite-1.3.0/application/forms/TimeRangePicker/CustomForm.php 0000664 0000000 0000000 00000017444 15163212266 0030100 0 ustar 00root root 0000000 0000000
// SPDX-License-Identifier: GPL-3.0-or-later
namespace Icinga\Module\Graphite\Forms\TimeRangePicker;
use DateInterval;
use DateTime;
use DateTimeZone;
use Icinga\Module\Graphite\Util\TimeRangePickerTools;
use Icinga\Module\Graphite\Web\Form\Decorator\Proxy;
use Icinga\Web\Form;
class CustomForm extends Form
{
/**
* @var string
*/
protected $dateTimeFormat = 'Y-m-d\TH:i';
/**
* @var string
*/
protected $timestamp = '/^(?:0|-?[1-9]\d*)$/';
/**
* Right now
*
* @var DateTime
*/
protected $now;
public function init()
{
$this->setName('form_timerangepickercustom_graphite');
$this->setAttrib('data-base-target', '_self');
}
public function createElements(array $formData)
{
$this->addElements([
[
'date',
'start_date',
[
'placeholder' => 'YYYY-MM-DD',
'label' => $this->translate('Start'),
'description' => $this->translate('Start of the date/time range')
]
],
[
'time',
'start_time',
[
'placeholder' => 'HH:MM',
'label' => $this->translate('Start'),
'description' => $this->translate('Start of the date/time range')
]
],
[
'date',
'end_date',
[
'placeholder' => 'YYYY-MM-DD',
'label' => $this->translate('End'),
'description' => $this->translate('End of the date/time range')
]
],
[
'time',
'end_time',
[
'placeholder' => 'HH:MM',
'label' => $this->translate('End'),
'description' => $this->translate('End of the date/time range')
]
]
]);
$this->groupDateTime('start');
$this->groupDateTime('end');
$this->setSubmitLabel($this->translate('Update'));
$this->urlToForm('start', $this->getRelativeTimestamp());
$this->urlToForm('end');
}
public function addSubmitButton()
{
$result = parent::addSubmitButton();
$this->getElement('btn_submit')->class = 'flyover-toggle';
return $result;
}
public function onSuccess()
{
$start = $this->formToUrl('start', '00:00');
$end = $this->formToUrl('end', '23:59', 'PT59S');
if ($start > $end) {
$absoluteRangeParameters = TimeRangePickerTools::getAbsoluteRangeParameters();
$this->getRedirectUrl()->getParams()
->set($absoluteRangeParameters['start'], (string) $end)
->set($absoluteRangeParameters['end'], (string) $start);
}
$this->getRedirectUrl()->remove(TimeRangePickerTools::getRelativeRangeParameter());
}
/**
* Add display group for a date and a time input belonging together
*
* @param string $part Either 'start' or 'end'
*/
protected function groupDateTime($part)
{
$this->addDisplayGroup(["{$part}_date", "{$part}_time"], $part);
$group = $this->getDisplayGroup($part);
foreach ($group->getElements() as $element) {
/** @var \Zend_Form_Element $element */
$elementDecorators = $element->getDecorators();
$element->setDecorators([
'Zend_Form_Decorator_ViewHelper' => $elementDecorators['Zend_Form_Decorator_ViewHelper']
]);
}
assert(isset($elementDecorators), '$elementDecorators not initialized in the loop');
assert(isset($element), '$element not initialized in the loop');
$decorators = [];
foreach ($elementDecorators as $key => $decorator) {
if ($key === 'Zend_Form_Decorator_ViewHelper') {
$decorators['Zend_Form_Decorator_FormElements'] =
$group->getDecorators()['Zend_Form_Decorator_FormElements'];
} else {
$decorators[$key] = (new Proxy())->setActualDecorator($decorator->setElement($element));
}
}
$group->setDecorators($decorators);
}
/**
* Set this form's elements' default values based on the redirect URL's parameters
*
* @param string $part Either 'start' or 'end'
* @param int $defaultTimestamp Fallback
*/
protected function urlToForm($part, $defaultTimestamp = null)
{
$params = $this->getRedirectUrl()->getParams();
$absoluteRangeParameters = TimeRangePickerTools::getAbsoluteRangeParameters();
$timestamp = $params->get($absoluteRangeParameters[$part], $defaultTimestamp);
if ($timestamp !== null) {
if (preg_match($this->timestamp, $timestamp)) {
list($date, $time) = explode(
'T',
DateTime::createFromFormat('U', $timestamp)
->setTimezone(new DateTimeZone(date_default_timezone_get()))
->format($this->dateTimeFormat)
);
$this->getElement("{$part}_date")->setValue($date);
$this->getElement("{$part}_time")->setValue($time);
} else {
$params->remove($absoluteRangeParameters[$part]);
}
}
}
/**
* Get the relative range start (if any) set by {@link CommonForm}
*
* @return int|null
*/
protected function getRelativeTimestamp()
{
$seconds = TimeRangePickerTools::getRelativeSeconds($this->getRedirectUrl()->getParams());
return is_int($seconds) ? $this->getNow()->getTimestamp() - $seconds : null;
}
/**
* Change the redirect URL's parameters based on this form's elements' values
*
* @param string $part Either 'start' or 'end'
* @param string $defaultTime Default if no time given
* @param string $addInterval Add this interval to the result
*
* @return int|null The updated timestamp (if any)
*/
protected function formToUrl($part, $defaultTime, $addInterval = null)
{
$date = $this->getValue("{$part}_date");
$time = $this->getValue("{$part}_time");
$params = $this->getRedirectUrl()->getParams();
$absoluteRangeParameters = TimeRangePickerTools::getAbsoluteRangeParameters();
if ($date === '' && $time === '') {
$params->remove($absoluteRangeParameters[$part]);
} else {
$dateTime = DateTime::createFromFormat(
$this->dateTimeFormat,
($date === '' ? $this->getNow()->format('Y-m-d') : $date)
. 'T' . ($time === '' ? $defaultTime : $time)
);
if ($dateTime === false) {
$params->remove($absoluteRangeParameters[$part]);
} else {
if ($addInterval !== null) {
$dateTime->add(new DateInterval($addInterval));
}
$params->set($absoluteRangeParameters[$part], $dateTime->format('U'));
return $dateTime->getTimestamp();
}
}
}
/**
* Get {@link now}
*
* @return DateTime
*/
public function getNow()
{
if ($this->now === null) {
$this->now = new DateTime();
}
return $this->now;
}
/**
* Set {@link now}
*
* @param DateTime $now
*
* @return $this
*/
public function setNow($now)
{
$this->now = $now;
return $this;
}
}
icingaweb2-module-graphite-1.3.0/application/views/ 0000775 0000000 0000000 00000000000 15163212266 0022255 5 ustar 00root root 0000000 0000000 icingaweb2-module-graphite-1.3.0/application/views/scripts/ 0000775 0000000 0000000 00000000000 15163212266 0023744 5 ustar 00root root 0000000 0000000 icingaweb2-module-graphite-1.3.0/application/views/scripts/config/ 0000775 0000000 0000000 00000000000 15163212266 0025211 5 ustar 00root root 0000000 0000000 icingaweb2-module-graphite-1.3.0/application/views/scripts/config/advanced.phtml 0000664 0000000 0000000 00000000314 15163212266 0030022 0 ustar 00root root 0000000 0000000
= /** @var \Icinga\Web\Widget\Tabs $tabs */ $tabs ?>
= /** @var \Icinga\Module\Graphite\Forms\Config\AdvancedForm $form */ $form ?>
icingaweb2-module-graphite-1.3.0/application/views/scripts/config/backend.phtml 0000664 0000000 0000000 00000000313 15163212266 0027643 0 ustar 00root root 0000000 0000000
= /** @var \Icinga\Web\Widget\Tabs $tabs */ $tabs ?>
= /** @var \Icinga\Module\Graphite\Forms\Config\BackendForm $form */ $form ?>
icingaweb2-module-graphite-1.3.0/application/views/scripts/list/ 0000775 0000000 0000000 00000000000 15163212266 0024717 5 ustar 00root root 0000000 0000000 icingaweb2-module-graphite-1.3.0/application/views/scripts/list/hosts.phtml 0000664 0000000 0000000 00000003704 15163212266 0027131 0 ustar 00root root 0000000 0000000
= $tabs ?>
= $paginator ?>
= $limiter ?>
= $sortBox ?>
= $filterEditor ?>
= $timeRangePicker ?>
';
echo '';
foreach ($hosts as $host) {
$hostGraphs = (string) (new Host($host))->setPreloadDummy()->handleRequest();
if ($hostGraphs !== '') {
echo '
'
. '
'
. $this->qlink(
$host->host_name === $host->host_display_name
? $host->host_display_name
: $host->host_display_name . ' (' . $this->escape($host->host_name) . ')',
$baseUrl->with(['host' => $host->host_name]),
null,
['data-base-target' => '_next']
)
. '
'
. $hostGraphs
. '';
}
}
if ($hasMoreHosts) {
echo '
'
. $this->qlink(
mt('monitoring', 'Show More'),
$this->url()->without(array('view', 'limit')),
null,
[
'class' => 'action-link',
'data-base-target' => '_next'
]
)
. '
';
}
echo '
';
} else {
echo '' . $this->escape(mt('monitoring', 'No hosts found matching the filter.')) . '
';
}
?>
icingaweb2-module-graphite-1.3.0/application/views/scripts/list/services.phtml 0000664 0000000 0000000 00000005163 15163212266 0027615 0 ustar 00root root 0000000 0000000
= $tabs ?>
= $paginator ?>
= $limiter ?>
= $sortBox ?>
= $filterEditor ?>
= $timeRangePicker ?>
';
echo '';
foreach ($services as $service) {
echo '
'
. '
'
. $this->qlink(
$service->host_name === $service->host_display_name
? $service->host_display_name
: $service->host_display_name . ' (' . $this->escape($service->host_name) . ')',
$hostBaseUrl->with(['host' => $service->host_name]),
null,
['data-base-target' => '_next']
)
. ': '
. $this->qlink(
$service->service_description === $service->service_display_name
? $service->service_display_name
: $service->service_display_name . ' (' . $this->escape($service->service_description) . ')',
$serviceBaseUrl->with([
'host' => $service->host_name,
'service' => $service->service_description
]),
null,
['data-base-target' => '_next']
)
. '
';
echo (new Service($service))->setPreloadDummy()->handleRequest();
echo '';
}
if ($hasMoreServices) {
echo '
'
. $this->qlink(
mt('monitoring', 'Show More'),
$this->url()->without(array('view', 'limit')),
null,
[
'class' => 'action-link',
'data-base-target' => '_next'
]
)
. '
';
}
echo '
';
} else {
echo '' . $this->escape(mt('monitoring', 'No services found matching the filter.')) . '
';
}
?>
icingaweb2-module-graphite-1.3.0/application/views/scripts/test/ 0000775 0000000 0000000 00000000000 15163212266 0024723 5 ustar 00root root 0000000 0000000 icingaweb2-module-graphite-1.3.0/application/views/scripts/test/apache.phtml 0000664 0000000 0000000 00000000430 15163212266 0027207 0 ustar 00root root 0000000 0000000
= $this->tabs ?>
images as $base => $img): ?>
= $this->escape($base) ?>
icingaweb2-module-graphite-1.3.0/application/views/scripts/test/cpu.phtml 0000664 0000000 0000000 00000001232 15163212266 0026556 0 ustar 00root root 0000000 0000000 images as $base => $cpus) {
$maxCnt = max($maxCnt, count($cpus));
}
?>
= $this->tabs ?>
CPUs
| |
CPUs |
images as $base => $cpus): ?>
| = $this->escape($base) ?> |
$img): ?>
|
icingaweb2-module-graphite-1.3.0/configuration.php 0000664 0000000 0000000 00000002277 15163212266 0022205 0 ustar 00root root 0000000 0000000
// SPDX-License-Identifier: GPL-3.0-or-later
/** @var \Icinga\Application\Modules\Module $this */
/** @var \Icinga\Application\Modules\MenuItemContainer $section */
use Icinga\Module\Graphite\ProvidedHook\Icingadb\IcingadbSupport;
$section = $this->menuSection(N_('Graphite'), ['icon' => 'chart-area']);
if ($this::exists('icingadb') && IcingadbSupport::useIcingaDbAsBackend()) {
$section->add(N_('Hosts'), ['url' => 'graphite/hosts']);
$section->add(N_('Services'), ['url' => 'graphite/services']);
} else {
$section->add(N_('Hosts'), ['url' => 'graphite/list/hosts']);
$section->add(N_('Services'), ['url' => 'graphite/list/services']);
}
$this->provideConfigTab('backend', array(
'title' => $this->translate('Configure the Graphite Web backend'),
'label' => $this->translate('Backend'),
'url' => 'config/backend'
));
$this->provideConfigTab('advanced', array(
'title' => $this->translate('Advanced configuration'),
'label' => $this->translate('Advanced'),
'url' => 'config/advanced'
));
$this->providePermission('graphite/debug', $this->translate('Allow debugging directly via the web UI'));
icingaweb2-module-graphite-1.3.0/doc/ 0000775 0000000 0000000 00000000000 15163212266 0017362 5 ustar 00root root 0000000 0000000 icingaweb2-module-graphite-1.3.0/doc/01-About.md 0000664 0000000 0000000 00000001333 15163212266 0021174 0 ustar 00root root 0000000 0000000 # Icinga Web Graphite Integration
This module integrates an existing [Graphite](https://graphite.readthedocs.io/en/latest/)
installation in your [Icinga Web](https://icinga.com/products/infrastructure-monitoring/)
frontend.
 | 
--------------------------------------|--------------------------------------------
It provides a new menu section with two general overviews for hosts and
services as well as an extension to the host and service detail view of
the monitoring module.
## Documentation
* [Installation](02-Installation.md)
* [Configuration](03-Configuration.md)
* [Templates](04-Templates.md)
* [Troubleshooting](05-Troubleshooting.md)
icingaweb2-module-graphite-1.3.0/doc/02-Installation.md 0000664 0000000 0000000 00000002770 15163212266 0022572 0 ustar 00root root 0000000 0000000
# Installing Icinga Web Graphite Integration
It is recommended to use prebuilt packages
for all supported platforms from our official release repository.
Of course [Icinga Web](https://icinga.com/docs/icinga-web) itself
is required to run its Graphite integration.
The latter uses Graphite Web, so that is required as well.
If they are not already set up, it is best to do this first.
The following steps will guide you through installing
and setting up Icinga Web Graphite Integration.
## Installing the Package
If the [repository](https://packages.icinga.com) is not configured yet, please add it first.
Then use your distribution's package manager to install the `icinga-graphite` package
or install [from source](02-Installation.md.d/From-Source.md).
## Prepare Icinga 2
Enable the graphite feature:
```
# icinga2 feature enable graphite
```
Adjust its configuration in `/etc/icinga2/features-enabled/graphite.conf`:
```
object GraphiteWriter "graphite" {
host = "192.0.2.42"
port = 2003
enable_send_thresholds = true
}
```
And then restart Icinga2. Enabling thresholds is not a hard requirement.
However, some templates look better if they are able to render a max
value or similar.
## Configuring the Icinga Web Graphite Integration
For required additional steps see the [Configuration](03-Configuration.md) chapter.
icingaweb2-module-graphite-1.3.0/doc/02-Installation.md.d/ 0000775 0000000 0000000 00000000000 15163212266 0023063 5 ustar 00root root 0000000 0000000 icingaweb2-module-graphite-1.3.0/doc/02-Installation.md.d/From-Source.md 0000664 0000000 0000000 00000001255 15163212266 0025551 0 ustar 00root root 0000000 0000000 # Installing Icinga Web Graphite Integration from Source
Please see the Icinga Web documentation on
[how to install modules](https://icinga.com/docs/icinga-web-2/latest/doc/08-Modules/#installation) from source.
Make sure you use `graphite` as the module name. The following requirements must also be met.
## Requirements
* PHP ≥ 8.2
* [Icinga Web](https://github.com/Icinga/icingaweb2) ≥ 2.12.5
* [Icinga DB Web](https://github.com/Icinga/icingadb-web) ≥ 1.0
* [Icinga PHP Library (ipl)](https://github.com/Icinga/icinga-php-library) ≥ 0.19.0
* [Icinga PHP Thirdparty](https://github.com/Icinga/icinga-php-thirdparty) ≥ 0.15.0
icingaweb2-module-graphite-1.3.0/doc/03-Configuration.md 0000664 0000000 0000000 00000004270 15163212266 0022736 0 ustar 00root root 0000000 0000000 # Configuration
## Basics
Open up the Icinga Web frontend and navigate to:
`Configuration > Modules > graphite > Backend`
Enter the Graphite Web URL. (e.g. `https://192.0.2.42:8003/`)
The HTTP basic authentication credentials are only required
if your Graphite Web is protected by such a mechanism.
## Advanced
Open up the Icinga Web frontend and navigate to:
`Configuration > Modules > graphite > Advanced`
### UI
The settings *Default time range* and *Default time range unit* set the default
time range for displayed graphs both in the graphs lists and in monitored
objects' detail views.
If you'd like to suppress the *No graphs found* messages, check *Disable "no
graphs found"*. (This may cause unexpected blank pages in the graphs lists.)
### Icinga 2 (Core)
The settings *Host name template* and *Service name template* both are only
required if you are using a different naming schema than the default Icinga 2
is using. (As outlined [here](https://www.icinga.com/docs/icinga2/latest/doc/14-features/#current-graphite-schema))
The setting *Obscured check command custom variable* is only required if there
are wrapped check commands (see below) and the "actual" check command is stored
in another custom variable than `check_command`.
## Wrapped check commands
If a monitored object is checked remotely and not via an Icinga 2 agent, but
e.g. by check_by_ssh or check_nrpe, the monitored object's effective check
command becomes by_ssh or nrpe respectively. This breaks the respective
monitored objects' graphs as graph templates are applied to monitored objects
via their check commands. (They fall back to the default template.)
To make the respective graphs working as expected you have to tell the
monitored object's "actual" check command by setting its custom variable
`check_command`, e.g.:
```
apply Service "by_ssh-disk" {
import "generic-service"
check_command = "by_ssh"
vars.by_ssh_address = "192.0.2.1"
vars.by_ssh_command = "/usr/lib64/nagios/plugins/check_disk -w 20 -c 10"
vars.check_command = "disk" // <= HERE
assign where host.name == NodeName
}
```
## Further reading
* [Templates](04-Templates.md)
* [Troubleshooting](05-Troubleshooting.md)
icingaweb2-module-graphite-1.3.0/doc/04-Templates.md 0000664 0000000 0000000 00000017302 15163212266 0022066 0 ustar 00root root 0000000 0000000 # Templates
A template defines what kind of data a graph visualizes, which kind of graph to
use and its style. Essentially the Icinga Web Graphite Integration is using
templates to tell Graphite how to render which graphs.
* [Location](04-Templates.md#templates-location)
* [Structure](04-Templates.md#templates-structure)
* [graph](04-Templates.md#templates-structure-graph)
* [metric_filters](04-Templates.md#templates-structure-metric-filters)
* [urlparams](04-Templates.md#templates-structure-urlparams)
* [functions](04-Templates.md#templates-structure-functions)
* [Example](04-Templates.md#templates-example)
* [Default Template Settings](04-Templates.md#templates-default-settings)
## Template Location
There are a bunch of templates already included, located in
the installation path. (e.g. `/usr/share/icingaweb2/modules/graphite`)
To add additional/customized templates, place them in its configuration path.
(e.g. `/etc/icingaweb2/modules/graphite/templates`) These will either extend
the available templates or override some of them. Subfolders placed here will
also be included in the same way, while additionally extending or overriding
templates of its parent folders.
> **Note:**
>
> Hidden files and directories (with a leading dot) are ignored.
## Template Structure
Templates are organized within simple INI files. However, it is perfectly valid
to define multiple templates in a single file.
The name of a section consists of two parts separated by a dot:
[hostalive-rta.graph]
The first part is the name of the template and the second part the name of one
of the following configuration topics:
> **Note:**
>
> Template file will be ignored if the [graph] or [metric_filters] section is missing.
### Template Structure: graph
Supports a single option called `check_command` and should be set to the name
of a Icinga 2 [check-command](https://www.icinga.com/docs/icinga2/latest/doc/03-monitoring-basics/#check-commands).
To get multiple graphs for hosts and services with this check-command, multiple
templates can reference the same check-command.
If multiple check commands do effectively the same thing and yield the same
perfdata, all of them may be specified separated by comma. E.g.:
```ini
[ping-rta.graph]
check_command = "ping, ping4, ping6"
```
### Template Structure: metric_filters
Define what metric to use and how many curves to display in the resulting graph.
Each option's key represents the name of a curve. Its value the path to the
metric in Icinga 2's [graphite naming schema](https://www.icinga.com/docs/icinga2/latest/doc/14-features/#current-graphite-schema).
Curve names are used to map Graphite functions to metrics. (More on this below)
However, they are fully arbitrary and have no further meaning outside template
configurations.
A curve's metric path must begin with either the macro `$host_name_template$`
or `$service_name_template$` and is substituted with Icinga 2's prefix label.
The rest of the path is arbitrary, but to get meaningful results use a valid
path to one of the performance data metrics:
.perfdata..
An example path which points to the metric `value` of the `rta` perfdata-label
looks as follows:
$host_name_template$.perfdata.rta.value
To dynamically render a graph for each performance data label found, define a
macro in place for the actual perfdata-label:
$host_name_template$.perfdata.$perfdata_label$.value
You can also use wildcards. To define a wildcard, please use the following syntax:
$macro:wildcard syntax here$
Some Examples:
$perfdata_label:{abc,def}$
$perfdata_label:{a*c,de*}$
$perfdata_label:{a[vbn]c,def}$
> **Note:**
>
> The name of the macro for the perfdata-label is also arbitrary. You may as
> well use a more descriptive name such as `$disk$` for the disk check. `$disk$`
> is the same as `$disk:*$`.
### Template Structure: urlparams
Allows to define additional URL parameters to be passed to Graphite's render
API.
Each option represents a single parameter's name and value. A list of all
supported parameters can be found [here](https://graphite.readthedocs.io/en/latest/render_api.html#graph-parameters).
If you have used a macro for the curve's perfdata-label you may utilize it
here as well:
title = "Disk usage on $disk$"
You may also define URL parameters once for all templates
(including the shipped ones) in the `default_url_params` section in
`/etc/icingaweb2/modules/graphite/config.ini`:
[default_url_params]
yUnitSystem = "none"
These may be overridden in the template itself:
yUnitSystem = "binary"
### Template Structure: functions
Allows to define Graphite functions which are applied to the metric of a
specific curve on the graph.
Each option's key must match a curve's name in order to apply the function
to the curve's metric. A list of all supported functions can be found [here](https://graphite.readthedocs.io/en/latest/functions.html#functions).
The metric in question can be referenced in the function call using the macro
`$metric$` as shown in the following example:
alias(color(scale($metric$, 1000), '#1a7dd7'), 'Round trip time (ms)')
In addition you may utilize all other macros here as well:
alias(color(scale(divideSeries($metric$, $service_name_template$.perfdata.$disk$.max), 100), '#1a7dd7'), 'Used (%)')
## Template Example
The configuration examples used in this document are borrowed from the template
for the `hostalive` check-command:
```ini
[hostalive-rta.graph]
check_command = "hostalive"
[hostalive-rta.metrics_filters]
rta.value = "$host_name_template$.perfdata.rta.value"
[hostalive-rta.urlparams]
areaAlpha = "0.5"
areaMode = "all"
min = "0"
yUnitSystem = "none"
[hostalive-rta.functions]
rta.value = "alias(color(scale($metric$, 1000), '#1a7dd7'), 'Round trip time (ms)')"
[hostalive-pl.graph]
check_command = "hostalive"
[hostalive-pl.metrics_filters]
pl.value = "$host_name_template$.perfdata.pl.value"
[hostalive-pl.urlparams]
areaAlpha = "0.5"
areaMode = "all"
min = "0"
yUnitSystem = "none"
[hostalive-pl.functions]
pl.value = "alias(color($metric$, '#1a7dd7'), 'Packet loss (%)')"
```
## Default Template Settings
Next to maintaining templates for specific commands, you can
specify the default template settings in the [default.ini](https://github.com/Icinga/icingaweb2-module-graphite/blob/main/templates/default.ini)
configuration file.
The following example adjusts the background and foreground colors
to setup the "dark mode" for graphs.
First, copy the package provided configuration into the configuration
path. Then add the `bgcolor` and `fgcolor` settings into the [urlparams](04-Templates.md#templates-structure-urlparams)
sections for `default-host` and `default-service`.
```
cp /usr/share/icingaweb2/modules/graphite/templates/default.ini /etc/icingaweb2/modules/graphite/templates/default.ini
vim /etc/icingaweb2/modules/graphite/templates/default.ini
[default-host.urlparams]
bgcolor = "black"
fgcolor = "white"
[default-service.urlparams]
bgcolor = "black"
fgcolor = "white"
```
The settings make use the `urlparams` section which adds the
parameters to the render API.
> **Note**
>
> Instead of modifying the color settings in the default template,
> you can also change the Graphite configuration explained in
> [this community topic](https://community.icinga.com/t/how-to-adjust-the-graphite-background-color/3172/3).
## Further reading
* [Troubleshooting](05-Troubleshooting.md)
icingaweb2-module-graphite-1.3.0/doc/05-Troubleshooting.md 0000664 0000000 0000000 00000005637 15163212266 0023330 0 ustar 00root root 0000000 0000000 # Troubleshooting
## Graphs missing or not shown as expected
If too less or too many graphs are shown for a host/service or the graphs don't
look as expected, debugging becomes harder if there's no obvious error message
like "Could not resolve host: example.com".
In such cases the "graphs assembling debugger" may help:
1. Navigate to the respective host/service as usual
2. Add `&graph_debug=1` to the URL
3. Inspect the log displayed under "Graphs assembling process record"
### Example
Example debug log for the host "icinga.com":
```
+ Icinga check command: 'hostalive'
+ Obscured check command: NULL
+ Applying templates for check command 'hostalive'
++ Applying template 'hostalive-rta'
+++ Fetched 1 metric(s) from 'https://example.com/metrics/expand?query=icinga2.icinga_com.host.hostalive.perfdata.rta.value'
+++ Excluded 0 metric(s)
+++ Combined 1 metric(s) to 1 chart(s)
++ Applying template 'hostalive-pl'
+++ Fetched 1 metric(s) from 'https://example.com/metrics/expand?query=icinga2.icinga_com.host.hostalive.perfdata.pl.value'
+++ Excluded 0 metric(s)
+++ Combined 1 metric(s) to 1 chart(s)
+ Applying default templates, excluding previously used metrics
++ Applying template 'default-host'
+++ Fetched 2 metric(s) from 'https://example.com/metrics/expand?query=icinga2.icinga_com.host.hostalive.perfdata.%2A.value'
+++ Excluded 2 metric(s)
+++ Combined 0 metric(s) to 0 chart(s)
++ Not applying template 'default-service'
```
The log describes how the Icinga Web Graphite Integration assembled the
displayed graphs (or why no graphs could be assembled). The plus signs indent
the performed actions to visualize their hierarchy, e.g. all actions below
`Applying templates for check command 'hostalive'` indented with more than one
plus sign (until `Applying default templates, (...)`) are sub-actions of the
above one.
#### Details
At first the host's check command is being determined. Then all templates made
for that check command are applied. Finally, the default template is applied.
For each template the available Graphite metrics are fetched and combined to
graphs if possible. (See also [Templates](04-Templates.md).) The actual metrics
are not shown not to make the log too large. But they can be viewed at the shown
URLs.
Example result of the first URL:
```
{"results": ["icinga2.icinga_com.host.hostalive.perfdata.rta.value"]}
```
## Special chars in host or service name
Graphite cannot work with special characters. The host and service name should
therefore only contain Latin characters. If you want to use special characters
in host and service names, please set a `display_name` for the object.
### Example
```
object Host "Only latin chars here" {
display_name = "Special chars are welcome"
...
}
object Service "Only latin chars here" {
display_name = "Special chars are welcome"
...
}
apply Service "Only latin chars here" {
display_name = "Special chars are welcome"
...
}
```
icingaweb2-module-graphite-1.3.0/doc/06-Development.md 0000664 0000000 0000000 00000000753 15163212266 0022416 0 ustar 00root root 0000000 0000000 # Development
There is a CLI command for demonstrating
graph templates (useful for developing them):
```bash
icingacli graphite icinga2 config
```
It generates Icinga 2 config based on the present graph templates.
With this config Icinga will (also) "monitor" dummy services yielding random
perfdata as expected by the graph templates.
I. e.: If that Icinga is also writing to the Graphite that is read by your
Icinga Web Graphite Integration, you'll get dummy graphs for all templates.
icingaweb2-module-graphite-1.3.0/doc/img/ 0000775 0000000 0000000 00000000000 15163212266 0020136 5 ustar 00root root 0000000 0000000 icingaweb2-module-graphite-1.3.0/doc/img/service-detail-view.png 0000664 0000000 0000000 00000273423 15163212266 0024527 0 ustar 00root root 0000000 0000000 ‰PNG
IHDR À < ÿQ”í pHYs % %IR$ð IDATxÚìÝy|eþðïdr4iÓ¤7½him)´PÊÜ—€ŠÊ‚ ‚ø[]ÀU×]ÑÕUP<ðVPXWAE„U¹–[9ÊQÚB9J¡-½ïôHšû˜ßƒ1¦'ÐBS>ï×¾ö•Lž™Lf2±óáy¾C« 4M€C Ð<( -@€ Ð( -@€ ÐaÙYAJ":¥ªÏ1ZpV C¹óÊÓ¾oŠ‘K®ïÉ7EËÆ‰ €Žãáy:ÂwÕÄ^öô„ˆæÆ‡l‹ ÇP^ÕpáуåRœ è îäžI¾ò ¹[£/ÍŽ
8q:§§ãàž‰ƒÐ®˜Õ¿à tXw²J
êÅ €+¸“Ê ¾ÞÔx†²;W…s Ä®²âXNÃ…‡òT»«487 ÐAÜá åË¥ßf9.I)}ò`&N tÂ;¾ÿ»V5-6𽓹F³í\U=úž @Gs甿ö_w®ðµóÅ8 Ð1Ýá!<³‚•±¾ž+™ €«
”¥1ÅsÏôfBè®û×þá[/—å`>c èÀn5@™ä+_1::Hî$w{ihä¬`eë׬ì¤üüï @‡vËJ„ãÓ!7 üµøö¬ò=N td· ìÎU9>íâîÖÊ'ùÊû)ß9wsï)Î
Râü ÀmpËJ•æÛŒë%`ͯžé»eLLS'ùÊ#%×çýy2!䦻Ÿ¬•½`؆©}Ô†}Ú¿Î" ´«6˜Æxî±ì¹Ç²ùÇ“|å_ÞÛëôÔ>Ús/
)Î |(: Ú×þJrQõï!ßžº‰·ûl@øÜøþ±\"\Ô?,S¥Y[…s í¤§1Þ]¥¹gs
1täá¤I¾òYÁÊ#'½44Ò1=!¢!!ÞDôñ˜h{‡”Ö›Û;ÄiÉC18‘ Ð~m¾Å£eÀÖ³)%u?Më»á>Aò&«¢ôRîz Ïf(òíå' Ú ¶ûÎé<ޏ›Eûzl¼·W‹Í"%§ºù®uùᤆ¯^®¬Ç‰ €ö#l§í¾qO”TتŽ!ýƒ”Kcߺ\ê´|°\ÚÇ×}P°r`2Ú×£Tc<]R»>£¸Roúp\¬‡øúžgUÕ¿ž’‡ Çq\VV–Íf‹eäö[3¨[°\úCféwE587íѯ™±Zãüä
€öÖ.Ê`¹tt¸OëÛ?Ì(ƒåÒáÁŠÁÊAÊ ¹›=4ù¥¸Öq¾žÃßž|6!$D.=U\»ùZ%_öŽx,ÔkzL )¨^™Uæôê?bºõ6ÛlÿË©Z›‡2·×]»víøñãEEED$—Ë“’’ú÷ï/nÛìٳ端¾"¢™3gNŸ>ýn;þ+ûv½7Ò/ÀÃM@dá¸b~ãÅÒ†ßÞöóõÐÈy ¡D4"Ì;ó‡”Ôz#.Š›ø= ±‹õ‘ЉHá&zèÐe@hѬ`å=!^±~±>rÑrYUŸ©ªßrµòæfÄ ¸Û´K€22XyCí£}=¶Œ‰qM>KÉw
Må-‹Nçu„Ã7%ÊJw"’
Ù†7<LJðÕs=Å¢¶
PÂEì÷÷ör³+ëgýzÅU¾mæÍ7ßÌÎÎv\xøða¡PøÆoDEEÝžÝÐjµüƒªª»+ÕŠ—ŠwLëÛU!u\èï.îÛE1³g`¿é·g7ìÝÇÜ„¬—XH„ åf~O<…¬„½;*$¢ÎýÓ·.B,|XÔÔ˜.Ž×~¤·lrwÿ§û…½w"wù…b @³Ú%@ ö”Üð=•HøæñìôJký;˜ÉjãXm¶†¯š9®™WoŃá>ƒ•†ñ•Š]åXq÷î»ïÚÓ“ ¹\ž••ED‹åå—_^µj•¯¯ïmØ“{î¹çÊ•+6›í¾û®ö
SzÛÓ««7Yä¡€aˆ(1PqàÞÞc÷œ¿
»±;§rhˆ·L$H/¯;PůðÍýždèM'‹kû*ŒVîûKÅø§nÝ$_ù—÷ö
”K8¢ÿeW¦•Õ]ªªWH„=}å£Ã}zúy¼:¼ûøHßvž¯°Úp¸ š"ì û±î|Ñ÷ŵ8t^uý¶Ó`q™?vËËË333‰H$½ýöÛaaaDd4?ÿüócÇŽYÖŸþyþüù·aOºtéòòË/ßmß™iŠŸë³‰ÿ’_=jW…‹Ø-÷Å÷í¢ ¢¡¡^#•²_juí½'_Týí \Å·nÜžwÃOÜ"Vð鏨@¹¤DcxáàåïKþS{Jr&ï>¡‹ú‡
özopÄÜcÙ8b Mé(Ê5õ]ÔÃðî÷FúyþÖë¾PmX›VàTF÷ç±±÷tõ–‹…Dd¶qEjý‡§óVçV†‹Øí$„xHùŽÝ¼d…³™l¶‡wœëàµ$Ôj5ÿ`ñâÅ|zBD‰dáÂ…§N2›ÍƱ}vvö7ß|“——GD2™ìÞ{ï½ï¾ûì¥RŠ‹‹×['“É{ì±½{÷9r¤¦¦æå—_ÎÏÏ¿råŠÕj}úé§ÝÝÝùÆÇmذ¡¼¼ÜÓÓsþüùeee_}õ•X,ž={vHHˆ½MrròÖ[+**ˆÈÛÛ{Ò¤IcÇŽu,ηùá‡êêêˆÈÇÇgæÌ™t‰o]¨\"`ˆˆÊê|zBDyfë¼=Ïà!
Œ\ô{áçõ
þ¿¾]ýݯ÷S(©7~‘š¿"óú·ôÏÕzº±ìŽ«åÝ}d‰]”¡àªJk°X»x¸ fÅñ«^à‡‹Øõ“K„&‹íñ=ç+ôæÓúzˆ„•ûžð׍(þ;oã¸"ñÃÓ×>Í®pºv¦t÷w]o“U]¼?Ó1ôᇨÄxº±"2Ymç*Ô‹^Vv¤4Û¸—~½üU~uó¿5?‡'Ç÷RðëZ8.³ªþÙƒ—í‡÷‹Ýîh¶Ñ¿Ó骊YÁÎ+åa
)Òß=‘ã8òèÜC‰]<Ü8Žì»×‘Iæ>¡G‹j®T7ßò>?ùˆPï—ÓŒçß“wu‹ð’©ô¦ñ?¦]Ô›œ^5rÜéyuúÏ&Æ=r0_µ¾¥ nc€’\\û—¤h_o²Ü=ì.<Ü/ÎWî¸$\)}sttb çŒÃY×ã¤YÕ¿ª`L”·û§zvMÎýþJyw/wéo7ºQˆÂÍbã\¨–DrròàÁƒíÓ߸»»÷ÝwNm¶nݺaÃûSN÷í·ßîÝ»÷Ýwßõðð "V›žžNDÇ·73›Íjµ:99™ˆÆŒ“˜˜È/×h4»ví2›Í¡¡¡ÃhµÚsçÎѸqãø Åb±¼ùæ›çÏŸw|ǵk×nܸñÓO?åß‘ã¸7Þxƒ_ÑÞfåÊ•ƒ
zþùç]h6_™øÙîþöt#CozpKº§Hh¶ÙvTjìwæ“¢üþ¿xº½1*:ÎÏcö‘«DîéÖÅC"`˜ÇBìmÜ„Ãð™ËÄ?û[ÌŠòïæ%0ŒÞlµØ¸QÁÊO7ÃD8|ɳéßÃÇý÷ï<1Ý”ÒOÆ÷êe¿.œ®–˜^~òÿ=’ôÊ/WøûöažÒý3û» ϼ¤vP°×/3û?¸%½ó
ŠTJ½e"ލ›§Œ¨ºù_¡77óÓÑÏCràá$¥›Èñðö ð<0³ÿkG®¾q©„ˆ¼¥b/©˜ˆ^ö{¹" +°þ¤ßß=À <¨èå/0Œãj
ÖŽŸžü46vrwÿ?›,í8¿¥¼®©–Óëïëí!ÆúzLÛ©ãg(C<¥sãCˆè•_®4LOìVåVŽÌ,ø÷A‘ëÏào# €FµËÔ'ß×–jnàf~ëå2W?Žžá£!^Ïv÷·ÿoZ€ÂCäœOíg¿,«7¦”Ôê-VþffZL—§#üˆè“¤0>=±Ú¸3¥µ»³+Tz fN|°Úb½¬ª¯Ð^ÿ;˜#ºV«ÏÑÕ˜:zù?™LÆ?8yòä¼yóöïß_VVÆq±¿á\¼x‘OO†™6mÚ‚‚ƒƒ‰¨¢¢bÕªU׿¸œµ'111!!ÁÛÛ{ôèÑüÇ`åÒ¥Kf³™ˆ¦N*ÎøóÝwßñé ÿ޳gωD|ò²eË¾ÍÆùôD(Î;×ÞæäÉ“ìø_ÑRíõ{=¡€y\ÏË'5-@ADªµ[ÊëìéÉK±#}íßÒô²:þÈÍŒz$HAD:ón‰ëM–*¹LkÜ“SÉ/‰ð´¿:.‡ïõp¶\ýKξ®ýÞs×ø8{z¿#]Ñä(ÿ‘Jmk¿v²«uö6Vð÷Á‘üò·Gtçã½Ùz(OõK~•Ùj#"±ðÅï÷Ä^ÅÂq-~üæ:6MI°§'YUõåjm
˜wëç!!"ó+³¨ô¦Zƒ¹´Þ¸/÷úIósÿ- ™Ó…?éÙÕºfòˆbÃÈèÉÝýùcµþ¾ÞüuÑLzBD“¢ü6ŒŒîøß¥rÃ\ª¬ÿüZ5³?J+ ¢h÷`!‹¿ Õ^CxÖg½84²5-ëM–ÏÏ»úqâ5(Ä«ù6ñRñð®×Û$UýùzŠ“à!0ÌSýº®Î”K®Ÿ”ƒ×ª&ì½È¯¸gz¢»ˆÑ›óÌÖÄ-é#•²ƒê/`˜k5ºÈ§]â…„„Lš4i÷îÝD¤Õj׬Yɉ‰&L°÷á8nýúõ|ªòÁðÑÉĉW¬X‘žžž––VYYéç÷{ç¹\¾råJ{õYŽã"""rssOž<ùç?ÿY$qwìØ1>ø°¿‹#µZ½wï^§wœ4iÒ“O>©Õj÷îÝ;kÖ,½^Ïﹿ¿ÿ'Ÿ|"
‰hìØ±‹-Òjµ[·n5j”= ê˜6—Ö=U¨æËG!ѾѾŇԛ,™ªú¿¾b©1·w0ëkÿ–Ñ…éýâüå†Y˜ÐuSÉjÍîήœ¼ï¢ýKþxB°‡Xèï.~$H±©¤.\ÄÆùÉù;öW*š¿.ލFì<Ï_gè#KEìCݪ/éæÃodmjþŸO]ãWüef’—Tì#LìúBZTÈòm–Îú8»‚ˆ–ƾ0¸›€aŒ.U³5¿'
5ÿñ›ùéx)6°›—Ôéð.Žòÿ`l¬PÀxJDÏ÷sœ²ÇjãÞ;™ûâÙÂßOzŸP±Pá&šáÇwBéx}.¶}¹.0×U ü÷’º|†B
ú¡8¦'
×ê°zúzÑ¥*M‹-×éUz“T<ÔÏcsi ÀmP–ž+áÓ?°åùŒW˹KÆïŒ
VÊD,éÍÖ—\/Ô—g¶®Ï(zaHCä%‘Ã$¯cºù’ðkêËÌÒàoO:nªûos©°®3r„ˆæÏŸùÅ_ðýAø¼#55555566öŸÿü§D"Ñét%%%DäííÍWqssKHHHOO·Z………ŽÊÂ…çîafܸqkÖ¬1999111F£‘ï9/—ËîUii)¿?>ø Ÿž‘D"Y¶lYII‰T*eY¶¤¤Ä`0Ñ!C¬V+?²››[xxøÅ‹«ªªÔjµ——WGÿax÷±"Vàx»Ø?PyðOýצqó—ýabµcEÕöô„?é+5ƒ½36ÜgeVÙ0Oiˆ\ÂÿÖýtµ¼ãº9ÿ»´õ¾x{·©†JÃô$£\=ç—:þGã/ØœêæŠC?ÞÕ{R¤qCD/ê6£V¥Z÷ò¹Bü‘ p;"°õì¶1±D4Óæ¥CYãö¦ N¥Z+tb±Ù+ÿà¶\(7:–½