高性能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测试娱乐,生产环境还要专业人才进行部署。
附上参考文档
最后祝各位新年快乐,我们下期再见