読者です 読者をやめる 読者になる 読者になる

ログの差分出力スクリプト

昔Perlで書いたやつRuby1.9版をとりあえず作りました

ログファイルを指定すると、前回実行からの差分を出力してくれます

$ echo 111 > test.log
$ ./logtail.rb test.log
111

$ echo 222 >> test.log
$ echo 333 >> test.log
$ ./logtail.rb test.log
222
333


ソースコード

#!/usr/local/bin/ruby

# ログの差分出力スクリプト
# 使用例
#   前回実行からの追記分を出力する
#   $ logtail.rb access_log
#
#   ログのローテートに対応させる場合はオプションでローテートファイルを指定する
#   $ logtail.rb access_log -r access_log.1
#
#   デフォルトでは/tmp/<ログファイル名>.logtail.offsetに前回出力情報が保存される。
#   日付つきファイルなどファイル名が変わる場合、
#   同じファイルを複数バッチから処理する場合はこのファイルパスを個別に指定すること
#   $ logtail.rb access_log.20130602 -r access_log.20130601 -o /tmp/test_access_log.logtail.offset
#
# TODO:オフセットファイルの作成、書き込み権限のチェック
# TODO:ファイル名が同じだとデフォルトのオフセットファイル名が重複する。フルパスのハッシュ値とかつけたほうがいいかな

require 'optparse'
require 'pp'

options = {}
OptionParser.new do |opt|
  opt.on('-r', '--rotated_log=VAL') {|v| options[:rotated_log] = v}
  opt.on('-o', '--offset_file=VAL') {|v| options[:offset_file] = v}
  opt.parse!
end

log = ARGV.shift
exit 1 unless log


class LogTail
  def initialize(log, options)
    @log = log
    @rotated_log = options[:rotated_log]
    @offset_file = options[:offset_file] || "/tmp/#{File.basename(log)}.logtail.offset"
    @offset = load_offset
  end

  def load_offset
    return nil unless File.exist? @offset_file

    line = File.read(@offset_file)
    ino, pos = line.split(',')
    { ino: ino.to_i, pos: pos.to_i}
  end

  def save_offset(ino, pos)
    File.write(@offset_file, "#{ino},#{pos}")
  end
  
  def tail_print(log, pos)
    file = File.open(log)
    file.seek(pos)
    file.each{|line| puts line}
    end_pos = file.pos
    file.close
  
    return end_pos
  end
  
  def tail_rotated_log
    return unless @offset && @rotated_log && File.exists?(@rotated_log)

    stat = File.stat(@rotated_log)
    if @offset[:ino] == stat.ino && @offset[:pos] < stat.size
      end_pos = tail_print(@rotated_log, @offset[:pos])
      save_offset(stat.ino, end_pos)
    end
  end

  def tail_log
    return unless @log && File.exists?(@log)

    stat = File.stat(@log)
    if @offset && @offset[:ino] == stat.ino
      if @offset[:pos] < stat.size
        start_pos = @offset[:pos]
      elsif @offset[:pos] > stat.size
        save_offset(stat.ino, stat.size)
        return
      else
        return
      end
    else
      start_pos = 0
    end
    end_pos = tail_print(@log, start_pos)
    save_offset(stat.ino, end_pos)
  end

  def execute 
    tail_rotated_log
    tail_log
  end
end
  
LogTail.new(log, options).execute


muninやnagiosとかと連携して、各種ログの監視、集計結果のグラフ化などに使えます。
例えばこういうふうにApacheの平均レスポンスタイムをmuninでグラフ化したい場合、


httpd.confのLogFormatにレスポンスタイムを追加して、

  # %Dを追加
  LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\" %D" combined

 # cronologやrotatelogsで日付ファイルに出力してる場合
  CustomLog "|/usr/sbin/cronolog -S /var/log/httpd/access_log.latest /var/log/httpd/access_log.%Y%m%d" combined


こういうmuninプラグインを作ればOKです

#!/bin/bash

today=$(date +%Y%m%d)
yesterday=$(date +%Y%m%d -d "1 days ago")
log=/var/log/httpd/access_log.$today
rotated_log=/var/log/httpd/access_log.$yesterday
offset_file=/tmp/munin_app_response_time.logtail.offset

if [ "$1" = "config" ]; then
   echo 'graph_title response time'
   echo 'graph_args --base 1000 -l 0'
   echo 'graph_vlabel msec'
   echo 'graph_scale no'
   echo 'graph_category app'
   echo 'avg.label avg'
   exit 0
fi

# TODO:muninをがしばらく停止すると次回実行時に大きな値が出てしまうので、オフセットファイルのタイムスタンプをチェックする

if [ -e "$offset_file" ];then
  avg_msec=$(/usr/local/bin/logtail.rb $log -r $rotated_log -o $offset_file | perl -alne '$c++;$s+=$F[-1];END{print int(($s/$c)/1000)}')
else
  # 初回は集計せずに捨てる
  /usr/local/bin/logtail.rb $log -r $rotated_log -o $offset_file > /dev/null
  avg_msec='N'
fi
echo "avg.value $avg_msec"


仕組みや実装上、あまり厳密な集計には向かないですがこういうグラフ作成とかログ監視には充分かな