高性能Laravel日志服务

高性能Laravel日志服务

介绍

  • 利用高性能seaslog日志扩展
  • 会介绍三种方式将seaslog集成到laravel的日志服务
  • 使用docker部署elk,将日志输出到elasticsearch

安装配置seaslog扩展

/path/to/phpize
./configure --with-php-config=/path/to/php-config
make && make install
# 修改配置文件
# 重启fpm
# php -m 确保成功加载SeasLog

[SeasLog]
extension = seaslog.so
;是否以目录区分Logger 1是(默认) 0否
seaslog.disting_folder = 1
;是否开启抛出SeasLog自身异常  1开启(默认) 0关闭
seaslog.throw_exception = 1
;是否开启忽略SeasLog自身warning  1开启(默认) 0关闭
seaslog.ignore_warning = 1
;日志格式模板 默认"%T | %L | %P | %Q | %t | %M"
seaslog.default_template = "%L | %T | %Q | %M | %t | %I | %D | %R | %m"
;是否启用buffer 1是 0否(默认)
seaslog.use_buffer = 1
;cli运行时关闭buffer 1是 0否(默认)
seaslog.buffer_disabled_in_cli = 1
;记录日志级别,数字越大,根据级别记的日志越多。
seaslog.level = 8
;自动记录错误 默认1(开启)
seaslog.trace_error = 0
;日志存储介质 1File 2TCP 3UDP (默认为1)
seaslog.appender = 1
;是否开启性能追踪 1开启 0关闭(默认)
seaslog.trace_performance = 0

通过logservice集成seaslog

# LogManager源码简单分析
<?php

namespace Illuminate\Log;

use Illuminate\Support\ServiceProvider;

class LogServiceProvider extends ServiceProvider
{
    /**
     * Register the service provider.
     *
     * @return void
     */
    public function register()
    {
        $this->app->singleton('log', function ($app) {
            // 这就是laravel的log服务了
            return new LogManager($app);
        });
    }
}

# LogManager作为Log facade背后的真实服务提供者 先来看__call方法
public function __call($method, $parameters)
{	
    # 可以看到driver()方法一定是核心了
    return $this->driver()->$method(...$parameters);
}

# driver方法是文档中对外暴露的channel方法的底层方法
public function driver($driver = null)
{
    return $this->get($driver ?? $this->getDefaultDriver());
}

# get方法根据提供的日志驱动解析出来真正的具备处理日志能力的loghandler
protected function get($name)
{
    try {
        return $this->channels[$name] ?? with($this->resolve($name), function ($logger) use ($name) {
            return $this->channels[$name] = $this->tap($name, new Logger($logger, $this->app['events']));
        });
    } catch (Throwable $e) {
        return tap($this->createEmergencyLogger(), function ($logger) use ($e) {
            $logger->emergency('Unable to create configured logger. Using emergency logger.', [
                'exception' => $e,
            ]);
        });
    }
}

# 根据指定的channel返回driver
protected function resolve($name)
{
    $config = $this->configurationFor($name);

    if (is_null($config)) {
        throw new InvalidArgumentException("Log [{$name}] is not defined.");
    }

    // 如果通过extend方法扩展了log服务 那么就返回自定义的driver
    if (isset($this->customCreators[$config['driver']])) {
        return $this->callCustomCreator($config);
    }

    // 如果没匹配到指定的那么就创建内置的dirver
    $driverMethod = 'create'.ucfirst($config['driver']).'Driver';

    if (method_exists($this, $driverMethod)) {
        return $this->{$driverMethod}($config);
    }

    throw new InvalidArgumentException("Driver [{$config['driver']}] is not supported.");
}

# extend方法
public function extend($driver, Closure $callback)
{	
    # 将传入的闭包复制到
    $this->customCreators[$driver] = $callback->bindTo($this, $this);

    return $this;
}

# 获取monolog日志驱动的方法
protected function createMonologDriver(array $config)
{
    if (! is_a($config['handler'], HandlerInterface::class, true)) {
        throw new InvalidArgumentException(
            $config['handler'].' must be an instance of '.HandlerInterface::class
        );
    }

    $with = array_merge(
        ['level' => $this->level($config)],
        $config['with'] ?? [],
        $config['handler_with'] ?? []
    );

    return new Monolog($this->parseChannel($config), [$this->prepareHandler(
        $this->app->make($config['handler'], $with), $config
    )]);
}

# 获取custom日志驱动的方法
protected function createCustomDriver(array $config)
{
    $factory = is_callable($via = $config['via']) ? $via : $this->app->make($via);

    return $factory($config);
}
# 以上便是三种扩展laravel日志驱动的方法

# 通过monolog的方式扩展laravel日志驱动

# 1 创建一个serviceprovider
php artisan make:provider SeasLogServiceProvider

# 2 编写provider
public function boot()
{
    // 确保顺利加载了seaslog扩展
    // 可以将配置单独提取出来 我这里直接写死了
    \SeasLog::setBasePath(storage_path('logs/seaslog'));
    \SeasLog::setRequestId((string) Str::uuid());
}
    
# 3 注册provider
App\Providers\SeasLogServiceProvider::class,
    
# 4 创建一个SeasLogHandler
<?php

namespace App\Logging;

use Monolog\Handler\AbstractProcessingHandler;

class SeasLogHandler extends AbstractProcessingHandler
{
    public function write(array $record): void
    {
        \SeasLog::log($record['level_name'], $record['message']);
    }
}

# 5 配置logging.php
...
'seaslog' => [
    'driver' => 'monolog',
    'handler' => App\Logging\SeasLogHandler::class,
]
    
# 6 测试使用
Route::get('abc', function() {
    Log::channel('seaslog')->debug('aaaaaa');
});

# 使用custom扩展laravel日志驱动
# 我们查看laravel内建的Logger类,是一个已经为我们实现好的psr3规范的代理类, 最主要的方法如下
protected function writeLog($level, $message, $context)
{
    $this->logger->{$level}($message = $this->formatMessage($message), $context);

    $this->fireLogEvent($level, $message, $context);
}

# 创建provider和之前一样
# 1 new Logger第一个参数要求是一个实现了LoggerInterface的实例
<?php

namespace App\Logging;

use Illuminate\Log\Logger;
use Illuminate\Support\Arr;
use Psr\Log\LoggerInterface;
use Psr\Log\InvalidArgumentException;

class SeasLog extends Logger implements LoggerInterface
{
    public function __invoke(array $config)
    {
        return $this;
    }
	
    ...
    public function info($message, array $context = [])
    {
        $this->log(SEASLOG_INFO, $message, $context);
    }

    public function debug($message, array $context = [])
    {
        $this->log(SEASLOG_DEBUG, $message, $context);
    }

    public function log($level, $message, array $context = [])
    {
        try {
            $message = is_string($message) ? $message : (string) $message;
        } catch (\Throwable $th) {
            throw new InvalidArgumentException("args type invalid");
        }

        if (!empty($context) && !Arr::isAssoc($context)) {
            throw new InvalidArgumentException("args type invalid");
        }

        foreach ($context as $key => $value) {
            $message .=  sprintf(" %s : {%s}", $key, $key);
        }

        if (!empty($context)) {
            $k = array_keys($context);
            $v = array_values($context);
            $k = array_map(fn($v) => '{'. $v . '}', $k);
            $context = array_combine($k, $v);
        }
        \SeasLog::log($level, trim($message), $context);
    }
}

# 2 配置logging.php
'seas' => [
    'driver' => 'custom',
    'via' => App\Logging\SeasLog::class,
],

# 3 测试一下
Route::get('abc', function() {
    Log::channel('seas')->debug('aaaaaa', ['name' => 'abc']);
    Log::channel('seas')->emergency('bbbbbbbbbbbb');
    Log::channel('seas')->log('debug', 'zbc', ['name' => 'tom', 'age' => 123]);
});

# 第三种方式通过$app['log']->extend()方式进行扩展 这里就不展开了,各位可自行尝试

部署elk,使用logstash将日志输出到elasticsearch

# 创建network
docker network create estack

# 启动es
docker run -d --name es --net estack -p 9200:9200 -p 9300:9300 -e ES_JAVA_OPTS="-Xms512m -Xmx512m" -e "discovery.type=single-node" imageid

# 启动kibana
docker run -d --name kibana --net estack -p 5601:5601 imageid

# 创建索引
PUT seaslog

PUT seaslog/_mapping
{
  "properties": {
    "level": {
      "type": "keyword"
    },
    "created_at": {
      "format": "yyyy-MM-dd HH:mm:ss",
      "type": "date"
    },
    "request_batch": {
      "type": "keyword"
    },
    "msg": {
      "type": "text"
    },
    "timestamp": {
      "type": "float"
    },
    "remote_addr": {
      "type": "ip"
    },
    "domain": {
      "type": "keyword"
    },
    "uri": {
      "type": "text"
    },
    "method": {
      "type": "keyword"
    }
  }
}

# 配置logstash  seaslog.conf
input {
    file {
        path => "/var/log/seaslog/*.log"
        discover_interval => 2
        start_position => "end"
    }
}

filter {
    mutate {
        split => ["message"," | "]
        add_field => {
            "level" => "%{[message][0]}"
        }
        add_field => {
            "created_at" => "%{[message][1]}"
        }
        add_field => {
            "request_batch " => "%{[message][2]}"
        }
        add_field => {
            "msg" => "%{[message][3]}"
        }
        add_field => {
            "timestamp" => "%{[message][4]}"
        }
        add_field => {
            "remote_addr" => "%{[message][5]}"
        }
        add_field => {
            "domain" => "%{[message][6]}"
        }        
        add_field => {
            "uri" => "%{[message][7]}"
        }
        add_field => {
            "method" => "%{[message][8]}"
        }
        remove_field => ["message", "host", "path", "@version", "@timestamp"]
    }
}

output {
    elasticsearch {
        hosts=>["es:9200"]
        manage_template => false
        index => "seaslog"
    }
}

# 启动logstash
docker run -d --name logstash --net estack -v /home/vagrant/code/laralog/storage/logs/seaslog/default/:/var/log/seaslog/ -v /home/vagrant/elastic/volumes/logstash/conf.d/seaslog.conf:/usr/share/logstash/pipeline/logstash.conf imageid

测试使用

# 请求测试路由
Route::get('abc', function() {
    Log::channel('seas')->debug('aaaaaa', ['name' => 'abc']);
    Log::channel('seas')->emergency('bbbbbbbbbbbb');
    Log::channel('seas')->log('debug', 'zbc', ['name' => 'tom', 'age' => 123]);
});

GET seaslog/_search
{
  "query": {
    "match_all": {}
  }
}

本文主要以服务提供者和配置文件的方式继承了seaslog到laravel,并且配合monolog进行操作,可以比较轻松的移植到其他框架中,比如开箱就支持monolog的hyperf、symfony等框架或者其他实现psr3的框架,或者干脆在框架中来一套自己的日志服务,随你喜欢。各位可根据实际环境使用消息中间件来对日志进行缓存,从而减少对es的冲击。以上只是docker测试娱乐,生产环境还要专业人才进行部署。

附上参考文档

docker使用logstash

seaslog api

日志收集

最后祝各位新年快乐,我们下期再见

posted @ 2021-02-14 00:20  alwayslinger  阅读(866)  评论(0编辑  收藏  举报