アプリケーション開発において、ログは非常に優先順位の高い要素です。

ログの出力により、開発時と運用時の両方でアプリの状態を可視化することができます。さらに、これの構造を整えることで、アプリの障害の原因やパフォーマンスのボトルネックを早い段階で特定できるため、アプリケーションの保守が容易になります。

Laravelには堅牢なログシステムが搭載されており、ログの構造的出力を一手に担ってくれます。Laravel 6.5で導入されたこの新しいシステムは便利ですので、知っておいて損はないはずです。

この記事では、Laravelのログの基本、そして、これを使うメリットをご紹介します。また、構造化ログと集中化ログについて、さらに、ToDoアプリケーションの構築を通してLaravelログの実装方法も扱います。

すでに以下の内容を習得している方は特に、この記事から多くの学びが得られるはずです。

  • ウェブ開発に関する十分な知識
  • Laravelの基本的な理解
  • Laravelを使ったアプリの構築

Laravelのログとは

Laravelでは、MonologというPHPのログ管理ライブラリを使って、エラーなどのログを処理することができます。Laravelは、人気の高い既存のライブラリを使用してさまざまな機能を実装することを是としており、LaravelのすべてのロギングにMonologが利用されています。

Monologは非常に柔軟で圧倒的な人気を誇るPHPロギングライブラリです。ファイル、ソケット、データベース、その他ウェブサービスにログを送信するように設定可能です。Monologは、標準的なテキストファイルから高度なサードパーティログ管理サービスまで、開発者であれば使い慣れているであろうインターフェースから扱うことができます。LaravelでのMonologの使用には、標準的なログ設定ファイルが使用されます。

Monologとその機能の詳細については、この記事の範囲外であるため、公式ドキュメントをご覧ください。

Monologを使用したLaravelのログの設定と実装に入る前に、Laravelでログを使用する理由とその種類に触れておきましょう。

なぜLaravelのログが重要なのか

そもそも、ログの存在価値とは。

Twelve-Factor Appマニフェストでは、ログは、パフォーマンスとモニタリングの鍵であり、現代アプリケーションを構成する重要な要素の1つとされています。

ログは、開発段階で発生したエラーとその場所を特定するのに便利です。さらに、ログ構造を整えておけば、特定のユーザー、エラーの原因となったアクション、そして、素早いバグ修正とメンテナンスとしての解決策を見いだすことができます。

構造化ログは、不具合のトラブルシューティングや本番環境での問題解決に役立ち、本番環境でのアプリケーションの救世主となり得ます。加えて、専用のロギングツールを使ってすべてのログメッセージをリアルタイムで監視・収集し、その場で分析したり、レポートを生成したりできます。

このような理由から、アプリケーションプロジェクトにおいては、構造化ログを最優先事項として扱うことをおすすめします。

ログにはいくつかの種類がありますので見てみましょう。

Laravelのログの基本

ログの基本を学ぶことは、Laravelにおけるログの処理、構造化ログの実践やその改善を行うための第一歩です。

実装方法の前段階として、ログの2つの重要な概念を考えてみましょう。

Laravelの構造化ログ

ソフトウェア開発において、構造化ログとは、アプリケーションログにあらかじめ決められた(一貫した)メッセージフォーマットを実装することを意味します。このフォーマットにより、メッセージはデータとして扱われ、通常のテキストフォーマットの情報よりもはるかに優れた監視、操作、視覚化が可能です。

開発中のアプリケーションでの問題時には、ログファイルこそが、開発者にとっての大事な資産となります。

LaravelではMonologが採用されています。開発技術があれば、特定の種類の情報を受け取るようにロガーを構成し、ログファイルをさまざまな形式で保存、各種サードパーティ系ログ管理サービスに送信して可視化というように、構造化ログを素早く活用することができます。

Laravelの集中化ログ

集中化ログは、複数のソースから集中化ログ管理(CLM)ソリューションにログを送信し、簡単に統合、見える化するというものです。複数のソースからログメッセージを収集、データを統合、効率的な処理と可視化を可能にします。

つまり、CLMには、データの収集だけでなく、ログデータの解析や解析後のデータのわかりやすい表示という側面も求められます。

構造化ログと基本的ログの違い

続いては、構造化ログと基本的(非構造化)ログの違いを考えてみましょう。Laravelプロジェクトで構造化ログの使用が好ましい理由が明確になるはずです。

基本的ログ

基本的ログでは、ログファイルは生のフォーマットで保存されます。個々のログをクエリして識別するためのデータは限定的です。

基本的ログを選ぶと、開発者による独自のツールの開発を決行しない限り、特定のログ形式をサポートする既存のツールに頼る(ある意味で縛られる)ことになります。その他の柔軟なサードパーティ分析ツールを用いたログの読み取り、表示、分析とは無縁です。

基本的ログの使用を避けるべき理由は、主に3つあります。

  1. 中央集権的なログ管理システムは、追加のサポートなしではデータを扱うことができない
  2. 基本的ログソリューションのデータを読み取り、解析するためには、それ専用のソリューションを個別に用意する必要がある
  3. 基本的ログは生データであり、構造化されていないため、管理者による理解が困難になり得る

構造化ログ

構造化ログであれば、その構造をサポートするオープンソースのサードパーティログ分析ツールを使用し、ログの読み取り、表示、および分析を行うことで、開発者による実働時間を削減することができます。

ログは、正しいデータ(以下に挙げるような)を含む時に、大きな価値を持つもので、これこそが、構造化ログの目指すところです。構造化ログのデータから、ダッシュボード、グラフ、チャートといった、アプリケーションの健全性の判断に有用な視覚情報を作成できます。

構造化ログやメッセージに便利な情報の基本的な例を以下にご紹介します。もちろん、個別の状況に合わせて対象とするデータを調整していきます。

構造化ログで収集できるデータの例です。

  1. 関数の実行に使用されたポート
  2. イベントが発生した日時
  3. 顧客のユーザー名またはID
  4. イベントの説明(ログメッセージ)
  5. 関数を実行するために使用したプロトコル
  6. イベントが発生した場所(APIまたは実行中のアプリ)
  7. 一意のイベントID
  8. 引き起こされたアクションのタイプ(ログレベル)

ログには、ソリューションやログイベントの背景を簡単に視覚化できるだけのデータが無ければなりません。その他にも、パスワードや機密データなどはログに保存しないように注意が必要です。

さて、Laravelのロギングがどのようなものかを垣間見たところで、ログを第一級オブジェクトとしてアプリケーションを構築し、Laravelログの実装に移りましょう。

ToDoアプリでLaravelのログを実装する方法

では、これまで学んだことを応用して、新規Laravelプロジェクトを作成し、Laravelのログを実装していきます。

Laravelにまだ触れたことがない方は、「Laravelとは」そして「Laravelのおすすめチュートリアル」をあわせてご確認ください。

Laravelのセットアップ

まず、以下のコマンドを使用して、Laravelのインスタンスを作成します。詳細は、公式ドキュメントをご参照ください。

コンソールを開き、PHPプロジェクトを保存している場所に移動してから、以下のコマンドを実行します。Composerがインストールされ、正しく設定されていることを確認してください。

composer create-project laravel/laravel laravel-logging-app
cd laravel-logging-app // Change directory to current Laravel installation
php artisan serve // Start Laravel development server

データベースの設定とシーディング

次に、データベースを設定し、新しいTodo モデルを作成し、テスト用に200個の偽データを生成します。

データベースクライアントを開き、新しいデータベースを作成します。ここでは、laravel_logging_app_dbという名前でデータベースを作成し、.envファイルにデータベースの認証情報を入力します。

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel_logging_app_db
DB_USERNAME=//DBのユーザー名をここに記載
DB_PASSWORD=//DBのパスワードをここに記載

次に、以下のコマンドを実行して、マイグレーションファイルとTodoモデルを同時に作成します。

php artisan make:model Todo -mc

作成されたマイグレーションファイル (「database/migrations/xxx-create-todos-xxx.php」)を開き、以下のコードを貼り付けます。

<?php
use IlluminateSupportFacadesSchema;
use IlluminateDatabaseSchemaBlueprint;
use IlluminateDatabaseMigrationsMigration;
class CreateTodosTable extends Migration
{
  /**
  * Run the migrations.
  *
  * @return void
  */
  public function up()
  {
    Schema::create('todos', function (Blueprint $table) {
      $table->id();
      $table->string('title');
      $table->text('description')->nullable();
      $table->boolean('is_completed')->default(false);
      $table->timestamps();
    });
  }
  /**
  * Reverse the migrations.
  *
  * @return void
  */
  public function down()
  {
    Schema::dropIfExists('todos');
  }
}

Laravelでの偽データの使用については、データベースのシーディングもあわせてご確認ください。

Monologの基本

LaravelでMonologを使用することで、メール、Slack、ファイル、ソケット、メール受信箱、データベース、その他各種ウェブサービスなど、さまざまなチャネルに構造化ログを送信することができます。設定ファイル(「config/logging.php」)からログの設定が可能です。

設定ファイルには、あらかじめ定義されたログドライバが複数あります。デフォルトのドライバはstackで、singleチャネルを使用して、「storage/logs」フォルダにある「laravel.log」ファイルにログが記録されます。以下では、Laravelのログドライバを複数使用して、構造化ロギングを実演します。

Laravelには、ログを扱うメソッドがいくつかあります。ここでは、「TodosController.php」コントローラファイルで実行します。

コントローラでログメッセージを書き込む

「app/Http/Controllers」フォルダにある、新規作成した「TodosController.php」コントローラファイルを開き、以下のコードを貼り付けます。


<?php
namespace AppHttpControllers;
use AppModelsTodo;
use IlluminateHttpRequest;
use AppHttpControllersController;
use IlluminateSupportFacadesAuth;
use IlluminateSupportFacadesLog;
class TodosController extends Controller
{
  public function index(Request $request)
  {
    $todos = Todo::all();
    Log::warning('User is accessing all the Todos', ['user' => Auth::user()->id]);
    return view('dashboard')->with(['todos' => $todos]);
  }
  public function byUserId(Request $request)
  {
    $todos = Todo::where('user_id', Auth::user()->id)->get();
    Log::info('User is accessing all his todos', ['user' => Auth::user()->id]);
    return view('dashboard')->with(['todos' => $todos]);
  }
  public function show(Request $request, $id)
  {
    $todo = Todo::find($id);
    Log::info('User is accessing a single todo', ['user' => Auth::user()->id, 'todo' => $todo->id]);
    return view('show')->with(['todo' => $todo]);
  }
  public function update(Request $request, $id)
  {
    # Validations before updating
    $todo = Todo::where('user_id', Auth::user()->id)->where('id', $id)->first();
    Log::warning('Todo found for updating by user', ['user' => Auth::user()->id, 'todo' => $todo]);
    if ($todo) {
      $todo->title = $request->title;
      $todo->desc = $request->desc;
      $todo->status = $request->status == 'on' ? 1 : 0;
      if ($todo->save()) {
        Log::info('Todo updated by user successfully', ['user' => Auth::user()->id, 'todo' => $todo->id]);
        return view('show', ['todo' => $todo]);
      }
      Log::warning('Todo could not be updated caused by invalid todo data', ['user' => Auth::user()->id, 'todo' => $todo->id, 'data' => $request->except('password')]);
      return; // 422
    }
    Log::error('Todo not found by user', ['user' => Auth::user()->id, 'todo' => $id]);
    return; // 401
  }
  public function store(Request $request)
  {
    Log::warning('User is trying to create a single todo', ['user' => Auth::user()->id, 'data' => $request->except('password')]);
    # Validations before updating
    $todo = new Todo;
    $todo->title = $request->title;
    $todo->desc = $request->desc;
    $todo->user_id = Auth::user()->id;
    if ($todo->save()) {
      Log::info('User create a single todo successfully', ['user' => Auth::user()->id, 'todo' => $todo->id]);
      return view('show', ['todo' => $todo]);
    }
    Log::warning('Todo could not be created caused by invalid todo data', ['user' => Auth::user()->id, 'data' => $request->except('password')]);
    return; // 422
  }
  public function delete(Request $request, $id)
  {
    Log::warning('User is trying to delete a single todo', ['user' => Auth::user()->id, 'todo' => $id]);
    $todo = Todo::where('user_id', Auth::user()->id)->where('id', $id)->first();
    if ($todo) {
      Log::info('User deleted a single todo successfully', ['user' => Auth::user()->id, 'todo' => $id]);
      $todo->delete();
      return view('index');
    }
    Log::error('Todo not found by user for deleting', ['user' => Auth::user()->id, 'todo' => $id]);
    return; // 404
  }
}

TodoControllerの各メソッド内に、送信するエラーの種類を定義するために、特定のログレベルを持つLogファサードを追加しました。storeメソッドで Log ファサードを使用した例を以下に示します。

public function store(Request $request)
{
  Log::warning('User is trying to create a single todo', ['user' => Auth::user()->id, 'data' => $request->except('password')]);
  # Validations before updating
  $todo = new Todo;
  $todo->title = $request->title;
  $todo->desc = $request->desc;
  $todo->user_id = Auth::user()->id;
  if ($todo->save()) {
    Log::info('User create a single todo successfully', ['user' => Auth::user()->id, 'todo' => $todo->id]);
    return view('show', ['todo' => $todo]);
  }
  Log::warning('Todo could not be created caused by invalid todo data', ['user' => Auth::user()->id, 'data' => $request->except('password')]);
  return; // 422
}

ログメッセージの書式設定

LaravelのデフォルトのLineFormatterでも読みやすく役に立つメッセージが生成できるのですが、何らかの理由で変更したいとします。

そんな場合には、好みのフォーマッタオブジェクトを作り、アプリケーション全体で使用することができます。

Monologの公式ドキュメントには、利用可能なフォーマッタ一覧が掲載されています。これを参考にして、簡単に作成可能です。

Laravelでは、「config/logging.php」設定ファイルに以下のようにコードを追加することで、どのドライバでも簡単に指定のフォーマットを使用するように設定できます。

'daily' => [
  'driver' => 'daily',
  'path' => storage_path('logs/laravel.log'),
  'level' => env('LOG_LEVEL', 'debug'),
  'days' => 14,
  'formatter' => MonologFormatterHtmlFormatter::class,
  'formatter_with' => [
    'dateFormat' => 'Y-m-d',
  ]
],

上記の例では、dailyドライバにMonologFormatterHtmlFormatterを追加し、dailyチャンネル設定内のformatterformatter_withキーを使って、日付のフォーマットを変更しています。

別のチャネルにログを送信する

Monologを扱うことで、Laravelから別の(または複数の)チャネルに同時にログを送信することができます。

以下の簡単な手順で、Slackにログを送信する方法を実演してみましょう。.envファイルで、デフォルトのログチャネルをSlackに変更し、Slack Webhook URLを追加します。

LOG_CHANNEL=slack
LOG_SLACK_WEBBHOOK_URL= Slack Webhook URLをここに記述

次に、以下のようなLogファサードを使ってメッセージを記録して、設定をテストします。

Log::debug("The API instance is on fire caused by:", ['user' => 1])

Webhook URLで指定した目的のチャネルで、エラーが出力されるかどうかを確認できます。

まとめ

ログの取得は、アプリケーションの他の要素と同じくらい、いや、それ以上に重要です。Twelve-Factor Appマニフェストで、現代アプリケーションの最も重要な事項の1つとして挙がっているだけのことはあります。

効果的なロギングにより、本番環境で発生したエラーや不具合を簡単に読み取り、表示し、可視化することができます。そのためには、プロジェクトの初期段階から、構造化ログをアプリケーションに実装することが重要です。

この記事では、Laravelのロギングの基礎、その重要性、構造化ログと集中化ログの両方についてご紹介しました。さらに、Todoアプリケーションを構築することで、Laravelログを実装する方法も学びました。

あなたは、次のアプリ開発プロジェクトにどのようなログを実装しようと考えていますか?コメント欄でお聞かせください。

Solomon Eseme

ベストプラクティスと業界標準に従って、高性能で革新的な製品の構築を行うソフトウェアエンジニア、コンテンツクリエイター。Masteringbackend.comで、自身の仕事に関連するトピックについて執筆も行う。詳しい仕事情報は、X、LinkedIn、About Meで公開中。