📋

CaddyでJSON形式のアクセスログをカスタムする

に公開

何?

Caddy v2でアクセスログをJSON形式でいい感じにカスタムして出力する方法をメモします。ログのフィルター機能とか、カスタムフィールドの追加とか、スニペットでの再利用とか、そのあたりを含めてまとめてみました。
実際に設定するときは https://caddyserver.com/docs/caddyfile/directives/log, https://caddyserver.com/docs/logging#structured-logs あたりも参照してください。

最小構成なJSONログの設定

CaddyでアクセスログをJSON形式で出力する基本となる設定はこんな感じ:

:8080 {
    log {
        format json
        output file /var/log/caddy/access.log
    }
}

より高度なカスタム設定

フィルターで不要フィールドを割愛する

基本の設定ではいらない情報がいっぱい含まれてしまうので、filterで必要なものだけ残すようにします:

log access-log {
    format filter {
        request delete
        bytes_read delete
        resp_headers delete
        wrap json {
            time_key time # ts -> time
            time_format rfc3339
            time_local
            duration_format ms
        }
    }
    output file /var/log/caddy/access_log {
        mode 644
        roll_size 500mb
        roll_keep 5
    }
}

ここでやってること:

  • request delete: リクエストボディの詳細を除外
  • bytes_read delete: 読み取りバイト数を除外
  • resp_headers delete: レスポンスヘッダーを除外
  • time_format rfc3339: RFC 3339形式のタイムスタンプ
  • time_local: ローカル時間で記録

ts, level, logger, msg フィールドにはfilterをかけることができないので注意

カスタムフィールドを突っ込む

log_appendで独自のフィールドを追加できます:

route {
    log_append method {http.request.method}
    log_append uri {http.request.orig_uri}
    log_append referer {http.request.header.Referer}
    log_append user-agent {http.request.header.User-Agent}
}

これで以下のしていしたフィールドがアクセスログに追加されます

スニペットで設定を再利用

複数のサイトで同じログ設定を使いまわしたい場合、スニペットが便利です:

{
    # Caddy本体のログ設定
    log default {
        format json
        output file /var/log/caddy/default_log {
            mode 644
            roll_size 500mb
            roll_keep 5
        }
        exclude http.log.access  # アクセスログを除外
    }
}

# 再利用可能なアクセスログスニペット
(access-log) {
    log access-log {
        format filter {
            request delete
            bytes_read delete
            resp_headers delete
            wrap json {
                time_key time
                time_format rfc3339
                time_local
                duration_format ms
            }
        }
        output file /var/log/caddy/{args[0]}_access_log {
            mode 644
            roll_size 500mb
            roll_keep 5
        }
    }

    route {
        log_append method {http.request.method}
        log_append uri {http.request.orig_uri}
        log_append referer {http.request.header.Referer}
        log_append user-agent {http.request.header.User-Agent}
    }

    intercept {
		@userId header X-User-Id *
		handle_response @userId {
			vars http.auth.user.id {http.response.header.X-User-Id}
		}
	}
}

# 各サイトでスニペットを使用
:8000 {
    root /app/public
    import access-log api
}

こうするとimport access-log apiみたいな形で簡単に呼び出せて、引数(この場合api)をファイル名として使うようになります。

ユーザーIDを動的にセットしたいとき

レスポンスヘッダーからユーザーIDを取得してログに記録したい場合はこんな感じ:

intercept {
	@userId header X-User-Id *
	handle_response @userId {
		# ログとして出力される json の user_id フィールドが予約語になっていて log_append による独自の値を詰めることができない
		# user_id は http.auth.user.id を無理やり上書きする必要がある(本来Basic認証で使うuser_idが入る)
		# http.response.header を vars でセットするには intercept ディレクティブで行う必要がある
		# @see: https://github.com/caddyserver/caddy/pull/6108
		vars http.auth.user.id {http.response.header.X-User-Id}
	}
}

X-User-Idヘッダーがあるときだけ、その値がuser_idフィールドに記録されます。user_idフィールドは予約語なので、無理やりhttp.auth.user.idを上書きしてます。
そのための例外対応が施されているようです。 https://github.com/caddyserver/caddy/pull/6108
汎用的な設定口を用意してほしいですね...。

※内情としては、ここでアクセスログのuser_idに、http.auth.user.idがセットされるようになっていて、本来ならここで認証されたユーザーIDがセットされる想定のようです...。

出力例

こんな感じのJSONログが出てきます:

{
  "level": "info",
  "time": "2025-08-26T10:30:45+09:00",
  "logger": "http.log.access.access-log",
  "msg": "handled request",
  "method": "GET",
  "uri": "/api/users?page=1",
  "status": 200,
  "duration": 114.514191,
  "size": 1024,
  "referer": "https://example.com/dashboard",
  "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)",
  "user_id": "12345"
}

Caddyのログ設定はクセがあると感じています。デフォルトのままでOKなら問題ないのですが、ログの構造に縛りがあるなど、スキーマを任意に調整したいときに悩むことが多いのでメモしました。

Discussion