導入
実務におけるシステム運用で最も避けたいのは、「何が起きたか不明なログ」に追い詰められることです。特にHaskellのKatipライブラリを使用している場合、単に例外を文字列として出力するだけでは不十分です。本記事では、Exceptionからメタデータを抽出し、JSON形式の構造化ログとして出力することで、後の調査コストを劇的に下げるための設計手法を解説します。
基礎知識
Katipは、Haskellにおける強力な構造化ロギングライブラリです。構造化ロギングとは、ログを単なる文字列ではなく、フィールド(キーと値のペア)を持つデータ構造として扱う手法を指します。
ここで重要になるのが「例外のキャッチ」と「型の分離」です。HaskellのControl.Exception.Safeなどのライブラリを用い、発生した例外を型として捉え、その情報をKatipの「Payload」としてJSONに埋め込むことで、ELKスタックやDatadogなどのログ集計ツールでの検索性が飛躍的に向上します。
実装/解決策
例外を構造化するために、以下の3つの要素をログに含めます。
1. エラー型(Error Type):どのクラスの例外か
2. メッセージ(Message):例外に含まれる詳細な説明
3. 環境情報(Context):呼び出し元の関数名やリクエストIDなど
これらをKatipの「KatipContext」インスタンスを介して送信します。
サンプルプログラム
以下は、例外をキャッチして構造化ログとして出力する実装例です。
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE DeriveGeneric #-}
import Katip
import Control.Exception.Safe
import Data.Aeson (ToJSON)
import GHC.Generics (Generic)
-- ログに埋め込むための例外データ構造
data ExceptionPayload = ExceptionPayload
{ errorType :: String
, message :: String
} deriving (Generic, Show)
instance ToJSON ExceptionPayload
instance ToLogStr ExceptionPayload where
toLogStr = ls . show
-- 例外をキャッチして構造化ログを投げる関数
logException :: (KatipContext m) => SomeException -> m ()
logException e = do
let payload = ExceptionPayload
{ errorType = show (typeOf e)
, message = show e
}
-- 構造化データとしてログを記録
$(logTM) ErrorS $ ls (show payload)
-- 使用例
runApp :: KatipContextT IO ()
runApp = do
-- 何らかの処理を囲む
handle (\e -> logException e) $ do
throwString "データベース接続に失敗しました"
応用・注意点
現場で活用する際の注意点は、「スタックトレースの取り扱い」です。Haskellでは標準のExceptionにはスタックトレースが含まれないことが多いため、必要に応じて `GHC.Stack` の `HasCallStack` 制約を利用し、ログ送信時に呼び出し元情報を動的に付与することをお勧めします。
また、ログの出力先が分散している場合、例外発生時の「トランザクションID」をPayloadに含めることを忘れないでください。これにより、Webサーバーのアクセスログとバックエンドの例外ログを紐付け、原因特定までの時間を大幅に短縮できます。単なる文字列出力で満足せず、分析のしやすさを意識したログ設計を心がけましょう。

コメント