2026年8月7日
2026年8月7日
シェルスクリプトでログ出力を実装する方法:レベル・タイムスタンプ・エラー捕捉・ローテーション
シェルスクリプトのログ出力は、「時刻・レベル・スクリプト名・メッセージ」を1行にまとめる log 関数を作り、エラー時の自動記録(trap)とログファイルのローテーションを組み合わせると、運用で困らない形になります。この記事では、cron で動くバックアップやメンテナンス用スクリプトを想定して、そのまま使えるログ関数の作り方と、出力先の選び方、秘密情報を漏らさないための注意点を説明します。
まず決めること:何を、どこに出すか
ログを書き始める前に、出力先の役割を分けておくと後で迷いません。
- 標準出力(stdout): スクリプトの「結果」そのもの。別のコマンドにパイプで渡すデータはここに出す。
- 標準エラー出力(stderr): 人が見るための進捗や警告。ログはこちらに出すのが基本。
- ログファイル: cron など、画面がない環境で後から追跡するための記録。
- syslog / journald: サーバー全体のログと一緒に集約・検索したいとき。
ログを stdout に混ぜると、./script.sh | jq のように結果を加工したいときに壊れます。「データは stdout、ログは stderr かファイル」と分けるのが最初のポイントです。
基本のログ関数
次の関数は、レベルによる絞り込み、タイムスタンプ、プロセスIDを付けてファイルに追記し、警告以上は stderr にも出します。
#!/usr/bin/env bash
set -Eeuo pipefail
readonly SCRIPT_NAME="${0##*/}"
LOG_FILE="${LOG_FILE:-/var/log/wp-jobs/${SCRIPT_NAME%.sh}.log}"
LOG_LEVEL="${LOG_LEVEL:-INFO}"
level_num() {
case "$1" in
DEBUG) echo 10 ;;
INFO) echo 20 ;;
WARN) echo 30 ;;
ERROR) echo 40 ;;
*) echo 20 ;;
esac
}
log() {
local level="$1"; shift
if (( $(level_num "$level") < $(level_num "$LOG_LEVEL") )); then
return 0
fi
local ts line
printf -v ts '%(%Y-%m-%dT%H:%M:%S%z)T' -1
line="${ts} [${level}] ${SCRIPT_NAME}[$$]: $*"
printf '%s\n' "$line" >> "$LOG_FILE"
if [[ "$level" == "WARN" || "$level" == "ERROR" || -t 2 ]]; then
printf '%s\n' "$line" >&2
fi
}
mkdir -p "$(dirname "$LOG_FILE")"
log INFO "start"
log DEBUG "this line appears only when LOG_LEVEL=DEBUG"
いくつか補足します。
printf -v ts '%(...)T' -1は Bash 4.2 以降の組み込み機能で、dateコマンドを毎回起動せずに現在時刻を整形できます。ログを大量に書くループでは差が出ます。macOS 標準の古い Bash では使えないので、その場合はdate '+%Y-%m-%dT%H:%M:%S%z'に置き換えてください。echoではなくprintf '%s\n'を使うのは、メッセージが-nなどで始まったときにオプションと解釈されるのを避けるためです。-t 2は「stderr が端末につながっているか」の判定です。手動実行時は画面にすべて表示し、cron 実行時は警告以上だけを stderr に出す、という使い分けになります。- タイムゾーン付き(
%z)にしておくと、サーバー移転や UTC 設定のサーバーが混在したときにも時刻を突き合わせやすくなります。
LOG_LEVEL=DEBUG ./script.sh のように環境変数で詳しさを切り替えられるので、普段は INFO、調査時だけ DEBUG という運用ができます。
エラーを自動で記録する(trap)
set -e で止まったスクリプトは、どこで何が失敗したかを自分では語りません。trap を使って、失敗した行番号とコマンドを記録します。
on_error() {
local status="$1" line="$2" cmd="$3"
log ERROR "failed (exit ${status}) at line ${line}: ${cmd}"
}
trap 'on_error "$?" "$LINENO" "$BASH_COMMAND"' ERR
trap 'log INFO "finished (exit $?)"' EXIT
set -E(先頭の set -Eeuo pipefail の E)を付けておかないと、関数の中で起きたエラーに ERR トラップが働きません。$? はトラップの中で最初に評価しないと別のコマンドの結果に上書きされるため、引数として渡しています。EXIT トラップで終了コードを毎回残しておくと、「途中で止まったのか、最後まで走ったのか」がログだけで判別できます。
スクリプト全体の出力をまとめて残す
既存のスクリプトを大きく書き換えずにログを残したい場合は、実行時のリダイレクトが手軽です。cron なら次のように書きます。
MAILTO=""
15 3 * * * /usr/local/bin/wp-backup.sh >> /var/log/wp-jobs/wp-backup.log 2>&1
2>&1 を忘れるとエラーメッセージがログに残らず、cron のメール(設定されていれば)に飛ぶか、そのまま消えます。スクリプトの中で exec > >(tee -a "$LOG_FILE") 2>&1 とする方法もありますが、プロセス置換は非同期に動くため、終了直前の行が欠けたり順序が前後したりすることがあります。行ごとに時刻も付かないので、基本は log 関数を使い、リダイレクトは「想定外の出力の取りこぼし防止」と位置づけるのがよいでしょう。
syslog / journald に送る
複数のスクリプトのログをサーバー全体のログと一緒に扱いたいなら、logger コマンドが使えます。
logger -t wp-backup -p user.info "backup started"
logger -t wp-backup -p user.err "backup failed"
# systemd 環境での確認
journalctl -t wp-backup --since today
-t で付けたタグで検索できるので、ファイルの置き場所や権限を気にせずに済みます。ログ集約の仕組み(rsyslog の転送や監視サービス)がすでにある環境では、こちらに寄せる方が管理が楽になります。
ログファイルのローテーション
ファイルに追記し続けると、いずれディスクを圧迫します。logrotate の設定を用意しておきます。
# /etc/logrotate.d/wp-jobs
/var/log/wp-jobs/*.log {
weekly
rotate 8
compress
delaycompress
missingok
notifempty
create 0640 root adm
}
上の log 関数は書き込みのたびにファイルを開き直すので、ローテーション後は自動的に新しいファイルへ書き込みます。一方、exec >> file のようにファイルを開いたまま長時間動くプロセスでは、ローテーション後も古いファイルに書き続けてしまうため、copytruncate を使うかプロセスを再起動する必要があります。設定を入れたら logrotate -d /etc/logrotate.d/wp-jobs で、実際には回さずに動作を確認できます。adm グループは Debian/Ubuntu 系の例なので、環境に合わせて変えてください。
秘密情報をログに残さない
ログは権限の広い人に読まれたり、バックアップに含まれて外部に出たりしがちです。次の点に注意します。
- API トークン、DB パスワード、Application Passwords などをメッセージに含めない。
curlのコマンドラインをそのまま記録するのも避ける。 set -xのトレースは変数の中身まで展開して出力します。デバッグ時はBASH_XTRACEFDでトレース先を別ファイルに分け、秘密を扱う区間の前後でset +x/set -xを切り替える。- ログファイルの権限は
0640程度にし、Web から見えるディレクトリ(WordPress のドキュメントルート配下など)には置かない。 - 個人情報(メールアドレスや IP アドレス)を記録する場合は、保存期間を決めてローテーションで消えるようにし、サイトのプライバシーポリシーとの整合も確認する。
複数のプロセスが同じファイルに同時に追記する場合、1行を1回の printf で書いていれば行の途中で混ざることは通常ありませんが、厳密な順序が必要なら flock で排他するか、プロセスごとにファイルを分けます。
まとめ
- 結果は stdout、ログは stderr かファイルに分ける。
log関数で時刻・レベル・スクリプト名・PID を1行にまとめ、LOG_LEVELで詳しさを切り替える。set -Eと ERR / EXIT トラップで、失敗箇所と終了コードを自動で記録する。- cron では
>> file 2>&1で取りこぼしを防ぎ、集約したいときはloggerと journald を使う。 - logrotate でローテーションし、トークンや個人情報をログに残さない(2026年10月現在の一般的な Linux 環境を前提にした例です)。