🍣

go-history で PostgreSQL に行レベル履歴を追加する

に公開

この記事は ChatGPT を使って書かれた

TL;DR

https://github.com/mickamy/go-history

github.com/mickamy/go-history は、アプリ側の書き換えなしで PostgreSQL
に監査向けの履歴テーブルを追加するための部品群です。

  • history.Config で監査対象テーブルや PK、記録する操作を宣言的に定義(YAML / Go 両対応)
  • postgres.BuildDDLs / postgres.Migrate が履歴テーブル + トリガーを冪等な SQL として生成
  • postgres.Installdatabase/sql ドライバをラップし、history.WithMeta で渡した Actor/Trace 情報をセッション変数経由でトリガーへ伝達

アプリは従来どおり db.ExecContext 等を呼ぶだけで、トリガーが orders_history のようなテーブルに history.Row
レコードを書き込んでくれます。CLI (cmd/viewer) も同梱されているので、log/diff/serve で履歴を素早く確認できます。

なぜまた監査ライブラリ?

プロジェクトごとに「users_history を自前で作る」パターンは珍しくありませんが、運用が進むと次のような課題が出てきます。

  1. サービスごとに保存する列や JSON 形式が微妙に違う
  2. どの環境にどのトリガーが入っているか追いきれない
  3. 操作を実行した利用者やリクエスト Trace と紐付けられない

go-history は設定ファイル 1 つをソースに履歴テーブル・トリガーを生成し、history.Row
という共通スキーマに落とし込むことでこれらを解消します。アプリから受け取ったメタデータを必ず書き込むため、誰がどのリクエストで更新したのかを後から追跡できます。生成される
SQL は冪等なので、マイグレーションツールに組み込んで繰り返し適用しても安全です。

リポジトリ構成

Path 説明
/config.go, /meta.go, /row.go 設定/メタデータ/履歴行スキーマなどコア型定義
/postgres PostgreSQL 用 DDL 生成・マイグレーション・ドライバラッパ
/cmd/viewer logdiffserve を備えた 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