在 Laravel 10.0 使用日誌記錄 HTTP 請求和響應

實作

過濾函式

新增 app/Helpers/Helper.php 檔,建立一個消毒函式,把請求中的敏感資訊過濾掉。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?php

namespace App\Helpers;

class Helper
{
static function sanitize($data)
{
$fields = ['password', 'secret', 'token'];

if (is_array($data)) {
foreach ($data as $key => $value) {
if (is_array($value)) {
$data[$key] = self::sanitize($value, $fields);
}
foreach ($fields as $field) {
if (stripos($key, $field) !== false) {
$data[$key] = '******';
}
}
}
}

return $data;
}
}

建立中介層

建立 app/Http/Middleware/AssignRequestId.php 檔,為所有日誌添加共用的 REQUEST_ID 資訊。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpFoundation\Response;

class AssignRequestId
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
$requestId = (string) Str::uuid();

Log::withContext([
'REQUEST_ID' => $requestId
]);

$request->headers->set('Request-Id', $requestId);

return $next($request);
}
}

建立 app/Http/Middleware/LogRequestResponse.php 檔,記錄從前端發送過來的請求,以及從後端發送回去的響應。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<?php

namespace App\Http\Middleware;

use App\Helpers\Helper;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpFoundation\Response;

class LogRequestResponse
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
$response = $next($request);

Log::info('LOG_REQUEST_RESPONSE', [
'REQUEST_METHOD' => $request->method(),
'REQUEST_URL' => $request->url(),
'REQUEST_BODY' => Helper::sanitize(json_decode(str_replace("\\n", '', $request->getContent()), true)),
'RESPONSE_STATUS' => $response->getStatusCode(),
'RESPONSE_BODY' => Helper::sanitize(json_decode(str_replace("\\n", '', $response->getContent()), true)),
'TIME' => now(),
]);

return $response;
}
}

修改 app/Http/Kernel.php 檔,將中介層添加到 api 列表中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?php

namespace App\Http;

use Illuminate\Foundation\Http\Kernel as HttpKernel;

class Kernel extends HttpKernel
{
// ...

/**
* The application's route middleware groups.
*
* @var array<string, array<int, class-string|string>>
*/
protected $middlewareGroups = [
// ...

'api' => [
// ...
\App\Http\Middleware\AssignRequestId::class,
\App\Http\Middleware\LogRequestResponse::class,
],
];

// ...
}

建立監聽器

建立 app/Listeners/LogRequestSending.php 檔,記錄後端發送給第三方服務的請求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<?php

namespace App\Listeners;

use App\Helpers\Helper;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Support\Facades\Log;

class LogRequestSending
{
/**
* Create the event listener.
*/
public function __construct()
{
//
}

/**
* Handle the event.
*/
public function handle(object $event): void
{
/** @var \Illuminate\Http\Client\Request */
$request = $event->request;

Log::info('LOG_REQUEST_SENDING', [
'REQUEST_METHOD' => $request->method(),
'REQUEST_URL' => $request->url(),
'REQUEST_BODY' => Helper::sanitize(json_decode(str_replace("\\n", '', $request->body()), true)),
'TIME' => now(),
]);
}
}

建立 app/Listeners/LogResponseReceived.php 檔,記錄後端接收到第三方服務的響應。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<?php

namespace App\Listeners;

use App\Helpers\Helper;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Log;

class LogResponseReceived
{
/**
* Create the event listener.
*/
public function __construct()
{
//
}

/**
* Handle the event.
*/
public function handle(object $event): void
{
/** @var \Illuminate\Http\Client\Request */
$request = $event->request;

/** @var \Illuminate\Http\Client\Response */
$response = $event->response;

Log::info('LOG_RESPONSE_RECEIVED', [
'REQUEST_METHOD' => $request->method(),
'REQUEST_URL' => $request->url(),
'REQUEST_BODY' => Helper::sanitize(json_decode(str_replace("\\n", '', $request->body()), true)),
'RESPONSE_STATUS' => $response->status(),
'RESPONSE_BODY' => Helper::sanitize(json_decode(str_replace("\\n", '', $response->body()), true)),
'TIME' => now(),
]);
}
}

修改 app/Providers/EventServiceProvider.php 檔,將監聽器添加到 listen 列表中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<?php

namespace App\Providers;

use App\Listeners\LogRequestSending;
use App\Listeners\LogResponseReceived;
use Illuminate\Auth\Events\Registered;
use Illuminate\Auth\Listeners\SendEmailVerificationNotification;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
use Illuminate\Http\Client\Events\RequestSending;
use Illuminate\Http\Client\Events\ResponseReceived;
use Illuminate\Support\Facades\Event;

class EventServiceProvider extends ServiceProvider
{
/**
* The event to listener mappings for the application.
*
* @var array<class-string, array<int, class-string>>
*/
protected $listen = [
Registered::class => [
SendEmailVerificationNotification::class,
],
RequestSending::class => [
LogRequestSending::class,
],
ResponseReceived::class => [
LogResponseReceived::class,
],
];

/**
* Register any events for your application.
*/
public function boot(): void
{
//
}

/**
* Determine if events and listeners should be automatically discovered.
*/
public function shouldDiscoverEvents(): bool
{
return false;
}
}

參考資料