summaryrefslogtreecommitdiffstats
path: root/library/Graphite/Graphing
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 12:44:18 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 12:44:18 +0000
commit23be945fd2810ee82e3a23cbcd2352c9bda43d4f (patch)
treedd511b321f308264952cffb005a4288ea4e478e6 /library/Graphite/Graphing
parentInitial commit. (diff)
downloadicingaweb2-module-graphite-upstream.tar.xz
icingaweb2-module-graphite-upstream.zip
Adding upstream version 1.2.2.upstream/1.2.2upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'library/Graphite/Graphing')
-rw-r--r--library/Graphite/Graphing/Chart.php385
-rw-r--r--library/Graphite/Graphing/GraphingTrait.php79
-rw-r--r--library/Graphite/Graphing/GraphiteWebClient.php198
-rw-r--r--library/Graphite/Graphing/MetricsDataSource.php48
-rw-r--r--library/Graphite/Graphing/MetricsQuery.php219
-rw-r--r--library/Graphite/Graphing/Template.php364
-rw-r--r--library/Graphite/Graphing/Templates.php321
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;
+ }
+}