本文へ移動
松葉 / Matsuba ・ 開発過程資料

記録の基盤/ The Log

何を、どんな形で、どこへ残しているか

記録の置き場は audit_logs という 1 本のテーブルです。この章では、そこに何が入るか(操作の種類・1 行の項目・行為者の種類)と、どう守られているか(追記だけを許す規則・月ごとの分割・書き込みの規律)を扱います。読み出す側は次の 3 章で扱います。

01

記録される操作What Gets Recorded

現在 163 種類です。一覧は共有の定数ファイル 1 本が唯一の置き場で、日本語の表示名も同じファイルに同数そろえてあります。片方だけ増えると自動テストが落ちるので、名前だけ増えて表示名が無い、という状態になりません。

163 Recorded Operations by Area
領域種類主な中身
ファイルとフォルダ39アップロード・差し替え・改名・移動・削除・復元・版の復元・完全削除・アーカイブ・保持の設定・ゴミ箱を空にする・検索・メタデータ編集・コメント・プレビュー・一括ダウンロード
認証とアカウント25サインイン・サインアウト・二要素認証の登録と解除・パスワード再設定・メールアドレスの変更・端末の信頼・規約への同意・プロフィールとアバターの変更・アカウントの停止と復帰・社内ディレクトリとの同期
Box とメンバー20Box の作成・設定変更・公開範囲の変更・保持期間の変更・無効化・メンバーの追加とロール変更と除外・オーナーの引き継ぎ・参加申請の受付と承認
管理と運用17管理画面の閲覧・ユーザの一括停止・Box の強制削除・ストレージの操作・指標の集計に伴う記録
ウイルス検査13検査の完了・感染の検出・検査の失敗・検証不能の確認・再検査・隔離ファイルの取り出しと完全削除
共有リンクとファイルリクエスト13発行・無効化・強制失効・受取側からのアクセス・提出の取り込み
通知の配信12通知の作成・送信・再送・日次まとめの生成・受信設定の変更・不達の記録
Drive 同期12同期の開始と完了・失敗・再同期・管轄外ファイルの検知・アーカイブ側の存否確認
監査とアクティビティ6監査ログの閲覧・CSV の取り出しとその受付・退避分の取得・アクティビティの取り出しとその受付
アラートと配信の運用6アラートを対応中にする・解決する・無視する・失敗イベントの再投入と破棄・滞留した通知の再キュー

名前は {対象}.{動作} の形で付けています(file.upload_initiated、member.role_change、share_link.revoke など)。新しい操作を実装するときは、先に定数へ追記してから実装する順序を規律にしています。逆順にすると、名前の綴りがコードのあちこちに散った状態で固まってしまうためです。

02

1 行に入るものAnatomy of a Row

列は 13 です。「いつ・誰が・何を・何に対して」を固定の列で持ち、操作ごとに違う中身だけを 1 つの自由項目にまとめています。

Columns of audit_logs
列必須内容
occurred_at必須発生時刻。月ごとの分割のキーでもある
id必須連番。月ごとに別の番号列を使う。主キーは (occurred_at, id) の組
actor_type必須行為者の種類(6 値・次節)
actor_id—操作した利用者。システムの操作では空
actor_share_link_id—共有リンク経由のときの発行元リンク
actor_file_request_id—ファイルリクエスト経由のときの発行元依頼
action必須操作の名前(163 種のいずれか)
box_id—対象の Box。Box に紐づかない操作(サインインなど)では空
target_type必須対象の種類(ファイル / フォルダ / メンバー / 共有リンク / コメント / Box / 利用者 / 通知)
target_id—対象の識別子
request_id—1 回のリクエストを束ねる識別子。同じ操作から派生した複数行を辿れる
reason—重大な操作の理由。専用の列で、10〜256 文字の制約がデータベース側に入っている
metadata—操作ごとに違う中身(版の番号・変更前後のロール・期限・件数など)。JSON

接続元の IP と端末情報の置き場

IP アドレスと端末の情報(ブラウザの種類など)は専用の列を持っていません。自由項目のなかに入っています。操作の 163 種すべてに付くものではなく、付ける意味があるのは一部(アップロードやサインインなど)だからです。

過去の行との綴りの違い: 端末情報のキー名は、実装の時期によって 2 通りの綴りが混在しています。読み出す側は両方の綴りを見て、あるほうを採る形にしてあります。片方だけを見る作りにすると、古い行の端末情報が黙って空になり、しかも「元から記録が無かった」と区別できません。

絞り込みを速くするための索引

索引は月ごとの子テーブルに 5 本ずつ張っています。うち 3 本は「値が入っている行だけ」を対象にした部分的な索引で、空の行がほとんどを占める列(Box や対象の種類)でも索引が小さく保たれます。

  • Box と時刻 — Box のアクティビティの主経路
  • 操作者と時刻 — 「この人の操作だけ」の絞り込み
  • 操作の名前と時刻 — 「差し替えだけ」「削除だけ」
  • 対象の種類と識別子と時刻 — 「このファイルに起きたこと」
  • 時刻のみ — 新しい順の一覧(既定の表示)

10 万行を投入した状態で実行計画を固定する自動テストを持っています。守っているのは「索引が並び順を供給し、先頭のページで打ち切れること」と「テーブル全体の走査が現れないこと」です。実行計画そのものを 1 つに固定するのではなく、許容できる索引の集合でアサートしています(絞り込みの有無でデータベースがどちらを選んでも正しいため)。

03

行為者の 6 種類Six Kinds of Actor

「誰が」の欄は、ログインした利用者だけではありません。アカウントを持たない相手の操作や、システムが自動で行った操作も同じテーブルに残るため、種類を 6 つに分けています。

Actor Types
種類誰の操作か識別子の入り方
利用者ログイン済みの一般の利用者(Box のメンバー)利用者の欄に入る
システム管理者全社の管理権限を持つ利用者利用者の欄に入る
社内の閲覧者社内に公開されている Box を、その Box のメンバーではない社内の利用者が開いたとき利用者の欄に入る
共有リンクリンクを受け取った社外の相手(アカウントなし)発行元リンクの欄に入る。利用者の欄は空
ファイルリクエスト提出を依頼された社外の相手(アカウントなし)発行元依頼の欄に入る。利用者の欄は空
システムバッチ・ワーカー・自動処理3 つの欄すべてが空

この 6 通りの「どの欄が埋まるか」は、データベースの制約として4 分岐で強制しています。たとえば共有リンク経由の行に利用者の識別子が入っていたら、その行は書き込みの時点で拒否されます。行為者の種類と識別子の組み合わせが崩れた行は、後から見つけても直せない(更新できない)ため、入る前に止めています。

04

追記だけを許すアクセス制御Append-Only Access

データベースには「誰がどの行に何をしてよいか」を規則として書く仕組みがあります。記録のテーブルでは、追加と参照の規則だけを書き、更新と削除の規則を書いていません。次の図は、その結果として何が通り何が止まるかを示したものです。

図 02 · 追記だけを許すアクセス制御 — 誰が、どの道を通り、どうなるか
誰が通る道結果追加各業務処理matsuba-api / matsuba-batch返さない一括追加の形だけ業務データと同じトランザクションで書く許可ログインの有無を問わず全員(社外の相手・システムを含む)参照システム管理者管理者向けの API(8 端点)理由・1 時間 5 回・閲覧の記録つき許可 — 生のデータ全部IP・端末情報・自由項目まで含む参照Box のメンバーbox_activity_for_memberこの関数だけが通り道(テーブルへの直接の参照権は無い)許可 — 絞ったうえで52 種のみ・項目も対応表で絞る参照上記以外(一般の利用者・未ログインの経路)テーブルへの直接の参照(アプリからは呼ばない経路)拒否エラーではなく 0 件が返る(存在そのものを伏せる)更新・削除誰であっても(システム管理者を含む)許可の規則そのものが存在しない(「権限を絞る」ではなく「規則が無い」)実行できない権限の付け替えでも緩まない

赤い枠は「通らない」ことを示します。4 行目の拒否がエラーではなく 0 件なのは、記録の存在そのものを伏せるためです。5 行目は権限の設定ミスで緩む余地がありません(規則が無いものは、権限をどう付け替えても許可になりません)。この 4 通りはすべてデータベースを実際に動かす自動テストで固定してあります。

自分の依頼を「成功」に偽装できないようにする

CSV の取り出し依頼を記録する台帳のテーブルには、もう 1 段の防御を入れています。利用者が追加できる列を限定し、状態・出力先・件数・完了時刻といった列は追加の対象から外してあります。外さないと、「状態は成功、出力先は退避済みの月のファイル」という行を自分で追加し、それに対してダウンロード用の URL を発行させる、という経路が成立してしまいます。

さらに URL を発行する側でも、出力先の文字列がその依頼の専用の場所を指しているかを毎回確かめています。1 段目(列の限定)を越えられても、2 段目で止まります。

05

月ごとに子テーブルへ分けるMonthly Partitions

記録のテーブルは、見た目は 1 本ですが、内部では月ごとの子テーブルに分かれています。書き込みも読み出しも窓口は親テーブル 1 つで、どの子に振り分けるかはデータベースが決めます。

図 03 · 月ごとに子テーブルへ分ける — 書き込みは最新の月だけに当たる
audit_logs(親)書き込みと参照の窓口はここ 1 つ。どの子へ振り分けるかはデータベースが決める2 年より前25 ヶ月を超えた月切り離して外へ読めなくなる過去の月参照のみ書き込みは当たらない月の数だけ最新の月書き込みが当たる唯一の子追記は 1 面だけ先の月空のまま先に作る残数を毎月点検する数面受け皿どの月にも入らない行の行き先入ったら警告audit_archive切り離した月が移る場所pg_partman + pg_cron週次で、数ヶ月先までの月を作り、25 ヶ月を超えた月を切り離す絞り込みも月単位で落ちる期間で絞ると、範囲外の月は走査の対象から外れる。過去 2 年分が積み上がっても追記の速度は変わらない

「受け皿」は、どの月の範囲にも当てはまらない行が入る子テーブルです。正常な運用では 1 行も入りません。ここに行が入るのは「先の月を作り忘れた」ときで、放置すると気づかないまま記録が 1 か所に溜まるため、月次の点検で件数を実際に数えて確認しています(統計の推定値は使っていません。未集計のときは鳴らず、古い値のときは誤って鳴るためです)。

06

書き込みの規律Write Discipline

記録は「業務処理のついでに書く」ものなので、書き方を揃えておかないと、実装のたびに少しずつ違う形が混ざります。次の 3 つを規律として固定しています。

Three Write Rules
規律内容破ったときに何が起きるか
同じ区切りで書く業務データの更新と記録の追加を、同じトランザクションに入れる記録だけが残る/業務データだけが残る、という食い違いが生じる
返さない一括追加ログイン利用者の経路では、追加した行を読み返さない形だけを使う本番で必ず失敗する(読み返しは参照に当たり、一般の利用者には参照の権限が無い)。レビューの見落としがあっても混入しない
名前を先に決める新しい操作は、共有の定数へ追記してから実装する綴りがコード中に散り、一覧と実態が合わなくなる。機械検査が実装より先に落ちる形にしてある

自由項目に個人情報を入れない

操作ごとに違う中身は自由項目(JSON)に入りますが、ここに何を入れるかは操作ごとに精査しています。とくにメールアドレスの実値は、原則として入れません。たとえば共有リンクの受取人や、ファイルリクエストの提出者のアドレスは、記録には残さず、表示が必要になった時点で発行元のテーブルから引き直し、伏せ字にしてから返す形にしてあります。記録に入れてしまうと、後から伏せることも消すこともできなくなるためです。

例外として、接続元の IP と端末情報は自由項目に入ります。これは調査に必要な情報で、参照できるのはシステム管理者だけ、取り出すには理由の入力が要る、という形で扱いを制限しています。