From f491471622414da8ea8fb7f5eea95b51a6b6c81f Mon Sep 17 00:00:00 2001 From: Thomas Gelf Date: Wed, 3 Jun 2015 10:30:57 +0200 Subject: [PATCH] Graphite module prototype: initial commit --- application/controllers/ShowController.php | 85 ++++++++++++++ application/views/scripts/show/host.phtml | 17 +++ application/views/scripts/test/apache.phtml | 12 ++ application/views/scripts/test/cpu.phtml | 28 +++++ library/Graphite/GraphDatasource.php | 81 +++++++++++++ library/Graphite/GraphTemplate.php | 120 +++++++++++++++++++ library/Graphite/GraphiteChart.php | 122 ++++++++++++++++++++ library/Graphite/GraphiteQuery.php | 117 +++++++++++++++++++ library/Graphite/GraphiteWeb.php | 37 ++++++ library/Graphite/HostActions.php | 17 +++ public/css/module.less | 14 +++ run.php | 4 + 12 files changed, 654 insertions(+) create mode 100644 application/controllers/ShowController.php create mode 100644 application/views/scripts/show/host.phtml create mode 100644 application/views/scripts/test/apache.phtml create mode 100644 application/views/scripts/test/cpu.phtml create mode 100644 library/Graphite/GraphDatasource.php create mode 100644 library/Graphite/GraphTemplate.php create mode 100644 library/Graphite/GraphiteChart.php create mode 100644 library/Graphite/GraphiteQuery.php create mode 100644 library/Graphite/GraphiteWeb.php create mode 100644 library/Graphite/HostActions.php create mode 100644 public/css/module.less create mode 100644 run.php diff --git a/application/controllers/ShowController.php b/application/controllers/ShowController.php new file mode 100644 index 0000000..24838c4 --- /dev/null +++ b/application/controllers/ShowController.php @@ -0,0 +1,85 @@ +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() + )); + } +} diff --git a/application/views/scripts/show/host.phtml b/application/views/scripts/show/host.phtml new file mode 100644 index 0000000..e50bab5 --- /dev/null +++ b/application/views/scripts/show/host.phtml @@ -0,0 +1,17 @@ +
+tabs ?> +

hostname ?>

+
+
+images as $type => $imgs): ?> + 0): ?> + +

escape(ucfirst($type)) ?>

+ +<?= $img->getTitle() ?> + +
+ + + +
diff --git a/application/views/scripts/test/apache.phtml b/application/views/scripts/test/apache.phtml new file mode 100644 index 0000000..069ccbe --- /dev/null +++ b/application/views/scripts/test/apache.phtml @@ -0,0 +1,12 @@ +
+tabs ?> +
+ +
+images as $base => $img): ?> +
+

escape($base) ?>

+ +
+ +
diff --git a/application/views/scripts/test/cpu.phtml b/application/views/scripts/test/cpu.phtml new file mode 100644 index 0000000..495e315 --- /dev/null +++ b/application/views/scripts/test/cpu.phtml @@ -0,0 +1,28 @@ +images as $base => $cpus) { + $maxCnt = max($maxCnt, count($cpus)); +} +?> +
+tabs ?> +

CPUs

+
+
+ + + + + +images as $base => $cpus): ?> + + + + + +
 CPUs
escape($base) ?> + $img): ?> +
+ +
+
diff --git a/library/Graphite/GraphDatasource.php b/library/Graphite/GraphDatasource.php new file mode 100644 index 0000000..ed757db --- /dev/null +++ b/library/Graphite/GraphDatasource.php @@ -0,0 +1,81 @@ +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); + } +} diff --git a/library/Graphite/GraphTemplate.php b/library/Graphite/GraphTemplate.php new file mode 100644 index 0000000..ddb34f7 --- /dev/null +++ b/library/Graphite/GraphTemplate.php @@ -0,0 +1,120 @@ +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; + } +} diff --git a/library/Graphite/GraphiteChart.php b/library/Graphite/GraphiteChart.php new file mode 100644 index 0000000..de0a81c --- /dev/null +++ b/library/Graphite/GraphiteChart.php @@ -0,0 +1,122 @@ +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); + } +} diff --git a/library/Graphite/GraphiteQuery.php b/library/Graphite/GraphiteQuery.php new file mode 100644 index 0000000..9d933bb --- /dev/null +++ b/library/Graphite/GraphiteQuery.php @@ -0,0 +1,117 @@ +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()); + } +} diff --git a/library/Graphite/GraphiteWeb.php b/library/Graphite/GraphiteWeb.php new file mode 100644 index 0000000..29e4c8d --- /dev/null +++ b/library/Graphite/GraphiteWeb.php @@ -0,0 +1,37 @@ +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); + } +} diff --git a/library/Graphite/HostActions.php b/library/Graphite/HostActions.php new file mode 100644 index 0000000..127300d --- /dev/null +++ b/library/Graphite/HostActions.php @@ -0,0 +1,17 @@ + Url::fromPath('graphite/show/host', array('host' => $host->host_name)) + ); + } +} diff --git a/public/css/module.less b/public/css/module.less new file mode 100644 index 0000000..3b1d2a5 --- /dev/null +++ b/public/css/module.less @@ -0,0 +1,14 @@ +div.images { + + display: inline-block; + + h3 { + clear: both; + } + + img.svg { + float: left; + border: none; + } + +} diff --git a/run.php b/run.php new file mode 100644 index 0000000..db3a6a0 --- /dev/null +++ b/run.php @@ -0,0 +1,4 @@ +registerHook('Monitoring\\HostActions', '\\Icinga\\Module\\Graphite\\HostActions'); +