source: phptop/phptop @ 264

Last change on this file since 264 was 264, checked in by zerodeux, 11 years ago

Releasing 0.3.1 - \n in error.log bugfix + more robust parsing

  • Property svn:executable set to *
File size: 7.0 KB
Line 
1#!/usr/bin/perl
2
3# phptop - Analyse quickly system ressource usage across many PHP queries
4# Copyright (C) 2009 Bearstech - http://bearstech.com/
5#
6# This program is free software: you can redistribute it and/or modify
7# it under the terms of the GNU General Public License as published by
8# the Free Software Foundation, either version 3 of the License, or
9# (at your option) any later version.
10#
11# This program is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License
17# along with this program.  If not, see <http://www.gnu.org/licenses/>.
18
19# Depends on LWP aka libwww-perl (what else ?)
20
21use strict;
22use warnings;
23use Getopt::Long qw(:config no_auto_abbrev no_ignore_case bundling);
24use HTTP::Date;
25use POSIX;
26
27my $package_name    = 'phptop';
28my $package_version = '0.3.1';
29my $package_owner   = 'Copyright (C) 2009 Bearstech - http://bearstech.com/';
30my $package_url     = 'http://forge.bearstech.com/trac/wiki/PhpTop';
31
32# Options
33my $help;
34my $version;
35my @log;
36my $full_query;
37my $count = 10;
38my $sortkeys = 'hit';
39my $span = 10;
40my $path_only;
41
42# Globals
43my @keys = qw/hit time user sys mem mem_max inc inc_max/;
44my %stat;
45my $now   = time();
46my $hits  = 0;
47my $bogus = 0;
48
49
50sub print_short_help {
51    print STDERR <<EOF;
52Usage: $package_name [options]
53
54Try '$package_name --help' for more information.
55EOF
56    exit 2;
57}
58
59sub print_help {
60    print STDERR <<EOF;
61Usage: $package_name [options]
62
63Options:
64  -c, --count N        Limit output to top N URIs (default is $count)
65  -f, --full-query     Consider full URI with CGI parameters
66  -h, --help           Display this information
67  -l, --log path       Logfiles to parse, you may use several -l and wildcards
68  -p, --path-only      Only print path, skip http://host
69  -s, --sort key,...   Sort key: hit, time, user, sys, mem or inc (default is $sortkeys)
70  -t, --time N         Consider log events from now back to N minutes (default is $span)
71  -v, --version        Display version number and copyright info
72
73Columns explanation:
74  Hit       Number of queries
75  Time      Total wall clock time (in seconds)
76  User      Total CPU time (in seconds)
77  Sys       Total system time (in seconds)
78  Mem/hit   Average allocated memory per query (MB/hit)
79  Mem_max   Maximum allocated memory across queries (MB)
80  Inc/hit   Average included files per query
81  Inc_max   Maximum included files across queries
82EOF
83    exit 0
84}
85
86sub print_version {
87    print "$package_name $package_version\n";
88    exit 0;
89}
90
91sub parse_log {
92    my $logfile = shift;
93
94    my $lh;
95    if (!open($lh, "<$logfile")) {
96        warn "$logfile: $!";
97        return 0;
98    }
99
100  LINE:
101    while (<$lh>) {
102        chomp;
103        next if not /\bphptop ([^ ]+) (.*)/;
104        my ($uri, $pairs) = ($1, $2);
105
106        # Only keep records within expected time span. If a time stamp is not found or
107        # cannot be parsed, we still process the item.
108        if (/^\[(.*?)\]/) {
109            my $stamp = str2time($1);
110            next if defined $stamp && ($now - $stamp) > $span * 60;
111        }
112
113        $pairs =~ s/, referer.*//;     # Apache may append a referer in error.log, ignore
114        $pairs =~ s/(\d),(\d)/$1.$2/g; # Be independent of the PHP locale, normalize on dot decimal separator
115        my @kv = split(/ /, $pairs);
116        next if @kv < 5;               # Protect against bogus/wrapped phptop records, count pairs
117
118        # Fix URIs (depending on options)
119        $uri =~ s/index\.php\/?//;  # / == /index.php (99.9% of the time)
120        $uri =~ s/\?.*// if not defined $full_query;
121        $uri =~ s/^https?:\/\/[^\/]+// if defined $path_only;
122
123        # For each 'key:<numeric val>' sum the numeric value per URI
124        foreach (@kv) {
125            my ($key, $val) = split(/:/, $_);
126
127            # If we have a bogus key/value pair, zap the whole record
128            if ($key eq '' or not defined $val or not $val =~ /^[0-9.]+$/) {
129                delete $stat{$uri};
130                $bogus++;
131                next LINE;
132            }
133
134            $stat{$uri}{$key} += $val;
135
136            # Compute max for some values (mem, inc)
137            if ($key eq 'mem') {
138                $stat{$uri}{'mem_max'} = $val if ($stat{$uri}{'mem_max'} || 0) < $val;
139            }
140            elsif ($key eq 'inc') {
141                $stat{$uri}{'inc_max'} = $val if ($stat{$uri}{'inc_max'} || 0) < $val;
142            }
143        }
144
145        $stat{$uri}{'hit'}++;
146        $hits++;
147    }
148
149    close($lh);
150    1;
151}
152
153sub fix_stat {
154    # Convert 'mem'  and 'inc' values from total to 'per hit', more useful and meaningful
155    while (my ($url, $i) = each %stat) {
156        $i->{'mem_max'} /= 2**20;            # Also scale memory values from bytes to MB
157        $i->{'mem'} /= $i->{'hit'} * 2**20;  # Idem
158        $i->{'inc'} /= $i->{'hit'};
159    }
160}
161
162sub text_report {
163    my $sortkey = shift;
164
165    # Sort queries according to $sortkey (they are all numeric)
166    my @sorted = sort { $stat{$b}{$sortkey} - $stat{$a}{$sortkey} } keys %stat;
167   
168    # Compute URI column width
169    my $width = 0;
170    my $n = 0;
171    foreach (@sorted) {
172        last if $n++ >= $count;
173        my $w = length($_);
174        $width = $w if $w > $width;
175    }
176   
177    my %total;
178    my @tkeys = qw/hit time user sys/;
179    $n = 0;
180   
181    printf("%-${width}s %s\n", "", join(' ', map { sprintf('%8s', ucfirst($_ =~ /^(mem|inc)$/ ? "$_/hit" : $_)) } @keys));
182    foreach (@sorted) {
183        my $s = $stat{$_};
184        $total{$_} += $s->{$_} foreach @tkeys;
185        next if $n++ >= $count; # Continue to loop and only sum totals if $count records have been displayed
186   
187        printf("%-${width}s", $_);
188        printf($_ =~ m/hit|inc/ ? ' %8d' : ' %8.1f', $s->{$_}) foreach @keys;
189        print "\n";
190    }
191    printf("\n%-${width}s", 'Total');
192    printf($_ =~ m/hit/ ? ' %8d' : ' %8.1f', $total{$_}) foreach @tkeys;
193    print "\n";
194}
195
196
197GetOptions(
198    'c|count=i'      => \$count,
199    'f|full-query'   => \$full_query,
200    'h|help'         => \$help,
201    'l|log=s'        => \@log,
202    'p|path'         => \$path_only,
203    's|sort=s'       => \$sortkeys,
204    't|time=s'       => \$span,
205    'v|version'      => \$version,
206)
207or print_short_help();
208
209print_help() if $help;
210print_version() if $version;
211
212POSIX::setlocale(POSIX::LC_ALL, 'C'); # Use . as decimal separator
213
214my @logfiles;
215push(@log, '/var/log/apache2/error*log', '/var/log/apache2/*/error*log') if !@log;
216map { push(@logfiles, glob($_)) } @log;
217
218my $parsed = 0;
219$parsed += parse_log($_) foreach @logfiles;
220
221if ($parsed == 0) {
222    print STDERR "Error: no log files found/processed. Tried: ".join(", ", @log)."\n";
223    exit 2;
224}
225if ($hits == 0) {
226    print STDERR "No phptop records found.\n";
227    exit 0;
228}
229if ($bogus >= $hits * .05) {
230    print STDERR "Warning: you have more than 5% malformed records ($bogus out of $hits).\n";
231}
232
233fix_stat();
234
235my @sortkeyl = split(/,/, $sortkeys);
236
237my $reportnb = 0;
238foreach (@sortkeyl) {
239  print "--\n" if $reportnb++;
240  text_report($_);
241}
Note: See TracBrowser for help on using the repository browser.