skimemo


skimemo - 日記/2019-12-06/Laravel5.6~のログをMySQLに出力して閲覧する

_ Laravel5.6~のログをMySQLに出力して閲覧する

Laravelのログは5.6以降で拡張方法が若干変わりました。
これは古い記事の更新版です。

e1.png

_ 修正・追加するファイル(概要)

修正、追加が必要なファイルは以下の通りです。

次の項で1つずつ見ていきましょう。

_ 修正・追加するファイル(ログの記録)

この記事ではログの記録と閲覧を行いますが、まずはログの記録編です。

  • .env
    参考文献1*1で実施している通り、トランザクション処理を考慮するとDBの接続はアプリ本体と分ける必要があります。そのための情報を記載します。
    DB_LOG_TABLE=logs
    DB_LOG_CONNECTION=mysql_log
    また、後ほど新たに定義するログチャンネルを指定します。
    LOG_CHANNEL=logtable
  • config/database.php
    前項で指定した「mysql_log」を定義します。
    基本的には定義済みの「mysql」のコピペです。
    また、log用変数を.envから読み込む設定を追加します。
        'default' => env('DB_CONNECTION', 'mysql'),
        'log' => env('DB_LOG_CONNECTION', 'mysql_log'),
        'logtable' => env('DB_LOG_TABLE', 'logs'),
    
            :
    
        'connections' => [
            :
        'mysql_log' => [
            'driver' => 'mysql',
            'host' => env('DB_HOST', '127.0.0.1'),
            'port' => env('DB_PORT', '3306'),
            'database' => env('DB_DATABASE', 'forge'),
            'username' => env('DB_USERNAME', 'forge'),
            'password' => env('DB_PASSWORD', ''),
            'unix_socket' => env('DB_SOCKET', ''),
            'charset' => 'utf8mb4',
            'collation' => 'utf8mb4_unicode_ci',
            'prefix' => '',
            'strict' => true,
            'engine' => null,
            'fetch' => PDO::FETCH_ASSOC,
        ], 
  • config/logging.php
    5.6からはログの設定はここにまとまるようになりました。
    新しいログチャンネルを定義します。
    'default' => env('LOG_CHANNEL', 'stack'),
        :
    'logtable' => [
               'driver' => 'monolog',
               'handler' => \App\Log\MySqlHandler::class,
               'formatter' => 'default',
               'with' => [
                   'level' => Logger::DEBUG,
                   'maxdays' => 7,
               ],
           ],
    記録するログレベルやログの残存期間も指定できます。
  • app/Log/MySqlHandler.php
    次に前項で指定した新しいHandlerを作成します。
    <?php
    
    namespace App\Log;
    
    use DB;
    use Monolog\Handler\AbstractHandler;
    use Monolog\Logger;
    
    class MySqlHandler extends AbstractHandler
    {
    
        protected $table;
        protected $connection;
        protected $maxdays;
        protected $isTesting;
    
        /**
         * @param int $level The minimum logging level at which this handler will be triggered
         * @param int $maxdays The days of keep the logs records.
         * @param bool $isTesting Whether the environment is testing or not.
         * @param bool $bubble Whether the messages that are handled can bubble up the stack or not
         */
        public function __construct($level = Logger::DEBUG, $maxdays = 7, $isTesting = false, $bubble = true)
        {
            $this->table      = config()->get('database.logtable');
            $this->connection = config()->get('database.log');
            $this->maxdays = $maxdays;
            $this->isTesting = $isTesting;
            parent::__construct($level, $bubble);
        }
    
        /**
         * {@inheritdoc}
         */
        public function handle(array $record)
        {
            if ($record['level'] < $this->level) {
                return false;
            }
    
            // write log to mysql table
            $data = [
                'message'     => $record['message'],
                'channel'     => $record['channel'],    // local/provider...
                'level'       => $record['level'],
                'level_name'  => $record['level_name'],
                'context'     => json_encode($record['context']),
                'remote_addr' => isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : null,
                'user_agent'  => isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : null,
                'created_at'  => $record['datetime']->format("Y-m-d H:i:s.u"),
            ];
    
            if( null!=$this->connection and null!=$this->table ) {
                DB::connection($this->connection)->table($this->table)->insert($data);
            }
    
            return true;
        }
    
        /**
         * delete old logs
         */
        public function close() {
            parent::close();
            // DB has a no function in case of testing.
            if( !$this->isTesting and null!=$this->connection and null!=$this->table ) {
                DB::connection($this->connection)
                    ->table($this->table)
                    ->where('created_at','<',DB::Raw('DATE_ADD(NOW(), INTERVAL -'.$this->maxdays.' DAY)'))
                    ->delete();
            }
        }
    
    }
  • logsテーブルの作成SQL
    最後にテーブルを作ります。
    -- DROP TABLE logs;
    CREATE TABLE logs (
    	id		BIGINT NOT NULL AUTO_INCREMENT,
    	channel		VARCHAR(10),
    	level		INT,
    	level_name	VARCHAR(10),
    	message		LONGTEXT,
    	context		TEXT,
    	remote_addr	VARCHAR(40),
    	user_agent	TEXT,
    	created_at	TIMESTAMP(6) default CURRENT_TIMESTAMP(6),
    	CONSTRAINT PRIMARY KEY( id )
    );
    CREATE INDEX logs_idx1 on logs ( remote_addr );
    CREATE INDEX logs_idx2 on logs ( level );

_ 修正・追加するファイル(ログの閲覧)

次にビューワの方を作成していきます。

Laravelではページネーション*2というページ管理機能を持っています。この機能が生成するHTMLは Bootstrap CSSフレームワーク に対応したものです。また、参考にした従来のビューア(rap2hpoutre/laravel-log-viewer)もBootstrapを使用していたことから、今回も全体的にBootstrapを使用しました。

  • route/web.php
    ログビューワのURLを定義します。どこでも良いのですが私はアクセスしやすいトップにしています。
    特定の環境からしか見られないようIPアドレスで制限をかけています。
    フィルタ指定条件などのフォームを扱うため、anyにしています。
    // IPアドレス制限をかける
    Route::group(array('middleware' => 'auth.ipaddress'), function () {
        Route::get('oldlogs', '\Rap2hpoutre\LaravelLogViewer\LogViewerController@index');
        Route::any('logs', 'LogViewController@anyView');
    }); 
    (移行時に古いログを見る必要もあるため、従来のログViewer用のURLも残しています)
  • app/Http/Controllers/LogViewController.php
    前項で指定したコントローラです。
    <?php
    
    namespace App\Http\Controllers;
    
    use DB;
    use Illuminate\Support\Facades\Input;
    use Monolog\Logger;
    use View;
    
    class LogViewController extends Controller {
    
        // 各行のクラス名
        private $levels_classes = [
            'DEBUG' => '',
            'INFO' => 'info',
            'NOTICE' => 'info',
            'WARNING' => 'warning',
            'ERROR' => 'danger',
            'CRITICAL' => 'danger',
            'ALERT' => 'danger',
            'EMERGENCY' => 'danger',
        ];
    
        // 行頭のアイコン
        private $levels_imgs = [
            'DEBUG' => 'debug',    // 出ないけどその方が分かりやすい
            'INFO' => 'info',
            'NOTICE' => 'info',
            'WARNING' => 'warning',
            'ERROR' => 'warning',
            'CRITICAL' => 'warning',
            'ALERT' => 'warning',
            'EMERGENCY' => 'warning',
        ];
    
        // levelでのDB検索用
        private $levels_value = [
            'debug' => Logger::DEBUG,
            'info' => Logger::INFO,
            'notice' => Logger::NOTICE,
            'warning' => Logger::WARNING,
            'error' => Logger::ERROR,
            'critical' => Logger::CRITICAL,
            'alert' => Logger::ALERT,
            'emergency' => Logger::EMERGENCY,
        ];
    
        // level用チェック初期値
        private $level_chk = [
            'debug'=>1,
            'info'=>1,
            'notice'=>1,
            'warning'=>1,
            'error'=>1,
            'critical'=>1,
            'alert'=>1,
            'emergency'=>1,
        ];
    
        // special words(検索文字列において特別な意味を持つワード)
        private $search_sw = [
            'ip:'=>'remote_addr',    // 'ip:'に続く文字列はremote_addrから検索する
            'ua:'=>'user_agent',
            ''=>'message',
        ];
    
        // 1ページの表示件数
        private $rpp_list = [
            25=>'25',
            50=>'50',
            100=>'100'
        ];
    
    
        /**
         * ログViewer: MySQLに格納されたログを表示する
         * @return mixed
         */
        public function anyView() {
            $search = Input::get('search');
            $rpp = intval(Input::get('rpp'));    // rows per page
            $chk_get = Input::get('level_chk');
            $delete = Input::get('delete');
            $level_chk = [];
    
            if( $delete == 'all' ){
                DB::connection(config()->get('database.log'))->table('logs')->delete();
                return redirect(route('logs',['rpp'=>$rpp]));        // URLから'delete=all'を消すためredirectする
            }
    
            // checkの初期値をセット
            if( null == $chk_get ){
                $level_chk = $this->level_chk;
            } else {
                foreach( $this->level_chk as $name => $check ) {
                    $level_chk[$name] = isset($chk_get[$name]) ? $chk_get[$name] : 0;
                }
            }
    
            // 検索(ログに記録されないようログ用のコネクションでアクセス)
            $tmpSql = DB::connection(config()->get('database.log'))
                ->table('logs')
                ->select('*',DB::raw('concat(created_at) as created_at_ms'))    // _ms=with microsec.
                ->orderBy('created_at','desc');
            if( !empty($search) ){
                // キーワード検索
                foreach( explode(" ", $search) as $keyword ){
                    foreach($this->search_sw as $sw => $field){
                        if( empty($sw) or substr($keyword,0,strlen($sw))==$sw ){
                            $tmpSql = $tmpSql->where($field,'like','%'.substr($keyword,strlen($sw)).'%');
                            break;
                        }
                    }
                }
            }
            // レベルで絞る
            $aryKeys = array_keys($level_chk,true);
            if( count($aryKeys)>0 and count($aryKeys)<count($level_chk) ){
                // 全チェックかノーチェック以外は条件を付ける
                $arySearch = [];
                foreach( $aryKeys as $value ){
                    $arySearch[] = $this->levels_value[$value];
                }
                $tmpSql = $tmpSql->whereIn('level',$arySearch);
            }
            // 検索GO!
            $logs = $tmpSql->paginate($rpp==0 ? current($this->rpp_list) : $rpp );
    
            // 表示用の値をセット
            foreach($logs as $key => $value){
                $logs[$key]->levels_classes = $this->levels_classes[$value->level_name];
                $logs[$key]->levels_imgs = $this->levels_imgs[$value->level_name];
                $uaa = explode(' ',$value->user_agent);
                $logs[$key]->short_user_agent = end($uaa);    // end()の中は必ず変数の必要あり
            }
            // View表示
            return View::make('admin.log_viewer',['logs'=>$logs, 'search'=>$search, 'rpp'=>$rpp, 'rpp_list'=>$this->rpp_list, 'level_chk'=>$level_chk]);
        }
    
    }
  • resources/views/admin/log_viewer.blade.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
     42
     43
     44
     45
     46
     47
     48
     49
     50
     51
     52
     53
     54
     55
     56
     57
     58
     59
     60
     61
     62
     63
     64
     65
     66
     67
     68
     69
     70
     71
     72
     73
     74
     75
     76
     77
     78
     79
     80
     81
     82
     83
     84
     85
     86
     87
     88
     89
     90
     91
     92
     93
     94
     95
     96
     97
     98
     99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    
    <!DOCTYPE html>
    <html lang="ja">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width">
        <meta name="author" content="">
        <meta name="description" content="">
        <META name="viewport" content="width=device-width,initial-scale=1.0"/>
        <title>Log Viewer</title>
     
        <!-- Bootstrap -->
        <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.1/css/bootstrap.min.css">
        <link rel="stylesheet" href="https://cdn.datatables.net/plug-ins/9dcbecd42ad/integration/bootstrap/3/dataTables.bootstrap.css">
     
        <script>
            // 削除確認
            function confirmDelete(){
                return ( confirm("!DELETE ALL LOGS! " + "Are you sure?") );
            }
        </script>
     
    </head>
     
    <BODY marginheight="0" marginwidth="0">
     
    {{--ヘッダ部分--}}
    {{Form::open(['id'=>"topform"])}}
    <div class="container-fluid form-inline">
        {{--タイトル--}}
        <div class="col-sm-4">
            <h2><span class="glyphicon glyphicon-list" aria-hidden="true"></span> Laravel Log Viewer</h2>
        </div>
        <div class="col-sm-8" style="margin-top: 20px; margin-bottom: 0;">
            {{--検索キーワード--}}
            <div class="form-group col-sm-7">
                <label for="search" class="control-label">Search:</label>
                {{Form::input('text','search',$search,['class'=>"form-control", 'id'=>"search", 'placeholder'=>"search string", 'style'=>'width:300px;'])}}
                <div style="margin-left: 60px;">
                    <small id="passwordHelpBlock" class="form-text text-muted">
                        Ex. "ip:127.0.0.1 ua:Firefox any strings"
                    </small>
                </div>
            </div>
            {{--表示件数--}}
            <div class="form-group col-sm-4">
                <label for="rpp" class="control-label">Show</label>
                {{Form::select('rpp',$rpp_list,null,['class'=>'form-control', 'id'=>'rpp'])}}
                <label for="rpp" class="control-label">Entries</label>
            </div>
            {{--updateボタン--}}
            <div class="col-sm-1">
                {{Form::submit('update',['class'=>'btn btn-primary'])}}
            </div>
        </div>
    </div>
     
    {{--明細部分--}}
    <div class="container-fluid">
        <div class="col-sm-12 col-md-12 table-container">
            <div class="row">
                {{--レベルのチェックボックス--}}
                <div class="col-sm-6" style="margin-top: 30px;">
                    @foreach($level_chk as $level_name => $check)
                        {{Form::checkbox("level_chk[$level_name]",true,$level_chk[$level_name],['class'=>'form-check-input','id'=>"chk_$level_name"])}}
                        <label class="form-check-label text-muted" for="chk_{{$level_name}}"><small>{{$level_name}}</small></label>&nbsp;
                    @endforeach
                </div>
                {{--ページャー--}}
                <div class="col-sm-6" align="right">
                    {{ $logs->appends(['rpp'=>$rpp, 'search'=>$search, 'level_chk'=>$level_chk])->links() }}
                </div>
            </div>
            <div class="row">
                <table id="table-log" class="table table-condensed">
                <thead>
                <tr>
                    <th width="10%">Level</th>
                    <th width="8%">IP Adrs</th>
                    <th>UA</th>
                    <th>Date</th>
                    <th>Content</th>
                </tr>
                </thead>
                <tbody>
     
                @foreach($logs as $key => $log)
                    <tr data-display="stack{{$key}}" class="{{$log->levels_classes}}">
                        <td class="text-{{$log->levels_classes}}">
                            <span class="glyphicon glyphicon-{{$log->levels_imgs}}-sign" aria-hidden="true"></span> &nbsp;{{$log->level_name}}
                        </td>
                        <td class="text">{{$log->remote_addr}}</td>
                        <td class="text">{{$log->short_user_agent}}</td>
                        <td class="date">{{$log->created_at_ms}}</td>
                        <td class="text">
                            @if(strpos($log->message,"\n")!==false)
                                <a class="pull-right expand btn btn-default btn-xs" data-display="stack{{$key}}">
                                    <span class="glyphicon glyphicon-search"></span>
                                </a>
                                {{substr($log->message,0,strpos($log->message,"\n"))}}
                            @else
                                {{$log->message}}
                            @endif
                            @if(strpos($log->message,"\n")!==false)
                                <div class="stack" id="stack{{$key}}" style="display: none; white-space: pre-wrap;">{{ trim(substr($log->message,strpos($log->message,"\n"))) }}</div>
                            @endif
                        </td>
                    </tr>
                @endforeach
     
                </tbody>
                </table>
            </div>
            <div align="right">
                {{--最上部へ--}}
                <div class="col-sm-2 text-muted" align="left" style="margin-top: 30px;">
                    <a href="{{URL::route('logs',"delete=all&rpp=$rpp")}}" onclick="return confirmDelete();">Delete all logs</a>
                </div>
                <div class="col-sm-4" align="right" style="margin-top: 20px;" id="totop">
                    <a href="#"><h4 class="glyphicon glyphicon-circle-arrow-up text-muted"></h4></a>
                </div>
                {{--ページャー--}}
                <div class="col-sm-6" align="right">
                    {{ $logs->appends(['rpp'=>$rpp, 'search'=>$search, 'level_chk'=>$level_chk])->links() }}
                </div>
            </div>
        </div>
     </div>
     {{Form::close()}}
     
     
     <!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
     {{Html::script("js/jquery-1.11.0.min.js")}}
     <script>
        $(document).ready(function () {
            // 折りたたみ展開/戻し
            $('.table-container tr').on('click', function () {
                $('#' + $(this).data('display')).toggle();
            });
        });
        $('#rpp').on('change', function () {
            $('#topform').submit();
        });
        $('#totop').on('click', function() {
            window.scrollTo(0,0);
        });
    </script>
     
    </body>
    </HTML> 

    以上で完成です!

    うまく動かない場合にデバッグが難しい(ログが出ないから)のですが、「var_dump() & exit;」を駆使するか、xdebugでステップ実行するなどして頑張ってみてください。


_ ソースコード中のuse文について

私はPhpStormで無駄な警告が出ないよう、Laravel IDE Helperを入れています。そのため、 use DB; なんて書き方をしています。IDE Helperを導入していない方は自分の環境に合わせて書き換えてくださいm(_ _)m。

_ SQL文をログ出力している場合の注意

SQL文をログ出力している場合、「SQL実行→LOG出力→LOGをDBに出力→SQL実行→・・・」の無限ループになります。
このため、ログ出力の為のSQL文をログ出力しないようif文を入れる必要があります。
私は app/Providers/AppServiceProvider.php でログ出力していたので、以下のように処理を追加しました。

public function boot()
 {
    // DBのSQLをログ出力する
    DB::listen(function ($query) {
        // LogのDBへの書き出しはログ出力しない(無限loopになる)
        if( $query->connectionName !=  config()->get('database.log') ) {
            Log::info("Query Time:[$query->time] $query->sql, data:[[" . implode(', ', $query->bindings). "]]");
        }
    }); 

_ unit test中のログについて

古い記事でも触れたように、テスト環境.env.testingでは、

LOG_CHANNEL=stack

とするのが良いと思います。

_ Laravel6以降の場合

Laravel6では(もっと前から?)ログのスタックトレース部などはmessageではなくcontextに入るようになったようです。これに対応するには、log_viewer.blade.phpの98行目付近を以下のようにします。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
                        @php($msg = $log->message)
                        @if(!empty(json_decode($log->context,true)))
                            @php($msg .= PHP_EOL . strip_tags(print_r(json_decode($log->context,true),true)))
                        @endif
                        @if(strpos($msg,"\n")!==false)
                            <a class="pull-right expand btn btn-default btn-xs" data-display="stack{{$key}}">
                                <span class="glyphicon glyphicon-search"></span>
                            </a>
                            {{substr($msg,0,strpos($msg,"\n"))}}
                            <div class="stack" id="stack{{$key}}" style="display: none; white-space: pre-wrap;">{{ trim(substr($msg,strpos($msg,"\n"))) }}</div>
                        @else
                            {{$msg}}
                        @endif 
Category: [Linux] - 17:06:03



 


LaravelのログをMysqlで管理する。
https://laravel.cg0.xyz/laravel-mysql-email-log/


Laravel 5.4 データベース:ペジネーション
https://readouble.com/laravel/5.4/ja/pagination.html


 
Last-modified: 2020-02-20 (木) 14:25:57 (242d)