一個日誌系統需要具備哪些功能

在項目開發和線上運行不同場景下,日誌系統都是不可或缺的,一般日誌有以下幾個作用:記錄錯誤、性能分析、查看服務間的調用關係、記錄時間等。所以我們的日誌系統,就需要圍繞這些需求出發來設計,一般要有如下功能點:

  • 日誌配置讀取:方便不同項目部署,通過更改配置文件即可
  • 日誌級別:爲了減少線上日誌大小,開發環境和線上環境記錄錯我的級別一般是不一樣的,比如一般線上只記fatal和error,開發環境則需要記錄info、rpc、warning、notice等
  • 自動捕獲錯誤:需要註冊error和shutdown時的回調方法
  • 記錄錯誤時的調用棧:當出現fatal和error級別的錯誤時,有時只靠錯誤信息時很難準確定位到錯誤代碼的,所以需要記錄函數的調用棧,方便排查錯誤
  • 動態改變日誌級別:記錄日誌時需要檢測當前配置的日誌級別,只記錄級別大於等於配置級別的日誌
  • 基本日誌字段:log_id、時間、耗時、產品線、模塊名稱、請求uri、分佈式調用xhop、錯誤信息、請求返回信息、客戶端IP
  • 分日期和小時記錄,方便定期歸檔
  • 隨機寫入:避免日誌截斷髮生(如果用加鎖寫日誌方法百分百不會截斷,但效率太低)

日誌類:

<?php

class Log
{
    public $debug;
    protected $config = array(
        'log_path' => '/tmp/logs/',
        'log_app'  => 'default',
        'product'  => 'default',
        'level'    => 3,
        'log_rpc'   => 500,
        'path'     => array(
            'FATAL' => 'php/php',
            'RPC'   => 'rpc/rpc',
            'SYS'   => 'sys/sys',
        ),
        'subffix'  => array(
            'WARNING' => '.wf',
        ),
        'area' => 10
    );
    protected $infoLog;
    protected $logPath;
    protected $open = true;
    protected $levels = array('FATAL' => 1, 'ERROR' => 2, 'INFO' => 3, 'RPC' => 4, 'WARNING' => 5, 'NOTICE' => 6, 'DEBUG' => 7, 'SYS' => 8);
    protected $dateFmt = 'Y-m-d H:i:s';
    private $logBase = array('level', 'logid', 'timestamp', 'millisecond', 'date', 'product', 'module', 'uri', 'service_id', 'instance_id', 'xhop', 'human_time', 'msg');
    private $marker;

    public function __construct()
    {
        $this->logPath = $this->config['log_path'];
        $this->init();
        set_error_handler(array($this, 'errorHandler'));
        register_shutdown_function(array($this, 'fatalHandler'));
        $this->requestStart(false);
    }

    public function turn($turn = true)
    {
        $this->open = $turn;
    }

    public function setConfig($name = 'default')
    {
        $config = require_once './log_config.php';
        $this->config = $config[$name] ?: $this->config;
        $this->logPath = $this->config['log_path'];
    }

    public function init($reset = false)
    {
        static $infoLog;
        if (!empty($infoLog) && is_array($infoLog) && false === $reset) {
            $this->infoLog = $infoLog;
            return $infoLog;
        }
        $this->infoLog['level']       = 'INFO';
        $this->infoLog['logid']       = self::genLogID($reset);
        $this->infoLog['timestamp']   = time();
        $this->infoLog['millisecond'] = intval(microtime(true) * 1000);
        $this->infoLog['date']        = date($this->dateFmt, $this->infoLog['timestamp']);
        $this->infoLog['product']     = isset($this->config['product']) ? $this->config['product'] : 'unknow';
        $this->infoLog['module']      = '';
        $this->infoLog['errno']       = '';
        $this->infoLog['msg']         = '';
        $this->infoLog['cookie']      = isset($_COOKIE) ? $_COOKIE : '';
        $this->infoLog['method']      = isset($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] : '';
        $this->infoLog['uri']         = isset($_SERVER['PATH_INFO']) ? $_SERVER['PATH_INFO'] : $this->getUri();
        $this->infoLog['caller_ip']   = self::getClientIp();
        $this->infoLog['host_ip']     = self::getServerHost();
        $this->infoLog['user_agent']  = isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '';

        $this->infoLog['service_id']  = $this->infoLog['product'];
        $this->infoLog['instance_id'] = $this->infoLog['host_ip'];
        $this->infoLog['x_hop']       = '';
        $this->infoLog['human_time']  = date('Y-m-d H:i:s,', $this->infoLog['timestamp']) . ($this->infoLog['millisecond'] % 1000);
        $path   =   explode('/',$this->infoLog['uri']);
        if( isset($path[0]) ){
            $this->infoLog['module']   =   $path[0];
        }

        $infoLog = $this->infoLog;
        return $infoLog;
    }

    public function requestStart($force = true)
    {
        if (true === $force || empty($this->marker['request_start'])) {
            $this->mark('request_start');
        }
    }

    public function rpcStart()
    {
        $this->mark('rpc_start');
    }


    public function errorHandler($errno, $message, $file, $line)
    {
        $warning = array(
            'errno'  => $errno,
            'errmsg' => $message,
            'file'   => $file,
            'line'   => $line,
        );
        $this->error($warning);
    }

    public function fatalHandler($msg = '')
    {
        $app = $this->config['log_app'];
        $message = $this->mergeLog($this->logBase, $this->init());

        $message['status_code'] = 0;
        $message['request_url'] = '';
        $message['uri_path']    = $message['uri'];
        if (error_get_last() && $this->config['level'] >= $this->levels['FATAL']) {
            $errorMsg = error_get_last();
            $message['error'] = substr($errorMsg['message'], 0, strpos($errorMsg['message'], 'Stack trace:'));
            $message['trace'] = $this->getTrace();
            $this->write('FATAL', $message, $app);
        } elseif (!empty($msg)) {
            if (is_array($msg)) {
                $message = array_merge($message, $msg);
            } else {
                $message['error'] = $msg;
            }
            $message['trace'] = $this->getTrace();
            $this->write('FATAL', $message, $app);
        }
    }

    public function info($module)
    {
        $this->infoLog['module'] = $module;
        $message['request_start'] = isset($this->marker['request_start']) ? $this->marker['request_start'] * 1000 : 0;
        $this->infoLog['elapsed_time']   = $this->elapsedTime('request_start', 'request_end') * 1000;
        return $this->write('INFO', $this->infoLog, $module);
    }

    public function addLog($key, $value)
    {
        if (isset($this->infoLog[$key]) && is_array($this->infoLog[$key]) && is_array($value)) {
            $this->infoLog[$key] = array_merge($this->infoLog[$key], $value);
        } else {
            $this->infoLog[$key] = $value;
        }
    }

    public function rpc($rpcData, $module)
    {
        $message = $this->mergeLog($this->logBase, $this->init());
        $message = array_merge($message, $rpcData);
        $message['module']   = $module;
        $message['rpc_start'] = isset($this->marker['rpc_start']) ? $this->marker['rpc_start'] * 1000 : 0;
        $message['elapsed_time'] = $this->elapsedTime('rpc_start', 'rpc_end') * 1000;

        return $this->write('RPC', $message, $module);
    }

    public function error($warning)
    {
        $module = $this->config['log_app'];
        $message = $this->mergeLog($this->logBase, $this->init());
        $message['module'] = $this->config['log_app'];
        $message['trace']  = $this->getTrace();
        $message = array_merge($message, $warning);

        return $this->write('ERROR', $message, $module);
    }

    private function getTrace()
    {
        $trace  = debug_backtrace();
        $need   = array(
            'object_name',
            'type',
            'class',
            'function',
            'file',
            'line',
        );
        $returnTrace = array();
        foreach ($trace as $key => $value) {
            $value['object_name'] = isset($value['object']) ? get_class($value['object']) : '';
            $message              = $this->mergeLog($need, $value);
            $returnTrace[]       = $message;
        }
        return $returnTrace;
    }

    public static function genLogID($reset = false)
    {
        static $logid;
        if (!empty($logid) && false === $reset) {
            return $logid;
        }
        if (!empty($_SERVER['HTTP_X_YMT_LOGID']) && intval(trim($_SERVER['HTTP_X_YMT_LOGID'])) !== 0) {
            $logid = trim($_SERVER['HTTP_X_YMT_LOGID']);
        } elseif (isset($_REQUEST['logid']) && intval($_REQUEST['logid']) !== 0) {
            $logid = trim($_REQUEST['logid']);
        } else {
            $ip        = intval(self::getServerHost());
            $timestamp = explode(' ', microtime());
            $item1     = sprintf('%04d', $timestamp[1] % 3600);
            $item2     = sprintf('%04d', intval(($timestamp[0] * 1000000) % 1000));
            $item3     = sprintf('%04d', mt_rand(0, 987654321) % 1000);
            $item4     = sprintf('%04d', crc32($ip * (mt_rand(0, 987654321) % 1000)) % 10000);
            $logid     = ($item1 . $item2 . $item3 . $item4 . $item1 . $item3);
        }
        return $logid;
    }

    private static function getClientIp()
    {
        $ip = array_key_exists('HTTP_X_REAL_IP', $_SERVER) ? $_SERVER['HTTP_X_REAL_IP'] : (
        array_key_exists('HTTP_X_FORWARDED_FOR', $_SERVER) ? $_SERVER['HTTP_X_FORWARDED_FOR'] : (
        array_key_exists('REMOTE_ADDR', $_SERVER) ? $_SERVER['REMOTE_ADDR'] :
            '0.0.0.0'));
        return $ip;
    }

    private static function getServerHost()
    {
        return isset($_SERVER['SERVER_ADDR']) ? $_SERVER['SERVER_ADDR'] : '';
    }

    private function mergeLog($items, $array)
    {
        $return = array();
        is_array($items) or $items = array($items);
        foreach ($items as $item) {
            $return[$item] = array_key_exists($item, $array) ? $array[$item] : '';
        }
        return $return;
    }
    
    private function write($level, $msg, $module = '')
    {
        $msg['module'] = $msg['module'] ?: $module;
        $level = strtoupper($level);

        $isLog = true;
        if (!$this->open){
            $isLog = false;
        }else{
            if ( isset($this->levels[$level]) && $this->config['level'] < $this->levels[$level] ){
                $isLog = false;
            }elseif ( $level == 'RPC' && isset($this->config['log_rpc']) && intval($msg['elapsed_time']) < $this->config['log_rpc'] ){
                $isLog = false;
            }
        }
        if (!$isLog){
            return false;
        }
        $msg['level'] = $level = empty($level) ? $msg['level'] : $level;
        $subffix = isset($this->config['subffix'][$level]) ? $this->config['subffix'][$level] : '.log';
        $host     = trim(gethostname());
        $hostname = 'UNKNOWNHOST';
        if (!empty($host)) {
            $hosts    = explode('.', $host);
            $hostname = !empty($hosts[0]) ? $hosts[0] : $hostname;
        }

        $level = strtolower($level);
        $fileBase = rtrim($this->logPath, '/') . '/' . $this->config['log_app'] . '/' . $level;
        $filePath = $fileBase . '/' . $level . '.' . $hostname  .  date('YmdH');
        $symlink = $fileBase . '/' . $level . $subffix;
        if (!file_exists($filePath)) {
            @mkdir($filePath, 0777, true);
            @unlink($fileBase);
            @symlink($filePath, $symlink);
            @chmod($filePath, 0777);
        }
        if (is_dir($filePath)) {
            $area = isset($this->config['area']) && $this->config['area'] > 0 ? intval($this->config['area']) : 10;

            file_put_contents($filePath . "/" . rand(0, $area - 1), json_encode($msg) . "\n", FILE_APPEND);
        } else {
            file_put_contents($filePath, json_encode($msg) . "\n", FILE_APPEND);
        }
        return true;
    }

    private function mark($name)
    {
        $this->marker[$name] = microtime(true);
    }

    private function elapsedTime($point1 = '', $point2 = '', $decimals = 4)
    {
        if (!isset($this->marker[$point1])) {
            return 0;
        }
        $this->marker[$point2] = microtime(true);
        return number_format($this->marker[$point2] - $this->marker[$point1], $decimals);
    }

    protected function getUri()
    {
        if (!isset($_SERVER['REQUEST_URI'], $_SERVER['SCRIPT_NAME'])) {
            return '';
        }

        $uri   = parse_url($_SERVER['REQUEST_URI']);
        $query = isset($uri['query']) ? $uri['query'] : '';
        $uri   = isset($uri['path']) ? $uri['path'] : '';

        if (isset($_SERVER['SCRIPT_NAME'][0])) {
            if (strpos($uri, $_SERVER['SCRIPT_NAME']) === 0) {
                $uri = (string) substr($uri, strlen($_SERVER['SCRIPT_NAME']));
            } elseif (strpos($uri, dirname($_SERVER['SCRIPT_NAME'])) === 0) {
                $uri = (string) substr($uri, strlen(dirname($_SERVER['SCRIPT_NAME'])));
            }
        }

        // This section ensures that even on servers that require the URI to be in the query string (Nginx) a correct
        // URI is found, and also fixes the QUERY_STRING server var and $_GET array.
        if (trim($uri, '/') === '' && strncmp($query, '/', 1) === 0) {
            $query                   = explode('?', $query, 2);
            $uri                     = $query[0];
            $_SERVER['QUERY_STRING'] = isset($query[1]) ? $query[1] : '';
        } else {
            $_SERVER['QUERY_STRING'] = $query;
        }

        parse_str($_SERVER['QUERY_STRING'], $_GET);

        if ($uri === '/' or $uri === '') {
            return '/';
        }

        // Do some final cleaning of the URI and return it
        return '/' . $uri;
    }
}

日誌配置文件:

<?php
//日誌配置
return [
    'default' => [
        'log_path' => '/tmp/logs/',
        'log_app'  => 'default',
        'product'  => 'default',
        'level'    => 5,
        'log_rpc'   => 500,
        'path'     => array(
            'FATAL' => 'php/php',
            'RPC'   => 'rpc/rpc',
            'SYS'   => 'sys/sys',
        ),
        'subffix'  => array(
            'WARNING' => '.wf',
        ),
        'area' => 10
    ],
    'why' => [
        'log_path' => '/data/logs/',
        'log_app'  => 'why',
        'product'  => 'why',
        'level'    => 5,
        'log_rpc'   => 500,
        'path'     => array(
            'FATAL' => 'php/php',
            'RPC'   => 'rpc/rpc',
            'SYS'   => 'sys/sys',
            'INFO'  => 'info/info',
        ),
        'subffix'  => array(
            'WARNING' => '.wf',
        ),
        'area' => 10
    ]
];

測試代碼:

<?php
require_once './Log.php';

function getXhop($xhop = "", $reset = false)
{
    static $_bhop = "";
    static $_hop_num = 0;

    if ($reset) {
        $_bhop = "";
        $_hop_num = 0;
    }

    if (empty($_bhop)) {
        //初始化
        if (empty($xhop)) {
            $header = $_SERVER;
            if (!empty($header['X-Hop'])) {
                $xhop = $header['X-Hop'];
            } else {
                $xhop = "01";
            }

        }
        $_bhop = base_convert($xhop, 16, 2);
    } else {
        $xhop = base_convert(base_convert((1 << $_hop_num), 10, 2) . $_bhop, 2, 16);
        $_hop_num++;
    }
    return strlen($xhop) % 2 == 1 ? '0' . $xhop : $xhop;
}


$log = new Log();
$log->setConfig('why');
$log->addLog('x_hop', getXhop());
$log->addLog('result', ['code' => 0, 'msg' => 'success', 'data' => 'info']);

$log->rpcStart();
sleep(1);
$log->rpc([
    'input'  => ['params' => '123'],
    'output' => ['code' => '0', 'msg' => 'success', 'data' => 'rpc']
], 'test');

$log->info('test');

trigger_error('eflekgen');

function test($a, $b)
{
    echo 1;
}
test(1);

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章