diff --git a/application/controllers/ErrorController.php b/application/controllers/ErrorController.php index f2f6aa6cf..06034ae4e 100644 --- a/application/controllers/ErrorController.php +++ b/application/controllers/ErrorController.php @@ -28,7 +28,7 @@ */ // {{{ICINGA_LICENSE_HEADER}}} -# namespace Icinga\Application\Controllers; +// namespace Icinga\Application\Controllers; use Icinga\Web\Controller\ActionController; use Icinga\Application\Icinga; diff --git a/application/controllers/ListController.php b/application/controllers/ListController.php new file mode 100644 index 000000000..a2c1e5138 --- /dev/null +++ b/application/controllers/ListController.php @@ -0,0 +1,82 @@ + + * @license http://www.gnu.org/licenses/gpl-2.0.txt GPL, version 2 + * @author Icinga Development Team + * + */ +// {{{ICINGA_LICENSE_HEADER}}} + +use Icinga\Module\Monitoring\Controller; +use Icinga\Web\Hook; +use Icinga\Application\Config as IcingaConfig; +use Icinga\Web\Url; +use Icinga\Data\ResourceFactory; + +/** + * Class ListController + * + * Application wide controller for various listing actions + */ +class ListController extends Controller +{ + /** + * Add title tab + * + * @param string $action + */ + protected function addTitleTab($action) + { + $this->getTabs()->add($action, array( + 'title' => ucfirst($action), + 'url' => Url::fromPath( + 'list/' + . str_replace(' ', '', $action) + ) + ))->activate($action); + } + + /** + * Display the application log + */ + public function applicationlogAction() + { + $this->addTitleTab('application log'); + $config_ini = IcingaConfig::app()->toArray(); + if (!in_array('logging', $config_ini) || ( + in_array('type', $config_ini['logging']) && + $config_ini['logging']['type'] === 'stream' && + in_array('target', $config_ini['logging']) && + file_exists($config_ini['logging']['target']) + ) + ) { + $config = ResourceFactory::getResourceConfig('logfile'); + $resource = ResourceFactory::createResource($config); + $this->view->logData = $resource->select()->order('DESC')->paginate(); + } else { + $this->view->logData = null; + } + } +} +// @codingStandardsIgnoreEnd diff --git a/application/views/scripts/list/applicationlog.phtml b/application/views/scripts/list/applicationlog.phtml new file mode 100644 index 000000000..68b115152 --- /dev/null +++ b/application/views/scripts/list/applicationlog.phtml @@ -0,0 +1,24 @@ +
+ tabs->render($this) ?> +
+ logData ?> +
+ +
+ + + logData as $value): ?> + datetime); ?> + + + + + + +
+ escape($datetime->format('d.m. H:i')) ?>
+ escape($value->loglevel) ?> +
+ escape($value->message) ?> +
+
\ No newline at end of file diff --git a/config/config.ini.in b/config/config.ini.in index 4a27f73e0..4d12e67cb 100644 --- a/config/config.ini.in +++ b/config/config.ini.in @@ -49,5 +49,3 @@ configPath = "@icingaweb_config_path@/preferences" ; Use database storage to save preferences in either a MySQL or PostgreSQL database ;type = db ;resource = icingaweb-mysql - - diff --git a/config/menu.ini b/config/menu.ini index b63914202..65959c93a 100644 --- a/config/menu.ini +++ b/config/menu.ini @@ -18,8 +18,12 @@ title = "Configuration" url = "config" priority = 300 +[System.ApplicationLog] +title = "Application log" +url = "list/applicationlog" +priority = 400 + [Logout] url = "authentication/logout" icon = img/icons/logout.png priority = 300 - diff --git a/config/resources.ini.in b/config/resources.ini.in index 7fc4a1490..c5eafeb55 100644 --- a/config/resources.ini.in +++ b/config/resources.ini.in @@ -47,3 +47,9 @@ port = @ldap_port@ root_dn = "@ldap_rootdn@" bind_dn = "@ldap_binddn@" bind_pw = @ldap_bindpass@ + +[logfile] +type = file +filename = "/vagrant/var/log/icingaweb.log" +fields = "/^(?[0-9]{4}(-[0-9]{2}){2}T[0-9]{2}(:[0-9]{2}){2}(\\+[0-9]{2}:[0-9]{2})?) (?[A-Za-z]+)( \([0-9]+\))?: (?.*)$/" +; format: PCRE diff --git a/library/Icinga/Data/DataArray/Datasource.php b/library/Icinga/Data/DataArray/Datasource.php index e511c623b..0f1936935 100644 --- a/library/Icinga/Data/DataArray/Datasource.php +++ b/library/Icinga/Data/DataArray/Datasource.php @@ -56,8 +56,7 @@ class Datasource implements DatasourceInterface public function fetchAll(Query $query) { - $result = $this->getResult($query); - return $result; + return $this->getResult($query); } public function count(Query $query) diff --git a/library/Icinga/Data/ResourceFactory.php b/library/Icinga/Data/ResourceFactory.php index d8a38c8d0..3b4d14e3b 100644 --- a/library/Icinga/Data/ResourceFactory.php +++ b/library/Icinga/Data/ResourceFactory.php @@ -37,6 +37,7 @@ use Icinga\Data\Db\Connection as DbConnection; use Icinga\Protocol\Livestatus\Connection as LivestatusConnection; use Icinga\Protocol\Statusdat\Reader as StatusdatReader; use Icinga\Protocol\Ldap\Connection as LdapConnection; +use Icinga\Protocol\File\Reader as FileReader; class ResourceFactory implements ConfigAwareFactory { @@ -133,6 +134,9 @@ class ResourceFactory implements ConfigAwareFactory case 'livestatus': $resource = new LivestatusConnection($config->socket); break; + case 'file': + $resource = new FileReader($config); + break; default: throw new ConfigurationError('Unsupported resource type "' . $config->type . '"'); } diff --git a/library/Icinga/Protocol/File/Query.php b/library/Icinga/Protocol/File/Query.php new file mode 100644 index 000000000..066fbcd7e --- /dev/null +++ b/library/Icinga/Protocol/File/Query.php @@ -0,0 +1,83 @@ +sortDir = ($dir === null || strtoupper(trim($dir)) === 'DESC') ? self::SORT_DESC : self::SORT_ASC; + return $this; + } + + /** + * Return true if sorting descending, false otherwise + * + * @return bool + */ + public function sortDesc() + { + return $this->sortDir === self::SORT_DESC; + } + + /** + * Add an mandatory filter expression to be applied on this query + * + * @param string $expression the filter expression to be applied + * + * @return Query + */ + public function andWhere($expression) + { + $this->filters[] = $expression; + return $this; + } + + /** + * Get filters currently applied on this query + * + * @return array + */ + public function getFilters() + { + return $this->filters; + } +} \ No newline at end of file diff --git a/library/Icinga/Protocol/File/Reader.php b/library/Icinga/Protocol/File/Reader.php new file mode 100644 index 000000000..b2eb80c23 --- /dev/null +++ b/library/Icinga/Protocol/File/Reader.php @@ -0,0 +1,284 @@ +config = $config; + $this->filename = $config->filename; + } + + /** + * Instantiate a Query object + * + * @return Query + */ + public function select() + { + return new Query($this); + } + + /** + * Fetch result as an array of objects + * + * @return array + */ + public function fetchAll(Query $query) + { + $all = array(); + foreach ($this->fetchPairs($query) as $index => $value) { + $all[$index] = new \stdClass(); + foreach ($value as $key => $value_2) { + $all[$index]->{$key} = $value_2; + } + } + return $all; + } + + /** + * Fetch first result row + * + * @return object + */ + public function fetchRow(Query $query) + { + $all = $this->fetchAll($query); + if (isset($all[0])) { + return $all[0]; + } + return null; + } + + /** + * Fetch first result column + * + * @return array + */ + public function fetchColumn(Query $query) + { + $column = array(); + foreach ($this->fetchPairs($query) as $value) { + foreach ($value as $value_2) { + $column[] = $value_2; + break; + } + } + return $column; + } + + /** + * Fetch first column value from first result row + * + * @return mixed + */ + public function fetchOne(Query $query) + { + $pairs = $this->fetchPairs($query); + if (isset($pairs[0])) { + foreach ($pairs[0] as $value) { + return $value; + } + } + return null; + } + + /** + * Fetch result as a key/value pair array + * + * @return array + */ + public function fetchPairs(Query $query) + { + return $this->read($query); + } + + /** + * If given $line matches the $query's PCRE pattern and contains all the strings in the $query's filters array, + * return an associative array of the matches of the PCRE pattern. + * Otherwise, return false. + * If preg_match returns false, it failed parsing the PCRE pattern. + * In that case, throw an exception. + * + * @param string $line + * @param Query $query + * + * @return array|bool + * + * @throws \Exception + */ + public function validateLine($line, Query $query) + { + $data = array(); + $PCRE_result = @preg_match($this->config->fields, $line, $data); + if ($PCRE_result === false) { + throw new \Exception('Failed parsing regular expression!'); + } else if ($PCRE_result === 1) { + foreach ($query->getFilters() as $filter) { + if (strpos($line, $filter) === false) { + return false; + } + } + foreach ($data as $key => $value) { + if (is_int($key)) { + unset($data[$key]); + } + } + return $data; + } + return false; + } + + /** + * Skip and read as many lines as needed according to given $query. + * + * @param Query $query + * + * @return array result + */ + public function read(Query $query) + { + $skip_lines = $query->getOffset(); + $read_lines = $query->getLimit(); + if ($skip_lines === null) { + $skip_lines = 0; + } + return $this->{$query->sortDesc() ? 'readFromEnd' : 'readFromStart'}($skip_lines, $read_lines, $query); + } + + /** + * Backend for $this->read + * Direction: LIFO + */ + public function readFromEnd($skip_lines, $read_lines, Query $query) + { + $PHP_EOL_len = strlen(PHP_EOL); + $lines = array(); + $s = ''; + $f = fopen($this->filename, 'rb'); + fseek($f, 0, SEEK_END); + $pos = ftell($f); + while ($read_lines === null || count($lines) < $read_lines) { + fseek($f, --$pos); + $c = fgetc($f); + if ($c === false || $pos < 0) { + $l = $this->validateLine($s, $query); + if (!($l === false || $skip_lines)) { + $lines[] = $l; + } + break; + } + $s = $c . $s; + if (strpos($s, PHP_EOL) === 0) { + $l = $this->validateLine((string)substr($s, $PHP_EOL_len), $query); + if ($l !== false) { + if ($skip_lines) { + $skip_lines--; + } else { + $lines[] = $l; + } + } + $s = ''; + } + } + return $lines; + } + + /** + * Backend for $this->read + * Direction: FIFO + */ + public function readFromStart($skip_lines, $read_lines, Query $query) + { + $PHP_EOL_len = strlen(PHP_EOL); + $lines = array(); + $s = ''; + $f = fopen($this->filename, 'rb'); + while ($read_lines === null || count($lines) < $read_lines) { + $c = fgetc($f); + if ($c === false) { + $l = $this->validateLine($s, $query); + if (!($l === false || $skip_lines)) { + $lines[] = $l; + } + break; + } + $s .= $c; + if (strpos($s, PHP_EOL) !== false) { + $l = $this->validateLine((string)substr($s, 0, strlen($s) - $PHP_EOL_len), $query); + if ($l !== false) { + if ($skip_lines) { + $skip_lines--; + } else { + $lines[] = $l; + } + } + $s = ''; + } + } + return $lines; + } + + /** + * Return the number of available valid lines. + * + * @param Query $query + * + * @return int + */ + public function count(Query $query) { + $PHP_EOL_len = strlen(PHP_EOL); + $lines = 0; + $s = ''; + $f = fopen($this->filename, 'rb'); + fseek($f, 0, SEEK_END); + $pos = ftell($f); + while (true) { + fseek($f, --$pos); + $c = fgetc($f); + if ($c === false || $pos < 0) { + if ($this->validateLine($s, $query) !== false) { + $lines++; + } + break; + } + $s = $c . $s; + if (strpos($s, PHP_EOL) === 0) { + if ($this->validateLine((string)substr($s, $PHP_EOL_len), $query) !== false) { + $lines++; + } + $s = ''; + } + } + return $lines; + } +} \ No newline at end of file