From 67536e71aa92ee9c71592b5e3b48831e7c20a71a Mon Sep 17 00:00:00 2001 From: Robin McCorkell Date: Mon, 17 Aug 2015 23:06:42 +0100 Subject: [PATCH 1/4] Introduce occ command for logging management log:manage can set/display the log backend, log level and log timezone --- core/command/log/manage.php | 171 ++++++++++++++++++++++++++++++++++++ core/register_command.php | 2 + 2 files changed, 173 insertions(+) create mode 100644 core/command/log/manage.php diff --git a/core/command/log/manage.php b/core/command/log/manage.php new file mode 100644 index 000000000000..f3d18cffca09 --- /dev/null +++ b/core/command/log/manage.php @@ -0,0 +1,171 @@ + + * + * @copyright Copyright (c) 2015, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ + +namespace OC\Core\Command\Log; + +use \OCP\IConfig; + +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; + +class Manage extends Command { + + const DEFAULT_BACKEND = 'owncloud'; + const DEFAULT_LOG_LEVEL = 2; + const DEFAULT_TIMEZONE = 'UTC'; + + /** @var IConfig */ + protected $config; + + public function __construct(IConfig $config) { + $this->config = $config; + parent::__construct(); + } + + protected function configure() { + $this + ->setName('log:manage') + ->setDescription('manage logging configuration') + ->addOption( + 'backend', + null, + InputOption::VALUE_REQUIRED, + 'set the logging backend [owncloud, syslog, errorlog]' + ) + ->addOption( + 'level', + null, + InputOption::VALUE_REQUIRED, + 'set the log level [debug, info, warning, error]' + ) + ->addOption( + 'timezone', + null, + InputOption::VALUE_REQUIRED, + 'set the logging timezone' + ) + ; + } + + protected function execute(InputInterface $input, OutputInterface $output) { + // collate config setting to the end, to avoid partial configuration + $toBeSet = []; + + if ($backend = $input->getOption('backend')) { + $this->validateBackend($backend); + $toBeSet['log_type'] = $backend; + } + + if ($level = $input->getOption('level')) { + if (is_numeric($level)) { + $levelNum = $level; + // sanity check + $this->convertLevelNumber($levelNum); + } else { + $levelNum = $this->convertLevelString($level); + } + $toBeSet['loglevel'] = $levelNum; + } + + if ($timezone = $input->getOption('timezone')) { + $this->validateTimezone($timezone); + $toBeSet['logtimezone'] = $timezone; + } + + // set config + foreach ($toBeSet as $option => $value) { + $this->config->setSystemValue($option, $value); + } + + // display configuration + $backend = $this->config->getSystemValue('log_type', self::DEFAULT_BACKEND); + $output->writeln('Enabled logging backend: '.$backend); + + $levelNum = $this->config->getSystemValue('loglevel', self::DEFAULT_LOG_LEVEL); + $level = $this->convertLevelNumber($levelNum); + $output->writeln('Log level: '.$level.' ('.$levelNum.')'); + + $timezone = $this->config->getSystemValue('logtimezone', self::DEFAULT_TIMEZONE); + $output->writeln('Log timezone: '.$timezone); + } + + /** + * @param string $backend + * @throws \InvalidArgumentException + */ + protected function validateBackend($backend) { + if (!class_exists('OC_Log_'.$backend)) { + throw new \InvalidArgumentException('Invalid backend'); + } + } + + /** + * @param string $timezone + * @throws \Exception + */ + protected function validateTimezone($timezone) { + new \DateTimeZone($timezone); + } + + /** + * @param string $level + * @return int + * @throws \InvalidArgumentException + */ + protected function convertLevelString($level) { + $level = strtolower($level); + switch ($level) { + case 'debug': + return 0; + case 'info': + return 1; + case 'warning': + case 'warn': + return 2; + case 'error': + case 'err': + return 3; + } + throw new \InvalidArgumentException('Invalid log level string'); + } + + /** + * @param int $levelNum + * @return string + * @throws \InvalidArgumentException + */ + protected function convertLevelNumber($levelNum) { + switch ($levelNum) { + case 0: + return 'Debug'; + case 1: + return 'Info'; + case 2: + return 'Warning'; + case 3: + return 'Error'; + } + throw new \InvalidArgumentException('Invalid log level number'); + } +} diff --git a/core/register_command.php b/core/register_command.php index 6cd81b4c3b78..b05d0f3aebbd 100644 --- a/core/register_command.php +++ b/core/register_command.php @@ -58,6 +58,8 @@ $application->add(new OC\Core\Command\Encryption\SetDefaultModule(\OC::$server->getEncryptionManager())); $application->add(new OC\Core\Command\Encryption\Status(\OC::$server->getEncryptionManager())); + $application->add(new OC\Core\Command\Log\Manage(\OC::$server->getConfig())); + $application->add(new OC\Core\Command\Maintenance\MimeTypesJS()); $application->add(new OC\Core\Command\Maintenance\Mode(\OC::$server->getConfig())); $application->add(new OC\Core\Command\Maintenance\Repair(new \OC\Repair(\OC\Repair::getRepairSteps()), \OC::$server->getConfig())); From 07e119a3f8e660594bbab19246fcbf674c61bda1 Mon Sep 17 00:00:00 2001 From: Robin McCorkell Date: Mon, 17 Aug 2015 23:16:39 +0100 Subject: [PATCH 2/4] Introduce occ command to manage owncloud log backend log:owncloud can set/display the log filename and log file rotation size --- core/command/log/owncloud.php | 124 ++++++++++++++++++++++++++++++++++ core/register_command.php | 1 + 2 files changed, 125 insertions(+) create mode 100644 core/command/log/owncloud.php diff --git a/core/command/log/owncloud.php b/core/command/log/owncloud.php new file mode 100644 index 000000000000..a2d9e4bc7c85 --- /dev/null +++ b/core/command/log/owncloud.php @@ -0,0 +1,124 @@ + + * + * @copyright Copyright (c) 2015, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ + +namespace OC\Core\Command\Log; + +use \OCP\IConfig; + +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; + +class OwnCloud extends Command { + + /** @var IConfig */ + protected $config; + + public function __construct(IConfig $config) { + $this->config = $config; + parent::__construct(); + } + + protected function configure() { + $this + ->setName('log:owncloud') + ->setDescription('manipulate ownCloud logging backend') + ->addOption( + 'enable', + null, + InputOption::VALUE_NONE, + 'enable this logging backend' + ) + ->addOption( + 'file', + null, + InputOption::VALUE_REQUIRED, + 'set the log file path' + ) + ->addOption( + 'rotate-size', + null, + InputOption::VALUE_REQUIRED, + 'set the file size for log rotation, 0 = disabled' + ) + ; + } + + protected function execute(InputInterface $input, OutputInterface $output) { + $toBeSet = []; + + if ($input->getOption('enable')) { + $toBeSet['log_type'] = 'owncloud'; + } + + if ($file = $input->getOption('file')) { + $toBeSet['logfile'] = $file; + } + + if (($rotateSize = $input->getOption('rotate-size')) !== null) { + $rotateSize = \OCP\Util::computerFileSize($rotateSize); + $this->validateRotateSize($rotateSize); + $toBeSet['log_rotate_size'] = $rotateSize; + } + + // set config + foreach ($toBeSet as $option => $value) { + $this->config->setSystemValue($option, $value); + } + + // display config + if ($this->config->getSystemValue('log_type', 'owncloud') === 'owncloud') { + $enabledText = 'enabled'; + } else { + $enabledText = 'disabled'; + } + $output->writeln('Log backend ownCloud: '.$enabledText); + + $dataDir = $this->config->getSystemValue('datadirectory', \OC::$SERVERROOT.'/data'); + $defaultLogFile = rtrim($dataDir, '/').'/owncloud.log'; + $output->writeln('Log file: '.$this->config->getSystemValue('logfile', $defaultLogFile)); + + $rotateSize = $this->config->getSystemValue('log_rotate_size', 0); + if ($rotateSize) { + $rotateString = \OCP\Util::humanFileSize($rotateSize); + } else { + $rotateString = 'disabled'; + } + $output->writeln('Rotate at: '.$rotateString); + } + + /** + * @param mixed $rotateSize + * @throws \InvalidArgumentException + */ + protected function validateRotateSize(&$rotateSize) { + if ($rotateSize === false) { + throw new \InvalidArgumentException('Error parsing log rotation file size'); + } + $rotateSize = (int) $rotateSize; + if ($rotateSize < 0) { + throw new \InvalidArgumentException('Log rotation file size must be non-negative'); + } + } + +} diff --git a/core/register_command.php b/core/register_command.php index b05d0f3aebbd..9c13c0967f8f 100644 --- a/core/register_command.php +++ b/core/register_command.php @@ -59,6 +59,7 @@ $application->add(new OC\Core\Command\Encryption\Status(\OC::$server->getEncryptionManager())); $application->add(new OC\Core\Command\Log\Manage(\OC::$server->getConfig())); + $application->add(new OC\Core\Command\Log\OwnCloud(\OC::$server->getConfig())); $application->add(new OC\Core\Command\Maintenance\MimeTypesJS()); $application->add(new OC\Core\Command\Maintenance\Mode(\OC::$server->getConfig())); From 2663f12dc754dd55f8d8a349e992cd0ffa13e02c Mon Sep 17 00:00:00 2001 From: Robin McCorkell Date: Wed, 19 Aug 2015 12:55:39 +0100 Subject: [PATCH 3/4] config.sample.php logging documentation clarification --- config/config.sample.php | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/config/config.sample.php b/config/config.sample.php index 047e7ccdd120..70a58f4d5ea3 100644 --- a/config/config.sample.php +++ b/config/config.sample.php @@ -493,9 +493,10 @@ 'log_type' => 'owncloud', /** - * Change the ownCloud logfile name from ``owncloud.log`` to something else. + * Log file path for the ownCloud logging type. + * Defaults to ``[datadirectory]/owncloud.log`` */ -'logfile' => 'owncloud.log', +'logfile' => '/var/log/owncloud.log', /** * Loglevel to start logging at. Valid values are: 0 = Debug, 1 = Info, 2 = @@ -561,8 +562,8 @@ * Enables log rotation and limits the total size of logfiles. The default is 0, * or no rotation. Specify a size in bytes, for example 104857600 (100 megabytes * = 100 * 1024 * 1024 bytes). A new logfile is created with a new name when the - * old logfile reaches your limit. The total size of all logfiles is double the - * ``log_rotate_sizerotation`` value. + * old logfile reaches your limit. If a rotated log file is already present, it + * will be overwritten. */ 'log_rotate_size' => false, From c693e5d5d1f35834fd27693c140ad8c81a4a5aaf Mon Sep 17 00:00:00 2001 From: Robin McCorkell Date: Wed, 19 Aug 2015 13:49:41 +0100 Subject: [PATCH 4/4] Unit tests for occ log:* commands --- tests/core/command/log/managetest.php | 181 ++++++++++++++++++++++++ tests/core/command/log/owncloudtest.php | 121 ++++++++++++++++ 2 files changed, 302 insertions(+) create mode 100644 tests/core/command/log/managetest.php create mode 100644 tests/core/command/log/owncloudtest.php diff --git a/tests/core/command/log/managetest.php b/tests/core/command/log/managetest.php new file mode 100644 index 000000000000..6fb83347f238 --- /dev/null +++ b/tests/core/command/log/managetest.php @@ -0,0 +1,181 @@ + + * + * @copyright Copyright (c) 2015, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ + +namespace Tests\Core\Command\Log; + + +use OC\Core\Command\Log\Manage; +use Test\TestCase; + +class ManageTest extends TestCase { + /** @var \PHPUnit_Framework_MockObject_MockObject */ + protected $config; + /** @var \PHPUnit_Framework_MockObject_MockObject */ + protected $consoleInput; + /** @var \PHPUnit_Framework_MockObject_MockObject */ + protected $consoleOutput; + + /** @var \Symfony\Component\Console\Command\Command */ + protected $command; + + protected function setUp() { + parent::setUp(); + + $config = $this->config = $this->getMockBuilder('OCP\IConfig') + ->disableOriginalConstructor() + ->getMock(); + $this->consoleInput = $this->getMock('Symfony\Component\Console\Input\InputInterface'); + $this->consoleOutput = $this->getMock('Symfony\Component\Console\Output\OutputInterface'); + + $this->command = new Manage($config); + } + + public function testChangeBackend() { + $this->consoleInput->method('getOption') + ->will($this->returnValueMap([ + ['backend', 'syslog'] + ])); + $this->config->expects($this->once()) + ->method('setSystemValue') + ->with('log_type', 'syslog'); + + self::invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput]); + } + + public function testChangeLevel() { + $this->consoleInput->method('getOption') + ->will($this->returnValueMap([ + ['level', 'debug'] + ])); + $this->config->expects($this->once()) + ->method('setSystemValue') + ->with('loglevel', 0); + + self::invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput]); + } + + public function testChangeTimezone() { + $this->consoleInput->method('getOption') + ->will($this->returnValueMap([ + ['timezone', 'UTC'] + ])); + $this->config->expects($this->once()) + ->method('setSystemValue') + ->with('logtimezone', 'UTC'); + + self::invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput]); + } + + /** + * @expectedException \InvalidArgumentException + */ + public function testValidateBackend() { + self::invokePrivate($this->command, 'validateBackend', ['notabackend']); + } + + /** + * @expectedException \Exception + */ + public function testValidateTimezone() { + // this might need to be changed when humanity colonises Mars + self::invokePrivate($this->command, 'validateTimezone', ['Mars/OlympusMons']); + } + + public function convertLevelStringProvider() { + return [ + ['dEbug', 0], + ['inFO', 1], + ['Warning', 2], + ['wArn', 2], + ['error', 3], + ['eRr', 3], + ]; + } + + /** + * @dataProvider convertLevelStringProvider + */ + public function testConvertLevelString($levelString, $expectedInt) { + $this->assertEquals($expectedInt, + self::invokePrivate($this->command, 'convertLevelString', [$levelString]) + ); + } + + /** + * @expectedException \InvalidArgumentException + */ + public function testConvertLevelStringInvalid() { + self::invokePrivate($this->command, 'convertLevelString', ['abc']); + } + + public function convertLevelNumberProvider() { + return [ + [0, 'Debug'], + [1, 'Info'], + [2, 'Warning'], + [3, 'Error'], + ]; + } + + /** + * @dataProvider convertLevelNumberProvider + */ + public function testConvertLevelNumber($levelNum, $expectedString) { + $this->assertEquals($expectedString, + self::invokePrivate($this->command, 'convertLevelNumber', [$levelNum]) + ); + } + + /** + * @expectedException \InvalidArgumentException + */ + public function testConvertLevelNumberInvalid() { + self::invokePrivate($this->command, 'convertLevelNumber', [11]); + } + + public function testGetConfiguration() { + $this->config->expects($this->at(0)) + ->method('getSystemValue') + ->with('log_type', 'owncloud') + ->willReturn('log_type_value'); + $this->config->expects($this->at(1)) + ->method('getSystemValue') + ->with('loglevel', 2) + ->willReturn(0); + $this->config->expects($this->at(2)) + ->method('getSystemValue') + ->with('logtimezone', 'UTC') + ->willReturn('logtimezone_value'); + + $this->consoleOutput->expects($this->at(0)) + ->method('writeln') + ->with('Enabled logging backend: log_type_value'); + $this->consoleOutput->expects($this->at(1)) + ->method('writeln') + ->with('Log level: Debug (0)'); + $this->consoleOutput->expects($this->at(2)) + ->method('writeln') + ->with('Log timezone: logtimezone_value'); + + self::invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput]); + } + +} diff --git a/tests/core/command/log/owncloudtest.php b/tests/core/command/log/owncloudtest.php new file mode 100644 index 000000000000..3cb05221c37c --- /dev/null +++ b/tests/core/command/log/owncloudtest.php @@ -0,0 +1,121 @@ + + * + * @copyright Copyright (c) 2015, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ + +namespace Tests\Core\Command\Log; + + +use OC\Core\Command\Log\OwnCloud; +use Test\TestCase; + +class OwnCloudTest extends TestCase { + /** @var \PHPUnit_Framework_MockObject_MockObject */ + protected $config; + /** @var \PHPUnit_Framework_MockObject_MockObject */ + protected $consoleInput; + /** @var \PHPUnit_Framework_MockObject_MockObject */ + protected $consoleOutput; + + /** @var \Symfony\Component\Console\Command\Command */ + protected $command; + + protected function setUp() { + parent::setUp(); + + $config = $this->config = $this->getMockBuilder('OCP\IConfig') + ->disableOriginalConstructor() + ->getMock(); + $this->consoleInput = $this->getMock('Symfony\Component\Console\Input\InputInterface'); + $this->consoleOutput = $this->getMock('Symfony\Component\Console\Output\OutputInterface'); + + $this->command = new OwnCloud($config); + } + + public function testEnable() { + $this->consoleInput->method('getOption') + ->will($this->returnValueMap([ + ['enable', 'true'] + ])); + $this->config->expects($this->once()) + ->method('setSystemValue') + ->with('log_type', 'owncloud'); + + self::invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput]); + } + + public function testChangeFile() { + $this->consoleInput->method('getOption') + ->will($this->returnValueMap([ + ['file', '/foo/bar/file.log'] + ])); + $this->config->expects($this->once()) + ->method('setSystemValue') + ->with('logfile', '/foo/bar/file.log'); + + self::invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput]); + } + + public function changeRotateSizeProvider() { + return [ + ['42', 42], + ['0', 0], + ['1 kB', 1024], + ['5MB', 5 * 1024 * 1024], + ]; + } + + /** + * @dataProvider changeRotateSizeProvider + */ + public function testChangeRotateSize($optionValue, $configValue) { + $this->consoleInput->method('getOption') + ->will($this->returnValueMap([ + ['rotate-size', $optionValue] + ])); + $this->config->expects($this->once()) + ->method('setSystemValue') + ->with('log_rotate_size', $configValue); + + self::invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput]); + } + + public function testGetConfiguration() { + $this->config->method('getSystemValue') + ->will($this->returnValueMap([ + ['log_type', 'owncloud', 'log_type_value'], + ['datadirectory', \OC::$SERVERROOT.'/data', '/data/directory/'], + ['logfile', '/data/directory/owncloud.log', '/var/log/owncloud.log'], + ['log_rotate_size', 0, 5 * 1024 * 1024], + ])); + + $this->consoleOutput->expects($this->at(0)) + ->method('writeln') + ->with('Log backend ownCloud: disabled'); + $this->consoleOutput->expects($this->at(1)) + ->method('writeln') + ->with('Log file: /var/log/owncloud.log'); + $this->consoleOutput->expects($this->at(2)) + ->method('writeln') + ->with('Rotate at: 5 MB'); + + self::invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput]); + } + +}