diff options
Diffstat (limited to 'library/Graphite/Graphing')
-rw-r--r-- | library/Graphite/Graphing/Chart.php | 385 | ||||
-rw-r--r-- | library/Graphite/Graphing/GraphingTrait.php | 79 | ||||
-rw-r--r-- | library/Graphite/Graphing/GraphiteWebClient.php | 198 | ||||
-rw-r--r-- | library/Graphite/Graphing/MetricsDataSource.php | 48 | ||||
-rw-r--r-- | library/Graphite/Graphing/MetricsQuery.php | 219 | ||||
-rw-r--r-- | library/Graphite/Graphing/Template.php | 364 | ||||
-rw-r--r-- | library/Graphite/Graphing/Templates.php | 321 |
7 files changed, 1614 insertions, 0 deletions
diff --git a/library/Graphite/Graphing/Chart.php b/library/Graphite/Graphing/Chart.php new file mode 100644 index 0000000..ded8ae8 --- /dev/null +++ b/library/Graphite/Graphing/Chart.php @@ -0,0 +1,385 @@ +<?php + +namespace Icinga\Module\Graphite\Graphing; + +use Icinga\Module\Graphite\Util\MacroTemplate; +use Icinga\Module\Graphite\Web\Widget\GraphImage; +use Icinga\Web\Response; + +class Chart +{ + /** + * Used to render the chart + * + * @var GraphiteWebClient + */ + protected $graphiteWebClient; + + /** @var ?string This chart's background color */ + protected $bgcolor; + + /** @var ?string This chart's foreground color */ + protected $fgcolor; + + /** @var ?string This chart's major grid line color */ + protected $majorGridLineColor; + + /** @var ?string This chart's minor grid line color */ + protected $minorGridLineColor; + + /** + * This chart's base + * + * @var Template + */ + protected $template; + + /** + * Target metrics by curve name + * + * @var string[] + */ + protected $metrics; + + /** + * The chart's begin + * + * @var string + */ + protected $from = '-14400'; + + /** + * The chart's end + * + * @var string + */ + protected $until; + + /** + * The chart's width + * + * @var int + */ + protected $width = 350; + + /** + * The chart's height + * + * @var int + */ + protected $height = 200; + + /** + * Whether to show the chart's legend + * + * @var bool + */ + protected $showLegend = true; + + /** + * Constructor + * + * @param GraphiteWebClient $graphiteWebClient Used to render the chart + * @param Template $template This chart's base + * @param string[] $metrics Target metrics by curve name + */ + public function __construct(GraphiteWebClient $graphiteWebClient, Template $template, array $metrics) + { + $this->graphiteWebClient = $graphiteWebClient; + $this->template = $template; + $this->metrics = $metrics; + } + + /** + * Let Graphite Web render this chart and serve the result immediately to the user agent (via the given response) + * + * Does not return. + * + * @param Response $response + */ + public function serveImage(Response $response) + { + $image = new GraphImage($this); + + // Errors should occur now or not at all + $image->render(); + + $response + ->setHeader('Content-Type', 'image/png', true) + ->setHeader('Content-Disposition', 'inline; filename="graph.png"', true) + ->setHeader('Cache-Control', null, true) + ->setHeader('Expires', null, true) + ->setHeader('Pragma', null, true) + ->setBody($image) + ->sendResponse(); + + exit; + } + + /** + * Extract the values of the template's metrics filters' variables from the target metrics + * + * @return string[] + */ + public function getMetricVariables() + { + /** @var MacroTemplate[][] $curves */ + $curves = $this->template->getFullCurves(); + $variables = []; + + foreach ($this->metrics as $curveName => $metric) { + $vars = $curves[$curveName][0]->reverseResolve($metric); + if ($vars !== false) { + $variables = array_merge($variables, $vars); + } + } + + return $variables; + } + + /** + * Get Graphite Web client + * + * @return GraphiteWebClient + */ + public function getGraphiteWebClient() + { + return $this->graphiteWebClient; + } + + /** + * Get this chart's background color + * + * @return string|null + */ + public function getBackgroundColor(): ?string + { + return $this->bgcolor; + } + + /** + * Set this chart's background color + * + * @param string|null $color + * + * @return $this + */ + public function setBackgroundColor(?string $color): self + { + $this->bgcolor = $color; + + return $this; + } + + /** + * Get this chart's foreground color + * + * @return string|null + */ + public function getForegroundColor(): ?string + { + return $this->fgcolor; + } + + /** + * Set this chart's foreground color + * + * @param string|null $color + * + * @return $this + */ + public function setForegroundColor(?string $color): self + { + $this->fgcolor = $color; + + return $this; + } + + /** + * Get this graph's major grid line color + * + * @return string|null + */ + public function getMajorGridLineColor(): ?string + { + return $this->majorGridLineColor; + } + + /** + * Set this graph's major grid line color + * + * @param string|null $color + * + * @return $this + */ + public function setMajorGridLineColor(?string $color): self + { + $this->majorGridLineColor = $color; + + return $this; + } + + /** + * Get this graph's minor grid line color + * + * @return string|null + */ + public function getMinorGridLineColor(): ?string + { + return $this->minorGridLineColor; + } + + /** + * Set this graph's minor grid line color + * + * @param string|null $color + * + * @return $this + */ + public function setMinorGridLineColor(?string $color): self + { + $this->minorGridLineColor = $color; + + return $this; + } + + /** + * Get template + * + * @return Template + */ + public function getTemplate() + { + return $this->template; + } + + /** + * Get metrics + * + * @return string[] + */ + public function getMetrics() + { + return $this->metrics; + } + + /** + * Get begin + * + * @return string + */ + public function getFrom() + { + return $this->from; + } + + /** + * Set begin + * + * @param string $from + * + * @return $this + */ + public function setFrom($from) + { + $this->from = $from; + + return $this; + } + + /** + * Get end + * + * @return string + */ + public function getUntil() + { + return $this->until; + } + + /** + * Set end + * + * @param string $until + * + * @return $this + */ + public function setUntil($until) + { + $this->until = $until; + + return $this; + } + + /** + * Get width + * + * @return int + */ + public function getWidth() + { + return $this->width; + } + + /** + * Set width + * + * @param int $width + * + * @return $this + */ + public function setWidth($width) + { + $this->width = $width; + + return $this; + } + + /** + * Get height + * + * @return int + */ + public function getHeight() + { + return $this->height; + } + + /** + * Set height + * + * @param int $height + * + * @return $this + */ + public function setHeight($height) + { + $this->height = $height; + + return $this; + } + + /** + * Get whether to show the chart's legend + * + * @return bool + */ + public function getShowLegend() + { + return $this->showLegend; + } + + /** + * Set whether to show the chart's legend + * + * @param bool $showLegend + * + * @return $this + */ + public function setShowLegend($showLegend) + { + $this->showLegend = $showLegend; + + return $this; + } +} diff --git a/library/Graphite/Graphing/GraphingTrait.php b/library/Graphite/Graphing/GraphingTrait.php new file mode 100644 index 0000000..e32a52a --- /dev/null +++ b/library/Graphite/Graphing/GraphingTrait.php @@ -0,0 +1,79 @@ +<?php + +namespace Icinga\Module\Graphite\Graphing; + +use Icinga\Application\Config; +use Icinga\Application\Icinga; +use Icinga\Exception\ConfigurationError; +use Icinga\Module\Graphite\Web\FakeSchemeRequest; +use Icinga\Web\Url; + +trait GraphingTrait +{ + /** + * All loaded templates + * + * @var Templates + */ + protected static $allTemplates; + + /** + * Metrics data source + * + * @var MetricsDataSource + */ + protected static $metricsDataSource; + + /** + * Load and get all templates + * + * @return Templates + */ + protected static function getAllTemplates() + { + if (static::$allTemplates === null) { + $allTemplates = (new Templates())->loadDir( + Icinga::app() + ->getModuleManager() + ->getModule('graphite') + ->getBaseDir() . DIRECTORY_SEPARATOR . 'templates' + ); + + $path = Config::resolvePath('modules/graphite/templates'); + if (file_exists($path)) { + $allTemplates->loadDir($path); + } + + static::$allTemplates = $allTemplates; + } + + return static::$allTemplates; + } + + /** + * Get metrics data source + * + * @return MetricsDataSource + * + * @throws ConfigurationError + */ + public static function getMetricsDataSource() + { + if (static::$metricsDataSource === null) { + $config = Config::module('graphite'); + $graphite = $config->getSection('graphite'); + if (! isset($graphite->url)) { + throw new ConfigurationError('Missing "graphite.url" in "%s"', $config->getConfigFile()); + } + + static::$metricsDataSource = new MetricsDataSource( + (new GraphiteWebClient(Url::fromPath($graphite->url, [], new FakeSchemeRequest()))) + ->setUser($graphite->user) + ->setPassword($graphite->password) + ->setInsecure($graphite->insecure) + ); + } + + return static::$metricsDataSource; + } +} diff --git a/library/Graphite/Graphing/GraphiteWebClient.php b/library/Graphite/Graphing/GraphiteWebClient.php new file mode 100644 index 0000000..b06b6ce --- /dev/null +++ b/library/Graphite/Graphing/GraphiteWebClient.php @@ -0,0 +1,198 @@ +<?php + +namespace Icinga\Module\Graphite\Graphing; + +use Icinga\Web\Url; +use iplx\Http\Client; +use iplx\Http\ClientInterface; +use iplx\Http\Request; + +/** + * HTTP interface to Graphite Web + */ +class GraphiteWebClient +{ + /** + * Base URL of every Graphite Web HTTP request + * + * @var Url + */ + protected $baseUrl; + + /** + * HTTP basic auth user for every Graphite Web HTTP request + * + * @var string|null + */ + protected $user; + + /** + * The above user's password + * + * @var string|null + */ + protected $password; + + /** + * Don't verify the remote's TLS certificate + * + * @var bool + */ + protected $insecure = false; + + /** + * HTTP client + * + * @var ClientInterface + */ + protected $httpClient; + + /** + * Constructor + * + * @param Url $baseUrl Base URL of every Graphite Web HTTP request + */ + public function __construct(Url $baseUrl) + { + $this->httpClient = new Client(); + + $this->setBaseUrl($baseUrl); + } + + /** + * Send an HTTP request to the configured Graphite Web and return the response's body + * + * @param Url $url + * @param string $method + * @param string[] $headers + * @param string $body + * + * @return string + */ + public function request(Url $url, $method = 'GET', array $headers = [], $body = null) + { + $headers['User-Agent'] = 'icingaweb2-module-graphite'; + if ($this->user !== null) { + $headers['Authorization'] = 'Basic ' . base64_encode("{$this->user}:{$this->password}"); + } + + // TODO(ak): keep connections alive (TCP handshakes are a bit expensive and TLS handshakes are very expensive) + return (string) $this->httpClient->send( + new Request($method, $this->completeUrl($url)->getAbsoluteUrl(), $headers, $body), + ['curl' => [ + CURLOPT_SSL_VERIFYPEER => ! $this->insecure + ]] + )->getBody(); + } + + /** + * Complete the given relative URL according to the base URL + * + * @param Url $url + * + * @return Url + */ + public function completeUrl(Url $url) + { + $completeUrl = clone $this->baseUrl; + return $completeUrl + ->setPath(ltrim(rtrim($completeUrl->getPath(), '/') . '/' . ltrim($url->getPath(), '/'), '/')) + ->setParams($url->getParams()); + } + + /** + * Get the base URL of every Graphite Web HTTP request + * + * @return Url + */ + public function getBaseUrl() + { + return $this->baseUrl; + } + + /** + * Set the base URL of every Graphite Web HTTP request + * + * @param Url $baseUrl + * + * @return $this + */ + public function setBaseUrl(Url $baseUrl) + { + $this->baseUrl = $baseUrl; + + return $this; + } + + /** + * Get the HTTP basic auth user + * + * @return null|string + */ + public function getUser() + { + return $this->user; + } + + /** + * Set the HTTP basic auth user + * + * @param null|string $user + * + * @return $this + */ + public function setUser($user) + { + $this->user = $user; + + return $this; + } + + /** + * Get the HTTP basic auth password + * + * @return null|string + */ + public function getPassword() + { + return $this->password; + } + + /** + * Set the HTTP basic auth password + * + * @param null|string $password + * + * @return $this + */ + public function setPassword($password) + { + $this->password = $password; + + return $this; + } + + /** + * Get whether not to verify the remote's TLS certificate + * + * @return bool + */ + public function getInsecure() + { + return $this->insecure; + } + + /** + * Set whether not to verify the remote's TLS certificate + * + * @param bool $insecure + * + * @return $this + */ + public function setInsecure($insecure = true) + { + $this->insecure = $insecure; + + return $this; + } +} diff --git a/library/Graphite/Graphing/MetricsDataSource.php b/library/Graphite/Graphing/MetricsDataSource.php new file mode 100644 index 0000000..19787da --- /dev/null +++ b/library/Graphite/Graphing/MetricsDataSource.php @@ -0,0 +1,48 @@ +<?php + +namespace Icinga\Module\Graphite\Graphing; + +use Icinga\Data\Selectable; + +/** + * Provides an interface to Graphite Web's metrics list + */ +class MetricsDataSource implements Selectable +{ + /** + * HTTP interface to Graphite Web + * + * @var GraphiteWebClient + */ + private $client; + + /** + * Constructor + * + * @param GraphiteWebClient $client HTTP interface to Graphite Web + */ + public function __construct(GraphiteWebClient $client) + { + $this->client = $client; + } + + /** + * Initiate a new query + * + * @return MetricsQuery + */ + public function select() + { + return new MetricsQuery($this); + } + + /** + * Get the client passed to the constructor + * + * @return GraphiteWebClient + */ + public function getClient() + { + return $this->client; + } +} diff --git a/library/Graphite/Graphing/MetricsQuery.php b/library/Graphite/Graphing/MetricsQuery.php new file mode 100644 index 0000000..da05c17 --- /dev/null +++ b/library/Graphite/Graphing/MetricsQuery.php @@ -0,0 +1,219 @@ +<?php + +namespace Icinga\Module\Graphite\Graphing; + +use Icinga\Data\Fetchable; +use Icinga\Data\Filter\Filter; +use Icinga\Data\Filterable; +use Icinga\Data\Queryable; +use Icinga\Exception\NotImplementedError; +use Icinga\Module\Graphite\GraphiteUtil; +use Icinga\Module\Graphite\Util\IcingadbUtils; +use Icinga\Module\Graphite\Util\MacroTemplate; +use Icinga\Module\Graphite\Util\InternalProcessTracker as IPT; +use Icinga\Module\Icingadb\Compat\UrlMigrator; +use Icinga\Module\Icingadb\Model\Host; +use Icinga\Module\Monitoring\Object\Macro; +use Icinga\Module\Monitoring\Object\MonitoredObject; +use Icinga\Util\Json; +use Icinga\Web\Url; +use InvalidArgumentException; +use ipl\Orm\Model; +use ipl\Stdlib\Filter as IplFilter; + +/** + * Queries a {@link MetricsDataSource} + */ +class MetricsQuery implements Queryable, Filterable, Fetchable +{ + /** + * @var MetricsDataSource + */ + protected $dataSource; + + /** + * The base metrics pattern + * + * @var MacroTemplate + */ + protected $base; + + /** + * Extension of {@link base} + * + * @var string[] + */ + protected $filter = []; + + /** + * The object to render the graphs for + * + * @var MonitoredObject|Model + */ + protected $object; + + /** + * Constructor + * + * @param MetricsDataSource $dataSource + */ + public function __construct(MetricsDataSource $dataSource) + { + $this->dataSource = $dataSource; + } + + public function from($target, array $fields = null) + { + if ($fields !== null) { + throw new InvalidArgumentException('Fields are not applicable to this kind of query'); + } + + try { + $this->base = $target instanceof MacroTemplate ? $target : new MacroTemplate((string) $target); + } catch (InvalidArgumentException $e) { + throw new InvalidArgumentException('Bad target', $e); + } + + return $this; + } + + public function applyFilter(Filter $filter) + { + throw new NotImplementedError(__METHOD__); + } + + public function setFilter(Filter $filter) + { + throw new NotImplementedError(__METHOD__); + } + + public function getFilter() + { + throw new NotImplementedError(__METHOD__); + } + + public function addFilter(Filter $filter) + { + throw new NotImplementedError(__METHOD__); + } + + public function where($condition, $value = null) + { + $this->filter[$condition] = $this->escapeMetricStep($value); + + return $this; + } + + public function fetchAll() + { + $result = []; + foreach ($this->fetchColumn() as $metric) { + $result[] = (object) ['metric' => $metric]; + } + + return $result; + } + + public function fetchRow() + { + $result = $this->fetchColumn(); + return empty($result) ? false : (object) ['metric' => $result[0]]; + } + + public function fetchColumn() + { + $filter = []; + foreach ($this->base->getMacros() as $macro) { + if (isset($this->filter[$macro])) { + $filter[$macro] = $this->filter[$macro]; + continue; + } + + if (strpos($macro, '.') === false) { + continue; + } + + $workaroundMacro = str_replace('.', '_', $macro); + if ($this->object instanceof Model) { + // icingadb macro + $tranformFilter = UrlMigrator::transformFilter( + IplFilter::equal($workaroundMacro, ''), + $this->object instanceof Host ? 'hosts' : 'services' + ); + + if ($tranformFilter === false) { + continue; + } + + $migratedMacro = $tranformFilter->getColumn(); + + if ($migratedMacro === $workaroundMacro) { + $workaroundMacro = $macro; + } else { + $workaroundMacro = $migratedMacro; + } + + $icingadbMacros = IcingadbUtils::getInstance(); + $result = $icingadbMacros->resolveMacro($workaroundMacro, $this->object); + } else { + if ($workaroundMacro === 'service_name') { + $workaroundMacro = 'service_description'; + } + + $result = Macro::resolveMacro($workaroundMacro, $this->object); + } + + if ($result !== $workaroundMacro) { + $filter[$macro] = $this->escapeMetricStep($result); + } + } + + $client = $this->dataSource->getClient(); + $url = Url::fromPath('metrics/expand', [ + 'query' => $this->base->resolve($filter, '*') + ]); + $res = Json::decode($client->request($url)); + natsort($res->results); + + IPT::recordf('Fetched %s metric(s) from %s', count($res->results), (string) $client->completeUrl($url)); + + return array_values($res->results); + } + + public function fetchOne() + { + $result = $this->fetchColumn(); + return empty($result) ? false : $result[0]; + } + + public function fetchPairs() + { + throw new NotImplementedError(__METHOD__); + } + + /** + * Set the object to render the graphs for + * + * @param MonitoredObject|Model $object + * + * @return $this + */ + public function setObject($object) + { + $this->object = $object; + + return $this; + } + + /** + * Escapes a string for usage in a Graphite metric path between two dots + * + * @param string $step + * + * @return string + */ + protected function escapeMetricStep($step) + { + return preg_replace('/[^a-zA-Z0-9\*\-:^[\]$#%\']/', '_', $step); + } +} diff --git a/library/Graphite/Graphing/Template.php b/library/Graphite/Graphing/Template.php new file mode 100644 index 0000000..a030fb7 --- /dev/null +++ b/library/Graphite/Graphing/Template.php @@ -0,0 +1,364 @@ +<?php + +namespace Icinga\Module\Graphite\Graphing; + +use Icinga\Application\Config; +use Icinga\Exception\ConfigurationError; +use Icinga\Module\Graphite\Util\MacroTemplate; +use Icinga\Module\Graphite\Util\InternalProcessTracker as IPT; +use Icinga\Module\Monitoring\Object\MonitoredObject; +use InvalidArgumentException; +use ipl\Orm\Model; + +class Template +{ + /** + * The configured icinga.graphite_writer_host_name_template + * + * @var MacroTemplate + */ + protected static $hostNameTemplate; + + /** + * The configured icinga.graphite_writer_service_name_template + * + * @var MacroTemplate + */ + protected static $serviceNameTemplate; + + /** + * All curves to show in a chart by name with Graphite Web metric filters and Graphite functions + * + * [$curve => [$metricFilter, $function], ...] + * + * @var MacroTemplate[][] + */ + protected $curves = []; + + /** + * All curves to show in a chart by name with full Graphite Web metric filters and Graphite functions + * + * [$curve => [$metricFilter, $function], ...] + * + * @var MacroTemplate[][] + */ + protected $fullCurves; + + /** + * Additional URL parameters for rendering via Graphite Web + * + * [$key => $value, ...] + * + * @var MacroTemplate[] + */ + protected $urlParams = []; + + /** + * Constructor + */ + public function __construct() + { + } + + /** + * Get all charts based on this template and applicable to the metrics + * from the given data source restricted by the given filter + * + * @param MetricsDataSource $dataSource + * @param MonitoredObject|Model $object The object to render the graphs for + * @param string[] $filter + * @param MacroTemplate[] $excludeMetrics + * + * @return Chart[] + */ + public function getCharts( + MetricsDataSource $dataSource, + $object, + array $filter, + array &$excludeMetrics = [] + ) { + $metrics = []; + $metricsUsed = 0; + $metricsExcluded = 0; + + foreach ($this->getFullCurves() as $curveName => $curve) { + $fullMetricTemplate = $curve[0]; + + $query = $dataSource->select()->setObject($object)->from($fullMetricTemplate); + + foreach ($filter as $key => $value) { + $query->where($key, $value); + } + + foreach ($query->fetchColumn() as $metric) { + foreach ($excludeMetrics as $excludeMetric) { + if ($excludeMetric->reverseResolve($metric) !== false) { + ++$metricsExcluded; + continue 2; + } + } + + $vars = $curve[0]->reverseResolve($metric); + if ($vars !== false) { + $metrics[$curveName][$metric] = $vars; + ++$metricsUsed; + } + } + } + + switch (count($metrics)) { + case 0: + $metricsCombinations = []; + break; + + case 1: + $metricsCombinations = []; + + foreach ($metrics as $curveName => & $curveMetrics) { + foreach ($curveMetrics as $metric => & $_) { + $metricsCombinations[] = [$curveName => $metric]; + } + unset($_); + } + unset($curveMetrics); + + break; + + default: + $possibleCombinations = []; + $handledCurves = []; + foreach ($metrics as $curveName1 => & $metrics1) { + $handledCurves[$curveName1] = true; + + foreach ($metrics as $curveName2 => & $metrics2) { + if (! isset($handledCurves[$curveName2])) { + foreach ($metrics1 as $metric1 => & $vars1) { + foreach ($metrics2 as $metric2 => & $vars2) { + if ( + count(array_intersect_assoc($vars1, $vars2)) + === count(array_intersect_key($vars1, $vars2)) + ) { + $possibleCombinations[$curveName1][$curveName2][$metric1][$metric2] = true; + } + } + unset($vars2); + } + unset($vars1); + } + } + unset($metrics2); + } + unset($metrics1); + + $metricsCombinations = []; + $this->combineMetrics($metrics, $possibleCombinations, $metricsCombinations); + } + + $charts = []; + foreach ($metricsCombinations as $metricsCombination) { + $charts[] = new Chart($dataSource->getClient(), $this, $metricsCombination); + } + + IPT::recordf('Excluded %s metric(s)', $metricsExcluded); + IPT::recordf('Combined %s metric(s) to %s chart(s)', $metricsUsed, count($charts)); + + return $charts; + } + + /** + * Fill the given metrics combinations from the given metrics as restricted by the given possible combinations + * + * @param string[][][] $metrics + * @param bool[][][][] $possibleCombinations + * @param string[][] $metricsCombinations + * @param string[] $currentCombination + */ + protected function combineMetrics( + array &$metrics, + array &$possibleCombinations, + array &$metricsCombinations, + array $currentCombination = [] + ) { + if (empty($currentCombination)) { + foreach ($metrics as $curveName => & $curveMetrics) { + foreach ($curveMetrics as $metric => & $_) { + $this->combineMetrics( + $metrics, + $possibleCombinations, + $metricsCombinations, + [$curveName => $metric] + ); + } + unset($_); + + break; + } + unset($curveMetrics); + } elseif (count($currentCombination) === count($metrics)) { + $metricsCombinations[] = $currentCombination; + } else { + foreach ($metrics as $nextCurveName => & $_) { + if (! isset($currentCombination[$nextCurveName])) { + break; + } + } + unset($_); + + $allowedNextCurveMetricsPerCurrentCurveName = []; + foreach ($currentCombination as $currentCurveName => $currentCurveMetric) { + $allowedNextCurveMetricsPerCurrentCurveName[$currentCurveName] + = $possibleCombinations[$currentCurveName][$nextCurveName][$currentCurveMetric]; + } + + $allowedNextCurveMetrics = $allowedNextCurveMetricsPerCurrentCurveName[$currentCurveName]; + unset($allowedNextCurveMetricsPerCurrentCurveName[$currentCurveName]); + + foreach ($allowedNextCurveMetricsPerCurrentCurveName as & $allowedMetrics) { + $allowedNextCurveMetrics = array_intersect_key($allowedNextCurveMetrics, $allowedMetrics); + } + unset($allowedMetrics); + + foreach ($allowedNextCurveMetrics as $allowedNextCurveMetric => $_) { + $nextCombination = $currentCombination; + $nextCombination[$nextCurveName] = $allowedNextCurveMetric; + + $this->combineMetrics($metrics, $possibleCombinations, $metricsCombinations, $nextCombination); + } + } + } + + /** + * Get curves to show in a chart by name with Graphite Web metric filters and Graphite functions + * + * @return MacroTemplate[][] + */ + public function getCurves() + { + return $this->curves; + } + + /** + * Get curves to show in a chart by name with full Graphite Web metric filters and Graphite functions + * + * @return MacroTemplate[][] + */ + public function getFullCurves() + { + if ($this->fullCurves === null) { + $curves = $this->curves; + + foreach ($curves as &$curve) { + $curve[0] = new MacroTemplate($curve[0]->resolve([ + 'host_name_template' => static::getHostNameTemplate(), + 'service_name_template' => static::getServiceNameTemplate(), + '' => '$$' + ])); + } + unset($curve); + + $this->fullCurves = $curves; + } + + return $this->fullCurves; + } + + /** + * Set curves to show in a chart by name with Graphite Web metric filters and Graphite functions + * + * @param MacroTemplate[][] $curves + * + * @return $this + */ + public function setCurves(array $curves) + { + $this->curves = $curves; + + return $this; + } + + /** + * Get additional URL parameters for Graphite Web + * + * @return MacroTemplate[] + */ + public function getUrlParams() + { + return $this->urlParams; + } + + /** + * Set additional URL parameters for Graphite Web + * + * @param MacroTemplate[] $urlParams + * + * @return $this + */ + public function setUrlParams(array $urlParams) + { + $this->urlParams = $urlParams; + + return $this; + } + + /** + * Get {@link hostNameTemplate} + * + * @return MacroTemplate + * + * @throws ConfigurationError If the configuration is invalid + */ + protected static function getHostNameTemplate() + { + if (static::$hostNameTemplate === null) { + $config = Config::module('graphite'); + $template = $config->get( + 'icinga', + 'graphite_writer_host_name_template', + 'icinga2.$host.name$.host.$host.check_command$' + ); + + try { + static::$hostNameTemplate = new MacroTemplate($template); + } catch (InvalidArgumentException $e) { + throw new ConfigurationError( + 'Bad icinga.graphite_writer_host_name_template in "%s": %s', + $config->getConfigFile(), + $e->getMessage() + ); + } + } + + return static::$hostNameTemplate; + } + + /** + * Get {@link serviceNameTemplate} + * + * @return MacroTemplate + * + * @throws ConfigurationError If the configuration is invalid + */ + protected static function getServiceNameTemplate() + { + if (static::$serviceNameTemplate === null) { + $config = Config::module('graphite'); + $template = $config->get( + 'icinga', + 'graphite_writer_service_name_template', + 'icinga2.$host.name$.services.$service.name$.$service.check_command$' + ); + + try { + static::$serviceNameTemplate = new MacroTemplate($template); + } catch (InvalidArgumentException $e) { + throw new ConfigurationError( + 'Bad icinga.graphite_writer_service_name_template in "%s": %s', + $config->getConfigFile(), + $e->getMessage() + ); + } + } + + return static::$serviceNameTemplate; + } +} diff --git a/library/Graphite/Graphing/Templates.php b/library/Graphite/Graphing/Templates.php new file mode 100644 index 0000000..0765e46 --- /dev/null +++ b/library/Graphite/Graphing/Templates.php @@ -0,0 +1,321 @@ +<?php + +namespace Icinga\Module\Graphite\Graphing; + +use Icinga\Application\Config; +use Icinga\Data\ConfigObject; +use Icinga\Exception\ConfigurationError; +use Icinga\Module\Graphite\Util\MacroTemplate; +use Icinga\Web\UrlParams; +use InvalidArgumentException; +use RecursiveDirectoryIterator; +use RecursiveIteratorIterator; +use SplFileInfo; + +/** + * Templates collection + */ +class Templates +{ + /** + * All templates by their check command and name + * + * @var Template[][] + */ + protected $templates = []; + + /** + * All default templates by their name + * + * @var Template[] + */ + protected $defaultTemplates = []; + + /** + * Default URL params for all templates + * + * @var string[] + */ + protected $defaultUrlParams = []; + + /** + * Constructor + */ + public function __construct() + { + $config = Config::module('graphite'); + + foreach ($config->getSection('default_url_params') as $param => $value) { + try { + $this->defaultUrlParams[$param] = new MacroTemplate($value); + } catch (InvalidArgumentException $e) { + throw new ConfigurationError( + 'Invalid URL parameter "%s" ("%s") in file "%s"', + $param, + $value, + $config->getConfigFile(), + $e + ); + } + } + } + + /** + * Load templates as configured inside the given directory + * + * @param string $path + * + * @return $this + * + * @throws ConfigurationError If the configuration is invalid + */ + public function loadDir($path) + { + foreach ( + new RecursiveIteratorIterator( + new RecursiveDirectoryIterator( + $path, + RecursiveDirectoryIterator::KEY_AS_PATHNAME | RecursiveDirectoryIterator::CURRENT_AS_FILEINFO + | RecursiveDirectoryIterator::SKIP_DOTS | RecursiveDirectoryIterator::FOLLOW_SYMLINKS + ), + RecursiveIteratorIterator::LEAVES_ONLY + ) as $filepath => $fileinfo + ) { + /** @var SplFileInfo $fileinfo */ + + if ($fileinfo->isFile() && preg_match('/\A[^.].*\.ini\z/si', $fileinfo->getFilename())) { + $this->loadIni($filepath); + } + } + + return $this; + } + + /** + * Load templates as configured in the given INI file + * + * @param string $path + * + * @return $this + * + * @throws ConfigurationError If the configuration is invalid + */ + public function loadIni($path) + { + /** @var string[][][] $templates */ + $templates = []; + + foreach (Config::fromIni($path) as $section => $options) { + /** @var ConfigObject $options */ + + $matches = []; + if (! preg_match('/\A(.+)\.(graph|metrics_filters|urlparams|functions)\z/', $section, $matches)) { + throw new ConfigurationError('Bad section name "%s" in file "%s"', $section, $path); + } + + $templates[$matches[1]][$matches[2]] = $options->toArray(); + } + + $checkCommands = []; + + foreach ($templates as $templateName => $template) { + $checkCommands[$templateName] = isset($template['graph']['check_command']) + ? array_unique(preg_split('/\s*,\s*/', $template['graph']['check_command'], -1, PREG_SPLIT_NO_EMPTY)) + : []; + unset($template['graph']['check_command']); + + if (isset($template['graph'])) { + switch (count($template['graph'])) { + case 0: + break; + + case 1: + throw new ConfigurationError( + 'Bad option for template "%s" in file "%s": "graph.%s"', + $templateName, + $path, + array_keys($template['graph'])[0] + ); + + default: + $standalone = array_keys($template['graph']); + sort($standalone); + + throw new ConfigurationError( + 'Bad options for template "%s" in file "%s": %s', + $templateName, + $path, + implode(', ', array_map( + function ($option) { + return "\"graph.$option\""; + }, + $standalone + )) + ); + } + } + + /** @var MacroTemplate[][] $curves */ + $curves = []; + + if (isset($template['metrics_filters'])) { + foreach ($template['metrics_filters'] as $curve => $metricsFilter) { + try { + $curves[$curve][0] = new MacroTemplate($metricsFilter); + } catch (InvalidArgumentException $e) { + throw new ConfigurationError( + 'Bad metrics filter "%s" for curve "%s" of template "%s" in file "%s": %s', + $metricsFilter, + $curve, + $templateName, + $path, + $e->getMessage() + ); + } + + if ( + count(array_intersect( + $curves[$curve][0]->getMacros(), + ['host_name_template', 'service_name_template'] + )) !== 1 + ) { + throw new ConfigurationError( + 'Bad metrics filter "%s" for curve "%s" of template "%s" in file "%s": must include' + . ' either the macro $host_name_template$ or $service_name_template$, but not both', + $metricsFilter, + $curve, + $templateName, + $path + ); + } + + if (isset($template['functions'][$curve])) { + try { + $curves[$curve][1] = new MacroTemplate($template['functions'][$curve]); + } catch (InvalidArgumentException $e) { + throw new ConfigurationError( + 'Bad function "%s" for curve "%s" of template "%s" in file "%s": %s', + $template['functions'][$curve], + $curve, + $templateName, + $path, + $e->getMessage() + ); + } + + unset($template['functions'][$curve]); + } else { + $curves[$curve][1] = new MacroTemplate('$metric$'); + } + } + } + + if (isset($template['functions'])) { + switch (count($template['functions'])) { + case 0: + break; + + case 1: + throw new ConfigurationError( + 'Metrics filter for curve "%s" of template "%s" in file "%s" missing', + array_keys($template['functions'])[0], + $templateName, + $path + ); + + default: + $standalone = array_keys($template['functions']); + sort($standalone); + + throw new ConfigurationError( + 'Metrics filter for curves of template "%s" in file "%s" missing: "%s"', + $templateName, + $path, + implode('", "', $standalone) + ); + } + } + + $urlParams = $this->defaultUrlParams; + + if (isset($template['urlparams'])) { + foreach ($template['urlparams'] as $key => $value) { + try { + $urlParams[$key] = new MacroTemplate($value); + } catch (InvalidArgumentException $e) { + throw new ConfigurationError( + 'Invalid URL parameter "%s" ("%s") for template "%s" in file "%s": %s', + $key, + $value, + $templateName, + $path, + $e->getMessage() + ); + } + } + } + + $templates[$templateName] = empty($curves) ? null : (new Template()) + ->setCurves($curves) + ->setUrlParams($urlParams); + } + + foreach ($templates as $templateName => $template) { + if ($template === null) { + if (empty($checkCommands[$templateName])) { + unset($this->defaultTemplates[$templateName]); + } else { + foreach ($checkCommands[$templateName] as $checkCommand) { + unset($this->templates[$checkCommand][$templateName]); + + if (empty($this->templates[$checkCommand])) { + unset($this->templates[$checkCommand]); + } + } + } + } else { + if (empty($checkCommands[$templateName])) { + $this->defaultTemplates[$templateName] = $template; + } else { + foreach ($checkCommands[$templateName] as $checkCommand) { + $this->templates[$checkCommand][$templateName] = $template; + } + } + } + } + + return $this; + } + + /** + * Get all loaded templates for the given check command by their names + * + * @param string $checkCommand + * + * @return Template[] + */ + public function getTemplates($checkCommand) + { + return isset($this->templates[$checkCommand]) ? $this->templates[$checkCommand] : []; + } + + /** + * Get all loaded templates for all check commands + * + * @return Template[][] + */ + public function getAllTemplates() + { + return $this->templates; + } + + /** + * Get all loaded default templates by their names + * + * @return Template[] + */ + public function getDefaultTemplates() + { + return $this->defaultTemplates; + } +} |