#!/usr/bin/perl #++ # $Id: bigstat,v 1.37 2010/02/03 02:51:50 az Exp $ # # File: bigstat # Date: Sat Jul 10 20:21:23 2004 # Author: Alexander Zangerl (az) # # Abstract: statistics gatherer for mrtg # one script to rule them all, one script to bind them, # one script to bring them all and in the tempdir bind them # in the land of mrtg where there is no snmpd. # # License: GPL v1 or v2 # #-- use strict; use Getopt::Std; use LWP::UserAgent; use Net::Telnet; my %conf; chomp(my $hostname=`/bin/hostname`); # args: which tests to enable die "usage:\t$0 [-aclfmnsS] [-i if,if...] [-d diskspec,diskspec...]\n", "\t[-t targetdir] [-p persistdir] -H disk,disk\n", "[-P name:pathi:fieldnr:patho:fieldnr]\n", "[-D uname:passwd]\n", "[-x name:host:uname:passwd]\n", "diskspec is name=mountpoint,name=mp...\n\n", "-a: apache access log for 2xx,3xx,401 vs. the rest -c: cpu time user+nice vs. user+nice+system -l: loadavg 1min vs. 15min -f: iptables packet drops -s: maillog good mail vs. spam -n: newslog inbound vs. outbound articles (inaccurate) -m: used memory vs. swap -i: interface input bytes vs. output -d: disk used blocks vs. inodes -t: folder for current counter files -p: folder for keeping offsat and status files -T: netfilter tracked connections -S: sensors, first two temperatures -H: harddisk temperature, device as arg -P: output name, 2x path to a file and field index (ws-separated) -x: Thomson Adsl stats via telnet cli -D: Internode quota -I: inetd-mode, all output to stdout\n" if (!getopts("aclfmsni:d:t:p:TSH:IP:D:x:",\%conf)); my $targetdir=$conf{t}||"/tmp/bigstat"; if (!-d $targetdir) { mkdir($targetdir,0700) or die "can't mkdir $targetdir: $!\n"; } my $persistdir=$conf{p}||"/var/lib/bigstat"; if (!-d $persistdir) { mkdir($persistdir,0700) or die "can't mkdir $persistdir: $!\n"; } # get uptime open(F,"/proc/uptime") or die "can't read /proc/uptime: $!\n"; my $uptime=int((split(/\s+/,))[0]); close F; $uptime=sprintf("%d days, %d:%d:%d",$uptime/86400, $uptime%86400/3600, $uptime%86400%3600/60, $uptime%60); if ($conf{a}) { my($in,$out)=(0) x 2; # apache access logs: logtail on /var/log/apache/access.log my $offsetfile="$persistdir/apache.offset"; my $logfile="/var/log/apache/access.log"; ($in,$out)=readstat("apache"); foreach (logtail($logfile,$offsetfile)) { # using this custom format: # heffalump.snafu.priv.at 210.8.28.121 - - [10/Jul/2004:17:19:41 +1000] # "GET /mrtg/ HTTP/1.0" 304 - "-" "Googlebot/2.1 # (+http://www.googlebot.com/bot.html)" # considering 2xx, 3xx and 401 as ok if (/^\S+ \S+ \S+ \S+ \[[^\]]+\] \"[^\"]+\" (\d+)/) { $1=~/^([23]..|401)$/? $in++: $out++; } } writestat("apache",$in,$out); writeout("apache",$in,$out); } if ($conf{c}) { my($in,$out)=(0)x2; # cpu state logging open(F,"/proc/stat") or die "can't open /proc/stat: $!\n"; my $cpuline=; close F; if ($cpuline=~/^cpu\s+(\d.*)$/) { # in 2.6: added columns iowait,irq,softirq,steal my ($user,$nice,$system,$idle,$io,$irq,$int,$steal)=split(/\s+/,$1); # we output user and nice and everything bar idle as total activity writeout("cpu",$user+$nice,$user+$nice+$system+$io+$irq+$int+$steal); } } # read files directly if ($conf{P}) { my ($name,$fni,$fii,$fno,$fio)=split(/:/,$conf{P}); my($in,$out)=(0)x2; open(F,$fni) or die "can't open $fni: $!\n"; my $datai=; close F; open(F,$fno) or die "can't open $fno: $!\n"; my $datao=; close F; $in=(split(/\s+/,$datai))[$fii]; $out=(split(/\s+/,$datao))[$fio]; writeout($name,$in,$out,"in $fni:$fii out $fno:$fio"); } if($conf{T}) { my ($in,$out)=0; # old: read /proc/net/ip_conntrack or nf_conntrack, count lines, print # better: read count from /proc/sys/net/netfilter/nf_conntrack_count open(F,"/proc/sys/net/netfilter/nf_conntrack_count") or die "can't open /proc/sys/net/netfilter/nf_conntrack_count: $!\n"; my $conns=; chomp $conns; close F; writeout("conns",$conns,$conns); } if ($conf{l}) { my($in,$out)=(0)x2; # cpu load logging open(F,"/proc/loadavg") or die "can't open /proc/loadavg: $!\n"; my ($now,$five,$fifteen)=split(/\s+/,); close F; writeout("loadavg",int($now*100),int($fifteen*100)); } if ($conf{m}) { # free memory logging open(F,"/proc/meminfo") or die "cant open meminfo: $!\n"; my (@max,@values,%data); foreach my $line () { if ($line =~ /^(Mem|Swap)(Total|Free):\s+(\d+)\s+kB/) { $data{$1.$2}=$3*1024; } } close F; $values[0]=$data{MemTotal}-$data{MemFree}; $values[1]=$data{SwapTotal}-$data{SwapFree}; writeout("memory",@values,$data{MemTotal}." mem, " .$data{SwapTotal}." swap"); } if ($conf{d}) { # disk blocks, inodes logging # args: name=mp,name=mp... foreach my $set (split(/\s*,\s*/,$conf{d})) { my ($name,$fs)=split(/=/,$set); my (@max,@values); for my $cmd ("/bin/df -k","/bin/df -i") { my $val=join(" ",(`$cmd $fs`)[1..2]); my ($max,$used)=(split(/\s+/,$val))[1,2]; push @max,$max; push @values,$used; } writeout("disk-$name",@values,"$max[0] kb, $max[1] inodes"); } } if ($conf{H}) { # disk temperature via hddtemp -n # args: devnames without /dev/, comma separated for my $disk (split(/,/,$conf{H})) { my $temp=`/usr/sbin/hddtemp -n /dev/$disk`; chomp $temp; writeout("temp-$disk",$temp,0,"temperature $disk"); } } if ($conf{S}) { # first two temperatures in sensors output my (@values, @names); for my $l (`sensors`) { chomp $l; if ($l=~/^(\S+ temp):\s*[+-]?(\d+\.\d+)/i) { push @values,int($2+0.5); push @names,$1; last if (@values==2); } } writeout("temperature",@values,join(", ",@names)); } if ($conf{f}) { # remember previous counts and detect wraps! my($old,undef)=readstat("firewall"); # count dropped packets # Chain udp_in (1 references) # pkts bytes target prot opt in out source destination #11232386 3228662572 ACCEPT udp -- any any anywhere anywhere my ($new); open(F,"/sbin/iptables -n -x -v -L|") or die "can't fork iptables: $!\n"; while() { if (/^\s*(\d+)\s+\d+\s+(TARPIT|DROP|REJECT)/) { $new+=$1; } } close F; writestat("firewall",$new,$new); # wrapped? then forget the old value $old=$new if (!defined $old || $new<$old); # perhour doesn't work with absolute readings... $new=($new-$old)*3600; writeout("firewall",$new,$new); } if ($conf{i}) { # interface logging # args: if,if,if... my %ifs; foreach (split(/\s*,\s*/,$conf{i})) { $ifs{$_}=[readstat($_)]; } open(F,"/proc/net/dev") or die "cant open /proc/net/dev: $!\n"; foreach my $line () { # tosspot: has spaces after colon, cft and heffalump don't... if ($line =~s/^\s*(\S+):\s*(\S.+)$/$2/) { my $ifname=$1; next if (!$ifs{$ifname}); my ($in,$out)=(split(/\s+/,$2))[0,8]; writestat($ifname,$in,$out); # detect reboot wraps $ifs{$ifname}->[0]=$in if (!defined($ifs{$ifname}->[0]) || $ifs{$ifname}->[0]>$in); $ifs{$ifname}->[1]=$out if (!defined($ifs{$ifname}->[1]) || $ifs{$ifname}->[1]>$out); $ifs{$ifname}=[$in-$ifs{$ifname}->[0],$out-$ifs{$ifname}->[1]]; } } close F; foreach (keys %ifs) { writeout("$_",$ifs{$_}->[0],$ifs{$_}->[1]); } } if ($conf{s}) { my($in,$out)=(0)x2; my($smtpin,$smtprej)=0; # email/spam stats: logtail on /var/log/maillog # good is $in, crap is $out # smtp sessions that reach data in $smtpin, early rejects are in $smtprej my $offsetfile="$persistdir/mail.offset"; my $logfile="/var/log/maillog"; ($in,$out)=readstat("mail"); ($smtpin,$smtprej)=readstat("smtp"); foreach (logtail($logfile,$offsetfile)) { if (/sm-mta\[\d+\]: .+, stat=(virus|too much spam|sent)/i) { (lc($1) eq "sent") and $in++ or $out++; } elsif (/mimedefang\[\d+\]: filter_(sender|recipient) (rejected|tempfailed)/) { $smtprej++; } elsif (/sm-mta\[\d+\]: .+, nrcpts=(\d+)/) { ($1 > 0) && $smtpin++; } } writestat("mail",$in,$out); writeout("mail",$in,$out); writestat("smtp",$smtpin,$smtprej); writeout("smtp",$smtpin,$smtprej); } if ($conf{n}) { my($in,$out)=(0)x2; # news stats: logtail on /var/log/news/news # inbound articles should be exact, outbound are the peers things # are offered to. FIXME: parse innfeed info for that my $offsetfile="$persistdir/news.offset"; my $logfile="/var/log/news/news"; ($in,$out)=readstat("news"); foreach (logtail($logfile,$offsetfile)) { if (/^\w+\s+\d+\s+\d+:\d+:\d+\.\d+\s+(\S)\s+\S+\s+<[^>]+>\s+\d+\s+(.*)$/) { my ($status,$cand)=($1,$2); if ($status =~ /^[j+c]$/) { $in++; my @all=split(/\s+/,$cand); $out+=@all; } } } writestat("news",$in,$out); writeout("news",$in,$out); } # internode adsl usage, in MB if ($conf{D}) { my ($uname,$pwd)=split(/:/,$conf{D},2); my($in,$out)=(0) x 2; my $url="https://customer-webtools-api.internode.on.net/cgi-bin/padsl-usage"; my $ua=LWP::UserAgent->new; $ua->timeout(10); my $r=$ua->post($url,{username=>$uname,password=>$pwd}); my $text=$r->content; # mb used, total, rollover and excess if ($text=~/(\d+\.\d+)\s+(\d+)\s+(.*)$/) { $in=int($1); $out=int($2); writeout("internode",$in,$out,$3); } } # read dsl stats from thomson modems, via telnet (*sigh*) if ($conf{x}) { my ($name,$host,$uname,$pwd)=split(/:/,$conf{x},4); my $t=Net::Telnet->new(Timeout=>10, Prompt=>"/\{$uname\}=>/"); # $t->dump_log(*STDOUT); $t->open($host) || die "can't connect to $host: $!\n"; $t->login($uname,$pwd) || die "can't login on host $host: $!\n"; my @info = $t->cmd(":xdsl info expand=enabled counter_period_filter=current"); $t->close; my ($thisuptime,$resets,$errorsecs,$down,$up,$attdn,$attup,$margdn,$margup); for my $l (@info) { if ($l=~/^\s*Up time \(Days hh:mm:ss\):\s*(\d+)\s+days?,\s+(\d+):(\d+):(\d+)\s*$/) { my ($days,$hrs,$min,$secs)=($1,$2,$3,$4); $thisuptime="${days} days, $hrs:$min:$secs"; } elsif ($l=~/^\s*Number of reset:\s+(\d+)$/) { $resets=$1; } elsif ($l=~/^\s*Error second \(ES\):\s*(\d+)\s*$/) { $errorsecs=$1; } elsif ($l=~/^\s*Payload rate \[kbps\]:\s+(\d+)\s+(\d+)\s*$/) { ($down,$up)=($1,$2); } elsif ($l=~/^\s*Attenuation \[dB\]:\s+(\d+\.\d+)\s+(\d+\.\d+)\s*$/) { # rounding for mrtg ($attdn,$attup)=(int($1+0.5),int($2+0.5)); } elsif ($l=~/^\s*Margins \[dB\]:\s+(\d+\.\d+)\s+(\d+\.\d+)\s*$/) { ($margdn,$margup)=(int($1+0.5),int($2+0.5)); } } writeout("$name-errors",$errorsecs,$resets,undef,$thisuptime); writeout("$name-speed",$down,$up,undef,$thisuptime); writeout("$name-attenuation",$attdn,$attup,undef,$thisuptime); writeout("$name-margins",$margdn,$margup,undef,$thisuptime); } sub readstat { my ($service)=@_; my($in,$out)=(0)x2; my $statfile="$persistdir/$service.status"; if (!open(F,$statfile)) { warn "can't open $statfile: $!\n"; return undef; } else { my $laststatus=; close F; ($in,$out)=split(/\s+/,$laststatus); } return($in,$out); } sub writestat { my ($service,$in,$out)=@_; my $statfile="$persistdir/$service.status"; open(F,">$statfile") or die "cant write to $statfile: $!\n"; print F "$in\t$out\n"; close F; } sub writeout { my ($service,$in,$out,$extra,$thisuptime)=@_; $thisuptime||=$uptime; if (!$conf{I}) { open(F, ">$targetdir/$service") or die "can't write $targetdir/$service: $!\n"; print F "$in\n$out\n$thisuptime\n$hostname $service $extra\n"; close F; } else { print "$service\n$in\n$out\n$thisuptime\n$hostname $service $extra\n"; } } # code extracted/extended from logtail # Author: Paul Slootman 2001/03/14 # Licence: GPL # args: logfile and optional offsetfile # returns: array of loglines # dies on errors sub logtail { my @result; my ($logfile, $offsetfile) = @_; die "File $logfile cannot be read.\n" if (! -f $logfile); # offsetfile not given, use .offset/$logfile in the same directory $offsetfile = $logfile . '.offset' unless ($offsetfile); die "File $logfile cannot be read.\n" if (!open(LOGFILE, $logfile)); my ($inode, $offset) = (0, 0); if (open(OFFSET, $offsetfile)) { chomp($inode=); chomp($offset=) if (defined $inode); } my ($ino, $size); die "Cannot get $logfile file size.\n" unless ((undef,$ino,undef,undef,undef,undef,undef,$size) = stat $logfile); if ($inode == $ino) { return if $offset == $size; # short cut $offset = 0 if ($offset > $size); # no warning here } $offset = 0 if ($inode != $ino || $offset > $size); seek(LOGFILE, $offset, 0); @result=; $size = tell LOGFILE; close LOGFILE; die "File $offsetfile cannot be created. Check your permissions.\n" if (!open(OFFSET, ">$offsetfile")); die "Cannot set permissions on file $offsetfile\n" if (!chmod 0600, $offsetfile); print OFFSET "$ino\n$size\n"; close OFFSET; return @result; }