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

    日志收集

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

  • 相关阅读:
    Thymeleaf学习记录(1)--启动模板及建立Demo
    Redis教程基本命令
    错误:Attempted to load applicationConfig: [classpath:/application.yml] but snakeyaml was not found on the classpath
    备注
    MYSQL建表问题(转)
    Class.forName("com.mysql.jdbc.Driver")找不到类
    mySql连接报错
    电脑启动过程
    C++输入输出流--<iostream>详解
    类内部实例化自身可行吗?
  • 原文地址:https://www.cnblogs.com/alwayslinger/p/14401383.html
Copyright © 2011-2022 走看看