功能
- 可分级别记日志。可自由定制记录日志的级别。例如LOG_LEVEL_WARNING | LOG_LEVEL_ERROR 表示记录warning与error级别的日志。
- 可同时通过header输出。同样支持输出的日志级别,使用方式同上。
- 日志文件支持按年、月、周、日切割,或者不切割
- 支持打印任意数量的任意数据。
- 自动记录日志级别、请求时间、用户IP、请求的URL(path部分)、调用的文件位置(可设置记录的日志级别)
- 支持跟踪完整的一次请求,通过logid实现
- 支持额外的日志记录需求,比如用户ID,sessionid等等
测试代码
Log::init('test app', __DIR__); Log::extraLog('uid:123'); //每次日志都记录此值 //只记录ERROR级别的日志, 不输出到header(默认不输出log到header) Log::setLogWriteLevel(Log::LOG_LEVEL_ERROR); //only log LOG_LEVEL_ERROR Log::debug('this is a test. And wont write in file.'); Log::warning('another test. also not in file.'); Log::error('a error!'); //记录warning和error级的日志, 不输出header Log::setLogWriteLevel(Log::LOG_LEVEL_ERROR | Log::LOG_LEVEL_WARNING); //log LOG_LEVEL_ERROR and LOG_LEVEL_WARNING Log::debug('this is a test. orz...'); Log::warning('another test and some data', array(1,2,3)); Log::error('a error!'); //记录非debug级的日志, 输出debug级的日志到header Log::setLogWriteLevel(~Log::LOG_LEVEL_DEBUG); //except log LOG_LEVEL_DEBUG Log::openSendHeader(); //no online use. suggest for develop use. Log::setLogSendHeaderLevel(Log::LOG_LEVEL_DEBUG); //only send LOG_LEVEL_DEBUG log to header Log::debug('this is a test. And wont write in file. but in header~'); Log::notice('this a notice...'); Log::warning('another test and some data. will write to file.', array(1,2,3)); Log::error('a error!');
日志文件
[ERROR][2014-04-02 11:40:13][162977430][127.0.0.1][/Home/index][uid:123] a error![WebHomeControllerHomeController->indexAction() in D:wampwwwbootsphpWebHomeControllerHomeController.class.php(25)] [WARNING][2014-04-02 11:40:13][162977430][127.0.0.1][/Home/index][uid:123] another test and some data [1,2,3] [ERROR][2014-04-02 11:40:13][162977430][127.0.0.1][/Home/index][uid:123] a error![WebHomeControllerHomeController->indexAction() in D:wampwwwbootsphpWebHomeControllerHomeController.class.php(30)] [NOTICE][2014-04-02 11:40:13][162977430][127.0.0.1][/Home/index][uid:123] this a notice... [WARNING][2014-04-02 11:40:13][162977430][127.0.0.1][/Home/index][uid:123] another test and some data. will write to file. [1,2,3] [ERROR][2014-04-02 11:40:13][162977430][127.0.0.1][/Home/index][uid:123] a error![WebHomeControllerHomeController->indexAction() in D:wampwwwbootsphpWebHomeControllerHomeController.class.php(38)]
header
Cache-Control:Public Connection:Keep-Alive Content-Length:3002 Content-Type:text/html; charset=utf-8 Date:Wed, 02 Apr 2014 03:40:13 GMT Expires:Thu, 19 Nov 1981 08:52:00 GMT Keep-Alive:timeout=5, max=100 Log_533b869d090e1:[DEBUG][2014-04-02 11:40:13][162977430][127.0.0.1][/Home/index][uid:123] this is a test. And wont write in file. but in header~ Pragma:no-cache Server:Apache/2.4.4 (Win64) PHP/5.4.12 X-Powered-By:PHP/5.4.12
源码
<?php namespace BootsPHP; /** * 日志类 * @uses Log::init(日志文件名, 日志路径, [日志级别], [日志文件切割方式]); * 如需配合服务器其他日志统一日志ID,则需调用Log::setLogId($logId); * @author wclssdn <ssdn@vip.qq.com> * */ class Log{ /** * 日志级别: 无 * @var number */ const LOG_LEVEL_NONE = 0; /** * 日志级别: 提醒 * @var number */ const LOG_LEVEL_NOTICE = 1; /** * 日志级别: 警告 * @var number */ const LOG_LEVEL_WARNING = 2; /** * 日志级别: 错误 * @var number */ const LOG_LEVEL_ERROR = 4; /** * 日志级别: 调试 * @var number */ const LOG_LEVEL_DEBUG = 8; /** * 日志级别: 所有 * @var number */ const LOG_LEVEL_ALL = 255; /** * 日志文件切割方式: 按小时切分 * @var string */ const LOG_FILE_SPLIT_STYLE_HOUR = 'YmdH'; /** * 日志文件切割方式: 按天切分 * @var string */ const LOG_FILE_SPLIT_STYLE_DAY = 'Ymd'; /** * 日志文件切割方式: 按周切分 * @var string */ const LOG_FILE_SPLIT_STYLE_WEEK = 'YW'; /** * 日志文件切割方式: 按月切分 * @var string */ const LOG_FILE_SPLIT_STYLE_MONTH = 'Ym'; /** * 日志文件切割方式: 按年切分 * @var string */ const LOG_FILE_SPLIT_STYLE_YEAR = 'Y'; /** * 日志文件切割方式: 不切分 * @var string */ const LOG_FILE_SPLIT_STYLE_NONE = ''; /** * 记录日志的级别 * @var number */ protected static $logLevel = self::LOG_LEVEL_ALL; /** * 日志通过header发送到客户端的级别 * @var number */ protected static $sendHeaderLevel = self::LOG_LEVEL_NONE; /** * 是否通过header发送日志到客户端 * @var boolean */ protected static $sendHeader = false; /** * 记录文件以及行数的日志级别, 多个用level | level * @var number */ protected static $logFileLineLevel = self::LOG_LEVEL_ERROR; protected static $logFileName = 'log'; protected static $logFilePath = '/tmp/'; protected static $logFileSplitStyle = self::LOG_FILE_SPLIT_STYLE_MONTH; /** * 每次日志均打印的额外信息 * @var array */ protected static $extra = array(); /** * 用于跟踪一次完整请求的标识 * @var string */ protected static $logId = null; /** * 可减小不同用户生成的logId的冲突可能 * @var string */ protected static $logIdSalt = null; /** * 需要记录的数据 * @var array */ protected static $data; private function __construct(){ } private function __clone(){ } /** * 初始化Log, 设置应用名, 日志文件位置, 写日志等级, 日志文件切割方式 * @param string $logFileName 应用标识. 也就是日志文件名 * @param string $logPath 日志存储路径 * @param number $logLevel 日志等级. 多个用位或操作. 例如Log::LOG_LEVEL_WARNING | Log::LOG_LEVEL_ERROR * @param string $logFileSplitStyle 日志文件的切割方式, 默认按月 */ public static function init($logFileName, $logFilePath, $logLevel = self::LOG_LEVEL_ALL, $logFileSplitStyle = self::LOG_FILE_SPLIT_STYLE_MONTH){ static $init = false; if ($init === false){ $logFileName && self::$logFileName = $logFileName; self::$logFilePath = $logFilePath; self::$logFileSplitStyle = $logFileSplitStyle; self::resetData(); } } /** * 设置日志ID, 用于跟踪一次完整的请求过程. * 如果调用请确保在最开始的未知调用. * @param string $logId */ public static function setLogId($logId){ self::$logId = $logId; } /** * 获取log ID. * 如果指定了则返回指定的. 如果未指定则返回一个随机的 */ protected static function getLogId(){ if (self::$logId === null){ self::$logId = rand(111111111, 999999999) xor uniqid(); } return self::$logId; } /** * 设置日志记录级别 * @param number $level 默认为Log::LOG_LEVEL_ALL */ public static function setLogWriteLevel($level){ self::$logLevel = (int)$level; } /** * 设置记录文件以及行数的日志级别 * @param number $level 默认Log::LOG_LEVEL_ERROR */ public static function setLogFileLineLevel($level){ self::$logFileLineLevel = (int)$level; } /** * 设置log通过header发送到客户端 */ public static function openSendHeader(){ self::$sendHeader = true; } /** * 设置log不通过header发送到客户端 */ public static function closeSendHeader(){ self::$sendHeader = false; } /** * 设置log通过header发送到客户端的级别 默认为Log::LOG_LEVEL_ALL * @param number $level */ public static function setLogSendHeaderLevel($level){ self::$sendHeaderLevel = $level; } /** * 每次都记录的额外的日志信息 * @param mixed $extraLog */ public static function extraLog($extraLog){ $arguments = func_get_args(); for ($i = 0, $cnt = count($arguments); $i < $cnt; ++$i){ self::$extra[] = $arguments[$i]; } } /** * 记录DEBUG级别的日志 * @param string $log */ public static function debug($log){ self::resetData(); if (count(func_get_args()) > 1){ self::$data = array_slice(func_get_args(), 1); } self::writeLog($log, self::LOG_LEVEL_DEBUG); } /** * 记录NOTICE级别的日志 * @param string $log */ public static function notice($log){ self::resetData(); if (count(func_get_args()) > 1){ self::$data = array_slice(func_get_args(), 1); } self::writeLog($log, self::LOG_LEVEL_NOTICE); } /** * 记录WARNING级别的日志 * @param string $log */ public static function warning($log){ self::resetData(); if (count(func_get_args()) > 1){ self::$data = array_slice(func_get_args(), 1); } self::writeLog($log, self::LOG_LEVEL_WARNING); } /** * 记录ERROR级别的日志 * @param string $log */ public static function error($log){ self::resetData(); if (count(func_get_args()) > 1){ self::$data = array_slice(func_get_args(), 1); } self::writeLog($log, self::LOG_LEVEL_ERROR); } /** * 获取日志文件绝对路径 * @return string */ protected static function getLogFile(){ static $logFile = null; if ($logFile === null){ $logFile = rtrim(self::$logFilePath, '\/') . DIRECTORY_SEPARATOR . self::$logFileName; switch (self::$logFileSplitStyle){ case self::LOG_FILE_SPLIT_STYLE_DAY: $logFile .= date('Ymd'); break; case self::LOG_FILE_SPLIT_STYLE_WEEK: $logFile .= date('YW'); break; case self::LOG_FILE_SPLIT_STYLE_MONTH: $logFile .= date('Ym'); break; case self::LOG_FILE_SPLIT_STYLE_YEAR: $logFile .= date('Y'); break; case self::LOG_FILE_SPLIT_STYLE_NONE: default: break; } } return $logFile; } /** * 获取用户IP * @return Ambigous <NULL, string> */ protected static function getUserIP(){ static $ip = null; if ($ip === null){ if (isset($_SERVER['HTTP_X_FORWARDED_FOR']) && preg_match('#^d{1,3}.d{1,3}.d{1,3}.d{1,3}$#', $_SERVER['HTTP_X_FORWARDED_FOR'])){ $ip = $_SERVER['HTTP_X_FORWARDED_FOR']; } $ip = $_SERVER['REMOTE_ADDR']; } return $ip; } /** * 获取请求的path信息 * @return Ambigous <NULL, string> */ protected static function getUrlPath(){ static $path = null; if ($path === null){ $path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH); } return $path; } /** * 格式化日志 * @param string $log * @param number $logLevel */ protected static function formatLog($log, $logLevel){ $log = str_replace(array( "r", "n" ), ' ', $log); $level = ''; switch ($logLevel){ case self::LOG_LEVEL_DEBUG: $level = 'DEBUG'; break; case self::LOG_LEVEL_NOTICE: $level = 'NOTICE'; break; case self::LOG_LEVEL_WARNING: $level = 'WARNING'; break; case self::LOG_LEVEL_ERROR: $level = 'ERROR'; break; } $logId = self::getLogId(); $date = date('Y-m-d H:i:s'); $path = self::getUrlPath(); $ip = self::getUserIP(); $backTrace = debug_backtrace(); $traceInfo = array(); for ($i = count($backTrace) - 1; $i >= 0; --$i){ if ($backTrace[$i]['class'] == __CLASS__){ $file = $backTrace[$i]['file']; $line = $backTrace[$i]['line']; $pos = $i + 1; $function = isset($backTrace[$pos]['class']) ? "{$backTrace[$pos]['class']}{$backTrace[$pos]['type']}{$backTrace[$pos]['function']}" : $backTrace[$pos]['function']; break; } } $data = array(); if (self::checkData() === true){ foreach (self::$data as $d){ $data[] = json_encode($d); } } self::resetData(); $data = implode("t", $data); $data && $data = "t{$data}"; $extra = array(); if (self::$extra){ foreach (self::$extra as $e){ $extra[] = is_string($e) ? "[{$e}]" : json_encode($e); } } $extra = implode('', $extra); $log = "[{$level}][{$date}][{$logId}][{$ip}][{$path}]{$extra} {$log}{$data}"; if (self::$logFileLineLevel & $logLevel){ $log .= "[{$function}() in {$file}({$line})]"; } return $log . PHP_EOL; } /** * 是否有需要记录的数据 * @return boolean */ protected static function checkData(){ if (self::$data !== self::initData()){ return true; } return false; } /** * 初始化数据变量 */ protected static function initData(){ static $data = null; if ($data === null){ $data = rand(111111, 999999); } return $data; } /** * 重置数据变量 */ protected static function resetData(){ self::$data = self::initData(); } /** * 写日志到文件 */ protected static function writeLog($log, $logLevel){ if (!is_string($log)){ $log = json_encode($log); } if (self::$logLevel & $logLevel || self::$sendHeaderLevel & $logLevel){ $log = self::formatLog($log, $logLevel); } if (self::$logLevel & $logLevel){ $logFile = self::getLogFile(); file_put_contents($logFile, $log, FILE_APPEND); } if (self::$sendHeader && self::$sendHeaderLevel & $logLevel){ $uniq = uniqid(); header("Log_{$uniq}:{$log}"); } } }
Ps: 理想的日志类不需要用户关心文件记录的位置。其实最好也别关心,因为好多的服务器的日志最终要进行汇总分析。如果应用内进行文件位置设定,就可能会对日志收集分析产生一些阻碍。同样的,用户唯独的关心日志文件切分也会对日志收集分析造成一定的影响。比较好的处理方式是使用第三方的日志收集系统,由此系统负责日志的存储、切分。例如scribe。当然,这个太重了。。。所以才有了上边这个类。。。