TER1 (Statement): 98.45%
TER2 (Branch): 90.45%
TER3 (LCSAJ): 97.6% (40/41)
Approximate LCSAJ segments: 179
● Covered — this LCSAJ path was executed during testing.
● Not covered — this LCSAJ path was never executed. These are the paths to focus on.
Multiple dots on a line indicate that multiple control-flow paths begin at that line. Hovering over any dot shows:
start → end → jump
Uncovered paths show [NOT COVERED] in the tooltip.
1: package Log::Abstraction; 2: 3: use strict; 4: use warnings; 5: use Carp; # Import Carp for warnings 6: use Config::Abstraction 0.36; 7: use Data::Dumper; 8: use Params::Get 0.13; # Import Params::Get for parameter handling 9: use POSIX qw(strftime); 10: use Readonly::Values::Syslog 0.03; 11: use Return::Set; 12: use Scalar::Util 'blessed'; # Import Scalar::Util for object reference checking 13: use Sys::Syslog 0.28; # Import Sys::Syslog for syslog support 14: 15: =head1 NAME 16: 17: Log::Abstraction - Logging Abstraction Layer 18: 19: =head1 VERSION 20: 21: 0.29 22: 23: =cut 24: 25: our $VERSION = 0.29; 26: 27: =head1 SYNOPSIS 28: 29: use Log::Abstraction; 30: 31: my $logger = Log::Abstraction->new(logger => 'logfile.log'); 32: 33: $logger->debug('This is a debug message'); 34: $logger->info('This is an info message'); 35: $logger->notice('This is a notice message'); 36: $logger->trace('This is a trace message'); 37: $logger->warn({ warning => 'This is a warning message' }); 38: 39: =head1 DESCRIPTION 40: 41: The C<Log::Abstraction> class provides a flexible logging layer on top of different types of loggers, 42: including code references, arrays, file paths, and objects. 43: It also supports logging to syslog if configured. 44: 45: =head1 METHODS 46: 47: =head2 new 48: 49: my $logger = Log::Abstraction->new(%args); 50: 51: Creates a new C<Log::Abstraction> object. 52: 53: The argument can be a hash, 54: a reference to a hash or the C<logger> value. 55: The following arguments can be provided: 56: 57: =over 58: 59: =item * C<carp_on_warn> 60: 61: If set to 1, 62: and C<logger> is not given, 63: call C<Carp:carp> on C<warn()>. 64: 65: Causes C<error()> to C<carp> if C<croak_on_error> is not given. 66: 67: =item * C<croak_on_error> 68: 69: If set to 1, 70: and C<logger> is not given, 71: call C<Carp:croak> on C<error()>. 72: 73: =item * C<config_file> 74: 75: Points to a configuration file which contains the parameters to C<new()>. 76: The file can be in any common format, 77: including C<YAML>, C<XML>, and C<INI>. 78: This allows the parameters to be set at run time. 79: 80: On a non-Windows system, 81: the class can be configured using environment variables starting with C<"Log::Abstraction::">. 82: For example: 83: 84: export Log::Abstraction::script_name=foo 85: 86: It doesn't work on Windows because of the case-insensitive nature of that system. 87: 88: =item * C<level> 89: 90: The minimum level at which to log something, 91: the default is "warning". 92: 93: =item * C<logger> 94: 95: A logger can be one or more of: 96: 97: =over 98: 99: =item * a code reference 100: 101: The code will be called with a hashref containing: 102: 103: =over 104: 105: =item * class 106: 107: =item * file 108: 109: =item * line 110: 111: =item * level 112: 113: =item * message - an arrayref of messages 114: 115: =item * ctx - passed to C<new()>, an argument that can help to give context to the caller 116: 117: =back 118: 119: =item * an object 120: 121: =item * a hash of options 122: 123: =item * sendmail - send higher priority messages to an email address 124: 125: To send an e-mail, 126: you need L<require Email::Simple>, L<require Email::Sender::Simple> and L<Email::Sender::Transport::SMTP>. 127: 128: The C<sendmail> hash also accepts a C<min_interval> key (seconds). 129: When set, at most one email is sent per C<min_interval> seconds; any 130: messages that arrive during the cooldown are still logged to other 131: backends but do not trigger a new email. 132: The send time is stored in C<_last_email_sent> on the object, so each 133: instance has its own cooldown window; cloned objects inherit the 134: parent's last-send timestamp at the moment of cloning. 135: 136: =item * array - a reference to an array 137: 138: =item * fd - containing a file descriptor to log to 139: 140: =item * file - containing the filename 141: 142: =back 143: 144: Defaults to L<Log::Log4perl>. 145: In that case, 146: the argument 'verbose' to new() will raise the logging level. 147: 148: =item * C<format> 149: 150: The format of the message. 151: Expands: 152: 153: =over 154: 155: =item * %callstack% 156: 157: =item * %level% 158: 159: =item * %class% 160: 161: =item * %message% 162: 163: =item * %timestamp% 164: 165: =%item * %env_foo% 166: 167: Replaces with C<$ENV{foo}> 168: 169: =back 170: 171: =item * C<syslog> 172: 173: A hash reference for syslog configuration. 174: Only warnings and above will be sent to syslog. 175: This restriction should be lifted in the future, 176: since it's reasonable to send notices and above to the syslog. 177: 178: =item * C<script_name> 179: 180: The name of the script. 181: It's needed when C<syslog> is given, 182: if none is passed, the value is guessed. 183: 184: =back 185: 186: Clone existing objects with or without modifications: 187: 188: my $clone = $logger->new(); 189: 190: =cut 191: 192: sub new { ●193 → 198 → 205●193 → 198 → 0 193: my $class = shift; 194: 195: # Handle hash or hashref arguments 196: my %args; 197: 198: if((scalar(@_) == 1) && (ref($_[0]) ne 'HASH')) {Mutants (Total: 2, Killed: 2, Survived: 0)
199: $args{'logger'} = shift; 200: } elsif(my $params = Params::Get::get_params(undef, \@_)) { 201: %args = %{$params}; 202: } 203: 204: # Load the configuration from a config file, if provided ●205 → 205 → 225●205 → 205 → 0 205: if(exists($args{'config_file'})) {
Mutants (Total: 1, Killed: 1, Survived: 0)
206: # my $config = YAML::XS::LoadFile($params->{'config_file'}); 207: if(!-r $args{'config_file'}) {
Mutants (Total: 1, Killed: 1, Survived: 0)
208: croak("$class: ", $args{'config_file'}, ': File not readable'); 209: } 210: if(my $config = Config::Abstraction->new(config_dirs => [''], config_file => $args{'config_file'}, env_prefix => "${class}::")) {
Mutants (Total: 1, Killed: 1, Survived: 0)
211: $config = $config->all(); 212: if($config->{$class}) {
Mutants (Total: 1, Killed: 1, Survived: 0)
213: $config = $config->{$class}; 214: } 215: my $array = $args{'array'}; 216: %args = (%{$config}, %args); 217: if($array) {
218: $args{'array'} = $array; 219: } 220: } else { 221: croak("$class: Can't load configuration from ", $args{'config_file'}); 222: } 223: } 224: ●225 → 225 → 243●225 → 225 → 0 225: if(!defined($class)) {Mutants (Total: 1, Killed: 0, Survived: 1)
- COND_INV_217_4: Invert condition if to unless
MEDIUM: Add tests asserting both true and false outcomesMutants (Total: 1, Killed: 1, Survived: 0)
226: # Using Log::Abstraction:new(), not Log::Abstraction->new() 227: $class = __PACKAGE__; 228: } elsif(Scalar::Util::blessed($class)) { 229: # If $class is an object, clone it with new arguments 230: my $clone = bless { %{$class}, %args }, ref($class); 231: if(my $level = $args{'level'}) {
Mutants (Total: 1, Killed: 1, Survived: 0)
232: # The clone is at a different level 233: $level = lc($level); 234: if(!defined($syslog_values{$level})) {
Mutants (Total: 1, Killed: 1, Survived: 0)
235: Carp::croak("$class: invalid syslog level '$level'"); 236: } 237: $clone->{level} = $syslog_values{$level}; 238: } 239: $clone->{messages} = [ @{$class->{messages}} ]; # Deep copy 240: return $clone;
Mutants (Total: 2, Killed: 2, Survived: 0)
241: } 242: ●243 → 243 → 252●243 → 243 → 0 243: if($args{'syslog'} && !$args{'script_name'}) {
Mutants (Total: 1, Killed: 1, Survived: 0)
244: require File::Basename && File::Basename->import() unless File::Basename->can('basename'); 245: 246: # Determine script name 247: $args{'script_name'} = File::Basename::basename($ENV{'SCRIPT_NAME'} || $0); 248: 249: croak("$class: syslog needs to know the script name") if(!defined($args{'script_name'})); 250: } 251: ●252 → 253 → 267●252 → 253 → 0 252: my $level = $args{'level'}; 253: if(defined(my $logger = $args{logger})) {
Mutants (Total: 1, Killed: 1, Survived: 0)
254: if(Scalar::Util::blessed($logger) && (ref($logger) eq __PACKAGE__)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
255: croak("$class: attempt to encapulate ", __PACKAGE__, ' as a logging class, that would add a needless indirection'); 256: } 257: } elsif((!$args{'file'}) && (!$args{'array'})) { 258: # Default to Log4perl 259: require Log::Log4perl; 260: Log::Log4perl->import(); 261: 262: # FIXME: add default minimum logging level 263: Log::Log4perl->easy_init($args{verbose} ? $Log::Log4perl::DEBUG : $Log::Log4perl::ERROR); 264: $args{'logger'} = Log::Log4perl->get_logger(); 265: } 266: ●267 → 267 → 282●267 → 267 → 0 267: if($level) {
Mutants (Total: 1, Killed: 1, Survived: 0)
268: if(ref($level) eq 'ARRAY') {
Mutants (Total: 1, Killed: 1, Survived: 0)
269: $level = $level->[0]; 270: } 271: $level = lc($level); 272: if(!defined($syslog_values{$level})) {
Mutants (Total: 1, Killed: 1, Survived: 0)
273: Carp::croak("$class: invalid syslog level '$level'"); 274: } 275: $args{'level'} = $level; 276: } else { 277: # The default minimum level at which to log something is 'warning' 278: $args{'level'} = 'warning'; 279: } 280: 281: # Bless and return the object ●[NOT COVERED] 282 → 282 → 0 282: return bless { 283: messages => [], # Initialize messages array 284: %args, 285: level => $syslog_values{$args{'level'}}, 286: }, $class; 287: } 288: 289: =encoding utf-8 290: 291: =head2 _sanitize_email_header 292: 293: my $clean_value = _sanitize_email_header($raw_value); 294: 295: Internal routine to remove carriage return and line feed characters from an email header value to prevent header injection or formatting issues. 296: 297: =over 4 298: 299: =item * Input 300: 301: Takes a single scalar value, typically a string representing an email header field. 302: 303: =item * Behavior 304: 305: If the input is undefined, returns `undef`. Otherwise, removes all newline characters (`\n`), carriage returns (`\r`), and CRLF pairs from the string. 306: 307: =item * Output 308: 309: Returns the sanitized string with CR/LF characters removed. 310: 311: =back 312: 313: =head3 FORMAL SPECIFICATION 314: 315: If the input is undefined (â ), the output is also undefined (â ). 316: 317: If the input is defined, the result is a defined string with CR and LF characters removed. 318: 319: [CHAR] 320: 321: CR, LF : CHAR 322: CR == '\r' 323: LF == '\n' 324: 325: STRING == seq CHAR 326: 327: SanitizeEmailHeader 328: raw?: STRING 329: sanitized!: STRING 330: ------------------------------------------------- 331: sanitized! = [ c : raw? | c â CR â§ c â LF ] 332: 333: =cut 334: 335: sub _sanitize_email_header { 336: my $value = $_[0]; 337: 338: return unless defined $value; 339: $value =~ s/\r\n?|\n//g; # Remove CR/LF characters 340: 341: return Return::Set::set_return($value, { type => 'string', 'matches' => qr /^[^\r\n]*$/ }); 342: } 343: 344: # Internal method to log messages. This method is called by other logging methods. 345: # $logger->_log($level, @messages); 346: # $logger->_log($level, \@messages); 347: 348: sub _log { ●349 → 351 → 355●349 → 351 → 0 349: my ($self, $level, @messages) = @_; 350: 351: if(!UNIVERSAL::isa((caller)[0], __PACKAGE__)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
352: Carp::croak('Illegal Operation: _log is a private method'); 353: } 354: ●355 → 355 → 359●355 → 355 → 0 355: if(!defined($syslog_values{$level})) {
Mutants (Total: 1, Killed: 1, Survived: 0)
356: Carp::croak(ref($self), ": Invalid level '$level'"); # "Can't happen" 357: } 358: ●359 → 359 → 364●359 → 359 → 0 359: if($syslog_values{$level} > $self->{'level'}) {
Mutants (Total: 4, Killed: 4, Survived: 0)
360: # The level is too low to log 361: return; 362: } 363: ●364 → 364 → 368●364 → 364 → 0 364: if((scalar(@messages) == 1) && (ref($messages[0]) eq 'ARRAY')) {
Mutants (Total: 2, Killed: 2, Survived: 0)
365: # Passed a reference to an array 366: @messages = @{$messages[0]}; 367: } ●368 → 377 → 381●368 → 377 → 0 368: @messages = grep defined, @messages; 369: 370: my $str = join('', @messages); 371: chomp($str); 372: 373: # Push the message to the internal messages array 374: push @{$self->{messages}}, { level => $level, message => $str }; 375: 376: my $class = blessed($self) || $self; 377: if($class eq __PACKAGE__) {
Mutants (Total: 1, Killed: 1, Survived: 0)
378: $class = ''; 379: } 380: ●381 → 383 → 559●381 → 383 → 0 381: my $timestamp = strftime '%Y-%m-%d %H:%M:%S', localtime; 382: 383: if(my $logger = $self->{'logger'}) {
Mutants (Total: 1, Killed: 1, Survived: 0)
384: if(ref($logger) eq 'CODE') {
Mutants (Total: 1, Killed: 1, Survived: 0)
385: # If logger is a code reference, call it with log details 386: my $args = { 387: class => blessed($self) || __PACKAGE__, 388: file => (caller(1))[1], 389: # function => (caller(1))[3], 390: line => (caller(1))[2], 391: level => $level, 392: message => \@messages 393: }; 394: if(my $ctx = $self->{ctx}) {
Mutants (Total: 1, Killed: 1, Survived: 0)
395: $args->{ctx} = $ctx; 396: }; 397: $logger->($args); 398: } elsif(ref($logger) eq 'ARRAY') { 399: # If logger is an array reference, push the log message to the array 400: push @{$logger}, { level => $level, message => $str }; 401: } elsif(ref($logger) eq 'HASH') { 402: if(my $file = $logger->{'file'}) {
Mutants (Total: 1, Killed: 1, Survived: 0)
403: # if($file =~ /^([-\@\w.\/\\]+)$/) { 404: if($file =~ /^([^<>|*?;!`$"\0-\037]+)$/) {
Mutants (Total: 1, Killed: 1, Survived: 0)
405: $file = $1; # Will untaint 406: } else { 407: Carp::croak(ref($self), ": Invalid file name: $file"); 408: } 409: if(open(my $fout, '>>', $logger->{'file'})) {
Mutants (Total: 1, Killed: 1, Survived: 0)
410: my $format = $self->{'format'} || '%level%> [%timestamp%] %class% %callstack% %message%'; 411: my $ulevel = uc($level); 412: my $callstack = (caller(1))[1] . ' ' . (caller(1))[2]; 413: 414: $format =~ s/%level%/$ulevel/g; 415: $format =~ s/%class%/$class/g; 416: $format =~ s/%message%/$str/g; 417: $format =~ s/%callstack%/$callstack/g; 418: $format =~ s/%timestamp%/$timestamp/g; 419: $format =~ s/%env_(\w+)%/$ENV{$1}/g; 420: print $fout "$format\n" or Carp::croak(ref($self), ": Can't write to $file: $!"); 421: close $fout; 422: } 423: } 424: if(my $array = $logger->{'array'}) {
Mutants (Total: 1, Killed: 1, Survived: 0)
425: push @{$array}, { level => $level, message => $str }; 426: } 427: if(exists($logger->{'sendmail'}) && exists($logger->{'sendmail'}->{'to'})) {
Mutants (Total: 1, Killed: 1, Survived: 0)
428: # Send an email 429: if((!defined($logger->{'sendmail'}->{'level'})) ||
Mutants (Total: 1, Killed: 1, Survived: 0)
430: ($syslog_values{$level} <= $syslog_values{$logger->{'sendmail'}->{'level'}})) {
Mutants (Total: 3, Killed: 3, Survived: 0)
431: my $throttled = 0; 432: if(my $min_interval = $logger->{'sendmail'}->{'min_interval'}) {
Mutants (Total: 1, Killed: 1, Survived: 0)
433: my $now = time(); 434: # _last_email_sent: epoch time of the most recent successfully sent email 435: $throttled = defined($self->{_last_email_sent}) && ($now - $self->{_last_email_sent}) < $min_interval;
Mutants (Total: 3, Killed: 3, Survived: 0)
436: } 437: if(!$throttled) {
Mutants (Total: 1, Killed: 1, Survived: 0)
438: eval { 439: require Email::Simple; 440: require Email::Sender::Simple; 441: require Email::Sender::Transport::SMTP; 442: 443: Email::Simple->import(); 444: Email::Sender::Simple->import('sendmail'); 445: Email::Sender::Transport::SMTP->import(); 446: 447: my $email = Email::Simple->new(''); 448: $email->header_set('to', _sanitize_email_header($logger->{'sendmail'}->{'to'})); 449: if(my $from = $logger->{'sendmail'}->{'from'}) {
Mutants (Total: 1, Killed: 1, Survived: 0)
450: $email->header_set('from', _sanitize_email_header($from)); 451: } else { 452: $email->header_set('from', 'noreply@localhost'); 453: } 454: if(my $subject = $logger->{'sendmail'}->{'subject'}) {
Mutants (Total: 1, Killed: 1, Survived: 0)
455: $email->header_set('subject', _sanitize_email_header($subject)); 456: } 457: $email->body_set(join(' ', @messages)); 458: 459: # Configure SMTP transport (adjust for your SMTP server) 460: my $transport = Email::Sender::Transport::SMTP->new({ 461: host => $logger->{'sendmail'}->{'host'} || 'localhost', 462: port => $logger->{'sendmail'}->{'port'} || 25 463: }); 464: 465: sendmail($email, { transport => $transport }); 466: }; 467: 468: if ($@) {
Mutants (Total: 1, Killed: 1, Survived: 0)
469: Carp::carp("Failed to send email: $@"); 470: return; 471: } 472: $self->{_last_email_sent} = time(); # record send time for throttle 473: } 474: } 475: } 476: if(my $syslog = $logger->{'syslog'}) {
Mutants (Total: 1, Killed: 1, Survived: 0)
477: if((!defined($syslog->{'level'})) || ($syslog_values{$level} <= $syslog->{'level'})) {
Mutants (Total: 4, Killed: 4, Survived: 0)
478: if(!$self->{_syslog_opened}) {
Mutants (Total: 1, Killed: 1, Survived: 0)
479: # Open persistent syslog connection 480: my $facility = delete $syslog->{'facility'} || 'local0'; 481: my $min_level = delete $syslog->{'level'}; 482: # CHI uses server, Sys::Syslog uses host :-( 483: if($syslog->{'server'}) {
Mutants (Total: 1, Killed: 1, Survived: 0)
484: $syslog->{'host'} = delete $syslog->{'server'}; 485: } 486: Sys::Syslog::setlogsock($syslog) if(scalar keys %{$syslog}); 487: $syslog->{'facility'} = $facility; 488: $syslog->{'level'} = $min_level; 489: 490: openlog($self->{script_name}, 'cons,pid', 'user'); 491: $self->{_syslog_opened} = 1; # Flag to track active connection 492: } 493: 494: # Handle syslog-based logging 495: eval { 496: my $priority = ($level eq 'error') ? 'err' : 'warning'; 497: my $facility = $syslog->{'facility'}; 498: Sys::Syslog::syslog("$priority|$facility", join(' ', @messages)); 499: }; 500: if($@) {
Mutants (Total: 1, Killed: 1, Survived: 0)
501: my $err = $@; 502: $err .= ":\n" . Data::Dumper->new([$syslog])->Dump(); 503: Carp::carp($err); 504: } 505: } 506: } 507: 508: if(my $fout = $logger->{'fd'}) {
Mutants (Total: 1, Killed: 1, Survived: 0)
509: my $format = $self->{'format'} || '%level%> [%timestamp%] %class% %callstack% %message%'; 510: my $ulevel = uc($level); 511: my $callstack = (caller(1))[1] . ' ' . (caller(1))[2]; 512: 513: $format =~ s/%level%/$ulevel/g; 514: $format =~ s/%class%/$class/g; 515: $format =~ s/%message%/$str/g; 516: $format =~ s/%callstack%/$callstack/g; 517: $format =~ s/%timestamp%/$timestamp/g; 518: $format =~ s/%env_(\w+)%/$ENV{$1} \/\/ ''/ge; 519: 520: print $fout "$format\n" or Carp::croak(ref($self), ": Can't write to file descriptor: $!"); 521: } elsif(!$logger->{'file'} && !$logger->{'array'} && !$logger->{'syslog'} && !exists($logger->{'sendmail'}) && !$logger->{'fd'}) { 522: croak(ref($self), ": Don't know how to deal with the $level message"); 523: } 524: } elsif(!ref($logger)) { 525: # If logger is a file path, append the log message to the file 526: if(open(my $fout, '>>', $logger)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
527: my $format = $self->{'format'} || '%level%> [%timestamp%] %class% %callstack% %message%'; 528: my $ulevel = uc($level); 529: my $callstack = (caller(1))[1] . ' ' . (caller(1))[2]; 530: 531: $format =~ s/%level%/$ulevel/g; 532: $format =~ s/%class%/$class/g; 533: $format =~ s/%message%/$str/g; 534: $format =~ s/%callstack%/$callstack/g; 535: $format =~ s/%timestamp%/$timestamp/g; 536: $format =~ s/%env_(\w+)%/$ENV{$1} \/\/ ''/ge; 537: 538: print $fout "$format\n" or Carp::croak(ref($self), ": Can't write to $logger: $!"); 539: close $fout; 540: } 541: } elsif(Scalar::Util::blessed($logger)) { 542: # If logger is an object, call the appropriate method on the object 543: if(!$logger->can($level)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
544: if(($level eq 'notice') && $logger->can('info')) {
Mutants (Total: 1, Killed: 1, Survived: 0)
545: # Map notice to info for Log::Log4perl 546: $level = 'info'; 547: } else { 548: croak(ref($self), ': ', ref($logger), " doesn't know how to deal with the $level message"); 549: } 550: } 551: $logger->$level(@messages); 552: } else { 553: croak(ref($self), ": configuration error, no handler written for the $level message"); 554: } 555: } elsif($self->{'array'}) { 556: push @{$self->{'array'}}, { level => $level, message => $str }; 557: } 558: ●559 → 559 → 592●559 → 559 → 0 559: if($self->{'file'}) {
Mutants (Total: 1, Killed: 1, Survived: 0)
560: my $file = $self->{'file'}; 561: 562: # Untaint the file name 563: # if($file =~ /^([-\@\w.\/\\]+)$/) { 564: if($file =~ /^([^<>|*?;!`$"\0-\037]+)$/) {
Mutants (Total: 1, Killed: 1, Survived: 0)
565: $file = $1; # untainted version 566: } else { 567: croak(ref($self), ": Tainted or unsafe filename: $file"); 568: } 569: 570: if(open(my $fout, '>>', $file)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
571: my $ulevel = uc($level); 572: my $callstack = (caller(1))[1] . ' ' . (caller(1))[2]; 573: my $format; 574: 575: if(blessed($self) eq __PACKAGE__) {
Mutants (Total: 1, Killed: 1, Survived: 0)
576: $format = $self->{'format'} || '%level%> [%timestamp%] %callstack% %message%'; 577: } else { 578: $format = $self->{'format'} || '%level%> [%timestamp%] %class% %callstack% %message%'; 579: } 580: 581: $format =~ s/%level%/$ulevel/g; 582: $format =~ s/%class%/$class/g; 583: $format =~ s/%message%/$str/g; 584: $format =~ s/%callstack%/$callstack/g; 585: $format =~ s/%timestamp%/$timestamp/g; 586: $format =~ s/%env_(\w+)%/$ENV{$1} \/\/ ''/ge; 587: 588: print $fout "$format\n" or Carp::croak("ref($self): Can't write to ", $self->{'file'}, ": $!"); 589: close $fout; 590: } 591: } ●592 → 592 → 0 592: if(my $fout = $self->{'fd'}) {
Mutants (Total: 1, Killed: 1, Survived: 0)
593: my $ulevel = uc($level); 594: my $callstack = (caller(1))[1] . ' ' . (caller(1))[2]; 595: my $format; 596: 597: if(blessed($self) eq __PACKAGE__) {
Mutants (Total: 1, Killed: 1, Survived: 0)
598: $format = $self->{'format'} || '%level%> [%timestamp%] %callstack% %message%'; 599: } else { 600: $format = $self->{'format'} || '%level%> [%timestamp%] %class% %callstack% %message%'; 601: } 602: 603: $format =~ s/%level%/$ulevel/g; 604: $format =~ s/%class%/$class/g; 605: $format =~ s/%message%/$str/g; 606: $format =~ s/%callstack%/$callstack/g; 607: $format =~ s/%timestamp%/$timestamp/g; 608: $format =~ s/%env_(\w+)%/$ENV{$1} \/\/ ''/ge; 609: 610: print $fout "$format\n" or Carp::croak(ref($self), ": Can't write to file descriptor: $!"); 611: } 612: } 613: 614: =head2 level($self, $level) 615: 616: Get/set the minimum level to log at. 617: Returns the current level, as an integer. 618: 619: =cut 620: 621: sub level() 622: { ●623 → 625 → 632●623 → 625 → 0 623: my ($self, $level) = @_; 624: 625: if($level) {
Mutants (Total: 1, Killed: 1, Survived: 0)
626: if(!defined($syslog_values{$level})) {
Mutants (Total: 1, Killed: 1, Survived: 0)
627: Carp::carp(ref($self), ": invalid syslog level '$level'"); 628: return; 629: } 630: $self->{'level'} = $syslog_values{$level}; 631: } ●632 → 632 → 0 632: return Return::Set::set_return($self->{'level'}, { 'type' => 'integer', 'min' => 0, 'max' => 7 }); 633: } 634: 635: =head2 is_debug 636: 637: Are we at a debug level that will emit debug messages? 638: For compatibility with L<Log::Any>. 639: 640: =cut 641: 642: sub is_debug 643: { 644: my $self = $_[0]; 645: 646: return ($self->{'level'} && ($self->{'level'} >= $DEBUG)) ? 1 : 0;
Mutants (Total: 3, Killed: 3, Survived: 0)
647: } 648: 649: =head2 messages 650: 651: Return all the messages emitted so far 652: 653: =cut 654: 655: sub messages 656: { 657: my $self = $_[0]; 658: 659: return [ @{$self->{messages}} ]; # Return a shallow copy 660: } 661: 662: =head2 debug 663: 664: $logger->debug(@messages); 665: 666: Logs a debug message. 667: 668: =cut 669: 670: sub debug { 671: my $self = shift; 672: $self->_log('debug', @_); 673: } 674: 675: =head2 info 676: 677: $logger->info(@messages); 678: 679: Logs an info message. 680: 681: =cut 682: 683: sub info { 684: my $self = shift; 685: $self->_log('info', @_); 686: } 687: 688: =head2 notice 689: 690: $logger->notice(@messages); 691: 692: Logs a notice message. 693: 694: =cut 695: 696: sub notice { 697: my $self = shift; 698: $self->_log('notice', @_); 699: } 700: 701: =head2 error 702: 703: $logger->error(@messages); 704: 705: Logs an error message. This method also supports logging to syslog if configured. 706: If not logging mechanism is set, 707: falls back to C<croak>. 708: 709: =cut 710: 711: sub error { 712: my $self = shift; 713: 714: $self->_high_priority('error', @_); 715: } 716: 717: =head2 fatal 718: 719: $logger->fatal(@messages); 720: 721: Synonym of error. 722: 723: =cut 724: 725: sub fatal { 726: my $self = shift; 727: 728: $self->_high_priority('error', @_); 729: } 730: 731: =head2 trace 732: 733: $logger->trace(@messages); 734: 735: Logs a trace message. 736: 737: =cut 738: 739: sub trace { 740: my $self = shift; 741: $self->_log('trace', @_); 742: } 743: 744: =head2 warn 745: 746: $logger->warn(@messages); 747: $logger->warn(\@messages); 748: $logger->warn(warning => \@messages); 749: 750: Logs a warning message. This method also supports logging to syslog if configured. 751: If not logging mechanism is set, 752: falls back to C<Carp>. 753: 754: =cut 755: 756: sub warn { 757: my $self = shift; 758: 759: return if(scalar(@_) == 0);
Mutants (Total: 1, Killed: 1, Survived: 0)
760: $self->_high_priority('warn', @_); 761: } 762: 763: =head2 _high_priority 764: 765: Helper to handle important messages. 766: 767: =cut 768: 769: sub _high_priority 770: { ●771 → 784 → 796●771 → 784 → 0 771: my $self = shift; 772: my $level = shift; # 'warn' or 'error' 773: 774: return if(scalar(@_) == 0); # No message - return quickly
Mutants (Total: 1, Killed: 1, Survived: 0)
775: 776: # Only logging things at warning or higher 777: return if($syslog_values{$level} > $WARNING);
Mutants (Total: 3, Killed: 3, Survived: 0)
778: 779: my $warning; 780: 781: # Check if called as warn(warning => ...) or warn('plain', 'args') 782: my $params; 783: eval { $params = Params::Get::get_params('warning', @_) }; 784: if($@ || !$params || ref($params) ne 'HASH' || !exists($params->{warning})) {
Mutants (Total: 1, Killed: 1, Survived: 0)
785: # Plain list form â join directly 786: $warning = join('', grep { defined } @_); 787: return unless length($warning); 788: } else { 789: $warning = $params->{warning}; 790: return unless defined($warning); 791: if(ref($warning) eq 'ARRAY') {
Mutants (Total: 1, Killed: 1, Survived: 0)
792: $warning = join('', grep { defined } @{$warning}); 793: } 794: } 795: ●796 → 796 → 810●796 → 796 → 0 796: if($params && ref($params) eq 'HASH' && exists($params->{warning})) {
Mutants (Total: 1, Killed: 1, Survived: 0)
797: $warning = $params->{warning}; 798: return unless defined($warning); # warn(warning => undef) â no-op 799: if(ref($warning) eq 'ARRAY') {
Mutants (Total: 1, Killed: 1, Survived: 0)
800: # Given "warning => [ ref to array ]" 801: $warning = join('', grep { defined } @{$warning}); 802: } 803: } else { 804: # Plain list: warn('This ', 'is ', 'a ', 'list') 805: # Filter undefs and join 806: $warning = join('', grep { defined } @_); 807: return unless length($warning); # all-undef list â no-op 808: } 809: ●810 → 810 → 820●810 → 810 → 0 810: if($self eq __PACKAGE__) {
Mutants (Total: 1, Killed: 1, Survived: 0)
811: # If called from a class method, use croak/carp to warn 812: if($syslog_values{$level} <= $ERROR) {
Mutants (Total: 4, Killed: 4, Survived: 0)
813: Carp::croak($warning); 814: } 815: Carp::carp($warning); 816: return; 817: } 818: 819: # Log the warning message ●820 → 822 → 829●820 → 822 → 0 820: $self->_log($level, $warning); 821: 822: if($syslog_values{$level} <= $ERROR) {
Mutants (Total: 4, Killed: 4, Survived: 0)
823: # Fall back to croak if no logger or syslog is defined 824: if($self->{'croak_on_error'} || (!defined($self->{logger}) && (!defined($self->{array})))) {
Mutants (Total: 1, Killed: 1, Survived: 0)
825: Carp::croak($warning); 826: } 827: } 828: ●829 → 829 → 0 829: if($self->{'carp_on_warn'} || (!defined($self->{logger}) && (!defined($self->{array})))) {
Mutants (Total: 1, Killed: 1, Survived: 0)
830: # Fallback to Carp if no logger or syslog is defined 831: Carp::carp($warning); 832: } 833: } 834: 835: # Destructor to close syslog connection 836: sub DESTROY { ●837 → 839 → 0 837: my $self = $_[0]; 838: 839: if($self->{_syslog_opened}) {
Mutants (Total: 1, Killed: 1, Survived: 0)
840: Sys::Syslog::closelog(); 841: delete $self->{_syslog_opened}; 842: } 843: } 844: 845: =head1 AUTHOR 846: 847: Nigel Horne C<njh@nigelhorne.com> 848: 849: =head1 SEE ALSO 850: 851: =over 4 852: 853: =item * L<Test Dashboard|https://nigelhorne.github.io/Log-Abstraction/coverage/> 854: 855: =back 856: 857: =head1 SUPPORT 858: 859: This module is provided as-is without any warranty. 860: 861: Please report any bugs or feature requests to C<bug-log-abstraction at rt.cpan.org>, 862: or through the web interface at 863: L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Log-Abstraction>. 864: I will be notified, and then you'll 865: automatically be notified of progress on your bug as I make changes. 866: 867: You can find documentation for this module with the perldoc command. 868: 869: perldoc Log::Abstraction 870: 871: You can also look for information at: 872: 873: =over 4 874: 875: =item * MetaCPAN 876: 877: L<https://metacpan.org/dist/Log-Abstraction> 878: 879: =item * RT: CPAN's request tracker 880: 881: L<https://rt.cpan.org/NoAuth/Bugs.html?Dist=Log-Abstraction> 882: 883: =item * CPAN Testers' Matrix 884: 885: L<http://matrix.cpantesters.org/?dist=Log-Abstraction> 886: 887: =item * CPAN Testers Dependencies 888: 889: L<http://deps.cpantesters.org/?module=Log::Abstraction> 890: 891: =back 892: 893: =head1 COPYRIGHT AND LICENSE 894: 895: Copyright (C) 2025-2026 Nigel Horne 896: 897: Usage is subject to the GPL2 licence terms. 898: If you use it, 899: please let me know. 900: 901: =cut 902: 903: 1;