シェルスクリプトのログ設計パターン|ログレベル関数・タイムスタンプ付与・ファイルとコンソール分岐を実装する方法

運営:リナックスマスター.JP(代表 宮崎智広)
HOME > Linux技術 リナックスマスター.JP(Linuxマスター.JP) > シェルスクリプト > シェルスクリプトのログ設計パターン|ログレベル関数・タイムスタンプ付与・ファイルとコンソール分岐を実装する方法
「スクリプトを動かしたけど、後から何が起きたのか確認できない」「cronで自動実行したら静かに失敗していた」
こうした状況を防ぐには、スクリプト設計の段階からログ機能を組み込む必要があります。

この記事では、RHEL 9系/Ubuntu 24.04 LTSを想定環境として、シェルスクリプトのログ機能を設計・実装する方法を解説します。ログレベル関数の作り方から、タイムスタンプ付与、teeによるファイルとコンソールへの同時出力、trap EXITとの組み合わせまでを取り上げます。

この記事のポイント

・DEBUG/INFO/WARN/ERROR の4段階ログレベル関数を自作できる
・タイムスタンプとスクリプト名を付与して追跡しやすくする
・tee でコンソールとログファイルへ同時に書き出す設計を実装できる
・trap EXIT でスクリプト終了時の自動ログ記録を組み込める


「このままじゃマズい」と感じていませんか?
参考書を開く気力もない、同年代に取り残される不安——
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら

スクリプトにログ機能を組み込む理由

echo でメッセージを出力しているだけでは、cronで自動実行したときに何が起きたか後から追いにくいです。ログ機能と単なる echo の違いは、以下の3点に集約されます。

・タイムスタンプ:いつ処理が走ったかを記録する
・ログレベル:情報・警告・エラーを視覚的に区別する
・ファイル出力:実行後に確認できるよう永続化する

これらが揃わないと、スクリプトが失敗したときに「何時に」「どの処理で」「どのエラーが出たか」を特定するのに時間がかかります。保守性の高いスクリプトを書くには、最初からログ設計を組み込む習慣が重要です。

ログレベル関数の設計(DEBUG/INFO/WARN/ERROR)

ログレベルとは、メッセージの重要度を示す区分です。bash スクリプトではライブラリがないため、自前で関数を定義します。

1. ログレベル関数の定義

以下のようにファイルの先頭(またはライブラリファイル)に定義します。

#!/bin/bash set -euo pipefail # ログレベル定数(0=DEBUG 1=INFO 2=WARN 3=ERROR) LOG_LEVEL=${LOG_LEVEL:-1} LOG_FILE=${LOG_FILE:-/var/log/myscript.log} _log() { local level="$1" local level_num="$2" local msg="$3" local ts ts=$(date '+%Y-%m-%d %H:%M:%S') local line="${ts} [${level}] ${BASH_SOURCE[1]##*/}:${BASH_LINENO[0]} ${msg}" if [ "${level_num}" -ge "${LOG_LEVEL}" ]; then echo "${line}" | tee -a "${LOG_FILE}" fi } log_debug() { _log "DEBUG" 0 "$*"; } log_info() { _log "INFO" 1 "$*"; } log_warn() { _log "WARN" 2 "$*"; } log_error() { _log "ERROR" 3 "$*"; }

BASH_SOURCE[1] と BASH_LINENO[0] を使うことで、どのファイルの何行目で呼ばれたかを記録できます。

2. 呼び出し例

log_info "バックアップ処理を開始します" log_debug "対象ディレクトリ: /var/data" log_warn "ディスク使用率が80%を超えています" log_error "接続に失敗しました"

出力例(環境により異なります):

2026-10-19 10:00:01 [INFO] backup.sh:45 バックアップ処理を開始します 2026-10-19 10:00:01 [DEBUG] backup.sh:46 対象ディレクトリ: /var/data 2026-10-19 10:00:05 [WARN] backup.sh:52 ディスク使用率が80%を超えています 2026-10-19 10:00:08 [ERROR] backup.sh:60 接続に失敗しました

LOG_LEVEL=0 に設定するとDEBUGも含めて全レベルを出力し、LOG_LEVEL=3 にするとERRORだけを記録します。本番稼働時は INFO(1)または WARN(2)、問題調査時は DEBUG(0)に切り替える運用が一般的です。

タイムスタンプとスクリプト名を付与する

先の _log 関数では date '+%Y-%m-%d %H:%M:%S' でタイムスタンプを付けています。ここでよくある落とし穴がサブシェルのコストです。$(date) はコマンド置換であり、1行ログを出すたびに外部プロセスが起動します。

高頻度にログを出すスクリプトでは printf '%(%Y-%m-%d %H:%M:%S)T' -1(bash 4.2以降)を使うとコマンド置換を避けられます。

# bash 4.2以降: サブシェルを使わないタイムスタンプ取得 printf -v ts '%(%Y-%m-%d %H:%M:%S)T' -1 echo "${ts} [INFO] 処理開始"

スクリプト名は ${BASH_SOURCE[0]##*/} または $(basename "$0") で取得できます。スクリプトが別スクリプトから source されたときも BASH_SOURCE を使うと正しいファイル名を記録できます。

ファイルとコンソールに同時出力する(tee活用)

先の _log 関数では echo "${line}" | tee -a "${LOG_FILE}" を使っています。tee の役割は、標準入力から受け取った内容を標準出力(コンソール)とファイルの両方に書き出すことです。

3. 用途に応じた出力の切り替え

# ファイルのみに出力する(cronなどサイレント実行) echo "${line}" >> "${LOG_FILE}" # コンソール + ファイルの両方に出力する echo "${line}" | tee -a "${LOG_FILE}" # WARN以上だけ標準エラーへ出力する if [ "${level_num}" -ge 2 ]; then echo "${line}" | tee -a "${LOG_FILE}" >&2 else echo "${line}" >> "${LOG_FILE}" fi

シェルスクリプトの実務的な設計パターンについては、シェルスクリプト入門(shellscript.linuxmaster.jp)でも詳しく扱っています。

4. スクリプト全体の出力をまとめてログに記録する

関数ごとに tee を書くのが手間な場合は、スクリプト冒頭でファイルディスクリプタをリダイレクトする方法があります。

#!/bin/bash # スクリプト全体の stdout/stderr をログファイルへ記録する exec > >(tee -a /var/log/myscript.log) 2>&1 echo "この行もログに記録される" ls /nonexistent # エラーも記録される

exec > >(tee -a logfile) 2>&1 はスクリプト全体の標準出力と標準エラーをteeにリダイレクトします。個別のログ関数が不要な場面で有効ですが、ログレベルの区分がなくなる点に注意してください。

trap EXITでスクリプト終了時のログを自動記録する

スクリプトが正常終了したか異常終了したかを自動で記録するには、trap コマンドを使います。EXIT シグナルはスクリプトが終了するすべての経路(正常・エラー・シグナル受信)でトリガーされます。

#!/bin/bash set -euo pipefail LOG_FILE=/var/log/myscript.log SCRIPT_START=$(date '+%Y-%m-%d %H:%M:%S') # 終了時ハンドラ: 終了コードをログに記録する _on_exit() { local exit_code=$? local end_ts end_ts=$(date '+%Y-%m-%d %H:%M:%S') if [ "${exit_code}" -eq 0 ]; then echo "${end_ts} [INFO] スクリプト正常終了 (exit_code=0)" >> "${LOG_FILE}" else echo "${end_ts} [ERROR] スクリプト異常終了 (exit_code=${exit_code})" >> "${LOG_FILE}" fi } trap '_on_exit' EXIT echo "${SCRIPT_START} [INFO] 処理開始" >> "${LOG_FILE}"

_on_exit 関数内で $? を最初の行で受け取ることが重要です。後続のコマンドが $? を上書きしてしまうためです。

5. ERRシグナルで失敗行番号を記録する

set -e との組み合わせでは、ERR トラップがコマンド失敗時に発火します。EXIT と ERR を組み合わせると「どこで失敗し」「スクリプト全体がどの終了コードで終わったか」の両方が記録されます。

# ERR トラップ: 失敗したコマンドの行番号を記録する _on_err() { local exit_code=$? local line_no="${BASH_LINENO[0]}" echo "$(date '+%Y-%m-%d %H:%M:%S') [ERROR] ${BASH_SOURCE[1]##*/} line ${line_no} で失敗 (exit_code=${exit_code})" >> "${LOG_FILE}" } trap '_on_err' ERR

ログファイルの肥大化を防ぐ運用設計

毎日 cron で実行するスクリプトは、ログを上書きにするかローテーションと組み合わせるかを設計段階で決めておく必要があります。

6. 日付付きログファイル名で自動的に分割する

# 日付付きログファイル: /var/log/backup/2026-10-19.log のように自動分割 LOG_DIR=/var/log/backup mkdir -p "${LOG_DIR}" LOG_FILE="${LOG_DIR}/$(date '+%Y-%m-%d').log"

7. logrotateと連携する

固定パスのログファイルを使う場合は、logrotateに設定を追加してローテーションを自動化します。

# /etc/logrotate.d/myscript /var/log/myscript.log { daily rotate 30 compress missingok notifempty create 0644 root root }

compress で古いログを gzip 圧縮し、rotate 30 で30世代分を保持します。missingok はログファイルが存在しない場合にエラーを出さない設定で、スクリプトが一度も実行されていない状態でも logrotate が止まりません。

8. スクリプト側で古いログを自動削除する

# 30日以上古いログファイルを削除する find /var/log/backup -name '*.log' -mtime +30 -delete

-mtime +30 は最終更新日時が30日より前のファイルを対象にします。定期実行スクリプトの冒頭または末尾に置いておくと、ディスクの肥大化を自動で抑止できます。

よくあるトラブルと対処

「Permission denied」でログが書き込めない

LOG_FILE に指定したパスの親ディレクトリが存在しないか、書き込み権限がない場合に発生します。スクリプト冒頭で mkdir -p と権限確認を入れてください。

LOG_DIR=/var/log/backup mkdir -p "${LOG_DIR}" || { echo "ログディレクトリ作成失敗: ${LOG_DIR}"; exit 1; } LOG_FILE="${LOG_DIR}/myscript.log"

ERRトラップが発火しない

set -e を有効にしても、関数の戻り値を if や || で評価しているコマンドは「失敗を想定している」とみなされ、ERRトラップが発火しません。また、サブシェル内($(...))の失敗も親シェルのERRトラップを発火させない場合があります。デバッグ時は set -x で実行トレースを有効にして動作を確認します。

# ERRトラップが発火しないケース(if文の中は例外) if some_command; then log_info "成功" fi # ERRトラップが発火するケース(直接呼び出し) some_command log_info "成功"

teeのパイプでexitコードが消える

cmd | tee file の形では、パイプ全体の終了コードは最後のコマンド(tee)の終了コードになります。set -o pipefail を有効にするとパイプ内の最初の失敗コマンドの終了コードが使われます。set -euo pipefail を記事の冒頭に書く習慣を持つとこの問題を防げます。

本記事のまとめ

シェルスクリプトのログ設計に必要な要素をまとめます。

やりたいこと 実装方法
ログレベルを区分する _log() 関数 + LOG_LEVEL 変数
タイムスタンプを付ける date '+%Y-%m-%d %H:%M:%S' または printf '%(%Y-%m-%d %H:%M:%S)T' -1
ファイルとコンソールに同時出力 echo ... | tee -a logfile
全体の出力をまとめて記録 exec > >(tee -a logfile) 2>&1
終了時に結果を自動記録 trap '_on_exit' EXIT
失敗行番号を記録 trap '_on_err' ERR + BASH_LINENO[0]
ログを日付ごとに分割 LOG_FILE="dir/$(date '+%Y-%m-%d').log"
古いログを自動削除 find dir -name '*.log' -mtime +30 -delete

ログ機能を最初から設計に組み込んでおくと、本番で問題が起きたときに原因特定が格段に速くなります。まず _log 関数と trap EXIT の組み合わせを既存スクリプトに追加するところから始めてみてください。

現場で通用する安全なLinuxサーバー構築の「型」を体系的に身につけたい方へ、
シェルスクリプトの設計から本番運用の実践まで、体系的に学べる環境を用意しています。
>>シェルスクリプト入門はこちら

無料メルマガで学習を続ける

Linuxの実践スキルをメールで毎週お届け。
登録は30秒、解除もいつでも可。

登録無料・いつでも解除できます

暗記不要・1時間後にはサーバーが動く

3,100名以上が実践した「型」を無料で公開中

プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。

姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら

Linux無料マニュアル(図解60P) 名前とメールで30秒登録

運営

リナックスマスター.JP(代表 宮崎智広)

株式会社イーネットマーキュリーが運営するLinux教育サイトです。代表の宮崎智広は現役のLinuxサーバー管理者として20年以上の実務経験を持ち、累計3,100名以上のエンジニアを指導してきました。実践型のLinuxセミナーや無料マニュアルの配布を通じてLinux人材の育成に取り組んでいます。