From ed8a867b84e0ccb39c1012cff3054aa001b77ec0 Mon Sep 17 00:00:00 2001 From: Thomas Pulzer Date: Sat, 16 Mar 2019 23:36:21 +0100 Subject: [PATCH 1/8] Implemented logging to graylog server. Added the appropriate logging class. Provided the necessary system config examples. Added loading the log class from the LogFactory. Extended the LogFactory unit tests to include the graylog class. Added Graylog test class skeleton. Signed-off-by: Thomas Pulzer --- config/config.sample.php | 18 ++++++ lib/private/Log/Graylog.php | 105 +++++++++++++++++++++++++++++++ lib/private/Log/LogFactory.php | 2 + tests/lib/Log/GraylogTest.php | 30 +++++++++ tests/lib/Log/LogFactoryTest.php | 14 +++++ 5 files changed, 169 insertions(+) create mode 100644 lib/private/Log/Graylog.php create mode 100644 tests/lib/Log/GraylogTest.php diff --git a/config/config.sample.php b/config/config.sample.php index b25a4baeadd5d..7723923f491ec 100644 --- a/config/config.sample.php +++ b/config/config.sample.php @@ -743,6 +743,8 @@ * ``systemd``: the logs are sent to the Systemd journal. This requires a system * that runs Systemd and the Systemd journal. The PHP extension ``systemd`` * must be installed and active. + * ``graylog``: the logs are sent to a Graylog server. This requires a running + * Graylog service reachable by your Nextcloud instance. * * Defaults to ``file`` */ @@ -781,6 +783,22 @@ */ 'syslog_tag' => 'Nextcloud', +/** + * Your graylog host server name, for example ``localhost``, ``hostname``, + * ``hostname.example.com``, or the IP address. To specify a port use + * ``hostname:####`` + * Only effective when ``log_type`` set to ``graylog`` + */ +'graylog_host' => '', + +/** + * The protocol used for sending logs to the graylog server. + * Can be ``udp`` or ``tcp``. + * + * The default value is ``udp``. + */ +'graylog_method' => 'udp', + /** * Log condition for log level increase based on conditions. Once one of these * conditions is met, the required log level is set to debug. This allows to diff --git a/lib/private/Log/Graylog.php b/lib/private/Log/Graylog.php new file mode 100644 index 0000000000000..4499e2d2cd181 --- /dev/null +++ b/lib/private/Log/Graylog.php @@ -0,0 +1,105 @@ +, (t.pulzer@thesecretgamer.de) + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OC\Log; + + +use OCP\IConfig; +use OCP\ILogger; +use OCP\Log\IWriter; + +class Graylog implements IWriter { + + private static $VERSION = '1.1'; + private $host; + private $target; + private $port; + private $protocol; + + protected static $LEVELS = [ + 1 => LOG_ALERT, + ILogger::FATAL => LOG_CRIT, + ILogger::ERROR => LOG_ERR, + ILogger::WARN => LOG_WARNING, + 5 => LOG_NOTICE, + ILogger::INFO => LOG_INFO, + ILogger::DEBUG => LOG_DEBUG + ]; + + public function __construct(IConfig $config) { + $this->host = gethostname(); + $this->protocol = $config->getSystemValue('graylog_method', 'udp'); + $address = $config->getSystemValue('graylog_host', ''); + if(false !== strpos($address, ':')) { + $this->target = explode(':', $address)[0]; + $this->port = intval(explode(':', $address)[1]); + } else { + $this->target = $address; + $this->port = 514; + } + } + + /** + * sena a message to the Graylog server + * @param string $app + * @param string $message + * @param int $level + */ + public function write(string $app, $message, int $level) { + $chunks = []; + switch ($this->protocol) { + case 'udp': + $msg = '{"version":"'.self::$VERSION.'","host":"'. + $this->host.'","short_message":"'. + str_replace("\n", '\\n', '{'.$app.'} '.$message). + '","level":"'.self::$LEVELS[$level].'","timestamp":'. + time().'}'; + $chunks = str_split($msg, strlen($msg)/8000); + break; + case 'tcp': + $chunks[0] = '{"version":"'.self::$VERSION.'","host":"'. + $this->host.'","short_message":"'. + str_replace("\n", '\\n', '{'.$app.'} '.$message). + '","level":"'.self::$LEVELS[$level].'","timestamp":'. + time().'}'; + break; + } + $count = count($chunks); + $fp = fsockopen( + $this->protocol.'://'.$this->target, + $this->port, + $timeout = 5 + ); + switch ($count > 1) { + case true: + $id = random_bytes(8); + for ($i = 0; $i < $count; $i++) { + fwrite($fp, "\x1e\x0f".$id.$i.$count.$chunks[$i]."\0"); + } + break; + case false: + fwrite($fp, $chunks[0].chr(0)); + break; + } + fclose($fp); + } +} \ No newline at end of file diff --git a/lib/private/Log/LogFactory.php b/lib/private/Log/LogFactory.php index 5bb803cbaf240..22634f617d585 100644 --- a/lib/private/Log/LogFactory.php +++ b/lib/private/Log/LogFactory.php @@ -53,6 +53,8 @@ public function get(string $type):IWriter { return $this->c->resolve(Syslog::class); case 'systemd': return $this->c->resolve(Systemdlog::class); + case 'graylog': + return $this->c->resolve(Graylog::class); case 'file': return $this->buildLogFile(); diff --git a/tests/lib/Log/GraylogTest.php b/tests/lib/Log/GraylogTest.php new file mode 100644 index 0000000000000..d058acc79cc99 --- /dev/null +++ b/tests/lib/Log/GraylogTest.php @@ -0,0 +1,30 @@ +, (t.pulzer@thesecretgamer.de) + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OC\tests\lib\Log; + + +use Test\TestCase; + +class GraylogTest extends TestCase { + +} diff --git a/tests/lib/Log/LogFactoryTest.php b/tests/lib/Log/LogFactoryTest.php index ea6b12436e6e0..45b078d96ed6c 100644 --- a/tests/lib/Log/LogFactoryTest.php +++ b/tests/lib/Log/LogFactoryTest.php @@ -25,6 +25,7 @@ namespace Test\Log; use OC\Log\Errorlog; use OC\Log\File; +use OC\Log\Graylog; use OC\Log\LogFactory; use OC\Log\Syslog; use OC\Log\Systemdlog; @@ -156,4 +157,17 @@ public function testSystemdLog() { $log = $this->factory->get('systemd'); $this->assertInstanceOf(Systemdlog::class, $log); } + + /** + * @throws \OCP\AppFramework\QueryException + */ + public function testGraylogLog() { + $this->c->expects($this->once()) + ->method('resolve') + ->with(Graylog::class) + ->willReturn($this->createMock(Graylog::class)); + + $log = $this->factory->get('graylog'); + $this->assertInstanceOf(Graylog::class, $log); + } } From d9c25b8494a4b5be9d33556df48f07880c2f3de7 Mon Sep 17 00:00:00 2001 From: Thomas Pulzer Date: Sun, 24 Mar 2019 21:01:31 +0100 Subject: [PATCH 2/8] Reformatted and rearranged some code parts. Changed method of how to connect to remote server for writing logs. Wrote unit tests for chunked udp, unchunked udp and tcp connections. Signed-off-by: Thomas Pulzer --- lib/private/Log/Graylog.php | 41 +++++++++-------- tests/lib/Log/GraylogTest.php | 84 +++++++++++++++++++++++++++++++++++ 2 files changed, 104 insertions(+), 21 deletions(-) diff --git a/lib/private/Log/Graylog.php b/lib/private/Log/Graylog.php index 4499e2d2cd181..c3fe59ff3f0da 100644 --- a/lib/private/Log/Graylog.php +++ b/lib/private/Log/Graylog.php @@ -36,11 +36,9 @@ class Graylog implements IWriter { private $protocol; protected static $LEVELS = [ - 1 => LOG_ALERT, ILogger::FATAL => LOG_CRIT, ILogger::ERROR => LOG_ERR, ILogger::WARN => LOG_WARNING, - 5 => LOG_NOTICE, ILogger::INFO => LOG_INFO, ILogger::DEBUG => LOG_DEBUG ]; @@ -49,7 +47,7 @@ public function __construct(IConfig $config) { $this->host = gethostname(); $this->protocol = $config->getSystemValue('graylog_method', 'udp'); $address = $config->getSystemValue('graylog_host', ''); - if(false !== strpos($address, ':')) { + if (false !== strpos($address, ':')) { $this->target = explode(':', $address)[0]; $this->port = intval(explode(':', $address)[1]); } else { @@ -60,46 +58,47 @@ public function __construct(IConfig $config) { /** * sena a message to the Graylog server + * * @param string $app * @param string $message * @param int $level */ public function write(string $app, $message, int $level) { $chunks = []; + $msg = '{"version":"' . self::$VERSION . '","host":"' . + $this->host . '","short_message":"' . + str_replace("\n", '\\n', '{' . $app . '} ' . $message) . + '","level":"' . self::$LEVELS[$level] . '","timestamp":' . + time() . '}'; switch ($this->protocol) { case 'udp': - $msg = '{"version":"'.self::$VERSION.'","host":"'. - $this->host.'","short_message":"'. - str_replace("\n", '\\n', '{'.$app.'} '.$message). - '","level":"'.self::$LEVELS[$level].'","timestamp":'. - time().'}'; - $chunks = str_split($msg, strlen($msg)/8000); + $chunks = str_split($msg, 8000); break; case 'tcp': - $chunks[0] = '{"version":"'.self::$VERSION.'","host":"'. - $this->host.'","short_message":"'. - str_replace("\n", '\\n', '{'.$app.'} '.$message). - '","level":"'.self::$LEVELS[$level].'","timestamp":'. - time().'}'; + $chunks[0] = $msg; break; } $count = count($chunks); - $fp = fsockopen( - $this->protocol.'://'.$this->target, - $this->port, - $timeout = 5 + $errno = 0; + $errstr = ''; + $fp = stream_socket_client( + $this->protocol . '://' . $this->target . ':' . $this->port, + $errno, + $errstr, + 5 ); switch ($count > 1) { case true: $id = random_bytes(8); for ($i = 0; $i < $count; $i++) { - fwrite($fp, "\x1e\x0f".$id.$i.$count.$chunks[$i]."\0"); + fwrite($fp, pack('n', 0x1e0f) . $id . $i . $count . + $chunks[$i] . pack('x')); } break; case false: - fwrite($fp, $chunks[0].chr(0)); + fwrite($fp, $chunks[0]); break; } fclose($fp); } -} \ No newline at end of file +} diff --git a/tests/lib/Log/GraylogTest.php b/tests/lib/Log/GraylogTest.php index d058acc79cc99..c7a1233777930 100644 --- a/tests/lib/Log/GraylogTest.php +++ b/tests/lib/Log/GraylogTest.php @@ -23,8 +23,92 @@ namespace OC\tests\lib\Log; +use OC\Log\Graylog; +use OC\SystemConfig; use Test\TestCase; class GraylogTest extends TestCase { + /** @var string */ + private $graylog_method_restore; + /** @var string */ + private $graylog_host_restore; + /** @var SystemConfig */ + private $config; + /** @var string */ + private $buf; + /** @var string */ + private $from; + /** @var integer */ + private $port; + + protected function setUp() { + parent::setUp(); + $this->config = \OC::$server->getSystemConfig(); + $this->graylog_method_restore = $this->config->getValue('graylog_method'); + $this->graylog_host_restore = $this->config->getValue('graylog_host'); + $this->buf = ''; + $this->from = ''; + $this->port = 0; + } + + public function testUnchunkedUdp() { + $this->config->setValue('graylog_method', 'udp'); + $this->config->setValue('graylog_host', '127.0.0.1:5140'); + + // Create a mock server to send a test message to + $s = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP); + socket_bind($s, '127.0.0.1', 5140); + socket_set_nonblock($s); + + $graylog = new Graylog(\OC::$server->getConfig()); + $graylog->write('GraylogTest', 'UDP Graylog test < 8kb', 1); + + socket_recvfrom($s, $this->buf, 8000, 0, $this->from, $this->port); + socket_close($s); + + // The resulting GELF message has to be 130 characters long + $this->assertEquals(130, strlen($this->buf)); + } + + public function testChunkedUdp() { + $this->config->setValue('graylog_method', 'udp'); + $this->config->setValue('graylog_host', '127.0.0.1:5140'); + + // Create a mock server to send a test message to + $s = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP); + socket_bind($s, '127.0.0.1', 5140); + socket_set_nonblock($s); + + $graylog = new Graylog(\OC::$server->getConfig()); + $msg = "Very log message filled with garbage to execeed 8kb limit. "; + for($i = 0; $i < 8000; $i++) { + $msg .= "A"; + } + $graylog->write('GraylogTest', $msg, 3); + + socket_recvfrom($s, $this->buf, 8000, 0, $this->from, $this->port); + socket_close($s); + + // The first response should start with 0x1E 0x0F, has sequence 0 + // at position 10 and total count 2 at position 11 + $this->assertEquals(0x1e0f, unpack('n', $this->buf)[1]); + $this->assertEquals(0, intval(substr($this->buf, 10, 1))); + $this->assertEquals(2, intval(substr($this->buf, 11, 1))); + } + + protected function tearDown() { + if (isset($this->graylog_method_restore)) { + $this->config->setValue('graylog_method', $this->graylog_method_restore); + } else { + $this->config->deleteValue('graylog_method'); + } + if (isset($this->graylog_host_restore)) { + $this->config->setValue('graylog_host', $this->graylog_host_restore); + } else { + $this->config->deleteValue('graylog_host'); + } + parent::tearDown(); + } + } From 38caffe5a134840df8ca8afc0274aedb2e98f0a1 Mon Sep 17 00:00:00 2001 From: Thomas Pulzer Date: Mon, 25 Mar 2019 21:04:20 +0100 Subject: [PATCH 3/8] Renamed config key for Graylog protocol to use. Fixed wrong byte packing when sending chunked UDP packets. Improved test code and comments. Signed-off-by: Thomas Pulzer --- config/config.sample.php | 4 +- lib/private/Log/Graylog.php | 19 ++++---- tests/lib/Log/GraylogTest.php | 83 ++++++++++++++++++++++++----------- 3 files changed, 71 insertions(+), 35 deletions(-) diff --git a/config/config.sample.php b/config/config.sample.php index 7723923f491ec..58d36423215e7 100644 --- a/config/config.sample.php +++ b/config/config.sample.php @@ -786,7 +786,7 @@ /** * Your graylog host server name, for example ``localhost``, ``hostname``, * ``hostname.example.com``, or the IP address. To specify a port use - * ``hostname:####`` + * ``hostname:####``. The default port is 5410 * Only effective when ``log_type`` set to ``graylog`` */ 'graylog_host' => '', @@ -797,7 +797,7 @@ * * The default value is ``udp``. */ -'graylog_method' => 'udp', +'graylog_proto' => 'udp', /** * Log condition for log level increase based on conditions. Once one of these diff --git a/lib/private/Log/Graylog.php b/lib/private/Log/Graylog.php index c3fe59ff3f0da..77e1af9d7cdac 100644 --- a/lib/private/Log/Graylog.php +++ b/lib/private/Log/Graylog.php @@ -45,7 +45,7 @@ class Graylog implements IWriter { public function __construct(IConfig $config) { $this->host = gethostname(); - $this->protocol = $config->getSystemValue('graylog_method', 'udp'); + $this->protocol = $config->getSystemValue('graylog_proto', 'udp'); $address = $config->getSystemValue('graylog_host', ''); if (false !== strpos($address, ':')) { $this->target = explode(':', $address)[0]; @@ -72,27 +72,30 @@ public function write(string $app, $message, int $level) { time() . '}'; switch ($this->protocol) { case 'udp': - $chunks = str_split($msg, 8000); + $chunks = str_split($msg, 1024); break; case 'tcp': $chunks[0] = $msg; break; } $count = count($chunks); - $errno = 0; - $errstr = ''; + $errNo = 0; + $errStr = ''; $fp = stream_socket_client( $this->protocol . '://' . $this->target . ':' . $this->port, - $errno, - $errstr, + $errNo, + $errStr, 5 ); + if(false === $fp) { + return; + } switch ($count > 1) { case true: $id = random_bytes(8); for ($i = 0; $i < $count; $i++) { - fwrite($fp, pack('n', 0x1e0f) . $id . $i . $count . - $chunks[$i] . pack('x')); + fwrite($fp, pack('n', 0x1e0f) . $id . pack('CC', $i, $count) + . $chunks[$i]); } break; case false: diff --git a/tests/lib/Log/GraylogTest.php b/tests/lib/Log/GraylogTest.php index c7a1233777930..e51ceeb2f9e7c 100644 --- a/tests/lib/Log/GraylogTest.php +++ b/tests/lib/Log/GraylogTest.php @@ -30,9 +30,9 @@ class GraylogTest extends TestCase { /** @var string */ - private $graylog_method_restore; + private $protocol_restore; /** @var string */ - private $graylog_host_restore; + private $target_restore; /** @var SystemConfig */ private $config; /** @var string */ @@ -45,15 +45,15 @@ class GraylogTest extends TestCase { protected function setUp() { parent::setUp(); $this->config = \OC::$server->getSystemConfig(); - $this->graylog_method_restore = $this->config->getValue('graylog_method'); - $this->graylog_host_restore = $this->config->getValue('graylog_host'); + $this->protocol_restore = $this->config->getValue('graylog_proto'); + $this->target_restore = $this->config->getValue('graylog_host'); $this->buf = ''; $this->from = ''; $this->port = 0; } public function testUnchunkedUdp() { - $this->config->setValue('graylog_method', 'udp'); + $this->config->setValue('graylog_proto', 'udp'); $this->config->setValue('graylog_host', '127.0.0.1:5140'); // Create a mock server to send a test message to @@ -61,18 +61,22 @@ public function testUnchunkedUdp() { socket_bind($s, '127.0.0.1', 5140); socket_set_nonblock($s); + $id = 'GraylogTest'; + $msg = 'UDP Graylog test < 1kb'; $graylog = new Graylog(\OC::$server->getConfig()); - $graylog->write('GraylogTest', 'UDP Graylog test < 8kb', 1); + $graylog->write('GraylogTest', $msg, 1); - socket_recvfrom($s, $this->buf, 8000, 0, $this->from, $this->port); + socket_recvfrom($s, $this->buf, 1025, 0, $this->from, $this->port); socket_close($s); - // The resulting GELF message has to be 130 characters long - $this->assertEquals(130, strlen($this->buf)); + // The resulting GELF message has a length of 81 + length of host name + + // length of app name + length of log message + 3 formatting characters. + $expected = 81 + strlen(gethostname()) + strlen($msg) + strlen($id) + 3; + $this->assertEquals($expected, strlen($this->buf)); } public function testChunkedUdp() { - $this->config->setValue('graylog_method', 'udp'); + $this->config->setValue('graylog_proto', 'udp'); $this->config->setValue('graylog_host', '127.0.0.1:5140'); // Create a mock server to send a test message to @@ -80,31 +84,60 @@ public function testChunkedUdp() { socket_bind($s, '127.0.0.1', 5140); socket_set_nonblock($s); - $graylog = new Graylog(\OC::$server->getConfig()); - $msg = "Very log message filled with garbage to execeed 8kb limit. "; - for($i = 0; $i < 8000; $i++) { - $msg .= "A"; + $id = 'GraylogTest'; + $msg = "Very log message filled with garbage to exceed 1kb limit. "; + for($i = 0; $i < 1024; $i++) { + $msg .= "A"; } - $graylog->write('GraylogTest', $msg, 3); + $graylog = new Graylog(\OC::$server->getConfig()); + $graylog->write($id, $msg, 3); - socket_recvfrom($s, $this->buf, 8000, 0, $this->from, $this->port); + socket_recvfrom($s, $this->buf, 1034, 0, $this->from, $this->port); socket_close($s); - // The first response should start with 0x1E 0x0F, has sequence 0 - // at position 10 and total count 2 at position 11 + // The chunked GELF message must start start with 0x1E 0x0F, followed + // by 8 byte message id, 1 byte current sequence and 1 byte total chunk + // count. In this test the total chunk count is 2 and we examine the + // first chunk (zero-indexed). $this->assertEquals(0x1e0f, unpack('n', $this->buf)[1]); - $this->assertEquals(0, intval(substr($this->buf, 10, 1))); - $this->assertEquals(2, intval(substr($this->buf, 11, 1))); + $this->assertEquals(0, unpack('C', substr($this->buf, 10, 1))[1]); + $this->assertEquals(2, unpack('C', substr($this->buf, 11, 1))[1]); + } + + public function testTcp() { + $this->config->setValue('graylog_proto', 'tcp'); + $this->config->setValue('graylog_host', '127.0.0.1:5140'); + + // Create a mock server to send a test message to + $s = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); + socket_bind($s, '127.0.0.1', 5140); + socket_listen($s); + socket_set_nonblock($s); + + $id = 'GraylogTest'; + $msg = 'TCP Graylog test < 1kb'; + $graylog = new Graylog(\OC::$server->getConfig()); + $graylog->write($id, $msg, 3); + + $c = socket_accept($s); + $this->buf = socket_read($c, 1025); + socket_close($c); + socket_close($s); + + // The resulting GELF message has a length of 81 + length of host name + + // length of app name + length of log message + 3 formatting characters. + $expected = 81 + strlen(gethostname()) + strlen($msg) + strlen($id) + 3; + $this->assertEquals($expected, strlen($this->buf)); } protected function tearDown() { - if (isset($this->graylog_method_restore)) { - $this->config->setValue('graylog_method', $this->graylog_method_restore); + if (isset($this->protocol_restore)) { + $this->config->setValue('graylog_proto', $this->protocol_restore); } else { - $this->config->deleteValue('graylog_method'); + $this->config->deleteValue('graylog_proto'); } - if (isset($this->graylog_host_restore)) { - $this->config->setValue('graylog_host', $this->graylog_host_restore); + if (isset($this->target_restore)) { + $this->config->setValue('graylog_host', $this->target_restore); } else { $this->config->deleteValue('graylog_host'); } From b80ddd0523856d52b6bb0d112d07a6a516a60b40 Mon Sep 17 00:00:00 2001 From: Thomas Pulzer Date: Mon, 25 Mar 2019 21:28:29 +0100 Subject: [PATCH 4/8] Added log backend graylog to occ log:manage command. Signed-off-by: Thomas Pulzer --- core/Command/Log/Manage.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/Command/Log/Manage.php b/core/Command/Log/Manage.php index 5a1dd3d048b8e..a388e8993ea35 100644 --- a/core/Command/Log/Manage.php +++ b/core/Command/Log/Manage.php @@ -56,7 +56,8 @@ protected function configure() { 'backend', null, InputOption::VALUE_REQUIRED, - 'set the logging backend [file, syslog, errorlog, systemd]' + 'set the logging backend [file, syslog, errorlog, systemd, + graylog]' ) ->addOption( 'level', From f98ebeec81212196b068f901753a9399760ba287 Mon Sep 17 00:00:00 2001 From: Thomas Pulzer Date: Mon, 25 Mar 2019 21:52:16 +0100 Subject: [PATCH 5/8] Added ability to configure graylog remote host and connection protocol with occ Signed-off-by: Thomas Pulzer --- core/Command/Log/Manage.php | 46 +++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/core/Command/Log/Manage.php b/core/Command/Log/Manage.php index a388e8993ea35..d0cd07e618fd8 100644 --- a/core/Command/Log/Manage.php +++ b/core/Command/Log/Manage.php @@ -71,6 +71,18 @@ protected function configure() { InputOption::VALUE_REQUIRED, 'set the logging timezone' ) + ->addOption( + 'host', + null, + InputOption::VALUE_REQUIRED, + 'set the log server host:port if backend is graylog' + ) + ->addOption( + 'protocol', + null, + InputOption::VALUE_REQUIRED, + 'set the log server protocol if backend is graylog' + ) ; } @@ -100,6 +112,20 @@ protected function execute(InputInterface $input, OutputInterface $output) { $toBeSet['logtimezone'] = $timezone; } + if ($host = $input->getOption('host')) { + array_key_exists('log_type', $toBeSet) ? + $this->isBackendGraylogSet($toBeSet['log_type']) : + $this->isBackendGraylogSet(); + $toBeSet['graylog_host'] = $host; + } + + if ($protocol = $input->getOption('protocol')) { + array_key_exists('log_type', $toBeSet) ? + $this->isBackendGraylogSet($toBeSet['log_type']) : + $this->isBackendGraylogSet(); + $toBeSet['graylog_proto'] = $protocol; + } + // set config foreach ($toBeSet as $option => $value) { $this->config->setSystemValue($option, $value); @@ -109,6 +135,13 @@ protected function execute(InputInterface $input, OutputInterface $output) { $backend = $this->config->getSystemValue('log_type', self::DEFAULT_BACKEND); $output->writeln('Enabled logging backend: '.$backend); + if ('graylog' === $backend) { + $host = $this->config->getSystemValue('graylog_host'); + $output->writeln('Log server: '.$host); + $protocol = $this->config->getSystemValue('graylog_proto', 'udp'); + $output->writeln('Connection protocol: '.$protocol); + } + $levelNum = $this->config->getSystemValue('loglevel', self::DEFAULT_LOG_LEVEL); $level = $this->convertLevelNumber($levelNum); $output->writeln('Log level: '.$level.' ('.$levelNum.')'); @@ -127,6 +160,17 @@ protected function validateBackend($backend) { } } + /** + * @param string $new + * @throws \InvalidArgumentException + */ + protected function isBackendGraylogSet($new=null) { + $old = $this->config->getSystemValue('log_type', self::DEFAULT_BACKEND); + if ((null === $new && $old != 'graylog') || 'graylog' != $new) { + throw new \InvalidArgumentException('Graylog not set as backend'); + } + } + /** * @param string $timezone * @throws \Exception @@ -188,6 +232,8 @@ public function completeOptionValues($optionName, CompletionContext $context) { return ['debug', 'info', 'warning', 'error']; } else if ($optionName === 'timezone') { return \DateTimeZone::listIdentifiers(); + } else if ($optionName === 'protocol') { + return ['udp', 'tcp']; } return []; } From e675d3ab7e8c6aa11bc7f743ac1ec7deaaf242d9 Mon Sep 17 00:00:00 2001 From: Thomas Pulzer Date: Mon, 25 Mar 2019 22:06:00 +0100 Subject: [PATCH 6/8] Added test of the log:manage method if graylog backend is set. Signed-off-by: Thomas Pulzer --- tests/Core/Command/Log/ManageTest.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/Core/Command/Log/ManageTest.php b/tests/Core/Command/Log/ManageTest.php index 4b026f148475b..636b6b8465392 100644 --- a/tests/Core/Command/Log/ManageTest.php +++ b/tests/Core/Command/Log/ManageTest.php @@ -94,6 +94,13 @@ public function testValidateBackend() { self::invokePrivate($this->command, 'validateBackend', ['notabackend']); } + /** + * @expectedException \InvalidArgumentException + */ + public function testIsBackendGraylogSet() { + self::invokePrivate($this->command, 'isBackendGraylogSet', ['file']); + } + /** * @expectedException \Exception */ From 06aadc414ed57413e64140add0f90369be95ca28 Mon Sep 17 00:00:00 2001 From: Thomas Pulzer Date: Tue, 26 Mar 2019 08:16:17 +0100 Subject: [PATCH 7/8] Dropped constructing the GELF message manually in favour for json_encode This revealed former incorrect encoding of field 'level' (is nuber, but was encoded as string. Therefore, the test method had to be adjusted as well. Signed-off-by: Thomas Pulzer --- lib/private/Log/Graylog.php | 14 ++++++++------ tests/lib/Log/GraylogTest.php | 8 ++++---- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/lib/private/Log/Graylog.php b/lib/private/Log/Graylog.php index 77e1af9d7cdac..e0f7b509a999d 100644 --- a/lib/private/Log/Graylog.php +++ b/lib/private/Log/Graylog.php @@ -57,7 +57,7 @@ public function __construct(IConfig $config) { } /** - * sena a message to the Graylog server + * send a message to the Graylog server * * @param string $app * @param string $message @@ -65,11 +65,13 @@ public function __construct(IConfig $config) { */ public function write(string $app, $message, int $level) { $chunks = []; - $msg = '{"version":"' . self::$VERSION . '","host":"' . - $this->host . '","short_message":"' . - str_replace("\n", '\\n', '{' . $app . '} ' . $message) . - '","level":"' . self::$LEVELS[$level] . '","timestamp":' . - time() . '}'; + $msg = json_encode([ + 'version' => self::$VERSION, + 'host' => $this->host, + 'short_message' => '{'.$app.'} '.$message, + 'level' => self::$LEVELS[$level], + 'timestamp' => time() + ]); switch ($this->protocol) { case 'udp': $chunks = str_split($msg, 1024); diff --git a/tests/lib/Log/GraylogTest.php b/tests/lib/Log/GraylogTest.php index e51ceeb2f9e7c..4e4edddcd3405 100644 --- a/tests/lib/Log/GraylogTest.php +++ b/tests/lib/Log/GraylogTest.php @@ -69,9 +69,9 @@ public function testUnchunkedUdp() { socket_recvfrom($s, $this->buf, 1025, 0, $this->from, $this->port); socket_close($s); - // The resulting GELF message has a length of 81 + length of host name + + // The resulting GELF message has a length of 79 + length of host name + // length of app name + length of log message + 3 formatting characters. - $expected = 81 + strlen(gethostname()) + strlen($msg) + strlen($id) + 3; + $expected = 79 + strlen(gethostname()) + strlen($msg) + strlen($id) + 3; $this->assertEquals($expected, strlen($this->buf)); } @@ -124,9 +124,9 @@ public function testTcp() { socket_close($c); socket_close($s); - // The resulting GELF message has a length of 81 + length of host name + + // The resulting GELF message has a length of 79 + length of host name + // length of app name + length of log message + 3 formatting characters. - $expected = 81 + strlen(gethostname()) + strlen($msg) + strlen($id) + 3; + $expected = 79 + strlen(gethostname()) + strlen($msg) + strlen($id) + 3; $this->assertEquals($expected, strlen($this->buf)); } From c5a4be76a77261dfc9a9b60953804ab9dc63ba06 Mon Sep 17 00:00:00 2001 From: Thomas Pulzer Date: Tue, 26 Mar 2019 12:18:16 +0100 Subject: [PATCH 8/8] Updated autoloaders. Signed-off-by: Thomas Pulzer --- lib/composer/composer/autoload_classmap.php | 1 + lib/composer/composer/autoload_static.php | 1 + 2 files changed, 2 insertions(+) diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index a0fe19e011822..de3f5c8d55af6 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -922,6 +922,7 @@ 'OC\\Log\\Errorlog' => $baseDir . '/lib/private/Log/Errorlog.php', 'OC\\Log\\ExceptionSerializer' => $baseDir . '/lib/private/Log/ExceptionSerializer.php', 'OC\\Log\\File' => $baseDir . '/lib/private/Log/File.php', + 'OC\\Log\\Graylog' => $baseDir . '/lib/private/Log/Graylog.php', 'OC\\Log\\LogFactory' => $baseDir . '/lib/private/Log/LogFactory.php', 'OC\\Log\\Rotate' => $baseDir . '/lib/private/Log/Rotate.php', 'OC\\Log\\Syslog' => $baseDir . '/lib/private/Log/Syslog.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index 3dcca438cb67d..2a557d94f9313 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -952,6 +952,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OC\\Log\\Errorlog' => __DIR__ . '/../../..' . '/lib/private/Log/Errorlog.php', 'OC\\Log\\ExceptionSerializer' => __DIR__ . '/../../..' . '/lib/private/Log/ExceptionSerializer.php', 'OC\\Log\\File' => __DIR__ . '/../../..' . '/lib/private/Log/File.php', + 'OC\\Log\\Graylog' => __DIR__ . '/../../..' . '/lib/private/Log/Graylog.php', 'OC\\Log\\LogFactory' => __DIR__ . '/../../..' . '/lib/private/Log/LogFactory.php', 'OC\\Log\\Rotate' => __DIR__ . '/../../..' . '/lib/private/Log/Rotate.php', 'OC\\Log\\Syslog' => __DIR__ . '/../../..' . '/lib/private/Log/Syslog.php',