Powered by SmartDoc

3 trap に獲らまる

trapはshにおける割込み処理関数である.基本は実行時にシェルがシグナルを受けたときに実行する手続きを指定することで行なう.最近というか以前から良くというかハマったことがある.それはロギング時のログファイルの切り替えである.通常,ロギングはsyslogd+newsyslogで自動的にログファイルをローテートする方法を用いるが,newsyslogは指定された条件になれば実行プログラム側にシグナルを送り,その後しばらく後にログファイルをローテートする.だが,出力するファイルの切換えは実行プログラム側に任されている.したがって,同じファイルにログを出力していると新しいファイルにちゃんと出力されないことになる.とりあえず,newsyslogの環境を整えよう.まず,/etc/syslog.confにファシリティlocal5のレベルinfoで/var/log/test.logに出力することにしよう.

リスト 1 /etc/syslog.conf
local5.info /var/log/test.log

で,test.logを10KBに達っしたらxzで圧縮してtest.*.xzにローテートしていくことにしよう.

リスト 2 /usr/local/etc/newsyslog.conf.d/test.conf
/var/log/test.log 644 3 10 * X /var/run/test.pid 30

この例では3つのファイルにローテートし,ローテートの条件になったら/var/run/test.pidのPIDにシグナル30を送ることになる.ここでは/usr/local/etc/newsyslog.conf.d/test.confにコンフィグファイルを置いているので/etc/newsyslog.conf

リスト 3 /etc/newsyslog.conf
<include> /usr/local/etc/newsyslog.conf.d/*

を忘れないように.テストモードなのでnewsyslogを1分間隔で起動しよう.

リスト 4 /etc/crontab
*/1 * * * * root newsyslog -v

に変更し,syslogdを再起動する.

# service syslogd restart

で,肝心のテストプログラムだが,まず,何もしないで単にログを垂れ流す場合,

リスト 5 logrotate_bad0.sh
#!/bin/sh

doing()
{
    local __finish=false

    echo -n "$1"
    trap "__finish=true" 6
    while true; do
        if ${__finish}; then
            echo -n 'done'
            sleep 1
            break
        else
            echo -n '.'
            sleep 1
        fi
    done
    tput dl
    sleep 1
}

running()
{
    case $1 in
    start)
        shift
        doing "$1" &
        _doing_proc_id=$!
        ;;
    stop)
        kill -6 ${_doing_proc_id}
        sleep 1
        ;;
    esac
}

echo $$ > /var/run/test.pid
trap "running stop; kill -9 $$" 2
running start "Exec process"
exec >> /var/log/test.log 2>&1
: $((n = 1))
while true; do
    echo "=== Message $n"
    if [ $((n % 10)) -eq 0 ]; then
        sleep 1
    fi
    : $((n += 1))
done
running stop

簡単に説明すると,

  1. まず,起動スクリプトのPIDを/var/run/test.pidに入れる
  2. C-cが押されたらrunningを止めてスクリプトを停止するtrapをかける

     ちなみに running は何も出力されないのは寂しいので 1 秒おきに. を出力するだけの暇潰し関数 doing を制 御する関数です.running start "なんたら"で,"なんたら" を 出力してdoingをスタート,running stopdoing を停止します.この停止はシグナル 6 (ABORT) でやって ます.

  3. running start "Exec process"
  4. 以降の全ての出力を/var/log/test.logにリダイレクト
  5. テストメッセージを垂れ流す

     "=== Message 番号" を出力し続けるが,10 秒おきに sleep を入れている.これがないと newsyslog がローテートチェック する時間間隔(1分)までに 10KB を遥かに超える出力をしてしまう;-(

とりあえず実行

# sh ./logrotate_bad0.sh
Exec \
    process.........................................................................................................ユーザーシグナル1 \
# .....
# ............
# .........................
# 

newsyslogからはちゃんとシグナル30が送られていることがわかるが,更新されたログファイルにはロギングされてないことがわかる.

# ll -h /var/log/test.log*
-rw-r--r--  1 root  wheel    75B  5月 15 23:34 /var/log/test.log
-rw-r--r-- 1 root wheel 696B 5月 15 23:34 /var/log/test.log.0.xz
# cat /var/log/test.log
May 15 23:34:00 kazu newsyslog[21662]: logfile turned over due to \
    size>10K
# 

なお,C-cで終了しなが,これはシグナル30が送られた時点で実行スクリプト自体が落ちてしまい,doingがゾンビプロセスになったからだ.とりあえず手動で止める.

# ps -ax|grep bad0
21723  7  S+      0:00.00 grep bad0
21432  9  S       0:00.03 sh ./logrotate_bad0.sh
# kill -6 21432
# 

では次にシグナル30を拾ってログファイルに再書込みするtrapを入れよう.これはexecの前に

リスト 6 logrotate_bad1.sh
trap "exec >> /var/log/test.log 2>&1" 30

を入れれば良い.ではこれで実行してみよう.

# sh ./logrotate_bad1.sh
Exec \
    process................................................................................................................................................................................................................................^Cdone強制終了 \
# 

おっ今度はうまくいっているようだ.確かにちゃんとログファイルがローテートして入っている.

# ll -h /var/log/test.log*
-rw-r--r--  1 root  wheel   3.1k  5月 16 00:11 /var/log/test.log
-rw-r--r-- 1 root wheel 508B 5月 16 00:11 /var/log/test.log.0.xz
-rw-r--r-- 1 root wheel 568B 5月 16 00:10 /var/log/test.log.1.xz
-rw-r--r-- 1 root wheel 476B 5月 16 00:09 /var/log/test.log.2.xz
# 

ということで,いきなりmake buildworldに挑戦!テストプログラムの垂れ流しメッセージの部分をmake buildworldして流してみよう.

リスト 7 logrotate_bad2.sh
・・・
echo $$ > /var/run/test.pid
trap "running stop; kill -9 $$" 2
trap "exec >> /var/log/test.log 2>&1" 30
running start "Exec process"
exec >> /var/log/test.log 2>&1
cd /usr/src
make buildworld
running stop
# sh ./logrotate_bad2.sh
Exec \
    process................................................................................................................................................................................................................................^Cdone強制終了 \
# ll -h /var/log/test.log*
-rw-r--r--  1 root  wheel    74B  5月 16 00:19 /var/log/test.log
-rw-r--r-- 1 root wheel 2k 5月 16 00:19 /var/log/test.log.0.xz
# 

あれっ?全くローテートしていない.これはmakeなどの処理では複数プロセスが生成され,各々が別々に出力するため,元プロセスのみリダイレクトし直しても新たに生成されたファイルには出力されないとうことだ.言い換えると初めに生成されたファイルのi-nodeと同じファイルに出力すれば問題は解決されることになる.これはnewsyslog自体をそのように書換えないといけないということだ.ということでnewsyslogはあっさり諦めて自前でログローテートスクリプトを作ることにする.いきなりだが,

リスト 8 logrotate.sh
#!/bin/sh

conv_date_fmt()
{
    local __tz __time __sign __hour=0 __min=0 __arg_utc='' \
        __date __src_fmt __dest_fmt

    case $1 in
    -u)
        shift
        __tz=$1
        __time=`env LANG=C date -j -f "%Z" "${__tz}" "+%z"`
        __sign=${__time%%[0-9]*}
        __time=${__time#${__sign}}
        __hour=${__time%[0-9][0-9]}
        __min=${__time#[0-9][0-9]}
        case ${__sign} in
        +)
            __sign='-'
            ;;
        -)
            __sign='+'
            ;;
        esac
        __arg_utc="-v${__sign}${__hour}H -v${__sign}${__min}M"
        shift
        ;;
    *)
        ;;
    esac
    __date=$1
    __src_fmt=$2
    __dest_fmt=$3
env LANG=C date ${__arg_utc} -j -f "${__src_fmt}" "${__date}" \
    "+${__dest_fmt}"
}

putime()
{
local __time_now __option=$1 __key=$2 __time_start __time_end __time_int \
        __sec __min __hr __time_log_file

    __key=`echo "$2" | sed -Ee 's/[-.]/_/g'`
    __time_now=`env LANG=C date`
    case ${__option} in
    -a)
        conv_date_fmt "${__time_now}" '%a %b %d %T %Z %Y' '%s'
        ;;
    -e)
eval '_time_'${__key}'_end'=`conv_date_fmt "${__time_now}" '%a %b %d %T %Z \
    %Y' '%s'`
        ;;
    -i)
        eval __time_start='${_time_'${__key}'_start}'
        eval __time_end='${_time_'${__key}'_end}'
        : $((__time_int = __time_end - __time_start))
        : $((__sec = __time_int % 60))
        : $((__min = __time_int / 60 % 60))
        : $((__hr  = __time_int / 60 / 60 % 60))
        printf "%02d:%02d:%02d\n" ${__hr} ${__min} ${__sec}
        ;;
    -s)
eval '_time_'${__key}'_start'=`conv_date_fmt "${__time_now}" '%a %b %d %T \
    %Z %Y' '%s'`
        ;;
    -S)
        case $2 in
        ${dkgname})
            __time_log_file=${DAKEDIR}/+TIME
            ;;
        *)
__time_log_file=${DAKEDIR}/+TIME_`echo ${2}|tr '[:lower:]' '[:upper:]'`
            ;;
        esac
eval echo \"\${_time_${__key}_start} \${_time_${__key}_end} \
    $((_time_${__key}_end - _time_${__key}_start))\" > ${__time_log_file}
        ;;
    -T)
        __time_int=$2
        : $((__sec = __time_int % 60))
        : $((__min = __time_int / 60 % 60))
        : $((__hr  = __time_int / 60 / 60 % 60))
        printf "%02d:%02d:%02d\n" ${__hr} ${__min} ${__sec}
        ;;
    *)
        conv_date_fmt "${__time_now}" '%a %b %d %T %Z %Y' '%Y.%m.%d %T'
        ;;
    esac
}

doing()
{
    local __finish=false __time

    echo -n "$1"
    trap "__finish=true" 6
    __time=1
    while true; do
        if ${__finish}; then
            echo -n 'done'
            sleep 1
            break
        else
            echo -n '.'
            sleep 1
        fi
        : $((__time += 1))
        if [ $((__time % _log_rotate_interval)) -eq 0 ]; then
            logrotate
        fi
    done
    tput dl
    sleep 1
}

running()
{
    case $1 in
    start)
        shift
        doing "$1" &
        _doing_proc_id=$!
        ;;
    stop)
        kill -6 ${_doing_proc_id}
        sleep 1
        ;;
    esac
}

frotate()
{
    local __logfile __logfilename __logfiledir __logfiles __n

    __logfilename=`basename ${_logfile}`
    __logfiledir=`dirname ${_logfile}`
    __logfiles=`ls ${_logfile}.*${_logfile_sufx} 2> /dev/null | sort -r`
    if [ -z "${__logfiles}" ]; then
        __n=0
    else
        : $((__n = `echo "${__logfiles}" | wc -l`))
    fi
    if [ ${__n} -gt 0  -a  ${__n} -lt ${_log_rotate_count} ]; then
        for __logfile in ${__logfiles}; do
mv ${__logfile} ${__logfiledir}/${__logfilename}.${__n}${_logfile_sufx}
            : $((__n -= 1))
        done
    elif [ ${__n} -eq ${_log_rotate_count} ]; then
        __logfiles=`poped __logfiles`
        for __logfile in ${__logfiles}; do
            : $((__n -= 1))
mv ${__logfile} ${__logfiledir}/${__logfilename}.${__n}${_logfile_sufx}
        done
    elif [ ${__n} -gt ${_log_rotate_count} ]; then
        while [ ${__n}  -ge $((_log_rotate_count - 1)) ]; do
            rm -f `pop __logfiles`
            __logfiles=`poped __logfiles`
            : $((__n -= 1))
        done
        for __logfile in ${__logfiles}; do
mv ${__logfile} ${__logfiledir}/${__logfilename}.${__n}${_logfile_sufx}
            : $((__n -= 1))
        done
    fi
}

logrotate()
{
    local __logfile __logtmpdir='/var/tmp' __logfilename __logfdir \
        __install_options="-m ${_logfile_mode}"

    __logfilename=`basename ${_logfile}`
    __logdir=`dirname ${_logfile}`
    if [ ! -f ${_logfile} ]; then
        touch ${_logfile}
        chmod ${_logfile_mode} ${_logfile}
        if [ -n "${_logfile_owner}" ]; then
            chown ${_logfile_owner} ${_logfile}
        fi
        (cd ${__logdir}; pax -r -w -l ${__logfilename} ${__logtmpdir})
    else
        if [ -n "${_logfile_owner}" ]; then
__install_options="${__install_options} -o ${_logfile_owner%%:*} -g \
    ${_logfile_owner##*:}"
        fi
        __logfilesize=`du -k ${_logfile} | awk '{print $1}'`
        if [ ${__logfilesize} -gt ${_log_rotate_size} ]; then
install ${__install_options} ${__logtmpdir}/${__logfilename} ${_logfile}.0
printf "%-20s%12s[%d]: logfile turned over due to size>%dK\n" "`putime`" \
${dkgname} $$ ${_log_rotate_size} > ${__logtmpdir}/${__logfilename}
            frotate
            ${_logcommpresscmd} ${_logfile}.0
        fi
    fi
}

get_loginfo()
{
    local __mode __flags

    while [ $# -gt 0 ] ; do
        case $1 in
        -s)
            __mode=suffix
            ;;
        -c)
            __mode=ccommand
            ;;
        *)
            break
            ;;
        esac
        shift
    done
    __flags=$*
    case ${__mode} in
    suffix)
        case ${__flags} in
        *X*)
            echo '.xz'
            ;;
        *J*)
            echo '.bz2'
            ;;
        esac
        ;;
    ccommand)
        case ${__flags} in
        *X*)
            echo 'xz -z'
            ;;
        *J*)
            echo 'bzip2 -z'
            ;;
        esac
        ;;
    esac
}

dkgname='foo-1.0'

_log_rotate="\
# logfilename [owner:group] mode count size when flags interval
/var/log/test.log                       644  7     10   \*     X     1"

set -- `echo "${_log_rotate}" | egrep -e '^/'`
case $# in
8)
    _logfile=$1
    _logfile_owner=$2
    _logfile_mode=$3
    : $((_log_rotate_count = $4))
    : $((_log_rotate_size = $5))
    _log_rotate_when=$6
    _logfile_sufx=`get_loginfo -s $7`
    _logcommpresscmd=`get_loginfo -c $7`
    : $((_log_rotate_interval = $8 * 60))
    ;;
7)
    _logfile=$1
    _logfile_mode=$2
    : $((_log_rotate_count = $3))
    : $((_log_rotate_size = $4))
    _log_rotate_when=$5
    _logfile_sufx=`get_loginfo -s $6`
    _logcommpresscmd=`get_loginfo -c $6`
    : $((_log_rotate_interval = $7 * 60))
    ;;
*)
    error initialize "wrong logrotate specifiation"
    ;;
esac
echo $$ > /var/run/test.pid
trap "running stop; kill -9 $$" 2
logrotate
running start "Exec process"
exec >> /var/log/test.log 2>&1
cd /usr/src
make buildworld

簡単に説明すると,

  1. ログローテートの条件を_log_rotateで定義する

     グローバル変数_log_rotate に基本的に newsyslog.conf と同じ仕様で記述.ただし最後のフィールド (interval)にはログファイルをチェックする間隔を分単位で指 定

  2. _log_rotateから各々条件を読み出す
  3. 起動スクリプトのPIDを/var/run/test.pidに入れる
  4. C-cが押されたらrunningを止めてスクリプトを停止するtrapをかける
  5. ロギング環境を設定する

     logrotate は最初に呼出されたときにログファイルを指定さ れたオーナーとモードで生成する

  6. running start "Exec process"

     doing の中で _log_rotate_interval 毎に logrotate を起動.このときログファイルが _log_rotate_size を超えたら

    1.  /var/log/test.log/var/tmp/test.logにハードリンク
    2. /var/tmp/test.log/var/log/test.log.0へコピー
    3. /var/tmp/test.logをクリア
    4. ログファイルをローテート

  7. 以降の全ての出力を/var/log/test.logにリダイレクト

それでは実行してみよう.ただし,syslog.confnewsyslog.confおよびcrontabを元に戻しておくように.確かに

# ll -h /var/log/test.log*
-rw-r--r--  2 root  wheel    60k  5月 16 02:23 /var/log/test.log
-rw-r--r-- 1 root wheel 980B 5月 16 02:22 /var/log/test.log.0.xz
-rw-r--r-- 1 root wheel 1.1k 5月 16 02:21 /var/log/test.log.1.xz
-rw-r--r-- 1 root wheel 556B 5月 16 02:19 /var/log/test.log.2.xz
-rw-r--r-- 1 root wheel 1k 5月 16 02:16 /var/log/test.log.3.xz
-rw-r--r-- 1 root wheel 1.0k 5月 16 02:14 /var/log/test.log.4.xz
-rw-r--r-- 1 root wheel 1k 5月 16 02:13 /var/log/test.log.5.xz
-rw-r--r-- 1 root wheel 1.9k 5月 16 02:12 /var/log/test.log.6.xz
# 

ちゃんとローテートしている.メデタシメデタシ.