#!/usr/bin/perl use strict; use English qw/ $PID /; use FileHandle; use File::Basename qw/ basename /; use Getopt::Long qw/ :config no_ignore_case /; use Sys::Syslog; use Time::Local; use Time::localtime; use File::stat; # バージョン番号 my $VERSION = sprintf( '0.%d.%02d', q$Revision: 1.7 $ =~ /(\d+)\.(\d+)/ ); my $BASENAME = &basename( $0, ".perl" ); =head1 NAME ppplog - PPP の接続記録を集計するスクリプト =head1 SYNOPSIS ppplog [OPTIONS] [FILES...] =head1 DESCRIPTION 一般的な Linux システムでは,pppd(8) の接続記録は syslog(3) を通じて F に蓄積されている.F は,この接続記録を集 計し,通話時間・料金を計算して標準出力に出力する. =head1 OPTIONS =over 4 =item -a =item --all 全ての接続記録を集計する(デフォルトは,前回集計時の mark 以前の記録は 集計しない). =cut my $ALL; =item -q =item --quiet 集計結果を標準出力に出力しない(デフォルトは出力する). =cut my $QUIET; =item -l C =item --log-facility=C syslog に実行記録を送るときに用いる facility を指定する.明示的な指定 がなければ,I が用いられる.このオプションには,pppd と同じ facility と使うように設定しておくべきである. F は,syslog に記録された自分自身の動作記録を利用して,集計済 みの PPP 接続記録を検出している.言い換えれば,PPP の接続記録と,この スクリプトの動作記録が同一の記録ファイルに記録されていなければならない. そのためには,同じ facility を利用するのが最も簡単で確実な方法である. Debian では I が用いられているが,その他の環境では I が用いられていることが多い. =cut my $FACILITY = "local2"; # syslog に実行記録を送らずに動作確認だけを行う. my $DEBUG; &GetOptions( 'all|a+' => \$ALL, 'quiet|q+' => \$QUIET, 'log-facility|l=s' => \$FACILITY, 'debug|d+' => \$DEBUG ); =back =head1 FILES =over 4 =item F =item F ファイルが明示的に指定されなかった場合は,これらの2つのファイルを対象 として接続記録を集計する. =back =cut my( @LOGFILES ) = @ARGV; unless( @LOGFILES ){ @LOGFILES = ( '/var/log/messages', '/var/log/messages.0' ); } my $MARK = sprintf( '^(\w+) +(\d+) (\d+):(\d+):(\d+) \w+ %s(?:\.perl)?\[(\d+)\]: (MARK)\s*$', quotemeta($BASENAME) ); unless( $ALL or $DEBUG ){ openlog( $BASENAME, 'pid', $FACILITY ); syslog( "info", "MARK" ) } #====================================================================== # 接続記録を解析 #====================================================================== my $START; # 集計を開始した時間 my $END; # 集計を終了した時間 my $SECOND; # 通信時間の合計 my $CHARGE; # 通信料金の合計 my $COUNT; # 通信回数 my @LOG; my @ERROR; my $RETRY; TOTAL: { my $mark = ( $ALL or $DEBUG ) ? 1 : 0; my $end; # 通話が終了した時間 my $ppid; # 通話を終了した pppd の PID for my $file ( @LOGFILES ){ my $fh = new FileHandle( $file, "r" ); unless( $fh ){ &adderror( "Can't open $file: %s", $! ); next; } for( reverse( $fh->getlines ) ){ if( my( $mon, $mday, $hour, $min, $sec, $pid, $event ) = /$MARK/o ){ if( $mark++ == 0 ){ if( $PID == $pid ){ # 自分自身の集計開始 mark が発見された時間を記録する # => この時間までの接続記録が集計の対象となる $END = &logtime( $sec, $min, $hour, $mday, $mon ); } else { # 自分自身の集計開始 mark が発見できなかった場合 $fh->close; if( $RETRY++ < 5 ){ redo TOTAL; } else { &adderror( "No marker was found. Check the syslog facility of this script." ); last TOTAL; } } } else { # 以前の集計時の mark が発見された時間を記録する # => この時間以降の接続記録が集計の対象となる $START = &logtime( $sec, $min, $hour, $mday, $mon ); } } elsif( ! $mark ){ # 集計開始の mark が見つかるまで読み飛ばす. next; } elsif( ( $mon, $mday, $hour, $min, $sec, $pid, $event ) = /^(\w+) +(\d+) (\d+):(\d+):(\d+) \w+ pppd\[(\d+)\]: (Connect:|Connection terminated\.)/ ){ my $time = &logtime( $sec, $min, $hour, $mday, $mon ); if ( $event eq 'Connect:' ) { if( ! $end and $SECOND == 0 ){ # 次回集計時に対象となる接続記録なので無視する ; } elsif( $time <= $end and $ppid == $pid ){ my $lapse = $end - $time; my $charge = &charge( $time, $end ); $SECOND += $lapse; $CHARGE += $charge; $COUNT++; &addlog( "%6d: %s %s -> %s (%d)", $lapse, &date_string($time), &time_string($time), &time_string($end), $charge ); } elsif( ! $end ){ &adderror( "Missing the closing log of the connection opened at %s.", ctime($time) ); &addlog( " : %s %s -> unknown", &date_string($time), &time_string($time) ); } elsif( $time > $end ){ &adderror( "The connection closed at %s was opened in the future at %s.", ctime($end), ctime($time) ); } else { # $ppid != $pid &adderror( "Missing the closing log of the connection opened at %s.", ctime($time) ); } $end = 0; $ppid = 0; } else { # Disconnect. if( $end ){ &adderror( "Missing the opening log of the connection closed at %s.", ctime($end) ); &addlog( " : unknown -> %s %s", &date_string($end), &time_string($end) ); } $end = $time; $ppid = $pid; } # 前回集計時の mark の直前の動作記録が発見された時点で終了する last TOTAL if ( ! $ALL and $mark >= 2 ); } } $fh->close; } $mark or &adderror( "No marker was found. Check the syslog facility of this script." ); } sub addlog ($@) { my( $format, @arg ) = @_; push( @LOG, sprintf( $format."\n", @arg ) ); } sub adderror ($@) { my( $format, @arg ) = @_; push( @ERROR, sprintf( $format."\n", @arg ) ); } #====================================================================== # 解析結果を出力 #====================================================================== unless( $QUIET ){ unless( $ALL ){ $END ||= time; printf( "PPP Connection Log Summary\n from %s %s to %s %s\n\n", &date_string( $START ), &time_string( $START ), &date_string( $END ), &time_string( $END ) ); } printf( "Telephone charge: %6d yen.\n", $CHARGE ); printf( "Elapsed time: %7d seconds / %d times = %.2d seconds.\n\n", $SECOND, $COUNT, $SECOND / $COUNT ); @ERROR and print( "Errors:\n", reverse(@ERROR), "\n" ); print "Connections:\n", reverse(@LOG); } if( ! $DEBUG and @LOG ){ syslog( "info", "telephone charge = %d yen", $CHARGE ); syslog( "info", "elapsed time = %d seconds", $SECOND ); } exit 0; #====================================================================== # 内部関数 #====================================================================== # 指定された日付データを UNIX 形式の時刻表示に変換する関数 sub logtime { my( $sec, $min, $hour, $mday, $mon ) = @_; my %mon = qw/ Jan 0 Feb 1 Mar 2 Apr 3 May 4 Jun 5 Jul 6 Aug 7 Sep 8 Oct 9 Nov 10 Dec 11 /; $mon = $mon{$mon}; timelocal( $sec, $min, $hour, $mday, $mon, localtime->year() + ( ( $mon > localtime->mon() ) ? -1 : 0 ) ); } # 指定された時間から YYYY/MM/DD 形式の文字列を生成する関数 sub date_string { my( $time ) = @_; sprintf( "%04d/%02d/%02d", localtime($time)->year + 1900, localtime($time)->mon + 1, localtime($time)->mday ); } # 指定された時間から HH:MM:SS 形式の文字列を生成する関数 sub time_string { my( $time ) = @_; sprintf( "%02d:%02d:%02d", localtime($time)->hour, localtime($time)->min, localtime($time)->sec ); } # 通信料金を計算する関数 sub charge { my( $start, $end ) = @_; my $unit = ( localtime($start)->hour >= 22 && localtime($end)->hour < 6 ) ? 240 : 180; use integer; ( ( $end - $start + $unit + 10 ) / $unit ) * 10; } =head1 USAGE 本スクリプトを Debian で使用する場合は,F にこのス クリプトをインストールすると良い. Example: # install -o root -g root -m 755 ppplog.perl /etc/cron.weekly/ppplog これだけで,PPP の接続記録が週毎に集計され,root 宛にメールされるよう になる. 他の環境で利用する場合は cron(8) などを参照. =head1 SEE ALSO pppd(8), syslog(3), cron(8). =head1 AUTHOR =over 4 =item TSUCHIYA Masatoshi =back =head1 COPYRIGHT This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, you can either send email to this program's maintainer or write to: The Free Software Foundation, Inc.; 59 Temple Place, Suite 330; Boston, MA 02111-1307, USA. Last Update: $Date: 2002/12/13 13:04:22 $ =cut