/usr/share/doc/cron/examples/cron-stats.pl is in cron 3.0pl1-124.
This file is owned by root:root, with mode 0o644.
The actual contents of the file can be viewed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 | #!/usr/bin/perl -w
#
# Generate stats of Cron messages sent to syslog
#
#
# This script can be used to generate per-user and per-task
# statistics in order to determine how much time do cron tasks take
# and audit cron use. It might be useful to debug issues related
# to cron tasks that take too much time to run, or overlap, or get executed
# too fast..
#
# Hint: In some systems, cron tasks might not properly use lockfiles to
# prevent themselves from being run twice in a row and some "strange"
# things might happen if the system is overloaded and a cron task takes
# more than expected. And, no, this is not something that Cron should
# prevent (for more information see Debian bug #194805).
#
# In order for this script to work
#
# together with the logging enhancements included
# in Debian's vixie cron (3.0pl1-95) that make it possible to log
# when do cron tasks end.
#
#
# How to use:
# - Modify /etc/init.d/cron so that the calls to cron pass the '-L 2'
# argument
# (Hint: change lines 27 and 47 so that they end with '-- -L 2 $LSBNAMES'
# instead of '-- $LSBNAMES')
# In Debian you need cron-3.0pl1-95 or later.
# Note: future versions of cron might use an /etc/default/cron file
# to provide arguments to cron.
#
# - Review /etc/syslog.conf to determine where does the output of the cron
# facility goes to (might be /var/log/syslog together with other messages
# but you can also configure syslog to store them at /var/log/cron.log)
#
# - Run the script (by default it will read input /var/log/syslog but you
# can point to other file using the '-f' switch) and review the output
#
# - (Optionally) If you want to analyse log files for a long period
# concatenate different log files, extract all CRON related entries
# to a file and process that.
#
# You could do, for example, this:
#
# zcat -f /var/log/syslog* | grep CRON >cron-log ; \
# perl stats-cron.pl -f cron-log
#
#
# This program is copyright 2006 by Javier Fernandez-Sanguino <jfs@debian.org>
#
# 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 of the License, 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, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# For more information please see
# http://www.gnu.org/licenses/licenses.html#GPL
# TODO:
# - Print time internal of file analysis (from 'XXX' to 'XXX') based on the
# first and the last log entry read.
#
# - Detect overlaped entries for the same task (start of an entry before the
# previous one finished)
#
# - Make it possible to filter by users
#
# - Consider adapting to other log formats (other cron programs? Solaris?) by
# separating analysis and extraction in the code and making it possible
# to switch to different analysis methods.
# Required modules
eval 'use Parse::Syslog';
if ($@) {
print STDERR "ERROR: Parse::Syslog not installed, please install this Perl library\n";
print STDERR "ERROR: (in Debian it is provided by the 'libparse-syslog-perl' package)\n";
exit 1;
}
use Parse::Syslog;
use Getopt::Std;
# Parse command line
getopts('dhvf:');
$opt_d = 0 if ! defined ($opt_d) ;
if ( $opt_h ) {
print "Usage: $ARGV[0] [-dhv] [-f syslog file]";
exit 1;
}
# Note: in Debian's default syslog configuration log messages are
# sent to /var/log/syslog but you might have edited it to use
# /var/log/cron.log instead. Which, BTW, is more efficient because
# we do not need to parse the syslog to extract only cron messages.
my $LOGFILE = $opt_f || "/var/log/syslog";
# Sanity checks
if ( ! -r $LOGFILE ) {
print STDERR "ERROR: Cannot read logfile $LOGFILE";
}
my $parser = Parse::Syslog->new( $LOGFILE);
print STDERR "Starting parse of file $LOGFILE\n" if $opt_v;
while(my $log = $parser->next) {
if ( $log->{program} =~ /CRON/ ) {
print STDERR "DEBUG: Found syslog entry: $log->{pid}\n" if $opt_d;
# We extract the entries with the same text but with 'CMD' (start)
# and 'END' (end) to check if it's the same we use the PID reported
# by cron.
if ($log->{text} =~ /CMD \(/) {
my $pid;
if ($log->{text} =~ /\(\[(\d+)\]/ ) {
# Debian - with loglevel 8, the child process id is
# reported
$pid = $1;
} else {
# All the other cases, the process ID of the
# child process is the same one as the log entry
$pid = $log->{pid};
}
add_entry($log->{timestamp},$log->{text},$pid, $log->{pid})
}
if ($log->{text} =~ /END \(/) {
my $pid;
if ($log->{text} =~ /\(\[(\d+)\]/ ) {
# Debian - with loglevel 8
$pid = $1;
} else {
# Other cases
$pid = "UNKNOWN";
}
clear_entry($log->{timestamp},$log->{text},$pid,$log->{pid});
}
}
}
print STDERR "Finished parse of file $LOGFILE\n" if $opt_v;
# Analysis, we find all entries with a start and stop timestamp
# and print them
generate_analysis();
# Stats:
# - average time of execution of cron jobs
# - cronjob which executed slowest
# - cronjob which executed fastes
# - cronjobs which start before they finish
print_stats();
exit 0;
sub find_user {
# Extract a user from a cron entry
my ($text) = @_;
my $user = "";
if ( $text =~ /^\((.*?)\)/ ) {
$user = $1;
}
return $user;
}
sub find_prg {
# Extract a program from a cron entry
my ($text) = @_;
my $prg = "";
if ( $text =~ /(CM|EN)D\s*\((.*)\s*\)\s*$/ ) {
$prg = $2;
$prg =~ s/^\[\d+\]\s+//; # Strip off child process numbers
# (Debian loglevel 8)
}
return $prg;
}
sub add_entry {
# Get a syslog entry and add it to the table of entries for analysis
my ($time, $text, $pid, $ppid) = @_;
my ($user, $prg);
print STDERR "DEBUG: Entering add_entry with @_\n" if $opt_d;
$user = find_user($text);
if ( ! defined ($user) ) {
print STDERR "WARN: Cannot find user for cron job (pid $pid)\n";
return;
}
$prg = find_prg($text);
if ( ! defined ($prg) ) {
print STDERR "WARN: Cannot find program for cron job (pid $pid)\n";
return;
}
my $value = $pid."-".$time;
print STDERR "DEBUG: Adding cronjob of user $user to list: $prg\n" if $opt_d;
add_cron_entry($user, $prg, $value, $ppid);
$cron_entries{$user}{$prg}{'count'}++;
return;
}
sub clear_entry {
# Get a syslog entry, find the start entry and add the timestamp difference
# to the list of timestamps for that user
#
# There's two ways to match entries:
# - (in Debian, using loglevel 8): By matching the child process ID
# between the entries
# - in all the other cases: By matching the process ID of the
# END message (which belongs to the parent) with the process IF
# of the CMD message (which belongs to the child)
my ($timestamp, $text, $pid, $ppid) = @_;
my ($user, $prg, $entry, $count);
my @array; my @stack;
print STDERR "DEBUG: Entering clear_entry with @_\n" if $opt_d;
$user = find_user($text);
if ( $user eq "" ) {
print STDERR "WARN: Cannot find user for cron job (pid $pid)\n";
return;
}
$prg = find_prg($text);
if ( $prg eq "" ) {
print STDERR "WARN: Cannot find program for cron job (pid $pid)\n";
return;
}
if ( ! defined ( $cron_entries{$user}{$prg}{'item'} ) ) {
print STDERR "WARN: End entry without start entry (pid $pid)\n";
return;
}
@array = split(':', $cron_entries{$user}{$prg}{'item'});
$count = $#array + 1 ;
print STDERR "DEBUG: $count cron entries for $user, task '$prg'\n" if $opt_d;
my $finish = 0;
while ( $finish == 0 ) {
$entry = pop @array;
last if ! defined ($entry) ;
# Pop all entries and look for one whose pid matches
my ($spid, $stimestamp) = split ("-", $entry);
print STDERR "DEBUG: Comparing entry $spid to $pid\n" if $opt_d;
if ( ( $pid ne "UNKNOWN" && $pid == $spid ) || $ppid-$spid == 1 ) {
my $timediff = $timestamp-$stimestamp;
$timediff = 0 if $timediff < 0;
print STDERR "DEBUG: Entries match, time difference of $timediff\n" if $opt_d ;
$cron_entries{$user}{$prg}{'finished'}++;
if (defined ( $cron_times{$user}{$prg} ) ) {
$cron_times{$user}{$prg} = join(":", $cron_times{$user}{$prg}, $timediff);
} else {
$cron_times{$user}{$prg} = $timediff;
}
$finish = 1;
} else {
print STDERR "DEBUG: Pushing entry $spid into stack\n" if $opt_d;
push @stack, $entry;
}
}
# Push all remaining entries to the stack
$cron_entries{$user}{$prg}{'item'}="";
$count = $#array + 1 ;
if ($opt_d) {
print STDERR "DEBUG: Restoring all entries (size: array $count";
$count = $#stack + 1 ;
print STDERR ", stack $count)\n";
}
print STDERR "DEBUG: Total finished tasks: $cron_entries{$user}{$prg}{'finished'} out of $cron_entries{$user}{$prg}{'count'}\n" if defined $cron_entries{$user}{$prg}{'finished'} && $opt_d;
print STDERR "DEBUG: Unmatched now: $cron_entries{$user}{$prg}{'item'}\n" if $opt_d;
while ( $entry = pop @array ) {
add_cron_entry($user, $prg, $entry);
}
while ( $entry = pop @stack ) {
add_cron_entry($user, $prg, $entry);
}
print STDERR "DEBUG: Finish execution of clear_entry\n" if $opt_d;
return;
}
sub add_cron_entry {
my ($user, $prg, $entry) = @_;
if ( defined ($cron_entries{$user}{$prg}) && $cron_entries{$user}{$prg} ne "" ) {
$cron_entries{$user}{$prg}{'item'} = join(":", $cron_entries{$user}{$prg}{'item'}, $entry);
} else {
$cron_entries{$user}{$prg}{'item'} = $entry;
$cron_entries{$user}{$prg}{'count'} = 0;
$cron_entries{$user}{$prg}{'finished'} = 0;
$cron_entries{$user}{$prg}{'unmatched'} = 0 ;
$cron_entries{$user}{$prg}{'average'} = 0 ;
$cron_entries{$user}{$prg}{'minimum'} = 0 ;
$cron_entries{$user}{$prg}{'maximum'} = 0;
}
}
sub count_unmatched {
my ($user, $prg) = @_;
my ($count, @array);
return 0 if ! defined ( $cron_entries{$user}{$prg}{'item'} );
@array = split(':', $cron_entries{$user}{$prg}{'item'});
$count = $#array + 1 ;
return $count;
}
sub find_average {
my ($user, $prg) = @_;
my ($average, $count, $total, @array, $entry);
$total = 0 ;
return -1 if ! defined ( $cron_times{$user}{$prg} );
@array = split(':', $cron_times{$user}{$prg});
$count = $#array + 1 ;
while ( defined ( $entry = pop @array ) ) {
$total += $entry;
}
$average = $total / $count;
return $average;
}
sub find_minimum {
my ($user, $prg) = @_;
my ($minimum, @array, $entry);
$minimum = -1;
return -1 if ! defined ( $cron_times{$user}{$prg} );
@array = split(':', $cron_times{$user}{$prg});
while ( defined ( $entry = pop @array ) ) {
if ( $minimum == -1 ) {
$minimum = $entry;
} else {
$minimum = $entry if $entry < $minimum;
}
}
return $minimum;
}
sub find_maximum {
my ($user, $prg) = @_;
my ($maximum, @array);
$maximum = -1;
return -1 if ! defined ( $cron_times{$user}{$prg} );
@array = split(':', $cron_times{$user}{$prg});
while ( defined ( $entry = pop @array ) ) {
if ( $maximum == -1 ) {
$maximum = $entry ;
} else {
$maximum = $entry if $entry > $maximum;
}
}
return $maximum;
}
sub generate_analysis {
# For each user and program calculate the average time for the task
my ($user, $prg);
foreach $user (keys %cron_entries) {
print STDERR "DEBUG: Calculating data for user '$user'\n" if $opt_d;
foreach my $prg ( keys %{$cron_entries{$user}} ) {
print STDERR "DEBUG: Calculating data for task '$prg'\n" if $opt_d;
my $unmatched = count_unmatched($user, $prg);
my $average = find_average($user, $prg);
my $minimum = find_minimum($user, $prg);
my $maximum = find_maximum($user, $prg);
$cron_entries{$user}{$prg}{'unmatched'} = $unmatched;
$cron_entries{$user}{$prg}{'average'} = $average;
$cron_entries{$user}{$prg}{'minimum'} = $minimum;
$cron_entries{$user}{$prg}{'maximum'} = $maximum;
}
}
}
sub print_stats {
# Print information of cron statistics
my ($user, $prg);
foreach $user (keys %cron_entries) {
print "Cron statistics for user '$user'\n";
foreach $prg ( keys %{$cron_entries{$user}} ) {
print "\tTask: '$prg'\n";
print "\t\tDEBUG: $cron_times{$user}{$prg}\n" if $opt_d;
print <<EOF
\t\tTotal: $cron_entries{$user}{$prg}{'count'}
\t\tFinished: $cron_entries{$user}{$prg}{'finished'}
\t\tUnmatched: $cron_entries{$user}{$prg}{'unmatched'}
\t\tAvg time: $cron_entries{$user}{$prg}{'average'}
\t\tMin time: $cron_entries{$user}{$prg}{'minimum'}
\t\tMax time: $cron_entries{$user}{$prg}{'maximum'}
EOF
}
}
}
|