Graphite module prototype: initial commit

This commit is contained in:
Thomas Gelf 2015-06-03 10:30:57 +02:00
commit f491471622
12 changed files with 654 additions and 0 deletions

View file

@ -0,0 +1,85 @@
<?php
use Icinga\Exception\NotFoundError;
use Icinga\Module\Graphite\GraphiteWeb;
use Icinga\Module\Graphite\GraphTemplate;
use Icinga\Web\Controller;
use Icinga\Web\Widget;
use \DirectoryIterator;
class Graphite_ShowController extends Controller
{
protected $baseUrl;
protected $graphiteWeb;
protected $templates;
protected $filters;
public function init()
{
$this->baseUrl = $this->Config()->get('global', 'web_url');
$graphite = $this->graphiteWeb = new GraphiteWeb($this->baseUrl);
}
public function hostAction()
{
$hostname = $this->view->hostname = $this->params->get('host');
if (! $hostname) {
throw new NotFoundError('Host is required');
}
$this->tabs()->activate('host');
$hosts = $this->Config()->get('global', 'host_pattern');
$imgs = array();
foreach ($this->loadTemplates() as $type => $template) {
$imgs[$type] = $this->graphiteWeb
->select()
->from(
array('host' => $hosts),
$template->getFilterString()
)
->where('hostname', $hostname)
->getImages($template);
foreach ($imgs[$type] as $img) {
$img->setStart($this->params->get('start', '-1hours'))
->setWidth($this->params->get('width', '300'))
->setHeight($this->params->get('height', '200'))
->showLegend(! $this->params->get('hideLegend', false));
}
}
$this->view->images = $imgs;
}
protected function loadTemplates()
{
$dir = $this->Module()->getConfigDir() . '/templates';
$templates = array();
foreach (new DirectoryIterator($dir) as $file) {
if ($file->isDot()) continue;
$filename = $file->getFilename();
if (substr($filename, -5) === '.conf') {
$name = substr($filename, 0, -5);
$templates[$name] = GraphTemplate::load(
file_get_contents($file->getPathname())
);
}
}
ksort($templates);
return $templates;
}
protected function tabs()
{
return $this->view->tabs = Widget::create('tabs')->add('host', array(
'label' => 'Graphite - Single Host',
'url' => $this->getRequest()->getUrl()
));
}
}

View file

@ -0,0 +1,17 @@
<div class="controls">
<?= $this->tabs ?>
<h1><?= $this->hostname ?></h1>
</div>
<div class="content">
<?php foreach ($this->images as $type => $imgs): ?>
<?php if (count($imgs) > 0): ?>
<div class="images"><h3><?= $this->escape(ucfirst($type)) ?></h3>
<?php foreach ($imgs as $img): ?>
<img class="svg" src="<?= $img->getUrl() ?>" alt="<?= $img->getTitle() ?>" title="<?= $img->getTitle() ?>" />
<?php endforeach ?>
</div>
<?php endif ?>
<?php endforeach ?>
</div>

View file

@ -0,0 +1,12 @@
<div class="controls">
<?= $this->tabs ?>
</div>
<div class="content">
<?php foreach ($this->images as $base => $img): ?>
<div style="width: 260px; float: left; margin-right: 5px;">
<h3><?= $this->escape($base) ?></h3>
<img src="<?= $img ?>" />
</div>
<?php endforeach ?>
</div>

View file

@ -0,0 +1,28 @@
<?php
$maxCnt = 0;
foreach ($this->images as $base => $cpus) {
$maxCnt = max($maxCnt, count($cpus));
}
?>
<div class="controls">
<?= $this->tabs ?>
<h1>CPUs</h1>
</div>
<div class="content">
<table style="width: 100%;">
<tr>
<th style="width: 15em;">&nbsp;</th>
<th>CPUs</th>
</tr>
<?php foreach ($this->images as $base => $cpus): ?>
<tr>
<th style="vertical-align: top; text-align: right; padding-right: 2em;"><?= $this->escape($base) ?></th>
<td>
<?php foreach ($cpus as $num => $img): ?>
<div style="width: 53px; float: left;"><img src="<?= $img ?>" /><!--<br />CPU <?= $num ?>--></div>
<?php endforeach ?>
</td>
</tr>
<?php endforeach ?>
</table>
</div>

View file

@ -0,0 +1,81 @@
<?php
namespace Icinga\Module\Graphite;
use Icinga\Web\Url;
class GraphDatasource
{
protected $path;
protected $color;
protected $alias;
protected $scale;
protected $scaleToSeconds;
public function __construct($path)
{
$this->path = $path;
}
public function setColor($color)
{
$this->color = $color;
return $this;
}
public function setScale($scale)
{
$this->scale = $scale;
return $this;
}
public function setScaleToSeconds($seconds)
{
$this->scaleToSeconds = $seconds;
return $this;
}
public function setAlias($alias)
{
$this->alias = $alias;
return $this;
}
public function getName()
{
if ($this->alias !== null) {
return $this->alias;
}
return $this->path;
}
protected function func()
{
$args = func_get_args();
$function = array_shift($args);
return sprintf($function . '(%s)', implode(',', $args));
}
public function addToUrl(Url $url, $metric)
{
$target = $metric . '.' . $this->path;
if ($this->color !== null) {
$target = $this->func('color', $target, "'" . $this->color . "'");
}
if ($this->scaleToSeconds !== null) {
$target = $this->func('scaleToSeconds', $target, $this->scaleToSeconds);
}
if ($this->scale !== null) {
$target = $this->func('scale', $target, $this->scale);
}
if ($this->alias !== null) {
$target = $this->func('alias', $target, "'" . $this->alias . "'");
}
return $url->getParams()->add('target', $target);
}
}

View file

@ -0,0 +1,120 @@
<?php
namespace Icinga\Module\Graphite;
use Icinga\Exception\ConfigurationError;
use Icinga\Module\Graphite\GraphDatasource;
use Icinga\Web\Url;
/**
* TODO: allow same metric multiple times
* wording: metric vs datasource
* allow a template to generate multiple (different) graphs (e.g. traffic & errors)
*
*/
class GraphTemplate
{
protected $datasources = array();
protected $attributes = array();
protected $filterString;
/**
* Use one of the static constructors instead
*/
protected function __construct()
{
}
/**
* Load a template from a given string
*/
public static function load($string)
{
$tmpl = new static;
$tmpl->parse($string);
return $tmpl;
}
public function getFilterString()
{
return $this->filterString;
}
protected function parse($string)
{
$lines = preg_split('/\n/', $string);
foreach ($lines as $line) {
$line = trim($line, ' ');
if ($line === '') continue;
if ($line[0] === '#') continue;
if (preg_match('/^(\w+)\s*=\s*(.+)$/', $line, $m)) {
if ($m[1] === 'filter') {
$this->filterString = $m[2];
} else {
$this->attributes[$m[1]] = $m[2];
}
continue;
}
if (! preg_match('/^([^:\s]+)\s*:\s*(.+)$/', $line, $m)) {
throw new ConfigurationError('Got invalid template line: %s', $line);
}
$ds = new GraphDatasource($m[1]);
$params = preg_split('/\s*,\s*/', $m[2]);
$props = array();
foreach ($params as $p) {
list($k, $v) = preg_split('/\s*=\s*/', $p, 2);
$func = 'set' . ucfirst($k);
$ds->$func($v);
}
$this->datasources[$m[1]] = $ds;
}
}
/**
* Fill the given vars into the given string
*/
protected function fillVars($string, $vars)
{
$regexes = array();
$values = array();
foreach ($vars as $k => $v) {
$regexes[] = '/' . preg_quote('$' . $k, '/') . '/';
$values[] = $v;
}
return preg_replace(
$regexes,
$values,
$string
);
}
public function getTitle($vars)
{
return $this->fillVars($this->attributes['title'], $vars);
}
/**
* Extend the given URL and add all configured data sources based on the
* given metric string
*/
public function extendUrl(Url $url, $metric, $vars)
{
$params = $url->getParams();
foreach ($this->attributes as $k => $v) {
$params->add($k, $this->fillVars($v, $vars));
}
foreach ($this->datasources as $ds) {
$ds->addToUrl($url, $metric);
}
return $url;
}
}

View file

@ -0,0 +1,122 @@
<?php
namespace Icinga\Module\Graphite;
use Icinga\Web\Url;
class GraphiteChart
{
protected $web;
protected $metric;
protected $from = '-4hours';
protected $showLegend = true;
protected $height = 200;
protected $width = 300;
public function __construct(GraphiteWeb $web, GraphTemplate $template, $metric, $vars)
{
$this->web = $web;
$this->template = $template;
$this->metric = $metric;
$this->vars = $vars;
}
public function getTitle()
{
return $this->template->getTitle($this->vars);
}
public function setStart($start)
{
$this->from = $start;
return $this;
}
public function setWidth($width)
{
$this->width = $width;
return $this;
}
public function setHeight($height)
{
$this->height = $height;
return $this;
}
public function showLegend($show = true)
{
$this->showLegend = (bool) $show;
return $this;
}
public function setMetrics($metrics = array())
{
}
public function setFrom($from)
{
$this->from = $from;
return $this;
}
public function getFrom()
{
return $this->from;
}
protected function getParams()
{
return array(
'height' => $this->height,
'width' => $this->width,
'_salt' => time() . '.000',
'from' => $this->from,
'graphOnly' => (string) ! $this->showLegend,
'hideLegend' => (string) ! $this->showLegend,
'hideGrid' => 'true',
'vTitle' => 'Percent',
'lineMode' => 'connected', // staircase, slope
'xFormat' => '%a %H:%M',
'drawNullAsZero' => 'false',
'graphType' => 'line', // pie
'tz' => 'Europe/Berlin',
// 'hideAxes' => 'true',
// 'hideYAxis' => 'true',
// 'format' => 'svg',
// 'pieMode' => 'average',
);
}
public function getUrl()
{
$urlPattern = '/^' . preg_quote(Url::fromPath('/'), '/') . '/';
$url = Url::fromPath('/render', $this->getParams());
$this->template->extendUrl($url, $this->metric, $this->vars);
$url->getParams()->add('_ext', 'whatever.svg');
$url = preg_replace($urlPattern, $this->web->getBaseUrl() . '/', $url);
return $url;
}
public function fetchImage()
{
$options = array(
'http'=>array(
'method'=>"POST",
'header'=>
"Accept-language: en\r\n".
"Content-type: application/x-www-form-urlencoded\r\n",
'content'=> $data
)
);
$context = stream_context_create($options);
header('Content-Type: image/png');
return file_get_contents($this->getUrl(), false, $context);
}
}

View file

@ -0,0 +1,117 @@
<?php
namespace Icinga\Module\Graphite;
use Icinga\Module\Graphite\GraphiteWeb;
use Icinga\Web\Url;
class GraphiteQuery
{
protected $web;
protected $search;
protected $searchPattern;
public function __construct(GraphiteWeb $web)
{
$this->web = $web;
}
public function from($base, $pattern = null)
{
if (is_array($base)) {
$key = key($base);
if ($pattern === null) {
$this->search = current($base);
} else {
$this->search = $this->replace($pattern, $key, current($base));
}
} else {
// TODO: well... patterns might also work for non-aliases $base's
$this->search = $base;
}
$this->searchPattern = $this->search;
return $this;
}
public function getSearchPattern()
{
return $this->searchPattern;
}
protected function replace($string, $key, $replacement)
{
return preg_replace(
'/\$' . preg_quote($key) . '(\.|$)/',
$replacement . '\1',
$string
);
}
public function where($column, $search)
{
$this->search = $this->replace($this->search, $column, $search);
return $this;
}
/**
* Replace all variables ($some_thing) with an asterisk
*
* TODO: I'd opt for \w instead of [^\.]
*/
protected function replaceRemainingVariables($string)
{
return preg_replace('/\$[^\.]+(\.|$)/', '*\1', $string);
}
/**
* Create a filter string allowing us to filter metrics
*/
protected function toFilterString()
{
return $this->replaceRemainingVariables($this->search);
}
protected function extractVars($string, $pattern)
{
$regexVar = '/\$(\w+)/';
$vars = array();
if (preg_match_all($regexVar, $pattern, $m)) {
$varnames = $m[1];
$parts = preg_split($regexVar, $pattern);
foreach ($parts as $key => $val) {
$parts[$key] = preg_quote($val, '/');
}
$regex = '/' . implode('([^\.]+?)', $parts) . '/';
if (preg_match($regex, $string, $m)) {
array_shift($m);
$vars = array_combine($varnames, $m);
}
}
return $vars;
}
public function getImages(GraphTemplate $template)
{
$charts = array();
foreach ($this->listMetrics() as $metric) {
$vars = $this->extractVars($metric, $this->getSearchPattern());
$charts[] = new GraphiteChart($this->web, $template, $metric, $vars);
}
return $charts;
}
public function listMetrics()
{
return $this->web->listMetrics($this->toFilterString());
}
}

View file

@ -0,0 +1,37 @@
<?php
namespace Icinga\Module\Graphite;
use Icinga\Module\Graphite\GraphTemplate;
use Icinga\Module\Graphite\GraphiteQuery;
class GraphiteWeb
{
protected $baseUrl;
public function __construct($baseUrl)
{
$this->baseUrl = $baseUrl;
}
public function select()
{
return new GraphiteQuery($this);
}
public function getBaseUrl()
{
return $this->baseUrl;
}
public function listMetrics($filter)
{
$res = json_decode(
file_get_contents(
$this->baseUrl . '/metrics/expand?query=' . $filter
)
);
natsort($res->results);
return array_values($res->results);
}
}

View file

@ -0,0 +1,17 @@
<?php
namespace Icinga\Module\Graphite;
use Icinga\Module\Monitoring\Web\Hook\HostActionsHook;
use Icinga\Module\Monitoring\Object\Host;
use Icinga\Web\Url;
class HostActions extends HostActionsHook
{
public function getActionsForHost(Host $host)
{
return array(
'Graphite' => Url::fromPath('graphite/show/host', array('host' => $host->host_name))
);
}
}

14
public/css/module.less Normal file
View file

@ -0,0 +1,14 @@
div.images {
display: inline-block;
h3 {
clear: both;
}
img.svg {
float: left;
border: none;
}
}

4
run.php Normal file
View file

@ -0,0 +1,4 @@
<?php
$this->registerHook('Monitoring\\HostActions', '\\Icinga\\Module\\Graphite\\HostActions');