🧭

ログ設定は初期化コードではなく、運用契約として設計する

に公開

ログ設定は、最初はただの初期化コードに見える。

logger.info("started")

それでよいと思う。最初から完璧なログ設計を作ろうとすると、たぶんアプリ本体が進まない。

問題は、その小さな初期化コードが、あとから「どこへ出すのか」「どう残すのか」「何を失敗として扱うのか」という運用契約になっていくことだ。

ここでは、ログ設定を便利な初期化処理ではなく、あとから運用要求を受け止める境界として考える。

呼び出し箇所は広がる

ログの呼び出しは、アプリケーションのあちこちに散っていく。

logger.info("job started")
logger.warning("retrying")
logger.exception("failed")

一度広がった呼び出し箇所を後から総入れ替えするのはつらい。

だから、ログ運用を強くしたいときに最初に考えるのは、呼び出し箇所を変えることではない。ここでいう呼び出し箇所は、アプリケーションコード中にある logger.info(...)logger.warning(...) の場所を指している。

起動時の方針として、出力形式、出力先の切り替え、ハッシュ、伏せ字化、環境変数による上書きを足せないかを考える。

設定は便利機能ではなく契約になる

開発中のログ設定は、ちょっとした便利機能に見える。

でも本番に近づくと、設定は契約になる。

  • どこへ出すのか
  • どの形式で出すのか
  • いつファイルを切り替えるのか
  • 何を伏せるのか
  • どの環境変数で変えられるのか
  • どんな設定ミスは起動時に落とすのか

ここを曖昧にすると、障害時に「ログがあるはずなのにない」「設定したつもりだった」が起きる。

早めに止まる方が親切なことがある

ログ設定は、多少間違っていてもアプリ本体が動いてしまうことがある。

でも、それは優しさではない。

ログは障害時に見に行くものだ。障害時に初めて設定ミスが分かるくらいなら、起動時に落ちてほしい。

D-SafeLogger では、効果を持てない設定の組み合わせを、起動時に止める方向へ寄せている。

たとえば、構造化出力と独自フォーマッタの衝突、出力先の切り替えなしで保存期間の制御を期待する設定、ハッシュやアーカイブと出力先の切り替えの矛盾などだ。

柔軟さより、間違った期待で進まないことを優先した。

3 レイヤーで考える

自分はログ設定を 3 つの層で考えるのがしっくり来ている。

レイヤー 役割
コード アプリケーションの既定値
設定ファイル / dict 配置先ごとの差分
環境変数 一時的な運用上書き

D-SafeLogger もこの方向で作っている。

3 行で始められて、既存のログ呼び出しを壊しにくく、設定で運用要求を後から足せて、環境変数で運用時の上書きもできる。自分にとって D-SafeLogger はそういう位置づけだ。

from dsafelogger import ConfigureLogger, GetLogger

ConfigureLogger(
    log_path="./logs",
    pg_name="MyApp",
    routing_mode="daily",
    structured=True,
)

logger = GetLogger(__name__)
logger.info("Application started")

呼び出し箇所は小さく保ち、運用上の契約は設定側で育てる。

何でも外から変えればよいわけではない

環境変数による上書きは便利だ。

ただし、何でも外から変えられるようにすると危ない。

診断モードのように情報量が増える機能は、明示的に有効化する前提にした方がよい。由来不明の設定ファイルで勝手に有効化されるべきではない。

運用時に変えたいものと、変えられては困る境界を分ける。ここもログ設計の一部だと思っている。

ケーススタディとして見ると、主役は境界になる

D-SafeLogger は、この考え方を実装した一例にすぎない。

大事なのは、特定のライブラリを使うことではなく、ログ設定を「あとから育つ運用契約」として扱うことだ。

最初は logger.info("started") でよい。

でも、その呼び出しを将来の運用要求に耐えられる形で残すには、呼び出し箇所と実行時の方針の境界を早めに意識した方がいい。

関連記事

Zenn 連載:

Qiita の関連回:

Discussion