diff --git a/core/config.class.inc.php b/core/config.class.inc.php index b39f50547..e2a251fb2 100644 --- a/core/config.class.inc.php +++ b/core/config.class.inc.php @@ -99,6 +99,14 @@ class Config * @since 2.7.0 export_pdf_font param */ protected $m_aSettings = array( + 'log_level_min' => array( + 'type' => 'array', + 'description' => 'Optional min log level per channel', + 'default' => '', + 'value' => '', + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), 'app_env_label' => array( 'type' => 'string', 'description' => 'Label displayed to describe the current application environment, defaults to the environment name (e.g. "production")', diff --git a/core/log.class.inc.php b/core/log.class.inc.php index b57c6ec7b..86100ffb9 100644 --- a/core/log.class.inc.php +++ b/core/log.class.inc.php @@ -199,28 +199,36 @@ class FileLog } } - public function Error($sText) + public function Error($sText, $sChannel = '', $aContext = array()) { - $this->Write('Error | '.$sText); + $this->Write($sText, __FUNCTION__, $sChannel, $aContext); } - public function Warning($sText) + public function Warning($sText, $sChannel = '', $aContext = array()) { - $this->Write('Warning | '.$sText); + $this->Write($sText, __FUNCTION__, $sChannel, $aContext); } - public function Info($sText) + public function Info($sText, $sChannel = '', $aContext = array()) { - $this->Write('Info | '.$sText); + $this->Write($sText, __FUNCTION__, $sChannel, $aContext); } - public function Ok($sText) + public function Ok($sText, $sChannel = '', $aContext = array()) { - $this->Write('Ok | '.$sText); + $this->Write($sText, __FUNCTION__, $sChannel, $aContext); } - protected function Write($sText) + public function Debug($sText, $sChannel = '', $aContext = array()) { + $this->Write($sText, __FUNCTION__, $sChannel, $aContext); + } + + protected function Write($sText, $sLevel = '', $sChannel = '', $aContext = array()) + { + $sTextPrefix = empty($sLevel) ? '' : (str_pad($sLevel, 7).' | '); + $sTextSuffix = empty($sChannel) ? '' : " | $sChannel"; + $sText = "{$sTextPrefix}{$sText}{$sTextSuffix}"; $sLogFilePath = $this->oFileNameBuilder->GetLogFilePath(); if (empty($sLogFilePath)) @@ -233,6 +241,15 @@ class FileLog { flock($hLogFile, LOCK_EX); $sDate = date('Y-m-d H:i:s'); + if (empty($aContext)) + { + fwrite($hLogFile, "$sDate | $sText\n"); + } + else + { + $sContext = var_export($aContext, true); + fwrite($hLogFile, "$sDate | $sText\n$sContext\n"); + } fwrite($hLogFile, "$sDate | $sText\n"); fflush($hLogFile); flock($hLogFile, LOCK_UN); @@ -243,53 +260,161 @@ class FileLog abstract class LogAPI { + const CHANNEL_DEFAULT = ''; + + const LEVEL_DEBUG = 'Debug'; + const LEVEL_OK = 'Ok'; + const LEVEL_INFO = 'Info'; + const LEVEL_WARNING = 'Warning'; + const LEVEL_ERROR = 'Error'; +// const LEVEL_CRITICAL = 'Critical'; +// const LEVEL_ALERT = 'Alert'; +// const LEVEL_EMERGENCY = 'Emergency'; + + protected static $m_oMockMetaModelConfig = null; + + protected static $aLevelsPriority = array( + self::LEVEL_DEBUG => 100, + self::LEVEL_OK => 150, + self::LEVEL_INFO => 200, + self::LEVEL_WARNING => 300, + self::LEVEL_ERROR => 400, + ); + public static function Enable($sTargetFile) { // m_oFileLog is not defined as a class attribute so that each impl will have its own static::$m_oFileLog = new FileLog($sTargetFile); } - public static function Error($sText) + public static function MockStaticObjects($oFileLog, $oMetaModelConfig=null) { - if (static::$m_oFileLog) - { - static::$m_oFileLog->Error($sText); - } + static::$m_oFileLog = $oFileLog; + static::$m_oMockMetaModelConfig = $oMetaModelConfig; } - public static function Warning($sText) + + public static function Error($sMessage, $sChannel = null, $aContext = array()) { - if (static::$m_oFileLog) - { - static::$m_oFileLog->Warning($sText); - } + static::Log(self::LEVEL_ERROR, $sMessage, $sChannel, $aContext); } - public static function Info($sText) + + public static function Warning($sMessage, $sChannel = null, $aContext = array()) { - if (static::$m_oFileLog) - { - static::$m_oFileLog->Info($sText); - } + static::Log(self::LEVEL_WARNING, $sMessage, $sChannel, $aContext); } - public static function Ok($sText) + + public static function Info($sMessage, $sChannel = null, $aContext = array()) { - if (static::$m_oFileLog) - { - static::$m_oFileLog->Ok($sText); - } + static::Log(self::LEVEL_INFO, $sMessage, $sChannel, $aContext); } + + public static function Ok($sMessage, $sChannel = null, $aContext = array()) + { + static::Log(self::LEVEL_OK, $sMessage, $sChannel, $aContext); + } + + public static function Debug($sMessage, $sChannel = null, $aContext = array()) + { + static::Log(self::LEVEL_DEBUG, $sMessage, $sChannel, $aContext); + } + + public static function Log($sLevel, $sMessage, $sChannel = null, $aContext = array()) + { + if (! static::$m_oFileLog) + { + return; + } + + if (! isset(self::$aLevelsPriority[$sLevel])) + { + IssueLog::Error("invalid log level '{$sLevel}'"); + return; + } + + if (is_null($sChannel)) + { + $sChannel = static::CHANNEL_DEFAULT; + } + + $sMinLogLevel = self::GetMinLogLevel($sChannel); + + if ($sMinLogLevel === false || $sMinLogLevel === 'false') + { + return; + } + if (is_string($sMinLogLevel)) + { + if (! isset(self::$aLevelsPriority[$sMinLogLevel])) + { + throw new Exception("invalid configuration for log_level '{$sMinLogLevel}' is not within the list: ".implode(',', array_keys(self::$aLevelsPriority))); + } + elseif (self::$aLevelsPriority[$sLevel] < self::$aLevelsPriority[$sMinLogLevel]) + { + //priority too low regarding the conf, do not log this + return; + } + } + + static::$m_oFileLog->$sLevel($sMessage, $sChannel, $aContext); + } + + /** + * @param $sChannel + * + * @return mixed|null + */ + private static function GetMinLogLevel($sChannel) + { + $oConfig = (static::$m_oMockMetaModelConfig !== null) ? static::$m_oMockMetaModelConfig : \MetaModel::GetConfig(); + if (!$oConfig instanceof Config) + { + return self::LEVEL_OK; + } + + $sLogLevelMin = $oConfig->Get('log_level_min'); + + if (empty($sLogLevelMin)) + { + return self::LEVEL_OK; + } + + if (!is_array($sLogLevelMin)) + { + return $sLogLevelMin; + } + + if (isset($sLogLevelMin[$sChannel])) + { + return $sLogLevelMin[$sChannel]; + } + + if (isset($sLogLevelMin[static::CHANNEL_DEFAULT])) + { + return $sLogLevelMin[$sChannel]; + } + + return self::LEVEL_OK; + } + } class SetupLog extends LogAPI { + const CHANNEL_DEFAULT = 'SetupLog'; + protected static $m_oFileLog = null; } class IssueLog extends LogAPI { + const CHANNEL_DEFAULT = 'IssueLog'; + protected static $m_oFileLog = null; } class ToolsLog extends LogAPI { + const CHANNEL_DEFAULT = 'ToolsLog'; + protected static $m_oFileLog = null; } diff --git a/test/core/LogAPITest.php b/test/core/LogAPITest.php new file mode 100644 index 000000000..8eefd7de3 --- /dev/null +++ b/test/core/LogAPITest.php @@ -0,0 +1,161 @@ +mockFileLog = $this->createMock('FileLog'); + $this->oMetaModelConfig = $this->createMock('Config'); + } + + + /** + * @dataProvider LogApiProvider + * @test + * @backupGlobals disabled + */ + public function TestLogApi($oConfigObject, $sMessage, $Channel, $sExpectedLevel, $sExpectedMessage, $sExpectedChannel = '') + { + \IssueLog::MockStaticObjects($this->mockFileLog, $oConfigObject); + + $this->mockFileLog->expects($this->exactly(1)) + ->method($sExpectedLevel) + ->with($sExpectedMessage, $sExpectedChannel); + + \IssueLog::Error($sMessage, $Channel); + } + + public function LogApiProvider() + { + return [ + [ $this->oMetaModelConfig, "log msg", '' , "Error", "log msg"], + [ $this->oMetaModelConfig, "log msg", 'PoudlardChannel' , "Error", "log msg", 'PoudlardChannel'], + [ array(), "log msg", '' , "Error", "log msg"], // Bruno? + ]; + } + + /** + * @test + * @backupGlobals disabled + */ + public function TestUnknownLevel() + { + $this->mockFileLog->expects($this->exactly(1)) + ->method("Error") + ->with("invalid log level 'TotoLevel'"); + + \IssueLog::Log('TotoLevel', "log msg"); + } + + /** + * @dataProvider LogWarningWithASpecificChannelProvider + * @test + * @backupGlobals disabled + */ + public function TestLogWarningWithASpecificChannel($expectedCallNb, $sExpectedLevel, $ConfigReturnedObject, $bExceptionRaised=false) + { + $this->oMetaModelConfig + ->method("Get") + ->with('log_level_min') + ->willReturn($ConfigReturnedObject); + + \IssueLog::MockStaticObjects($this->mockFileLog, $this->oMetaModelConfig); + + $this->mockFileLog->expects($this->exactly($expectedCallNb)) + ->method($sExpectedLevel) + ->with("log msg", "GaBuZoMeuChannel"); + + try{ + \IssueLog::Warning("log msg", "GaBuZoMeuChannel"); + if ($bExceptionRaised) + { + $this->fail("raised should have been raised"); + } + } + catch(\Exception $e) + { + if (!$bExceptionRaised) + { + $this->fail("raised should NOT have been raised"); + } + } + } + + public function LogWarningWithASpecificChannelProvider() + { + return [ + "empty config" => [ 0, "Ok", ''], + "Default Unknown Level" => [ 0, "Ok", 'TotoLevel', true], + "Info as Default Level" => [ 1 , "Warning", 'Info'], + "Error as Default Level" => [ 0, "Warning", 'Error'], + "Empty array" => [ 0, "Ok", array()], + "Channel configured on an undefined level" => [ 0, "Ok", ["GaBuZoMeuChannel" => "TotoLevel"], true], + "Channel defined with Error" => [ 0, "Warning", ["GaBuZoMeuChannel" => "Error"]], + "Channel defined with Info" => [ 1, "Warning", ["GaBuZoMeuChannel" => "Info"]], + ]; + } + + /** + * @dataProvider LogOkWithASpecificChannel + * @test + * @backupGlobals disabled + */ + public function TestLogOkWithASpecificChannel($expectedCallNb, $sExpectedLevel, $ConfigReturnedObject, $bExceptionRaised=false) + { + $this->oMetaModelConfig + ->method("Get") + ->with('log_level_min') + ->willReturn($ConfigReturnedObject); + + \IssueLog::MockStaticObjects($this->mockFileLog, $this->oMetaModelConfig); + + $this->mockFileLog->expects($this->exactly($expectedCallNb)) + ->method($sExpectedLevel) + ->with("log msg", "GaBuZoMeuChannel"); + + try{ + \IssueLog::Ok("log msg", "GaBuZoMeuChannel"); + if ($bExceptionRaised) + { + $this->fail("raised should have been raised"); + } + } + catch(\Exception $e) + { + if (!$bExceptionRaised) + { + $this->fail("raised should NOT have been raised"); + } + } + } + + public function LogOkWithASpecificChannel() + { + return [ + "empty config" => [ 1, "Ok", ''], + "Empty array" => [ 1, "Ok", array()], + ]; + } + +}