【ElasticSearch系列(四)】为 Laravel 适配 ElasticSearch 日志驱动

1、概述

1.1、为什么要用 Elasticsearch 存储 Laravel 日志而不是直接使用默认的文件存储?
  • 当PHP部署在多台服务器时,如果需要查找日志则要在每台服务器上面进行查找。
  • 通常日志是按天分割的,如果不确定是哪一天还需要在好几个文件里面进行查找,然后需要查找的文件数就变成了不确定的天数*负载均衡的服务器数量。
  • 在服务器上面直接通过命令行查询查找日志内容真的不方便。
1.2、ElasticSearch 服务器

参见《【ElasticSearch系列(一)】环境准备》

2、环境

2.1、版本介绍
  • Laravel 5.5
  • ElasticSearch 6.3.0
  • PHP Lib elasticsearch/elasticsearch 6.0
2.2、配置

在 Laravel 项目中引入 ElasticSearch 库

composer require elasticsearch/elasticsearch

3、开始适配

3.1、.env 配置
ELASTIC_HOST=192.168.20.129:9200 # 这里是你的 ElasticSearch 服务器 IP 及端口号
ELASTIC_LOG_INDEX=bf_log # ElasticSearch 索引
ELASTIC_LOG_TYPE=log # ElasticSearch 类型
3.2、ElasticSearch 配置文件

文件位置

/config/elasticsearch.php

代码

<?php

return [
    'hosts' => [
        env('ELASTIC_HOST')
    ],

    'log_index' => env('ELASTIC_LOG_INDEX'),
    'log_type' => env('ELASTIC_LOG_TYPE'),
];
3.3、ElasticSearch 驱动接口

作用

定义 ElasticSearch 驱动要实现的功能。

文件位置

/app/Contracts/ElasticSearchClient.php

代码

<?php

namespace App\Contracts;

interface ElasticSearchClient
{
    /**
     * 获取 ElasticSearch 客户端
     * @return mixed
     * Written by Zhou Yubin(zhouyb@fengrongwang.com)
     */
    public function getClient();

    /**
     * 添加日志
     * @param array $document
     * @return mixed
     * Written by Zhou Yubin(zhouyb@fengrongwang.com)
     */
    public function addDocument(array $document);

    /**
     * 获取所有已添加日志
     * @return mixed
     * Written by Zhou Yubin(zhouyb@fengrongwang.com)
     */
    public function getDocuments();
}
3.4、ElasticSearch 驱动实现

作用

实现 ElasticSearch 驱动接口定义的功能。

文件位置

/app/Contracts/Foundation/ElasticSearchClient.php

代码

<?php

namespace App\Contracts\Foundation;

use Elasticsearch\ClientBuilder;
use App\Contracts\ElasticSearchClient as IElasticSearchClient;

class ElasticSearchClient implements IElasticSearchClient
{
    protected $client;

    protected $documents = [];

    public function __construct()
    {
        $hosts = config('elasticsearch.hosts');
        $this->client = ClientBuilder::create()->setHosts($hosts)->build(); // 实例化一个客户端
    }

    /**
     * 获取 ElasticSearch 客户端
     * @return \Elasticsearch\Client
     * Written by Zhou Yubin(zhouyb@fengrongwang.com)
     */
    public function getClient()
    {
        return $this->client;
    }

    /**
     * 添加日志
     * @param array $document
     * Written by Zhou Yubin(zhouyb@fengrongwang.com)
     */
    public function addDocument(array $document)
    {
        $this->documents[] = $document;
    }

    /**
     * 获取所有已添加日志
     * @return array
     * Written by Zhou Yubin(zhouyb@fengrongwang.com)
     */
    public function getDocuments()
    {
        return $this->documents;
    }
}
3.5、ElasticSearch 门面

作用

为了更优雅的使用 ElasticSearch 驱动,我们定义该门面,以静态形式使用 ElasticSearch 驱动。

文件位置

/app/Facades/ElasticSearchClient.php

代码

<?php

namespace App\Facades;

use Illuminate\Support\Facades\Facade;

class ElasticSearchClient extends Facade {

    protected static function getFacadeAccessor()
    {
        return 'App\Contracts\ElasticSearchClient';
    }
}
3.6、ElasticSearch 句柄

作用

该句柄提供给 Monolog 使用,以让 Monolog 识别 ElasticSearch 并作为日志驱动。

文件位置

/app/Contracts/Foundation/ElasticSearchLogHandler.php

代码

<?php

namespace App\Contracts\Foundation;

use Monolog\Handler\AbstractProcessingHandler;
use App\Facades\ElasticSearchClient;

class ElasticSearchLogHandler extends AbstractProcessingHandler
{
    protected function write(array $record)
    {
        if ($record['level'] >= 200) {
            ElasticSearchClient::addDocument($record);
        }
    }
}
3.7、ElasticSearch 服务提供者

作用

为了更优雅的使用 ElasticSearch 驱动,我们以服务的方式实例化 ElasticSearch 驱动。

文件位置

/app/Providers/ElasticSearchClientProvider.php

代码

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Log;
use App\Contracts\Foundation\ElasticSearchLogHandler;

class ElasticSearchClientProvider extends ServiceProvider
{
    /**
     * Bootstrap any application services.
     *
     * Written by Zhou Yubin(zhouyb@fengrongwang.com)
     */
    public function boot()
    {
        /**
         * 修改 Laravel 默认的 Log 存储方式为 Elasticsearch。
         */
        {
            $monolog = Log::getMonolog();
            $elasticSearchLogHandler = new ElasticSearchLogHandler();
            // $monolog->popHandler(); // 把默认的文件存储去掉,否则会将日志同时存储到文件和ElasticSearch
            $monolog->pushHandler($elasticSearchLogHandler); // 添加 ElasticSearch 日志存储句柄
        }
    }

    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        $this->app->singleton('App\Contracts\ElasticSearchClient', function ($app) {
            return new \App\Contracts\Foundation\ElasticSearchClient();
        });
    }
}

注册服务提供者 /config/app.php

<?php

return [
    // ......
    
    'providers' => [
        // ......
        App\Providers\ElasticSearchClientProvider::class,
        // ......
    ],
    
    // ......
];
3.8、ElasticSearch 任务队列

作用

为了提高日志写入效率及并发,我们将日志写入操作加入到任务队列。

文件位置

/app/Jobs/ElasticSearchLog.php

代码

<?php

namespace App\Jobs;

use Illuminate\Bus\Queueable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Elasticsearch\Client;
use App\Facades\ElasticSearchClient;

class ElasticSearchLog implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    private $params;

    /**
     * Create a new job instance.
     * ElasticSearchLog constructor.
     * @param array $records
     */
    public function __construct(array $records)
    {
        $this->params['body'] = [];
        foreach ($records as $record) {
            unset($record['context']);
            unset($record['extra']);
            $record['datetime'] = $record['datetime']->format('Y-m-d H:i:s');
            $this->params['body'][] = [
                'index' => [
                    '_index' => config('elasticsearch.log_index'),
                    '_type' => config('elasticsearch.log_type'),
                ],
            ];
            $this->params['body'][] = $record;
        }
    }

    /**
     * Execute the job.
     *
     * @return void
     */
    public function handle()
    {
        $client = ElasticSearchClient::getClient();
        if ($client instanceof Client) {
            $client->bulk($this->params);
        }
    }
}
3.9、ElasticSearch 中间件

作用

每写一次日志就会与 ElasticSearch 服务器创建连接并写入日志数据,这样性能会很低,于是我们打算搜集一次 http 请求过程中产生的所有日志,在请求得到响应后统计所有日志数据统一写入到 ElasticSearch 系统中。为了所有的请求都能使用到该中间件,我们将其加入到全局中间件中。

文件位置

/app/Http/Middleware/ElasticSearchLog.php

代码

<?php

namespace App\Http\Middleware;

use Closure;
use App\Jobs\ElasticSearchLog as JElasticSearchLog;
use App\Facades\ElasticSearchClient;

class ElasticSearchLog
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        return $next($request);
    }

    /**
     * @param $request
     * @param $response
     * Written by Zhou Yubin(zhouyb@fengrongwang.com)
     */
    public function terminate($request, $response)
    {
        $documents = ElasticSearchClient::getDocuments();
        // 需要判断是否有日志
        if (count($documents) > 0) {
            dispatch(new JElasticSearchLog($documents));
        }
    }
}

配置中间件 /app/Http/Kernel.php

<?php

namespace App\Http;

use Illuminate\Foundation\Http\Kernel as HttpKernel;

class Kernel extends HttpKernel
{
    // ......
    
    protected $middleware = [
        // ......
        \App\Http\Middleware\ElasticSearchLog::class,
    ];
    
    // ......
3.10、http 请求被异常中断

作用

当一次 http 请求被异常中断,浏览器便得不到正确的响应,因此我们在中间件中用来处理 ElasticSearch 日志任务触发的命令也就不会起作用。为此,我们需要在 Laravel 处理异常的地方来触发 ElasticSearch 日志任务。

文件位置

/app/Exception/Handler.php

代码

// ......
public function report(Exception $exception)
{
    try {
        $logs = ElasticSearchClient::getLogs();
        // 需要判断是否有日志
        if (count($logs) > 0) {
            dispatch(new JElasticSearchLog($logs));
        }
    } catch (\Exception $e) {
        Log::error($e->getMessage());
    }

    parent::report($exception);
}
// ......
3.11、执行命令 composer du

4、测试

4.1、测试 ElasticSearch 连接情况

文件位置

/routes/web.php

代码

Route::get('/test/elastic', function () {
    $hosts = [
        '192.168.20.129:9200',
    ];
    $client = \Elasticsearch\ClientBuilder::create()->setHosts($hosts)->build();
    try {
        $response = $client->info();
        return $response;
    } catch (\Exception $e) {
        return 'error: ' . $e->getMessage();
    }
});

执行结果

在浏览器中访问 http:xxx.com/test/elastic,将有以下信息出现。

{
    "name": "HfQ36uo",
    "cluster_name": "docker-cluster",
    "cluster_uuid": "879oaeOoT0CDPO02_vhaHA",
    "version": {
        "number": "6.3.0",
        "build_flavor": "default",
        "build_type": "tar",
        "build_hash": "424e937",
        "build_date": "2018-06-11T23:38:03.357887Z",
        "build_snapshot": false,
        "lucene_version": "7.3.1",
        "minimum_wire_compatibility_version": "5.6.0",
        "minimum_index_compatibility_version": "5.0.0"
    },
    "tagline": "You Know, for Search"
}

至此,说明我们已经成功连接 ElasticSearch 服务器,并且获取到的 ElasticSearch 服务器的基本信息。

4.2、测试 ElasticSearch 日志驱动

文件位置

/routes/web.php

代码

use Illuminate\Support\Facades\Log;
Route::get('/test/log', function () {
    // 日志同时写入 文件系统 和 ElasticSearch 系统
    Log::info('写入成功啦,日志同时写入 文件系统 和 ElasticSearch 系统', ['code' => 0, 'msg' => '成功了,日志同时写入 文件系统 和 ElasticSearch 系统', 'data' => [1,2,3,4,5]]);
});

执行结果

在浏览器中访问 http:xxx.com/test/log后,我们去看两个位置是否有相应的日志内容。

  • 第一个位置:文件系统日志文件 /storage/logs/laravel-2018-08-23.log,内容如下
[2018-08-23 11:03:02] local.INFO: 写入成功啦,日志同时写入 文件系统 和 ElasticSearch 系统 {"code":0,"msg":"成功了,日志同时写入 文件系统 和 ElasticSearch 系统","data":[1,2,3,4,5]} 
  • 第二个位置:ElasticSearch 服务器,可以通过下面的方式查看
http://192.168.20.129:9200/bf_log/log/_search/

返回的数据如下:

{
    "took": 13,
    "timed_out": false,
    "_shards": {
        "total": 5,
        "successful": 5,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": 1,
        "max_score": 1,
        "hits": [
            {
                "_index": "bf_log",
                "_type": "log",
                "_id": "0Xv4ZGUBMpjJDxlj_ikn",
                "_score": 1,
                "_source": {
                    "message": "写入成功啦,日志同时写入 文件系统 和 ElasticSearch 系统",
                    "level": 200,
                    "level_name": "INFO",
                    "channel": "local",
                    "datetime": "2018-08-23 11:03:02",
                    "formatted": "[2018-08-23 11:03:02] local.INFO: 写入成功啦,日志同时写入 文件系统 和 ElasticSearch 系统 {\"code\":0,\"msg\":\"成功了,日志同时写入 文件系统 和 ElasticSearch 系统\",\"data\":[1,2,3,4,5]} []\n"
                }
            }
        ]
    }
}

至此,我们已经成功的适配了 Laravel 的 ElasticSearch 日志驱动。

如果不想同时将日志写入文件系统,可以将下面的注释打开:

/app/Providers/ElasticSearchClientProvider.php

<?php
// ......
public function boot()
{
    // ......
    $monolog->popHandler(); // 把默认的文件存储去掉,否则会将日志同时存储到文件和ElasticSearch
    // ......
}
// ......

5、安装 Kibana

下载 Kibana

  • 注意和 ElasticSearch 版本保持一致

配置 Kibana

  • config/kibana.yml
elasticsearch.url: "http://192.168.20.129:9200"

启动 Kibana 服务

$ bin/kibana (或 $ bin\kibana.bat)

转载于:https://my.oschina.net/zobeen/blog/2250157

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值