go-history で PostgreSQL に行レベル履歴を追加する
この記事は ChatGPT を使って書かれた
TL;DR
github.com/mickamy/go-history は、アプリ側の書き換えなしで PostgreSQL
に監査向けの履歴テーブルを追加するための部品群です。
-
history.Configで監査対象テーブルや PK、記録する操作を宣言的に定義(YAML / Go 両対応) -
postgres.BuildDDLs/postgres.Migrateが履歴テーブル + トリガーを冪等な SQL として生成 -
postgres.Installがdatabase/sqlドライバをラップし、history.WithMetaで渡した Actor/Trace 情報をセッション変数経由でトリガーへ伝達
アプリは従来どおり db.ExecContext 等を呼ぶだけで、トリガーが orders_history のようなテーブルに history.Row
レコードを書き込んでくれます。CLI (cmd/viewer) も同梱されているので、log/diff/serve で履歴を素早く確認できます。
なぜまた監査ライブラリ?
プロジェクトごとに「users_history を自前で作る」パターンは珍しくありませんが、運用が進むと次のような課題が出てきます。
- サービスごとに保存する列や JSON 形式が微妙に違う
- どの環境にどのトリガーが入っているか追いきれない
- 操作を実行した利用者やリクエスト Trace と紐付けられない
go-history は設定ファイル 1 つをソースに履歴テーブル・トリガーを生成し、history.Row
という共通スキーマに落とし込むことでこれらを解消します。アプリから受け取ったメタデータを必ず書き込むため、誰がどのリクエストで更新したのかを後から追跡できます。生成される
SQL は冪等なので、マイグレーションツールに組み込んで繰り返し適用しても安全です。
リポジトリ構成
| Path | 説明 |
|---|---|
/config.go, /meta.go, /row.go
|
設定/メタデータ/履歴行スキーマなどコア型定義 |
/postgres |
PostgreSQL 用 DDL 生成・マイグレーション・ドライバラッパ |
/cmd/viewer |
log・diff・serve を備えた CLI |
/examples/postgres |
最小構成のサンプルアプリ |
/seed |
ローカル検証用にテーブルへデータ投入するユーティリティ |
docker compose up postgres を実行すると、history データベースと orders などのテーブルが立ち上がります。
監査対象の定義
YAML で完結させても良いし、Go コードで history.Config を組み立てても OK です。複合 PK やテーブルごとの操作設定を含む例は次の通りです。
driver: postgres
history_table:
suffix: "_history"
default_operations: all
tables:
orders: { } # pk は ["id"]、operations は all が自動で入る
order_items:
pk: [ "order_id", "line_no" ]
operations: insert|update
cfg.Normalize()(または history.LoadConfig("history.yaml"))を実行すると、デフォルト値の補完と検証が済んだ状態になります。この構造体を
postgres.BuildDDLs などへ渡すと、履歴テーブル名やトリガー対象が自動的に決定されます。
サンプルアプリの流れ
examples/postgres/main.go
に全体の配線がまとまっています。ざっくり要約すると以下の通りです。
cfg := history.Config{
Driver: "postgres",
Tables: map[string]history.TableConfig{"orders": {}},
}
if err := cfg.Normalize(); err != nil {
log.Fatal(err)
}
// 1. 履歴テーブル + トリガーを冪等に適用
if err := postgres.Migrate(ctx, baseDB, cfg, "orders"); err != nil {
log.Fatal(err)
}
// 2. ドライバをラップして history-aware な接続を作成
postgres.Install(stdlib.GetDefaultDriver())
db, err := sql.Open(postgres.Name, dsn)
// 3. コンテキストに Actor / Trace を載せて普通に SQL を実行
ctx = history.WithMeta(ctx, history.Meta{
ActorID: "example-postgres",
TraceID: uuid.NewString(),
})
_, _ = db.ExecContext(ctx, `INSERT INTO orders (item, amount) VALUES ($1, $2)`, "widget", 100)
実行すると orders_history から JSON が標準出力に流れ、before / after / actor_id / trace_id / at
などが格納されていることを確認できます。history.Row 型のフィールド構成は row.go にまとまっており、アプリでデシリアライズする際も同じ構造を利用できます。
CLI(go-history-viewer)の活用
履歴をさっと確認したいときは CLI が便利です。
# 直近 20 件の履歴を表示
DATABASE_URL=postgres://... go-history-viewer log orders
# 指定 PK の変更履歴をフィールド単位で diff 表示
go-history-viewer diff --pk "id=42" orders
# Web UI(ローカル開発用)を起動
go-history-viewer serve --addr :8080
どのサブコマンドも history.yaml を読み込み、基となるテーブル名から orders_history のような実テーブルを解決し、共通のリポジトリ層で取得した
history.Row を整形して表示します。serve サブコマンドを使えばブラウザ上で検索や diff 確認も可能です。
本番運用のヒント
-
history.yamlは通常のマイグレーションと同じリポジトリに置き、レビュー時に「テーブル追加と履歴設定」をまとめて確認できるようにする - API やジョブのエントリポイントで
history.WithMetaを呼ぶミドルウェアを用意し、全リクエストで Actor / Trace ID
が入るようにする -
history.Rowは JSON なので、そのままログ収集やデータ基盤(BigQuery, Redshift など)へ転送して分析に使うのも簡単 - 新しいテーブルを追加したら
postgres.Migrateを再実行してトリガーを更新(冪等なので繰り返し実行しても安全)
まとめ
go-history は小さなライブラリですが、「誰がいつ何を変更したか」を PostgreSQL
で統一的に記録できるようにすることで、監査やトラブルシュートをぐっと楽にしてくれます。README や examples/postgres
を参考に、自分のサービスへ組み込んでみてください。
Discussion