lib/Log/Abstraction.pm

Structural Coverage (Approximate)

TER1 (Statement): 98.45%
TER2 (Branch): 90.45%
TER3 (LCSAJ): 97.6% (40/41)
Approximate LCSAJ segments: 179

LCSAJ Legend

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.

Mutant Testing Legend

Survived (tests missed this) Killed (tests detected this) No mutation
    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 → 205193 → 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 → 225205 → 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) {

Mutants (Total: 1, Killed: 0, Survived: 1)
218: $args{'array'} = $array; 219: } 220: } else { 221: croak("$class: Can't load configuration from ", $args{'config_file'}); 222: } 223: } 224: 225 → 225 → 243225 → 225 → 0 225: if(!defined($class)) {

Mutants (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 → 252243 → 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 → 267252 → 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 → 282267 → 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 → 355349 → 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 → 359355 → 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 → 364359 → 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 → 368364 → 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 → 381368 → 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 → 559381 → 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 → 592559 → 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 → 632623 → 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 → 796771 → 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 → 810796 → 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 → 820810 → 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 → 829820 → 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;