zoukankan      html  css  js  c++  java
  • 高性能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 IlluminateLog;
    
    use IlluminateSupportServiceProvider;
    
    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
    AppProvidersSeasLogServiceProvider::class,
        
    # 4 创建一个SeasLogHandler
    <?php
    
    namespace AppLogging;
    
    use MonologHandlerAbstractProcessingHandler;
    
    class SeasLogHandler extends AbstractProcessingHandler
    {
        public function write(array $record): void
        {
            SeasLog::log($record['level_name'], $record['message']);
        }
    }
    
    # 5 配置logging.php
    ...
    'seaslog' => [
        'driver' => 'monolog',
        'handler' => AppLoggingSeasLogHandler::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 AppLogging;
    
    use IlluminateLogLogger;
    use IlluminateSupportArr;
    use PsrLogLoggerInterface;
    use PsrLogInvalidArgumentException;
    
    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' => AppLoggingSeasLog::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

    日志收集

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

  • 相关阅读:
    java Activiti 工作流引擎 SSM 框架模块设计方案
    自定义表单 Flowable 工作流 Springboot vue.js 前后分离 跨域 有代码生成器
    数据库设计的十个最佳实践
    activiti 汉化 stencilset.json 文件内容
    JAVA oa 办公系统模块 设计方案
    java 考试系统 在线学习 视频直播 人脸识别 springboot框架 前后分离 PC和手机端
    集成 nacos注册中心配置使用
    “感恩节 ”怼记
    仓颉编程语言的一点期望
    关于System.out.println()与System.out.print("\n")的区别
  • 原文地址:https://www.cnblogs.com/alwayslinger/p/14401383.html
Copyright © 2011-2022 走看看