From Code to Community: Sponsoring The Perl and Raku Conference 2025 Learn more

use strict;
{ our $VERSION = '1.8.0'; }
use Carp;
use POSIX ();
our ($TRACE, $DEBUG, $INFO, $WARN, $ERROR, $FATAL, $OFF, $DEAD);
my ($_instance, %name_of, %format_for, %id_for);
my $LOGDIE_MESSAGE_ON_STDERR = 1;
sub import {
my ($exporter, @list) = @_;
my ($caller, $file, $line) = caller();
no strict 'refs';
if (grep { $_ eq ':full_or_fake' } @list) {
@list = grep { $_ ne ':full_or_fake' } @list;
my $sue = 'use Log::Log4perl (@list)';
eval "
package $caller;
$sue;
1;
" and return;
unshift @list, ':fake';
} ## end if (grep { $_ eq ':full_or_fake'...})
my (%done, $level_set);
ITEM:
for my $item (@list) {
next ITEM if $done{$item};
$done{$item} = 1;
if ($item =~ /^[a-zA-Z]/mxs) {
*{$caller . '::' . $item} = \&{$exporter . '::' . $item};
}
elsif ($item eq ':levels') {
for my $level (qw( TRACE DEBUG INFO WARN ERROR FATAL OFF DEAD )) {
*{$caller . '::' . $level} = \${$exporter . '::' . $level};
}
}
elsif ($item eq ':subs') {
push @list, qw(
ALWAYS TRACE DEBUG INFO WARN ERROR FATAL
LOGWARN LOGDIE LOGEXIT LOGCARP LOGCLUCK LOGCROAK LOGCONFESS
get_logger
);
} ## end elsif ($item eq ':subs')
elsif ($item =~ /\A : (mimic | mask | fake) \z/mxs) {
# module name as a string below to trick Module::ScanDeps
if (!'Log::Log4perl'->can('easy_init')) {
$INC{'Log/Log4perl.pm'} = __FILE__;
*Log::Log4perl::import = sub { };
*Log::Log4perl::easy_init = sub {
my ($pack, $conf) = @_;
if (ref $conf) {
$_instance = __PACKAGE__->new($conf);
$_instance->level($conf->{level})
if exists $conf->{level};
$_instance->format($conf->{format})
if exists $conf->{format};
$_instance->format($conf->{layout})
if exists $conf->{layout};
$_instance->filter($conf->{filter})
if exists $conf->{filter};
} ## end if (ref $conf)
elsif (defined $conf) {
$_instance->level($conf);
}
};
} ## end if (!'Log::Log4perl'->...)
} ## end elsif ($item =~ /\A : (mimic | mask | fake) \z/mxs)
elsif ($item eq ':easy') {
push @list, qw( :levels :subs :fake );
}
elsif (lc($item) eq ':dead_if_first') {
get_logger()->_set_level_if_first($DEAD);
$level_set = 1;
}
elsif (lc($item) eq ':no_extra_logdie_message') {
$LOGDIE_MESSAGE_ON_STDERR = 0;
}
} ## end ITEM: for my $item (@list)
if (!$level_set) {
my $logger = get_logger();
$logger->_set_level_if_first($INFO);
$logger->level($logger->level());
}
return;
} ## end sub import
sub new {
my $package = shift;
my %args = ref($_[0]) ? %{$_[0]} : @_;
$args{format} = $args{layout} if exists $args{layout};
my $channels_input = [fh => \*STDERR];
if (exists $args{channels}) {
$channels_input = $args{channels};
}
else {
for my $key (qw< file_append file_create file_insecure file fh >) {
next unless exists $args{$key};
$channels_input = [$key => $args{$key}];
last;
}
} ## end else [ if (exists $args{channels...})]
my $channels = build_channels($channels_input);
$channels = $channels->[0] if @$channels == 1; # remove outer shell
my $self = bless {
fh => $channels,
level => $INFO,
}, $package;
for my $accessor (qw( level fh format filter )) {
next unless defined $args{$accessor};
$self->$accessor($args{$accessor});
}
$self->format('[%d] [%5p] %m%n') unless exists $self->{format};
if (exists $args{loglocal}) {
my $local = $args{loglocal};
$self->loglocal($_, $local->{$_}) for keys %$local;
}
return $self;
} ## end sub new
sub build_channels {
my @pairs = (@_ && ref($_[0])) ? @{$_[0]} : @_;
my @channels;
while (@pairs) {
my ($key, $value) = splice @pairs, 0, 2;
# some initial validation
croak "build_channels(): undefined key in list"
unless defined $key;
croak "build_channels(): undefined value for key $key"
unless defined $value;
# analyze the key-value pair and set the channel accordingly
my ($channel, $set_autoflush);
if ($key =~ m{\A(?: fh | sub | code | channel )\z}mxs) {
$channel = $value;
}
elsif ($key eq 'file_append') {
open $channel, '>>', $value
or croak "open('$value') for appending: $!";
$set_autoflush = 1;
}
elsif ($key eq 'file_create') {
open $channel, '>', $value
or croak "open('$value') for creating: $!";
$set_autoflush = 1;
}
elsif ($key =~ m{\A file (?: _insecure )? \z}mxs) {
open $channel, $value
or croak "open('$value'): $!";
$set_autoflush = 1;
}
else {
croak "unsupported channel key '$key'";
}
# autoflush new filehandle if applicable
if ($set_autoflush) {
my $previous = select($channel);
$|++;
select($previous);
}
# record the channel, on to the next
push @channels, $channel;
} ## end while (@pairs)
return \@channels;
} ## end sub build_channels
sub get_logger { return $_instance ||= __PACKAGE__->new(); }
sub LOGLEVEL { return get_logger()->level(@_); }
sub LEVELID_FOR {
my $level = shift;
return $id_for{$level} if exists $id_for{$level};
return;
} ## end sub LEVELID_FOR
sub LEVELNAME_FOR {
my $id = shift;
return $name_of{$id} if exists $name_of{$id};
return $id if exists $id_for{$id};
return;
} ## end sub LEVELNAME_FOR
sub loglocal {
my $self = shift;
my $key = shift;
my $retval = delete $self->{loglocal}{$key};
$self->{loglocal}{$key} = shift if @_;
return $retval;
} ## end sub loglocal
sub LOGLOCAL { return get_logger->loglocal(@_) }
sub filter {
my $self = shift;
$self->{filter} = shift if @_;
return $self->{filter};
}
sub FILTER { return get_logger->filter(@_) }
sub format {
my $self = shift;
if (@_) {
$self->{format} = shift;
$self->{args} = \my @args;
my $replace = sub {
if (defined $_[2]) { # op with options
my ($num, $opts, $op) = @_[0 .. 2];
push @args, [$op, $opts];
return "%$num$format_for{$op}[0]";
}
if (defined $_[4]) { # op without options
my ($num, $op) = @_[3, 4];
push @args, [$op];
return "%$num$format_for{$op}[0]";
}
# not an op
my $char = ((!defined($_[5])) || ($_[5] eq '%')) ? '' : $_[5];
return '%%' . $char; # keep the percent AND the char, if any
};
# transform into real format
my ($with_options, $standalone) = ('', '');
for my $key (keys %format_for) {
my $type = $format_for{$key}[2] || '';
$with_options .= $key if $type;
$standalone .= $key if $type ne 'required';
}
# quotemeta or land on impossible character class if empty
$_ = length($_) ? quotemeta($_) : '^\\w\\W'
for ($with_options, $standalone);
$self->{format} =~ s<
% # format marker
(?:
(?: # something with options
( -? \d* (?:\.\d+)? ) # number
( (?:\{ .*? \}) ) # options
([$with_options]) # specifier
)
| (?:
( -? \d* (?:\.\d+)? ) # number
([$standalone]) # specifier
)
| (.) # just any char
| \z # just the end of it!
)
>
{
$replace->($1, $2, $3, $4, $5, $6);
}gsmex;
} ## end if (@_)
return $self->{format};
} ## end sub format
*layout = \&format;
sub emit_log {
my ($self, $message) = @_;
my $fh = $self->{fh};
for my $channel ((ref($fh) eq 'ARRAY') ? (@$fh) : ($fh)) {
(ref($channel) eq 'CODE')
? $channel->($message, $self)
: print {$channel} $message;
}
return $message;
} ## end sub emit_log
sub log {
my $self = shift;
return if $self->{level} == $DEAD;
my $level = shift;
return if $level > $self->{level};
my %data_for = (
level => $level,
message => \@_,
(exists($self->{loglocal}) ? (loglocal => $self->{loglocal}) : ()),
);
my $message = sprintf $self->{format},
map { $format_for{$_->[0]}[1]->(\%data_for, @$_); } @{$self->{args}};
$message = $self->{filter}->($message) if $self->{filter};
return $self->emit_log($message);
} ## end sub log
sub ALWAYS { return $_instance->log($OFF, @_); }
sub _exit {
my $self = shift || $_instance;
exit $self->{logexit_code} if defined $self->{logexit_code};
exit $Log::Log4perl::LOGEXIT_CODE
if defined $Log::Log4perl::LOGEXIT_CODE;
exit 1;
} ## end sub _exit
sub logwarn {
my $self = shift;
my @message;
@message = __expand_message_list({message => \@_})
if $self->is_warn() || $LOGDIE_MESSAGE_ON_STDERR;
$self->warn(@message);
if ($LOGDIE_MESSAGE_ON_STDERR) {
# default warning when nothing is passed to warn
push @message, "Warning: something's wrong" unless @message;
# add 'at <file> line <line>' unless argument ends in "\n";
my (undef, $file, $line) = caller(1);
push @message, sprintf " at %s line %d.\n", $file, $line
if substr($message[-1], -1, 1) ne "\n";
# go for it!
CORE::warn(@message);
}
return
} ## end sub logwarn
sub logdie {
my $self = shift;
my @message;
@message = __expand_message_list({message => \@_})
if $self->is_fatal() || $LOGDIE_MESSAGE_ON_STDERR;
$self->fatal(@message);
if ($LOGDIE_MESSAGE_ON_STDERR) {
# default die message when nothing is passed to die
push @message, "Died" unless @message;
# add 'at <file> line <line>' unless argument ends in "\n";
my (undef, $file, $line) = caller(1);
push @message, sprintf " at %s line %d.\n", $file, $line
if substr($message[-1], -1, 1) ne "\n";
# go for it!
CORE::die(@message);
}
$self->_exit();
} ## end sub logdie
sub logexit {
my $self = shift;
$self->fatal(@_);
$self->_exit();
}
sub _carpstuff {
my $self = shift;
my $renderer = shift;
my $emitter = shift;
my $log_level = shift;
my $emit_log = $self->can("is_$log_level")->($self);
require Carp;
local $Carp::Internal{'' . __PACKAGE__} = 1;
local $Carp::CarpLevel = $Carp::CarpLevel + 2;
my @message;
@message = __expand_message_list({message => \@_})
if $emit_log || $LOGDIE_MESSAGE_ON_STDERR;
if ($emit_log) { # avoid unless we're allowed to emit
my $message = Carp->can($renderer)->(@message);
my $method = $self->can($log_level);
$self->$method($_) for split m{\n}mxs, $message;
}
if ($LOGDIE_MESSAGE_ON_STDERR) {
Carp->can($emitter)->(@message);
}
return;
}
sub logcarp {
my $self = shift;
return $self->_carpstuff(qw< shortmess carp warn >, @_);
} ## end sub logcarp
sub logcluck {
my $self = shift;
return $self->_carpstuff(qw< longmess cluck warn >, @_);
} ## end sub logcluck
sub logcroak {
my $self = shift;
$self->_carpstuff(qw< shortmess croak fatal >, @_);
$self->_exit();
} ## end sub logcroak
sub logconfess {
my $self = shift;
$self->_carpstuff(qw< longmess confess fatal >, @_);
$self->_exit();
} ## end sub logconfess
sub level {
my $self = shift;
$self = $_instance unless ref $self;
if (@_) {
my $level = shift;
return unless exists $id_for{$level};
$self->{level} = $id_for{$level};
$self->{_count}++;
} ## end if (@_)
return $self->{level};
} ## end sub level
sub _set_level_if_first {
my ($self, $level) = @_;
if (!$self->{_count}) {
$self->level($level);
delete $self->{_count};
}
return;
} ## end sub _set_level_if_first
sub __expand_message_list {
join(
(defined $, ? $, : ''),
map { ref($_) eq 'CODE' ? $_->() : $_; } @{shift->{message}}
);
}
BEGIN {
# Time tracking's start time. Used to be tied to $^T but Log::Log4perl
# does differently and uses Time::HiRes if available
my $has_time_hires;
my $gtod = sub { return (time(), 0) };
eval {
require Time::HiRes;
$has_time_hires = 1;
$gtod = \&Time::HiRes::gettimeofday;
};
my $start_time = [$gtod->()];
# For supporting %R
my $last_log = $start_time;
# Timezones are... differently supported somewhere
my $strftime_has_tz_offset =
POSIX::strftime('%z', localtime()) =~ m<\A [-+] \d{4} \z>mxs;
if (! $strftime_has_tz_offset) {
require Time::Local;
}
{ # alias to the one in Log::Log4perl, for easier switching towards that
no strict 'refs';
*caller_depth = *Log::Log4perl::caller_depth;
}
our $caller_depth;
$caller_depth ||= 0;
# %format_for idea from Log::Tiny by J. M. Adler
%format_for = ( # specifiers according to Log::Log4perl
c => [s => sub { 'main' }],
C => [
s => sub {
my ($internal_package) = caller 0;
my $max_i = 5;
my $i = 1;
my $package;
while ($i <= $max_i) {
($package) = caller $i;
return '*undef*' unless defined $package;
last if $package ne $internal_package;
++$i;
} ## end while ($i <= 4)
return '*undef' if $i > $max_i;
($package) = caller($i += $caller_depth) if $caller_depth;
return $package;
},
],
d => [
s => sub {
my ($epoch) = @{shift->{tod} ||= [$gtod->()]};
return POSIX::strftime('%Y/%m/%d %H:%M:%S', localtime($epoch));
},
],
D => [
s => sub {
my ($data, $op, $options) = @_;
$options = '{}' unless defined $options;
$options = substr $options, 1, length($options) - 2;
my %flag_for = map { $_ => 1 } split /\s*,\s*/, lc($options);
my ($s, $u) = @{$data->{tod} ||= [$gtod->()]};
$u = substr "000000$u", -6, 6; # padding left with 0
return POSIX::strftime("%Y-%m-%d %H:%M:%S.$u+0000", gmtime $s)
if $flag_for{utc};
my @localtime = localtime $s;
return POSIX::strftime("%Y-%m-%d %H:%M:%S.$u%z", @localtime)
if $strftime_has_tz_offset;
my $sign = '+';
my $offset = Time::Local::timegm(@localtime) - $s;
($sign, $offset) = ('-', -$offset) if $offset < 0;
my $z = sprintf '%s%02d%02d',
$sign, # sign
int($offset / 3600), # hours
(int($offset / 60) % 60); # minutes
return POSIX::strftime("%Y-%m-%d %H:%M:%S.$u$z", @localtime);
},
'optional'
],
e => [
s => sub {
my ($data, $op, $options) = @_;
$data->{tod} ||= [$gtod->()]; # guarantee consistency here
my $local = $data->{loglocal} or return '';
my $key = substr $options, 1, length($options) - 2;
return '' unless exists $local->{$key};
my $target = $local->{$key};
return '' unless defined $target;
my $reft = ref $target or return $target;
return '' unless $reft eq 'CODE';
return $target->($data, $op, $options);
},
'required',
],
F => [
s => sub {
my ($internal_package) = caller 0;
my $i = 1;
my ($package, $file);
while ($i <= 4) {
($package, $file) = caller $i;
return '*undef*' unless defined $package;
last if $package ne $internal_package;
++$i;
} ## end while ($i <= 4)
return '*undef' if $i > 4;
(undef, $file) = caller($i += $caller_depth) if $caller_depth;
return $file;
},
],
H => [
s => sub {
eval { require Sys::Hostname; Sys::Hostname::hostname() }
|| '';
},
],
l => [
s => sub {
my ($internal_package) = caller 0;
my $i = 1;
my ($package, $filename, $line);
while ($i <= 4) {
($package, $filename, $line) = caller $i;
return '*undef*' unless defined $package;
last if $package ne $internal_package;
++$i;
} ## end while ($i <= 4)
return '*undef' if $i > 4;
(undef, $filename, $line) = caller($i += $caller_depth)
if $caller_depth;
my (undef, undef, undef, $subroutine) = caller($i + 1);
$subroutine = "main::" unless defined $subroutine;
return sprintf '%s %s (%d)', $subroutine, $filename, $line;
},
],
L => [
d => sub {
my ($internal_package) = caller 0;
my $i = 1;
my ($package, $line);
while ($i <= 4) {
($package, undef, $line) = caller $i;
return -1 unless defined $package;
last if $package ne $internal_package;
++$i;
} ## end while ($i <= 4)
return -1 if $i > 4;
(undef, undef, $line) = caller($i += $caller_depth)
if $caller_depth;
return $line;
},
],
m => [s => \&__expand_message_list,],
M => [
s => sub {
my ($internal_package) = caller 0;
my $max_i = 5;
my $i = 1;
while ($i <= $max_i) {
my ($package) = caller $i;
return '*undef*' unless defined $package;
last if $package ne $internal_package;
++$i;
} ## end while ($i <= 4)
return '*undef' if $i > $max_i;
$i += $caller_depth if $caller_depth;
my (undef, undef, undef, $subroutine) = caller($i + 1);
$subroutine = "main::" unless defined $subroutine;
return $subroutine;
},
],
n => [s => sub { "\n" },],
p => [s => sub { $name_of{shift->{level}} },],
P => [d => sub { $$ },],
r => [
d => sub {
my ($s, $u) = @{shift->{tod} ||= [$gtod->()]};
$s -= $start_time->[0];
my $m = int(($u - $start_time->[1]) / 1000);
($s, $m) = ($s - 1, $m + 1000) if $m < 0;
return $m + 1000 * $s;
},
],
R => [
d => sub {
my ($sx, $ux) = @{shift->{tod} ||= [$gtod->()]};
my $s = $sx - $last_log->[0];
my $m = int(($ux - $last_log->[1]) / 1000);
($s, $m) = ($s - 1, $m + 1000) if $m < 0;
$last_log = [$sx, $ux];
return $m + 1000 * $s;
},
],
T => [
s => sub {
my ($internal_package) = caller 0;
my $level = 1;
while ($level <= 4) {
my ($package) = caller $level;
return '*undef*' unless defined $package;
last if $package ne $internal_package;
++$level;
} ## end while ($level <= 4)
return '*undef' if $level > 4;
# usage of Carp::longmess() and substitutions is mostly copied
# from Log::Log4perl for better alignment and easier
# transition to the "bigger" module
local $Carp::CarpLevel =
$Carp::CarpLevel + $level + $caller_depth;
chomp(my $longmess = Carp::longmess());
$longmess =~ s{(?:\A\s*at.*?\n|^\s*)}{}mxsg;
$longmess =~ s{\n}{, }g;
return $longmess;
},
],
);
# From now on we're going to play with GLOBs...
no strict 'refs';
for my $name (qw( FATAL ERROR WARN INFO DEBUG TRACE )) {
# create the ->level methods
*{__PACKAGE__ . '::' . lc($name)} = sub {
my $self = shift;
return $self->log($$name, @_);
};
# create ->is_level and ->isLevelEnabled methods as well
*{__PACKAGE__ . '::is' . ucfirst(lc($name)) . 'Enabled'} =
*{__PACKAGE__ . '::is_' . lc($name)} = sub {
return 0 if $_[0]->{level} == $DEAD || $$name > $_[0]->{level};
return 1;
};
} ## end for my $name (qw( FATAL ERROR WARN INFO DEBUG TRACE ))
for my $name (
qw(
FATAL ERROR WARN INFO DEBUG TRACE
LOGWARN LOGDIE LOGEXIT
LOGCARP LOGCLUCK LOGCROAK LOGCONFESS
)
)
{
*{__PACKAGE__ . '::' . $name} = sub {
$_instance->can(lc $name)->($_instance, @_);
};
} ## end for my $name (qw( FATAL ERROR WARN INFO DEBUG TRACE...))
for my $accessor (qw( fh logexit_code )) {
*{__PACKAGE__ . '::' . $accessor} = sub {
my $self = shift;
$self = $_instance unless ref $self;
$self->{$accessor} = shift if @_;
return $self->{$accessor};
};
} ## end for my $accessor (qw( fh logexit_code ))
my $index = -1;
for my $name (qw( DEAD OFF FATAL ERROR WARN INFO DEBUG TRACE )) {
$name_of{$$name = $index} = $name;
$id_for{$name} = $index;
$id_for{$index} = $index;
++$index;
} ## end for my $name (qw( DEAD OFF FATAL ERROR WARN INFO DEBUG TRACE ))
get_logger(); # initialises $_instance;
} ## end BEGIN
1; # Magic true value required at end of module