event
cakephp3

cakephp3 EventListenerでControllerにきたリクエストのログを全部とる

障害復旧時のために、リクエストデータを保存しておくために、イベントシステムを使って全部フックしてログを取る方法がわかったので、メモしておきます。

Twitterでパスワードが平文で残っていた障害ありましたが、似たような処理だったのかと、、、。passwordのinput nameは、統一しておいて、マスク処理も入れるよう、注意してください

全部のControllerのbeforeFilterに書くとか、基底クラスとしてAppControllerを用意してとか諸々方法あると思うのですが、なんかControllerが肥えていくのは嫌なので、イベントリスナーで解決してみました。

イベントシステム - cakephp 3.x Cookbook

EventListenerを作る。

ここでは、ServiceJournalListener.phpという名前にしました。(相変わらず名前がなんかダサいのですが)

src/Event/ServiceJournalListener.php
<?php
namespace App\Event;

use Cake\Controller\Controller;
use Cake\Core\Configure;
use Cake\Event\Event;
use Cake\Event\EventListenerInterface;
use Cake\Log\Log;
use Cake\Routing\Router;

class ServiceJournalListener implements EventListenerInterface {

    public function implementedEvents()
    {
        return [
            'Controller.initialize' => 'journalLog', // Controllerのinitializeイベントを全部フック
        ];
    }

    public function journalLog(Event $event, $order = 1) {
        $controller = @$event->subject; // controllerしかsubjectにこない
        // リクエスト情報とか色々
        $url = Router::reverse($controller->request);
        $user_agent = $controller->request->header('User-Agent');
        $referer = $controller->request->referer();
        $client_ip = $controller->request->clientIp();
        $server = $controller->request->env('SERVER_NAME');
        $port = $controller->request->env('SERVER_PORT');

        $controller = $controller->request->param("controller"); // controller名
        $action = $controller->request->param("action"); // controllerのaction名
        $matchedRoute = $controller->request->param("_matchedRoute");
        $session_cookie_name = Configure::read('Session.cookie'); // configからセッションのクッキー名をとる
        $session_cookie = $controller->request->cookie($session_cookie_name); // セッションのクッキー

        // リクエストフォームのデータ
        $data = $controller->request->data;
        $query = $controller->request->query;
        $data_info = compact("data","query");

        $log_info = compact('server','port',"url", "referer" ,'controller','action','matchedRoute','session_cookie',"client_ip", "user_agent") + $data_info;

        $message = json_encode($log_info,JSON_UNESCAPED_UNICODE); // 1行にまとめたいのでjsonに

        Log::info($message,["scope"=>["recovery"]]); // recoveryスコープにログレベルinfoで出力
    }

}

EventListenerをGlobalに登録

config/bootstrap.php
// 一番最後にでも
// ServiceJournalListener
use App\Event\ServiceJournalListener;
$svListener = new ServiceJournalListener();
\Cake\Event\EventManager::instance()->attach($svListener);

出力するログの定義をしておく

config/app.php
<?php
return [
//省略
    'Log' => [
        'recovery' => [
            'className' => 'Cake\Log\Engine\FileLog',
            'path' => LOGS,
            'file' => 'recovery',
            'scopes' => ['recovery'], // scopeを切っておく
            'levels' => ['info'],
        ],
//省略

これで、全部のControllerのinitializeがフックされてlogs/recovery.logにでてくるようになります。

2017-07-14 18:08:00 Info: {"server":"maimai-swap.example.com","port":"443","url":"/hoge?id=118624","referer":"https://maimai-swap.example.com/hogep/","controller":"Test","action":"hoge","matchedRoute":"/hoge","session_cookie":"nddffffaaaxxxxxbbbbgggaaa","client_ip":"192.168.33.1","user_agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36","data":[],"query":{"id":"118624"}}

みなさまのお役に立てればー!
リクエストのプロセスIDとかも入れて、エラーログとマッチングさせるとかもやりたいところですが、セッションID出力してあるからいいかな。
個人情報が入ると思うので、安全なところに置きましょう。