功能
- 可分级别记日志。可自由定制记录日志的级别。例如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。当然,这个太重了。。。所以才有了上边这个类。。。