背景
wp10-sql-runner の監査ログ(caitsith2022-main#1748)を実装する過程で、App Log 側に 2 種類の問題が見つかりました。
- 本家と各 PJT のコピーが双方向に乖離している(しかもバージョン番号が同じ)
- 監査ログの土台として使うには機能が足りない
SQL Runner の監査ログを「監査ログです」と顧客に申告すると、その時点で顧客側の保持ルールの対象になります。その前提として App Log 側の対応が要る、というのがこの Issue の動機です。
バージョンは 1.2 とします。
1. 本家と caitsith のコピーが乖離している
caitsith2022-main の src/wp-content/plugins/app-log/ と、このリポジトリの内容を比較しました。どちらも Version: 1.1.4 を名乗っていますが、中身が違います。
本家にあって、caitsith のコピーに無いもの
| 項目 |
場所 |
app_log_log_lifetime フィルタ(保持期間を上書きできる) |
admin/aplg-settings.php |
app_log_date_format フィルタ(ログファイル名の日付書式) |
classes/class-aplg-logger.php |
format_date_by_wp_version()(WP 5.3 以降は wp_date() を使う) |
classes/class-aplg-logger.php |
caitsith のコピーにあって、本家に無いもの
| 項目 |
内容 |
書き込みが file_put_contents( $log_file, $message, FILE_APPEND | LOCK_EX ) |
本家は fopen/fwrite/fclose |
なぜ問題か
- 保持期間を変えられないのはコピー側だけでした。本家には既にフィルタがあります。つまり「保持期間を変更できるようにする」は新規開発ではなく同期の問題です
- バージョン番号でコピーを識別できません。 両方
1.1.4 なので、どの PJT がどの実装を持っているかは差分を取らないと分かりません
- 各 PJT が個別に直した改善(上記の書き込み方式)が本家に還らないため、次に vendoring した PJT がまた同じ問題を踏みます
やること
2. 監査ログの土台として足りないもの(本家にも無い)
2-1. 書き込みの成否が分からない
Aplg_Logger::log() は void で、書き込み結果を呼び出し側に返しません。加えて、記録が黙って落ちる経路が 3 つあります。
$pre = apply_filters( 'pre_applog_write', null, ... );
if ( ! is_null( $pre ) ) {
return; // ① フィルタで握り潰せる(戻り値は呼び出し側に伝わらない)
}
$message = apply_filters( 'app_log_write_log_before', $message, ... );
if ( false === $message ) {
return; // ② 同上
}
$fp = fopen( $log_file, 'a' ); // ③ 失敗しても検知されない(後述)
fwrite( $fp, $message );
fclose( $fp );
監査ログの用途では「書けたつもりで書けていない」が最悪の失敗です。呼び出し側が applog_info() を呼んで正常に戻ってきた以上、記録は残っていると考えます。
2-2. 書き込み失敗で Fatal になりうる
上記③について。fopen() が失敗すると false を返し、PHP 8 では fwrite( false, ... ) が TypeError になります。ディスクフルや権限変更でログ出力が原因のホワイトスクリーンが起きえます。
caitsith のコピーは file_put_contents() を使っており、この経路では落ちません。取り込む価値があるのはこの性質です。
LOCK_EX については判断が要ります(未測定)
「本家はロックが無いから危険」という単純な話ではありません。
FILE_APPEND も fopen($f,'a') も O_APPEND で開くため、1 回の write に収まる大きさなら、どちらも行が混ざりません
- ただし O_APPEND の不可分性は書き込みサイズに依存します。実運用のログには 462KB の 1 行が存在する実績があります(kingsman#4469 の AppLog Viewer 実装時に実測)。この規模では分割されうるので、巨大な行に対しては
LOCK_EX だけが効きます
- 一方でコストがあります。caitsith の出力先は uploads = EFS マウントで、
LOCK_EX はログ 1 行ごとにネットワーク越しのロックになります
どちらが最適かは測定していません。 現時点の提案は「LOCK_EX を採る(巨大行の実績がある以上、外す根拠が無い)/ただしフィルタで外せるようにし、EFS で遅いと分かったら切れる状態にしておく」です。
2-3. 行頭に前置しない出力(raw / JSON モード)が無い
log() は必ず [日時] [LEVEL] (PID: N) を前置します。構造化ログを 1 行 JSON で出したい場合、JSON Lines として読めなくなります([...] {...} になる)。
参考として、kingsman の pm_log()(mu-plugin 版)には既にこの口があります。
function pm_log( mixed $message, string $dirname = '/log/', string $format = 'text', bool $include_msec = false ): void
$format = 'json' で {timestamp, process_id, message} の 1 行 JSON、$include_msec でミリ秒まで出せます。後継である App Log に、前身の分家が持っている機能が無い状態です。
2-4. 自動削除の glob が末尾スラッシュに依存している
$files = glob( $log_dir . '*' );
$log_dir が / で終わらない呼ばれ方をすると、ディレクトリ内のファイルではなく兄弟パスに当たります。log() 側は realpath( $log_dir ) . '/' . $filename と明示的に / を足しているのに、log_auto_delete() に渡すのは正規化前の生パスで、非対称です。
(delta-lint finding dl-6c702523)
2-5. delete_log() が書き込み先と別のディレクトリを見る
log() は第 2 引数 $dirname でサブディレクトリへ書けますが、delete_log() は get_path_to_log_dir() を引数なしで呼ぶため、常にルートのログディレクトリしか参照しません。書き込み先と削除・閲覧の探索先が一致しません。
(delta-lint finding dl-3a3bfc83)
進め方
同期 → 機能追加 → 書き込み方式の統一の順で進めます。
- 同期(1 章)— 本家を正としてコピー側の差分を取り込み、バージョンを 1.2 に。ここで保持期間のフィルタが各 PJT に届く
- 機能追加(2-1 / 2-3 / 2-4 / 2-5)— 成否の返却、raw モード、glob、
delete_log()
- 書き込み方式の統一(2-2)—
LOCK_EX の判断を含む。測定が要るので最後
1 が終わった時点で、SQL Runner の監査ログを 210 日保持に乗せられます。
影響範囲
- App Log を使っているすべての PJT
- 保持期間のフィルタが届くことで、これまで 90 日で消えていたログが消えなくなる環境が出ます。容量の見積もりが要ります
log() の戻り値変更は後方互換(void → bool)
関連
背景
wp10-sql-runnerの監査ログ(caitsith2022-main#1748)を実装する過程で、App Log 側に 2 種類の問題が見つかりました。SQL Runner の監査ログを「監査ログです」と顧客に申告すると、その時点で顧客側の保持ルールの対象になります。その前提として App Log 側の対応が要る、というのがこの Issue の動機です。
バージョンは 1.2 とします。
1. 本家と caitsith のコピーが乖離している
caitsith2022-mainのsrc/wp-content/plugins/app-log/と、このリポジトリの内容を比較しました。どちらもVersion: 1.1.4を名乗っていますが、中身が違います。本家にあって、caitsith のコピーに無いもの
app_log_log_lifetimeフィルタ(保持期間を上書きできる)admin/aplg-settings.phpapp_log_date_formatフィルタ(ログファイル名の日付書式)classes/class-aplg-logger.phpformat_date_by_wp_version()(WP 5.3 以降はwp_date()を使う)classes/class-aplg-logger.phpcaitsith のコピーにあって、本家に無いもの
file_put_contents( $log_file, $message, FILE_APPEND | LOCK_EX )fopen/fwrite/fcloseなぜ問題か
1.1.4なので、どの PJT がどの実装を持っているかは差分を取らないと分かりませんやること
2. 監査ログの土台として足りないもの(本家にも無い)
2-1. 書き込みの成否が分からない
Aplg_Logger::log()はvoidで、書き込み結果を呼び出し側に返しません。加えて、記録が黙って落ちる経路が 3 つあります。監査ログの用途では「書けたつもりで書けていない」が最悪の失敗です。呼び出し側が
applog_info()を呼んで正常に戻ってきた以上、記録は残っていると考えます。log()が成否を返す(bool、あるいは失敗理由つき)2-2. 書き込み失敗で Fatal になりうる
上記③について。
fopen()が失敗するとfalseを返し、PHP 8 ではfwrite( false, ... )が TypeError になります。ディスクフルや権限変更でログ出力が原因のホワイトスクリーンが起きえます。caitsith のコピーは
file_put_contents()を使っており、この経路では落ちません。取り込む価値があるのはこの性質です。LOCK_EXについては判断が要ります(未測定)「本家はロックが無いから危険」という単純な話ではありません。
FILE_APPENDもfopen($f,'a')も O_APPEND で開くため、1 回の write に収まる大きさなら、どちらも行が混ざりませんLOCK_EXだけが効きますLOCK_EXはログ 1 行ごとにネットワーク越しのロックになりますどちらが最適かは測定していません。 現時点の提案は「
LOCK_EXを採る(巨大行の実績がある以上、外す根拠が無い)/ただしフィルタで外せるようにし、EFS で遅いと分かったら切れる状態にしておく」です。LOCK_EXの有無をフィルタで切り替えられるようにする2-3. 行頭に前置しない出力(raw / JSON モード)が無い
log()は必ず[日時] [LEVEL] (PID: N)を前置します。構造化ログを 1 行 JSON で出したい場合、JSON Lines として読めなくなります([...] {...}になる)。参考として、kingsman の
pm_log()(mu-plugin 版)には既にこの口があります。$format = 'json'で{timestamp, process_id, message}の 1 行 JSON、$include_msecでミリ秒まで出せます。後継である App Log に、前身の分家が持っている機能が無い状態です。applog_raw()のような別関数か、$format引数か)pm_log()の$format/$include_msecを取り込む2-4. 自動削除の glob が末尾スラッシュに依存している
$log_dirが/で終わらない呼ばれ方をすると、ディレクトリ内のファイルではなく兄弟パスに当たります。log()側はrealpath( $log_dir ) . '/' . $filenameと明示的に/を足しているのに、log_auto_delete()に渡すのは正規化前の生パスで、非対称です。$log_dirを正規化してから glob する(delta-lint finding
dl-6c702523)2-5.
delete_log()が書き込み先と別のディレクトリを見るlog()は第 2 引数$dirnameでサブディレクトリへ書けますが、delete_log()はget_path_to_log_dir()を引数なしで呼ぶため、常にルートのログディレクトリしか参照しません。書き込み先と削除・閲覧の探索先が一致しません。delete_log()/ ダッシュボードが$dirnameを扱えるようにする(delta-lint finding
dl-3a3bfc83)進め方
同期 → 機能追加 → 書き込み方式の統一の順で進めます。
delete_log()LOCK_EXの判断を含む。測定が要るので最後1 が終わった時点で、SQL Runner の監査ログを 210 日保持に乗せられます。
影響範囲
log()の戻り値変更は後方互換(void→bool)関連