こんにちは、yebis0942です。今回はベースマキナの監査ログ機能の実装改善の取り組みをご紹介します。
簡単にまとめると、プロダクトの機能が充実するにつれて監査ログの仕様管理が複雑化し、メンテナンスコストが増加するという課題がありました。その解決策として、Protocol Buffersを採用し、スキーマベースで出力内容を定義しました。結果として、開発効率とメンテナンス性が向上しました。
以下で詳細についてご説明します。なお、本ブログ内で紹介するコード例は説明のため一部簡略化しています。
ベースマキナの監査ログ
ベースマキナは「安全なオペレーションに必要な管理画面を数分で」を掲げるローコードSaaSです。
私たちは管理画面に必要とされる機能をまとめて提供しており、その一つに「監査ログ機能」があります。管理画面で何らかの操作が行なわれたり、管理画面自体の設定が変更された際に、「いつ」「誰が」「どんな操作をしたか」をログに残して、後から管理者が証跡を確認できる仕組みです。

なお、お客様のAPIへの呼び出しやDBへの書き込み時に送信する情報には、ベースマキナ側では保持できない機密情報が含まれている可能性があるため、監査ログの機能では保存を行いません。代わりにお客様自身のS3やGCSに値を保存する監査ログストリーミング機能をご利用いただけます。
スプレッドシート管理の時代
これまで、監査ログの出力項目はスプレッドシートで管理していました。こちらがその一部です。

スペースの都合で全体を掲載することはできませんが、出力タイミング(監査ログの出力対象となる操作)は約50種類、共通出力項目は5種類(操作の種別によらず出力する共通の項目)、個別出力項目(操作の種別ごとに出し分ける項目)は約20種類となっています。
また、個別出力項目の内容もスプレッドシートで管理していました。

このスプレッドシートの内容を仕様として参照し、実装やユーザー向けドキュメントを更新します。
出力タイミングや個別出力項目は機能追加のたびに増加し、スプレッドシートはすくすくと育っていき、次第にメンテナンスや実装・ドキュメントへの反映が難しくなってきました。
スキーマ定義言語の選定
この問題を解決するため、監査ログの出力項目をスキーマ定義言語で定義することにしました。
サーバーサイドの実装言語であるGoの構造体などをそのまま使うことも検討しましたが、ユーザー向けドキュメントを自動生成したいという狙いがあったため、スキーマ定義言語を採用したほうがコストが少ないと判断しました。
主な選定基準は以下の3点です。
- 書きやすさ・読みやすさ
- 監査ログの出力形式の定義に適した仕様
- ネストしたオブジェクトで、型定義を使い回したい
- エコシステムの充実度(コード生成や他フォーマットへの変換が容易であること)
この選定基準に沿っていくつかのスキーマ定義言語を検討し、最終的にProtocol Buffersを採用することにしました。エコシステムが発達していて、コード生成や他フォーマットへの変換が容易であり、社内での利用実績もあったことが理由です。
Protocol Buffers導入後の開発の流れ
1. 出力タイミングに対応するmessageの定義
監査ログは、出力タイミングに応じて異なる項目を出力します。そのため、Protocol Buffersのmessage(Goの構造体と相互に変換可能なデータ型)を使い、出力タイミングごとの出力項目をスキーマとして定義しました。例えば、レビュー依頼を承認した際のログは以下のように記述します。
// レビュー依頼を承認する操作のmessage message ApproveReviewRequestOperation { Action action = 1; ReviewRequest review_request = 2; } // 個別出力項目(アクション) message Action { string id = 1; string name = 2; } // 個別出力項目(レビュー依頼) message ReviewRequest { string id = 1; }
2. protocを使ったコード生成
Protocol Buffersの標準ツールprotocを使用して、以下を自動生成しています。
- messageに対応するGo言語の構造体(公式に提供されている
protoc-gen-goプラグインを使用) - messageを監査ログ用のロガーに出力するためのメソッド(社内で開発したプラグインを使用)
社内で開発したプラグインでは、Goのコード生成にprotogenを利用しました。protoc-gen-goもこのヘルパーを利用しています。
たとえば、protoc-gen-goの以下のコードを見てみましょう。(protoc-gen-go/internal_gengo/main.goより引用)
// var g *protogen.GeneratedFile g.P("func (x *", m.GoIdent, ") String() string {") g.P("return ", protoimplPackage.Ident("X"), ".MessageStringOf(x)") g.P("}") g.P()
このコードからは以下のようなコードが生成されます。
func (x *Action) String() string { return protoimpl.X.MessageStringOf(x) }
g.P()は一見するとただのfmt.Sprintf()のように見えますが、実は、引数として渡したprotoimplPackageで参照しているパッケージのimport文がソースコードの先頭に自動で挿入されるという高度な仕組みも持っています。
3. messageに対応する構造体に値を詰めるコンストラクタを定義する
DBやHTTPリクエストから取得したデータを構造体に詰め替えるコンストラクタを実装します。このコンストラクタはスキーマからは自動生成できないため、手作業で実装しています。
先にご紹介したレビュー依頼の承認操作の監査ログのコンストラクタは以下のようになっています。
func NewApproveReviewRequestOperation( action *model.Action, reviewRequest *model.ReviewRequest, ) *ApproveReviewRequestOperation { return &ApproveReviewRequestOperation{ Action: NewAction(action), ReviewRequest: NewReviewRequest(reviewRequest), } }
4. 出力処理を実装する
あとは上記で作成した構造体を以下のように監査ログ用のロガーに渡すだけです。出力用のコードはステップ2で自動生成されているため、引数として渡すだけでスキーマに沿った項目が出力されます。
logger.Audit(
ctx,
loggerfield.NewApproveReviewRequestOperation(
action,
reviewRequest,
),
)
スキーマの導入の効果
スキーマから構造体などを自動生成することで、監査ログの出力項目の仕様との一貫性を保証しやすくなりました。また、スキーマ定義言語を導入したことで、監査ログの仕様の議論もスムーズに進められるようになりました。
最後に
Protocol Buffersを利用することで、監査ログの出力処理のメンテナンス性が向上しました。ベースマキナでは、これからも信頼できる管理画面基盤を提供するために改善を続けてまいります。