package Reporter; =pod =head1 NAME Reporter - Cron job XML report generator =head1 VERSION This file documents C version B<1.0> =head1 DESCRIPTION used to log activity in cron jobs (mostly) so can log numbers of records handled by a task and save the report for later. Could post-process this from another job which might report on long running or excessive or missing records. Sends an error report email if errors occur and an address for receipient is specified in 'onErrorEmail' =head1 DEPENDENCIES XML::Writer, MIME::Lite, IO::File =head1 SYNOPSIS use Reporter; my $xmlReport = new Reporter( title => 'Data Loader', description => 'Test load of some data from somewhere', output => 'test.xml', onErrorEmail => 'deans@mysite.com' ); # ...do cron stuff here handling records and keeping track of how # many we process, and calculating how long the whole process takes # in seconds.... $xmlReport->addRecord( type => 'publications', items => $pubsFound, # what we found expecting => 320, # what we expected tolerance => 20, # +/- what we tolerate (0 by default) skipped => $pubsSkipped # what we ignored ); $xmlReport->addRecord( type => 'people', items => $peopleFound, expecting => 40 ); $xmlReport->addTimings( timeStart => '09:10:01', # just a string, not used for checking, # will be logged to our output XML file tho timeEnd => '09:10:40', # -- ditto timeTotal => $timeTaken, # sec's taken, calculated earlier somewhere timeMax => 50 # generate error if timeTake is longer ); $xmlReport->end(); =head1 SAMPLE OUTPUT =cut BEGIN {use Exporter();use XML::Writer; use vars qw($VERSION);$VERSION = 1.0;} =pod =head1 METHODS =cut sub new { # --------------------------------------------------------------------------------- =pod =head2 new() instantiate the Reporter object, optionally add attributes for the output report filename, title of cron job, description of job and onErrorEmail which will be the address used to send any error summary emails to =cut my $invocant = shift; my $class = ref($invocant) || $invocant; my $self = { output => 'report.xml', # default in case not user-defined defaultOutDir => '/home/www/reports/', title => undef, description => undef, errors => undef, onErrorEmail => undef, textBody => '', scriptName => $0, userID => $<, userName => $ENV{USER}, pwd => $ENV{PWD}, @_ # remaining passed arguments (not expecting any actually) }; bless($self, $class); unless ($self->__init()) { die $self->{errors} . "\n"; } $self->{textBody} = "Script: $self->{scriptName}"; return $self; } sub addRecord { # --------------------------------------------------------------------------------- =pod =head2 addRecord() add a record for the data type being reported on. You might only have a single record your processing but in other cases you may have many, depends on nature of calling process. If passed a tolerance value will check to see that items count is within limits otherwise will assume items must match expected =cut my $self = shift; my %attribs = @_; my $writer = $self->{writer}; $writer->startTag('record', 'type' => $attribs{type}, # e.g. 'papers', 'publications', 'people' etc 'items' => $attribs{items}, # a count of number of records 'expecting' => $attribs{expecting}, # what we were actually expecting 'skipped' => $attribs{skipped}, # number that were skipped 'tolerance' => $attribs{tolerance}, # diff we can tolerate without an error (not a percentage) ); # tolerance is expected to be passed, if not will generate errors if 'items' doesnt match 'expected' # use a generous value for 'tolerance' if you think 'items' will vary my $min = $attribs{expecting} - $attribs{tolerance}; my $max = $attribs{expecting} + $attribs{tolerance}; if (($min <= $attribs{items}) and ($attribs{items} <= $max)) { $self->{textBody} .= "\nFound $attribs{items} $attribs{type} - OK"; } else { $self->{errors} .= "\nFound $attribs{items} $attribs{type} but expecting $attribs{expecting} (+/-$attribs{tolerance})"; } $writer->endTag('record'); } sub addTimings { # --------------------------------------------------------------------------------- =pod =head2 addTimings() output timings from calling process into an element in the XML report, We don't bother inspecting timeStart and timeEnd, just dump 'em out as generated by the caller. We do check timeTotal and timeMax thought and send an error email if the process is long-running =cut my $self = shift; my %attribs = @_; my $writer = $self->{writer}; $writer->endTag('records'); $writer->startTag('timing', start => $attribs{timeStart}, end => $attribs{timeEnd}, total => $attribs{timeTotal}, # in seconds max => $attribs{timeMax} # in seconds ); $writer->endTag('timing'); if ($attribs{timeTotal} > $attribs{timeMax}) { $self->{errors} .= "\nOvertime: took $attribs{timeTotal}s when should be $attribs{timeMax}s"; } else { $self->{textBody} .= "\nTime taken: $attribs{timeTotal}s - OK"; } } sub end { # --------------------------------------------------------------------------------- =pod =head2 end() tidy up the report, close open tags and call internal function _email() if neccessary on error =cut my $self = shift; my $writer = $self->{writer}; $writer->endTag('report'); $writer->end(); $self->{outputFH}->close; if ((defined $self->{errors}) and (defined $self->{onErrorEmail})) { # add errors to the text body $self->{textBody} .= "\nErrors:" . $self->{errors}; $self->__email(); } } =pod =head1 PRIVATE FUNCTIONS =cut sub __init { # --------------------------------------------------------------------------------- =pod =head2 __init() called during new() to initialise the XML::Writer object and check to see if we can write to the desired output file. If all is OK we create the parent xml elements too =cut my $self = shift; unless (-w $self->{output}) { $self->{errors} .= "Cant write to Report file: '$self->{output}'"; return 0; } use IO::File; # use IO::File ref otherwise output goes to STDOUT my $fh; if ($self->{output} =~ /\//) { # check if a dir specified (contains '/') $fh = new IO::File "> $self->{output}"; } else { # no dir specified so use default one $fh = new IO::File "> " . $self->{defaultOutDir} . $self->{output}; } unless (defined $fh) { return 0; # if we cant open the file then there's no point continuing } $self->{outputFH} = $fh; my $writer = new XML::Writer( #NEWLINES => 1, OUTPUT => $self->{outputFH} ); $writer->xmlDecl(); # so we can post-process w XSLT later $writer->startTag('report', 'script' => $self->{scriptName}, 'uid' => $self->{userID}, 'user' => $self->{userName} ); $writer->startTag('title'); $writer->characters($self->{title}); $writer->endTag('title'); $writer->startTag('description'); $writer->characters($self->{description}); $writer->endTag('description'); $writer->startTag('records'); $self->{writer} = $writer; } sub __email { # --------------------------------------------------------------------------------- =pod =head2 __email() Fires an error email off using MIME::Lite to whomever is specified by the onEmailError attribute. Uses the errors attribute as basis for the email body. =cut my $self = shift; use MIME::Lite; my $msg = new MIME::Lite From => $self->{userName}, To => $self->{onErrorEmail}, Subject => 'Error: ' . $self->{title}, Data => $self->{textBody}; $msg->send || die "Unable to send email to the intended recipient";} 1