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