File Coverage

blib/lib/Logging/MultiChannel.pm
Criterion Covered Total %
statement 72 130 55.3
branch 15 42 35.7
condition 2 6 33.3
subroutine 13 25 52.0
pod 15 20 75.0
total 117 223 52.4


line stmt bran cond sub pod time code
1             package Logging::MultiChannel;
2 1     1   23245 use vars qw($VERSION);
  1         2  
  1         95  
3             $VERSION = '1.0.1';
4             # -------------------- Notice ---------------------
5             # Copyright 2014 Paul LaPointe
6             # www.PaullaPointe.com/Logging-MultiChannel
7             # This program is dual licensed under the (Perl) Artistic License 2.0,
8             # and the Lesser GNU General Public License 3.0 (LGPL).
9             #
10             # This program is free software: you can redistribute it and/or modify
11             # it under the terms of the GNU Lesser General Public License as published by
12             # the Free Software Foundation, either version 3 of the License, or
13             # (at your option) any later version.
14             #
15             # This program is distributed in the hope that it will be useful,
16             # but WITHOUT ANY WARRANTY; without even the implied warranty of
17             # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18             # GNU Lesser General Public License 3.0 for more details.
19             # You should have received a copy of the GNU General Public License 3.0
20             # in the licenses directory along with this program. If not, see
21             # .
22             #
23             # You should have received a copy of the Artistic License 2.
24             # in the licenses directory along with this program. If not, see
25             # .
26             #
27             # -------------------- End Notice ---------------------
28             =head1 NAME
29              
30             Logging::MultiChannel - A full featured module for implimenting log messages on multiple channels to multiple targets.
31              
32             =head2 FEATURES
33              
34             Features:
35             - Multi-channel logging, with the ablity to enable or disable channels dynamically.
36              
37             - Channels can be mapped to multiple Log files for duplication of messages.
38              
39             - Channels can be optional color coded. Each log file can enable or disable the color feature.
40              
41             - Channels can be selectively enabled for messages from specific modules.
42              
43             Advanced features:
44              
45             - Channels can be mapped to your own handles (Eg. socket) for writting to things beside log files.
46              
47             - Each Log file can use its own print function, or default to the one provided.
48              
49             Features for limiting and cycling logs:
50              
51             - Log files can optionally be limited to a specific line count.
52              
53             - Old copies of log files can optional be perserved or overwritten.
54              
55             - Old log files can be optionally moved to a different directory.
56              
57             Coming soon:
58              
59             - Thread safety.
60              
61             =head1 AUTHOR
62              
63             Paul LaPointe -
64              
65             =head2 LICENSE
66              
67             This program is dual licensed under the (Perl) Artistic License 2.0,
68             and the Lesser GNU General Public License 3.0 (LGPL).
69              
70             =head2 BUGS
71              
72             Please report any bugs or feature requests to bugs@paullapointe.org
73              
74             JUL 31, 2014 - Changed the mapChannelToLog to be internal to avoid confusion in it's use.
75             - Added a name to the startLoggingOnHandle fn to provide a name for these logs to work with
76             - Added simple client-server example
77              
78             =head2 METHODS
79              
80             Please visit for complete documentation, examples, and more.
81              
82             =head2 METHODS
83              
84             =head3 Log ( channel, message, additional args... )
85              
86             Channel can be any string.
87             Message is the log message to write.
88             Additional args can be passed in for use by a custom log handler.
89              
90             =head3 startLogging( filename, preserve, limit, oldDir, printHandler )
91              
92             filename - the fully qualified filename for the log
93              
94             preserve - An option to retain old copies of the log before overwritting (0 or 1).
95              
96             limit - An optional limit on the number of lines that can be written before cycling this log
97              
98             oldDir - Move old log files to this fully qualified directory when overwritting
99              
100             printHandler - An optional special print handler for this file
101              
102             =head3 startLoggingOnHandle ( name, fileHandle, printHandler )
103              
104             name - Any arbitrary name for this log.
105              
106             filehandle - The filehandle to log with.
107              
108             printHandler - An optional special print handler for this file
109              
110             =head3 stopLogging ( Log filename )
111              
112             This will stop logging to the given log file.
113              
114             =head3 closeLogs();
115              
116             This will stop logging to ALL files (including any custom filehandles).
117              
118             =head3 mapChannel ( Channel, Log filename1, Log filename2, ... )
119              
120             This will map a channel to one or more log files by their name.
121              
122             =head3 mapChannelToLog ( Channel, Log filename )
123              
124             Maps a channel to this specific log name.
125              
126             =head3 unmapChannel ( Channel )
127              
128             Unmaps all logs from a channel.
129              
130             =head3 enableChannel ( Channel )
131              
132             Enables log messages from a specific channel.
133              
134             =head3 disableChannel ( Channel )
135              
136             Disables log messages from a specific channel.
137              
138             =head3 enableChannelForModule ( Channel, Module )
139              
140             Enables log messages from a specific module for the given channel.
141              
142             =head3 disableChannelForModule ( Channel, Module )
143              
144             Disabled log messages from a specific module for the given channel (overriden by channel control).
145              
146             =head3 assignColorCode ( Channel , Ascii color code )
147              
148             Assigns a (typically) ASCII color code to a specific channel
149              
150             =head3 enableColor ( Channel )
151              
152             Enables color on a specific channel.
153              
154             =head3 disableColor ( Channel )
155              
156             Disables color on a specific channel.
157              
158             =head3 logStats ()
159              
160             Returns a list with a count of all messages logged to each channel.
161              
162             =head3 EXAMPLES
163              
164             =head4 Example 1: The simplest use case:
165              
166             use Logging::MultiChannel qw(Log);
167             Logging::MultiChannel::startLogging('myLogFile.log');
168             Log('INF','This is an info message'); # This will default to the last log openned
169             ...
170             Logging::MultiChannel::stopLogging('myLogFile.log');
171             exit;
172              
173             =head4 Example 2: Using multiple logs and channels:
174              
175             use Logging::MultiChannel qw(Log);
176             Logging::MultiChannel::startLogging('myLogFile1.log');
177             Logging::MultiChannel::startLogging('myLogFile2.log');
178              
179             Logging::MultiChannel::mapChannel('INF','myLogFile1.log'); # Put INF messages in myLogFile1.log
180             Logging::MultiChannel::mapChannel('ERR','myLogFile2.log'); # Put ERR messages in myLogFile2.log
181              
182             Log('INF','This is an Error message for myLogFile1.log');
183             Log('ERR','This is an info message for myLogFile2.log');
184              
185             Logging::MultiChannel::closeLogs(); # This will close ALL log files that are open
186             exit;
187            
188             =head4 More Examples are available in the distribution and at http://paullapointe.org/MultiChannel
189              
190             =cut
191              
192             #
193 1     1   5 use strict;
  1         1  
  1         31  
194 1     1   5 use warnings;
  1         4  
  1         36  
195             require Exporter;
196 1     1   792 use UNIVERSAL;
  1         14  
  1         5  
197 1     1   1204 use IO::Handle;
  1         8404  
  1         2203  
198              
199             our @months = qw( Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec );
200             our @weekdays = qw( Sun Mon Tues Wed Thrus Fri Sat );
201              
202             our @ISA = 'Exporter';
203             our @EXPORT_OK = qw(Log startLogging startLoggingOnHandle stopLogging mapChannel unmapChannel enableChannel disableChannel enableChannelForModule disableChannelForModule assignColorCode enableColor disableColor logStats);
204              
205             my $defaultLog; # This tracks the last log file openned, which will be the default for unmapped channels
206              
207             my $channels; # This is a list of all available channels
208             # channel->{name}->{logs} - A list of all logs mapped to this channel
209             # channel->{name}->{count} - A count of all messages sent to this channel
210             # channel->{name}>{state} - 1 for on, 0 for off
211             # channel->{name}->{color} - An ascii color code to optional assign to the channel, for use with the default print handler
212              
213             my @logs; # This is a list of all available filehandles
214             # $logs[i]->{fh} - The actual filehandle
215             # $logs[i]->{count} - a count of messages sent to this filehandle since it was last openned or cycled
216             # $logs[i]->{limit} - a limit on the number of lines that can be printed to this filehandle before it will be cycled. 0 to disable cycling.
217             # $logs[i]->{oldDir} - a director name that old copies of this log will be moved to when overwritting.
218             # $logs[i]->{printHandler} - a print handler for this file
219             # $logs[i]->{filename} - the filename of for this filehandle
220             # $logs[i]->{colorOn} - This controls if this filehandle will use ascii color codes (for the default logPrint fn)
221              
222             my %filenameMap; # This maps a filename back to it's permenant Log object
223             # This will start a new log file and
224             # assign a set of channels to the log
225             # 0 - filename to open
226             # 1 - A limit for the number of lines written to this file, after which it will cycle
227             # 2- A Code reference to a special print handler for this file
228             #
229             sub startLogging {
230 3     3 1 4942 my $log;
231 3         13 $log->{filename} =shift; # Obviously, the fully qualified filename for the log
232 3         9 $log->{preserve} =shift; # An option to retain old copies of the log before overwritting.
233 3         8 $log->{limit} =shift; # An optional limit on the number of lines that can be written before cycling this log
234 3         6 $log->{oldDir} =shift; # Move old log files to this directory when overwritting
235 3         6 $log->{printHandler}=shift; # An optional special print handler for this file
236              
237             # If not provided, the printHandler will default to the std fn
238 3 50       15 unless ($log->{printHandler}) { $log->{printHandler}=\&logPrint; }
  3         10  
239            
240             # Check for an old copy of the log, and move it out of the way if desired
241 3 50       10 if ($log->{preserve}) {
242 0 0       0 if (-f $log->{filename}) { &moveOldLog($log); }
  0         0  
243             }
244              
245             # Open the file
246 3 50       545 open($log->{fh}, ">$log->{filename}") or die ("Error! Unable to open log file $log->{filename} for writing.\n");
247 3         32 $log->{fh}->autoflush;
248            
249             # Now initialize this log
250 3         164 startLoggingInternal($log);
251             }
252             # This will start a new log file and
253             # assign a set of channels to the log
254             # 0 - Any arbitray name for this log, so we can work with it.
255             # 1 - The already openned filehandle
256             # 2 - A Code reference to a special print handler for this file
257             #
258             sub startLoggingOnHandle {
259 0     0 1 0 my $log;
260 0         0 $log->{filename} =shift; # In this case, just any name - it can be any string
261 0         0 $log->{fh} =shift; # Obviously, the fully qualified filename for the log
262 0         0 $log->{printHandler}=shift; # An optional special print handler for this file
263              
264 0         0 $log->{preserve}=0; # Disabled
265 0         0 $log->{limit} =0; # Disabled
266 0         0 $log->{oldDir} =''; # Disabled
267              
268             # If not provided, the printHandler will default to the std fn
269 0 0       0 unless ($log->{printHandler}) { $log->{printHandler}=\&logPrint; }
  0         0  
270              
271             # Now initialize this log
272 0         0 startLoggingInternal($log);
273             }
274              
275             # This sets up the
276             sub startLoggingInternal {
277 3     3 0 6 my $log=shift;
278              
279             # Reset the counter for this log
280 3         8 $log->{count}=0;
281              
282             # Add this new Log to our list
283 3         5 push @logs,$log;
284            
285             # Also add this Log in the filenameMap, so we can easily find it with the name
286 3         16 $filenameMap{$log->{filename}}=$log;
287              
288             # Remember this most recent log as the new default for unmapped channels
289 3         5 $defaultLog=$log;
290              
291 3         8 return $log->{fh};
292             }
293              
294              
295              
296             # This will map a set of channels to list of log files, specified by their name.
297             # Note! Channels are enabled by default. You must disable them if you want
298             # them turned off.
299             #
300             # Channels can be copied to multiple log files by calling this fn multiple
301             # times with different filenames.
302             #
303             sub mapChannel {
304 3     3 1 25 my $channel=shift;
305              
306             # Turn the channel on
307 3         22 enableChannel($channel);
308              
309             # Map the channel to each individual Log
310 3         7 foreach my $filename (@_) { &mapChannelToLog_Internal($channel,$filenameMap{$filename}); }
  3         10  
311             }
312              
313             # This will map a set of channels to a specific log file object.
314             # Note! Channels are enabled by default. You must disable them if you want
315             # them turned off.
316             #
317             # Channels can be copied to multiple log files by calling this fn multiple
318             # times with different logs.
319             #
320             # Eg.
321             sub mapChannelToLog_Internal {
322 5     5 0 8 my $channelName=shift;
323 5         8 my $log=shift;
324            
325             # If there is an existing list of logs for this channel
326             # add this log to it.
327 5 100       15 if ($channels->{$channelName}->{logs}) {
328 1         5 push @{$channels->{$channelName}->{logs}},$log;
  1         9  
329             }
330             else {
331             # If this is the first log mapped to this channel
332             # start a new list
333 4         8 my @newLogList=($log);
334 4         60 $channels->{$channelName}->{logs}=\@newLogList;
335             }
336             }
337              
338             # This will remove all the mappings for a channel
339             sub unmapChannel {
340 0     0 1 0 undef $channels->{$_[0]}->{logs};
341             }
342              
343             # This will close down a log file handle
344             # Note it will NOT unmap any channels mapped to it
345             sub stopLogging {
346 0     0 1 0 my $filename=shift;
347 0         0 my $log=$filenameMap{$filename};
348              
349 0 0       0 if ($log->{fh}) { close($log->{fh}); }
  0         0  
350             }
351              
352             # Close all logs
353             sub closeLogs {
354 1     1 1 9805 foreach my $log (@logs) {
355 3 50       25 if ($log->{fh}) { close($log->{fh}); }
  3         63  
356             }
357             }
358              
359             # These will enable (1) or disable (0) a particular log channel
360 5     5 1 24 sub enableChannel { $channels->{$_[0]}->{state}=1; $channels->{$_[0]}->{count}=0; }
  5         19  
361 0     0 1 0 sub disableChannel { $channels->{$_[0]}->{state}=0; }
362              
363             # This will assign an (normally ascii) color code to a particular channel
364 0     0 1 0 sub assignColorCode { $channels->{$_[0]}->{color}=$_[1]; }
365              
366             # These will enable (1) or disable (0) a particular log channel, and particular module
367 0     0 1 0 sub enableChannelForModule { $channels->{$_[0]}->{"pkg:$_[1]"}=1; }
368 0     0 1 0 sub disableChannelForModule { undef $channels->{$_[0]}->{"pkg:$_[1]"}; }
369              
370             # These will enable or disable color codes for this particular Log
371 0     0 1 0 sub enableColor { my $log=$filenameMap{$_[0]}; $log->{colorOn}=1; }
  0         0  
372 0     0 1 0 sub disableColor { my $log=$filenameMap{$_[0]}; $log->{colorOn}=0; }
  0         0  
373              
374             # This is the main internal print routine for the logging.
375             # This function should not be called externally.
376             # These are the args:
377             # 0 - Epoch Time
378             # 1 - Local Time as a string
379             # 2 - Filehandle
380             # 3 - The log object
381             # 4 - source module
382             # 5 - source filename
383             # 6 - source line #
384             # 7 - desired color
385             # 8 - channel name
386             # 9 - message
387             # 10,etc - Additional parameters...
388              
389             sub logPrint {
390 5     5 0 6 my $fh=$_[2];
391              
392             # If color codes are turned on, add one now for the specified color
393 5 50       10 if ($_[7]) { print $fh $_[7]; }
  0         0  
394            
395             # Print the line content
396 5         197 printf $fh "$_[8] %24s %12s %s",$_[1],"$_[4]-$_[6]",$_[9];
397            
398             # If color codes are turned on, add one for black now
399 5 50       15 if ($_[7]) { print $fh "\e[49m"; }
  0         0  
400            
401             # end the line with a carriage return
402 5         46 print $fh "\n";
403            
404             # Increment the log line counter
405 5         11 $_[3]->{count}++;
406            
407             # If we've hit the log line limit, cycle the log
408 5 50 33     44 if (($_[3]->{limit}) and ($_[3]->{count} > $_[3]->{limit})) { &cycleLog($_[3]); }
  0         0  
409             }
410              
411             # This is the external function used to log messages on a particular
412             # channel. This are the args:
413             # 0 - channel
414             # 1 - message
415             sub Log {
416            
417             # Check that this channel is actually mapped to a log
418 4 100   4 1 4882 unless ($channels->{$_[0]}->{logs}) {
419 2 50       7 if ($defaultLog) {
420             # If its not, map it to the default log (last openned) and enable it.
421 2         15 &mapChannelToLog_Internal($_[0],$defaultLog);
422              
423             # Turn the channel on
424 2         13 enableChannel($_[0]);
425             }
426             else {
427 0         0 return; # Do nothing if there are no logs open
428             }
429             }
430            
431             # Only print if the channel is not enabled or if its enabled for a particular module
432 4         16 my ( $pkg, $srcfilename, $line ) = caller;
433 4 50 33     17 if (($channels->{$_[0]}->{state}) or ($channels->{$_[0]}->{"pkg-$pkg"})) {
434             # Get the time of the message
435 4         10 my $now=time();
436 4         276 my $localNow=localtime($now);
437 4         10 $channels->{$_[0]}->{count}++;
438              
439             # Print the message on each of the filehandles for this channel
440 4         6 foreach my $log (@{$channels->{$_[0]}->{logs}}) {
  4         17  
441 5 50       12 if ($log->{printHandler}) {
442 5         7 my $color;
443             # If this filehandle has color turned on, and this channel has a desired color, provide it
444 5 50       16 if ($log->{colorOn}) { $color=$channels->{$_[0]}->{color}; }
  0         0  
445 5         33 &{$log->{printHandler}}($now,$localNow,$log->{fh},$log,$pkg,$srcfilename,$line,$color,@_);
  5         20  
446             }
447             }
448             }
449             }
450              
451             # This will cycle a log file by closing it, and moving
452             # the current log to an archived filename. Then it will
453             # reopen the log.
454             # This function is overloaded - it could be called with
455             # a filename or filehandle
456             sub cycleLog {
457 0     0 0   my $log=shift;
458            
459             # Close the old log file
460 0 0         if ($log->{fh}) { close($log->{fh}); }
  0            
461              
462             # Move the old copy of the log out of the way
463 0 0         if ($log->{preserve}) { &moveOldLog($log); }
  0            
464              
465             # Reopen the file
466 0 0         open($log->{fh},">$log->{filename}") or die ("Error! Unable to open log file $log->{filename} for writing.\n");
467 0           $log->{fh}->autoflush;
468 0           $log->{count}=0;
469              
470 0           return $log->{fh};
471             }
472              
473             # This will move an old copy of a log out of the way
474             # so a new one can take it's place
475             sub moveOldLog {
476 0     0 0   my $log=shift;
477 0           my $filename=$log->{filename};
478              
479             # Get a timestamp, to add to the name of the old log file
480 0           my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time);
481 0           $year += 1900;
482 0           my $timestamp="$year-".$months[$mon]."-".$mday."_"."$hour.$min.$sec";
483            
484             # Rename the old file with the timestamp
485 0           my $cmd="mv -f $filename $filename\.$timestamp";
486             # If there's an old dir specified, move the file there instead
487 0 0         if ($log->{oldDir}) {
488 0           my $shortFilename=$filename;
489 0           $shortFilename =~ s{.*/}{}; # Remove path
490 0           $cmd="mv -f $filename $log->{oldDir}/$shortFilename\.$timestamp";
491              
492             # Make sure that old dir directory actually exists first
493 0 0         unless (-d $log->{oldDir}) {
494 0           system("mkdir -p $log->{oldDir}");
495             }
496             }
497 0           system($cmd);
498             }
499              
500             # This will show a breakdown of how many messages
501             # were logged on each channel since this fun
502             # was last called
503             sub logStats {
504 0     0 1   my @ret;
505 0           foreach my $channelName (keys %{$channels}) {
  0            
506 0           push @ret,"$channelName - $channels->{$channelName}->{count}";
507 0           $channels->{$channelName}->{count}=0;
508             }
509 0           return @ret;
510             }
511              
512             #--------------------------------------------------------------------------------------------------
513             # 2. Make it thread safe!
514              
515             1;