弁護士ドットコム株式会社 Creators’ blog

弁護士ドットコムがエンジニア・デザイナーのサービス開発事例やデザイン活動を発信する公式ブログです。

ログや例外についてレビューや実装時に意識していること

ログや例外について レビューや実装時に意識していること

この記事は弁護士ドットコム Advent Calendar 2023の 24 日目の記事です。

前日は @tttttt_621_s さんの Axios における request method の型定義を調査した話 でした。

はじめに

ビジネスメディア & ソリューション事業部開発チームの tsuchiya です。 普段は BUSINESS LAWYERS の開発、運用に携わっています。

この記事では、筆者個人がログや例外について普段の実装やレビュー時に意識していることを紹介します。 なお例外や実装の例に関しては筆者がよく書く PHP を前提とします。

ログ

ログフォーマットを揃える

新規にサービスやアプリケーションを作る際には、ログフォーマットを既存のサービスと揃えるようにしています。 ログフォーマットを揃えることで、ログ管理サービスで集約した際に検索をしやすくなりますし、ログの解析や可視化でも扱いやすくなります。

弁護士ドットコムでは多くのサービスを運用していますが、 PHP で実装されているサービスの大半が共通のログフォーマットで運用されています。

ログフォーマットを揃えるといっても、すべてのサービスで各々定義しているわけではありません。 アプリケーションのロガーはプライベートの Composer package として配布されており、ログフォーマットはその中で定義されています。

またウェブサーバーやアプリケーションサーバーに関しては、ベースとなる設定を Docker イメージとして配布しており、ログフォーマットもその中で定義されています。

そのため、新規にサービスを作る際には特に意識することなくログフォーマットを統一できます。

特化型ロガークラスを実装する

実装する機能において、ログが非常に重要な要素である場合、ロガークラスを実装するか検討します。

主にログレベルを意識させないためであったり、コンテキストとして渡すべき値をメソッドのパラメータとして強制することを目的とします。

以下はあまり実践的でないですが、雰囲気を伝えるための例です。

interface FooLoggerInterface {
    public function log(FooObject $fooObject): void;
}

class FooLogger implements FooLoggerInterface {
    public function __construct(private readonly \Psr\Log\LoggerInterface $logger) {}
    
    public function log(FooObject $fooObject): void
    {
        $this->logger->info('fooId: {id}', ['id' => $fooObject->id])
    }
}

キャッチした例外をコンテキストに含める

キャッチした例外をログ出力する際には、その例外をコンテキストに含めます。

} catch (MyException $e) {
    $this->logger->error('message', ['exception' => $e]);
    // ...
}

キャッチした例外のメッセージだけログに出力するような実装をたまにみかけますが、メッセージだけでは重要な例外の原因がわからなくなってしまいます。

例外

独自例外を実装する

重要な例外であれば例外を拡張し、独自の例外クラスを定義することを検討します。 特に、ライブラリやフレームワークのような汎用的なものを開発する際に行います。

例えば、ディレクトリに対して書き込み権限がない場合用いる例外には DirectoryNotWritableException のような例外クラスを実装します。

class DirectoryNotWritableException extends RuntimeException

単に RuntimeException とするよりも、より例外の原因がわかりやすくなります。

また Throwable を継承したマーカーインタフェースを用いるのも有用です。

interface MyPackageException extends Throwable

class MyRuntimeException extends \RuntimeException implements MyPackageException

class DirectoryNotWritableException extends MyRuntimeException

以下のように、MyPackage の例外をまとめてキャッチでき、 MyPackage の例外に共通の処理を実行するなどが可能です。

catch (MyPackageException $e) {
    // MyPackageの例外時に実行したい処理
}

previous を設定する

キャッチした例外を別の例外にしてスローする際には、previous にキャッチした例外を設定します。

} catch (FooException $e) {
    throw new BarException('message', 0, $e);
}

previous を設定しないと、例外のスタックトレースが切れてしまい、例外の原因がわかりにくくなります。

おわりに

本記事では、簡単ではありますがログや例外について普段の実装やレビュー時に意識していることについて紹介しました。

今回紹介した中で、いくつかの点は静的解析のルールによってチェックできるものもあります。1 今後はそういったルールを導入して、レビュー以前に指摘するなどしていきたいと考えています。

またログや例外についてはサービスとして提供する機能ではないため、ついつい後回しになったり、そもそも考慮できておらず、あまり検討せずに実装してリリースとなってしまうこともあります。 こういったことを避けられるよう、機能開発の初期の段階でしっかり検討できるような仕組みや体制についても検討していきたいと考えています。

最後になりましたが、ブログのネタに困っていると伝えたら、アイデアをくれた @Panda_Program さんにこころより感謝いたします。


弁護士ドットコム Advent Calendar 2023 の明日の担当は @xRx さんの「プロダクト開発はなぜ直観に反するのか」です。

いよいよ最終日! お楽しみに〜


  1. struggle-for-php/sfp-phpstan-psr-logなどがあります。