diff --git a/config/config.sample.php b/config/config.sample.php index 45aaf6a107..9a40f82252 100644 --- a/config/config.sample.php +++ b/config/config.sample.php @@ -486,6 +486,27 @@ $CONFIG = array( */ 'loglevel' => 2, +/** + * 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 + * debug specific requests, users or apps + * + * Supported conditions: + * - ``shared_secret``: if a request parameter with the name `log_secret` is set to + * this value the condition is met + * - ``users``: if the current request is done by one of the specified users, + * this condition is met + * - ``apps``: if the log message is invoked by one of the specified apps, + * this condition is met + * + * Defaults to an empty array. + */ +'log.condition' => [ + 'shared_secret' => '57b58edb6637fe3059b3595cf9c41b9', + 'users' => ['sample-user'], + 'apps' => ['files'], +], + /** * This uses PHP.date formatting; see http://php.net/manual/en/function.date.php */ diff --git a/lib/private/log.php b/lib/private/log.php index 56f36787e0..840713b2ed 100644 --- a/lib/private/log.php +++ b/lib/private/log.php @@ -28,6 +28,7 @@ namespace OC; use \OCP\ILogger; +use OCP\Security\StringUtils; /** * logging utilities @@ -41,15 +42,29 @@ use \OCP\ILogger; class Log implements ILogger { + /** @var string */ private $logger; + /** @var SystemConfig */ + private $config; + + /** @var boolean|null cache the result of the log condition check for the request */ + private $logConditionSatisfied = null; /** * @param string $logger The logger that should be used + * @param SystemConfig $config the system config object */ - public function __construct($logger=null) { + public function __construct($logger=null, SystemConfig $config=null) { + // FIXME: Add this for backwards compatibility, should be fixed at some point probably + if($config === null) { + $config = \OC::$server->getSystemConfig(); + } + + $this->config = $config; + // FIXME: Add this for backwards compatibility, should be fixed at some point probably if($logger === null) { - $this->logger = 'OC_Log_'.ucfirst(\OC_Config::getValue('log_type', 'owncloud')); + $this->logger = 'OC_Log_'.ucfirst($this->config->getValue('log_type', 'owncloud')); call_user_func(array($this->logger, 'init')); } else { $this->logger = $logger; @@ -158,8 +173,22 @@ class Log implements ILogger { * @param array $context */ public function log($level, $message, array $context = array()) { + $minLevel = min($this->config->getValue('loglevel', \OC_Log::WARN), \OC_Log::ERROR); + $logCondition = $this->config->getValue('log.condition', []); + if (isset($context['app'])) { $app = $context['app']; + + /** + * check log condition based on the context of each log message + * once this is met -> change the required log level to debug + */ + if(!empty($logCondition) + && isset($logCondition['apps']) + && in_array($app, $logCondition['apps'], true)) { + $minLevel = \OC_Log::DEBUG; + } + } else { $app = 'no app in context'; } @@ -172,7 +201,45 @@ class Log implements ILogger { // interpolate replacement values into the message and return $message = strtr($message, $replace); - $logger = $this->logger; - call_user_func(array($logger, 'write'), $app, $message, $level); + /** + * check for a special log condition - this enables an increased log on + * a per request/user base + */ + if($this->logConditionSatisfied === null) { + // default to false to just process this once per request + $this->logConditionSatisfied = false; + if(!empty($logCondition)) { + + // check for secret token in the request + if(isset($logCondition['shared_secret'])) { + $request = \OC::$server->getRequest(); + + // if token is found in the request change set the log condition to satisfied + if($request && StringUtils::equals($request->getParam('log_secret'), $logCondition['shared_secret'])) { + $this->logConditionSatisfied = true; + } + } + + // check for user + if(isset($logCondition['users'])) { + $user = \OC::$server->getUserSession()->getUser(); + + // if the user matches set the log condition to satisfied + if($user !== null && in_array($user->getUID(), $logCondition['users'], true)) { + $this->logConditionSatisfied = true; + } + } + } + } + + // if log condition is satisfied change the required log level to DEBUG + if($this->logConditionSatisfied) { + $minLevel = \OC_Log::DEBUG; + } + + if ($level >= $minLevel) { + $logger = $this->logger; + call_user_func(array($logger, 'write'), $app, $message, $level); + } } } diff --git a/lib/private/log/errorlog.php b/lib/private/log/errorlog.php index 007ab30772..ad3605136d 100644 --- a/lib/private/log/errorlog.php +++ b/lib/private/log/errorlog.php @@ -39,10 +39,7 @@ class OC_Log_Errorlog { * @param int $level */ public static function write($app, $message, $level) { - $minLevel = min(OC_Config::getValue("loglevel", OC_Log::WARN), OC_Log::ERROR); - if ($level >= $minLevel) { - error_log('[owncloud]['.$app.'] '.$message); - } + error_log('[owncloud]['.$app.']['.$level.'] '.$message); } } diff --git a/lib/private/log/owncloud.php b/lib/private/log/owncloud.php index 049b8dbdfa..0125164394 100644 --- a/lib/private/log/owncloud.php +++ b/lib/private/log/owncloud.php @@ -69,40 +69,40 @@ class OC_Log_Owncloud { * @param int $level */ public static function write($app, $message, $level) { - $minLevel=min(OC_Config::getValue( "loglevel", OC_Log::WARN ), OC_Log::ERROR); - if($level>=$minLevel) { - // default to ISO8601 - $format = OC_Config::getValue('logdateformat', 'c'); - $logtimezone=OC_Config::getValue( "logtimezone", 'UTC' ); - try { - $timezone = new DateTimeZone($logtimezone); - } catch (Exception $e) { - $timezone = new DateTimeZone('UTC'); - } - $time = new DateTime(null, $timezone); - $request = \OC::$server->getRequest(); - $reqId = $request->getId(); - $remoteAddr = $request->getRemoteAddress(); - // remove username/passwords from URLs before writing the to the log file - $time = $time->format($format); - if($minLevel == OC_Log::DEBUG) { - $url = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '--'; - $method = isset($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] : '--'; - $entry = compact('reqId', 'remoteAddr', 'app', 'message', 'level', 'time', 'method', 'url'); - } - else { - $entry = compact('reqId', 'remoteAddr', 'app', 'message', 'level', 'time'); - } - $entry = json_encode($entry); - $handle = @fopen(self::$logFile, 'a'); - @chmod(self::$logFile, 0640); - if ($handle) { - fwrite($handle, $entry."\n"); - fclose($handle); - } else { - // Fall back to error_log - error_log($entry); - } + $config = \OC::$server->getSystemConfig(); + + // default to ISO8601 + $format = $config->getValue('logdateformat', 'c'); + $logtimezone = $config->getValue( "logtimezone", 'UTC' ); + try { + $timezone = new DateTimeZone($logtimezone); + } catch (Exception $e) { + $timezone = new DateTimeZone('UTC'); + } + $time = new DateTime(null, $timezone); + $request = \OC::$server->getRequest(); + $reqId = $request->getId(); + $remoteAddr = $request->getRemoteAddress(); + // remove username/passwords from URLs before writing the to the log file + $time = $time->format($format); + $minLevel=min($config->getValue( "loglevel", OC_Log::WARN ), OC_Log::ERROR); + if($minLevel == OC_Log::DEBUG) { + $url = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '--'; + $method = isset($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] : '--'; + $entry = compact('reqId', 'remoteAddr', 'app', 'message', 'level', 'time', 'method', 'url'); + } + else { + $entry = compact('reqId', 'remoteAddr', 'app', 'message', 'level', 'time'); + } + $entry = json_encode($entry); + $handle = @fopen(self::$logFile, 'a'); + @chmod(self::$logFile, 0640); + if ($handle) { + fwrite($handle, $entry."\n"); + fclose($handle); + } else { + // Fall back to error_log + error_log($entry); } } diff --git a/lib/private/log/syslog.php b/lib/private/log/syslog.php index 1763e6de6d..8595b707d5 100644 --- a/lib/private/log/syslog.php +++ b/lib/private/log/syslog.php @@ -47,10 +47,7 @@ class OC_Log_Syslog { * @param int $level */ public static function write($app, $message, $level) { - $minLevel = min(OC_Config::getValue("loglevel", OC_Log::WARN), OC_Log::ERROR); - if ($level >= $minLevel) { - $syslog_level = self::$levels[$level]; - syslog($syslog_level, '{'.$app.'} '.$message); - } + $syslog_level = self::$levels[$level]; + syslog($syslog_level, '{'.$app.'} '.$message); } } diff --git a/tests/lib/logger.php b/tests/lib/logger.php index 700a847917..9a9f5be12f 100644 --- a/tests/lib/logger.php +++ b/tests/lib/logger.php @@ -21,14 +21,38 @@ class Logger extends TestCase { parent::setUp(); self::$logs = array(); - $this->logger = new Log('Test\Logger'); + $this->config = $this->getMockBuilder( + '\OC\SystemConfig') + ->disableOriginalConstructor() + ->getMock(); + $this->logger = new Log('Test\Logger', $this->config); } public function testInterpolation() { $logger = $this->logger; - $logger->info('{Message {nothing} {user} {foo.bar} a}', array('user' => 'Bob', 'foo.bar' => 'Bar')); + $logger->warning('{Message {nothing} {user} {foo.bar} a}', array('user' => 'Bob', 'foo.bar' => 'Bar')); - $expected = array('1 {Message {nothing} Bob Bar a}'); + $expected = array('2 {Message {nothing} Bob Bar a}'); + $this->assertEquals($expected, $this->getLogs()); + } + + public function testAppCondition() { + $this->config->expects($this->any()) + ->method('getValue') + ->will(($this->returnValueMap([ + ['loglevel', \OC_Log::WARN, \OC_Log::WARN], + ['log.condition', [], ['apps' => ['files']]] + ]))); + $logger = $this->logger; + + $logger->info('Don\'t display info messages'); + $logger->info('Show info messages of files app', ['app' => 'files']); + $logger->warning('Show warning messages of other apps'); + + $expected = [ + '1 Show info messages of files app', + '2 Show warning messages of other apps', + ]; $this->assertEquals($expected, $this->getLogs()); }