| File: | blib/lib/Log/Any/Adapter/Abstraction.pm |
| Coverage: | 100.0% |
| line | stmt | bran | cond | sub | time | code |
|---|---|---|---|---|---|---|
| 1 | package Log::Any::Adapter::Abstraction; | |||||
| 2 | ||||||
| 3 | # Route Log::Any log calls through a Log::Abstraction backend | |||||
| 4 | 3 3 3 | 45047 2 25 | use strict; | |||
| 5 | 3 3 3 | 3 1 62 | use warnings; | |||
| 6 | ||||||
| 7 | 3 3 3 | 2 3 9 | use parent 'Log::Any::Adapter::Base'; | |||
| 8 | ||||||
| 9 | use Carp (); | |||||
| 10 | use Log::Abstraction; | |||||
| 11 | use Scalar::Util (); | |||||
| 12 | ||||||
| 13 | our $VERSION = '0.40'; | |||||
| 14 | ||||||
| 15 - 147 | =head1 NAME
Log::Any::Adapter::Abstraction - Log::Any adapter backed by Log::Abstraction
=head1 VERSION
0.40
=head1 SYNOPSIS
use Log::Any::Adapter;
use Log::Abstraction;
my @messages;
# Option A: pass a pre-built Log::Abstraction instance
my $logger = Log::Abstraction->new(logger => \@messages, level => 'debug');
Log::Any::Adapter->set('Abstraction', instance => $logger);
# Option B: let the adapter create a Log::Abstraction for you
Log::Any::Adapter->set('Abstraction', level => 'debug', logger => \@messages);
# Any module that uses Log::Any will now route through Log::Abstraction
use Log::Any '$log';
$log->info('Hello world');
=head1 DESCRIPTION
C<Log::Any::Adapter::Abstraction> is a L<Log::Any> adapter that routes
messages from any module using L<Log::Any> through a L<Log::Abstraction>
backend. Configure a single C<Log::Abstraction> instance (with file, syslog,
email, array, or code-ref backends) and all C<Log::Any>-using CPAN modules
automatically send their output there.
=head2 Level mapping
Each of Log::Any's nine severity levels has a Log::Abstraction method of
the same name, except C<warning>:
Log::Any level Log::Abstraction method
--------------- -----------------------
trace trace
debug debug
info info
notice notice
warning warn
error error
critical critical
alert alert
emergency emergency
Before version 0.36, C<critical>, C<alert> and C<emergency> were all sent
to C<error()>.
=head1 METHODS
=head2 init
Called automatically by C<Log::Any::Adapter::Base::new()>. Initialises the
internal C<Log::Abstraction> backend.
=head3 Arguments (passed as key=E<gt>value pairs to C<Log::Any::Adapter-E<gt>set>)
=over 4
=item * C<instance>
An existing C<Log::Abstraction> object. When supplied, the adapter wraps it
directly without creating a new instance. Anything other than a
C<Log::Abstraction> object (or subclass) is a fatal error.
=item * Any C<Log::Abstraction-E<gt>new()> argument
C<logger>, C<level>, C<file>, C<fd>, C<array>, C<format>, C<ctx>,
C<script_name>, C<verbose>, C<carp_on_warn>, C<croak_on_error>,
C<config_file>, C<max_messages>, C<redact>, C<timestamp_format>,
C<timestamp_precision>, C<utc>, C<rotate_size>, C<rotate_interval>,
C<rotate_keep>, C<reopen_interval>. Used to build a fresh C<Log::Abstraction>
instance when C<instance> is not supplied.
=back
=head3 Example
Log::Any::Adapter->set('Abstraction',
level => 'debug',
format => 'json',
file => '/var/log/myapp.log',
);
=head3 API SPECIFICATION
=head4 Input
{
instance => { type => OBJECT, isa => 'Log::Abstraction', optional => 1 },
logger => { optional => 1 },
level => { type => SCALAR, optional => 1 },
file => { type => SCALAR, optional => 1 },
fd => { optional => 1 },
array => { type => ARRAYREF, optional => 1 },
format => { type => SCALAR, optional => 1 },
ctx => { optional => 1 },
script_name => { type => SCALAR, optional => 1 },
verbose => { type => BOOLEAN, optional => 1 },
carp_on_warn => { type => BOOLEAN, optional => 1 },
croak_on_error => { type => BOOLEAN, optional => 1 },
config_file => { type => SCALAR, optional => 1 },
max_messages => { type => INTEGER, min => 0, optional => 1 },
redact => { optional => 1 },
timestamp_format => { type => SCALAR, optional => 1 },
timestamp_precision => { type => INTEGER, min => 0, max => 9, optional => 1 },
utc => { type => BOOLEAN, optional => 1 },
rotate_size => { type => SCALAR, optional => 1 },
rotate_interval => { type => SCALAR, optional => 1 },
rotate_keep => { type => INTEGER, min => 0, optional => 1 },
reopen_interval => { type => INTEGER, min => -1, optional => 1 },
}
=head4 Output
{ type => 'object', class => 'Log::Any::Adapter::Abstraction' }
=head3 MESSAGES
Error Meaning / Action
---------------------------------------- -----------------------------------------
"...: instance must be a Log::Abstraction 'instance' was given something other
object" than a Log::Abstraction object.
(any Log::Abstraction croak) The supplied constructor args are invalid.
See Log::Abstraction for detail.
=cut | |||||
| 148 | ||||||
| 149 | # --------------------------------------------------------------------------- | |||||
| 150 | # Map Log::Any level names to Log::Abstraction method names, which are also | |||||
| 151 | # its level names (for the is_* threshold checks). Only 'warning' differs. | |||||
| 152 | # --------------------------------------------------------------------------- | |||||
| 153 | my %LA_TO_METHOD = ( | |||||
| 154 | trace => 'trace', | |||||
| 155 | debug => 'debug', | |||||
| 156 | info => 'info', | |||||
| 157 | notice => 'notice', | |||||
| 158 | warning => 'warn', | |||||
| 159 | error => 'error', | |||||
| 160 | critical => 'critical', | |||||
| 161 | alert => 'alert', | |||||
| 162 | emergency => 'emergency', | |||||
| 163 | ); | |||||
| 164 | ||||||
| 165 | # --------------------------------------------------------------------------- | |||||
| 166 | # init -- create or store the Log::Abstraction instance backing this adapter | |||||
| 167 | # | |||||
| 168 | # Purpose: Called by Log::Any::Adapter::Base::new() after blessing the | |||||
| 169 | # adapter hash. Either wraps an existing Log::Abstraction object | |||||
| 170 | # (from the 'instance' key) or builds a new one from the remaining | |||||
| 171 | # adapter constructor arguments. | |||||
| 172 | # Entry: $self -- adapter hash blessed into this class. | |||||
| 173 | # Exit: Sets $self->{_logger}; returns nothing. | |||||
| 174 | # Side effects: May construct a Log::Abstraction object (which may open | |||||
| 175 | # file handles, start syslog, etc.). | |||||
| 176 | # --------------------------------------------------------------------------- | |||||
| 177 | sub init { | |||||
| 178 | my $self = shift; | |||||
| 179 | ||||||
| 180 | # Reuse a caller-supplied Log::Abstraction instance if one was provided; | |||||
| 181 | # anything else is a mistake, not a request for a default logger | |||||
| 182 | if(defined(my $inst = $self->{instance})) { | |||||
| 183 | if(!Scalar::Util::blessed($inst) || !$inst->isa('Log::Abstraction')) { | |||||
| 184 | Carp::croak(__PACKAGE__, ': instance must be a Log::Abstraction object'); | |||||
| 185 | } | |||||
| 186 | $self->{_logger} = $inst; | |||||
| 187 | return; | |||||
| 188 | } | |||||
| 189 | ||||||
| 190 | # Build a fresh Log::Abstraction from the constructor key=value pairs | |||||
| 191 | my %new_args; | |||||
| 192 | for my $key (qw( | |||||
| 193 | logger level file fd array format ctx script_name verbose | |||||
| 194 | carp_on_warn croak_on_error config_file max_messages redact | |||||
| 195 | timestamp_format timestamp_precision utc | |||||
| 196 | rotate_size rotate_interval rotate_keep reopen_interval | |||||
| 197 | )) { | |||||
| 198 | $new_args{$key} = $self->{$key} if exists $self->{$key}; | |||||
| 199 | } | |||||
| 200 | $self->{_logger} = Log::Abstraction->new(%new_args); | |||||
| 201 | } | |||||
| 202 | ||||||
| 203 - 316 | =head2 Logging methods
=over 4
=item trace
=item debug
=item info
=item notice
=item warning
=item error
=item critical
=item alert
=item emergency
=back
$adapter->info($message);
Called by L<Log::Any> with the formatted message; each sends it to the
matching L<Log::Abstraction> method (see L</Level mapping>). These methods
are generated when the module loads. A croak from Log::Abstraction (e.g.
C<croak_on_error>) is turned into a C<Carp::carp>, so logging never dies.
=head3 API SPECIFICATION
=head4 Input
{ message => { type => 'string' } }
=head4 Output
{ type => 'undef' }
=head2 structured
$adapter->structured($level, $category, @parts, \%fields);
Called by L<Log::Any> in place of the logging methods, with the message
parts and, when there are any, a final hashref of structured fields: the
proxy's C<context> merged with a hashref passed as the last argument of the
log call. The parts are joined with a space and sent, with the fields, to
the matching L<Log::Abstraction> method, so
$log->info('login', { user_id => 42 });
reaches Log::Abstraction as C<info('login', { user_id =E<gt> 42 })>. See
L<Log::Abstraction/Structured fields> for where the fields go. As with the
logging methods, a croak is turned into a C<Carp::carp>.
=head3 API SPECIFICATION
=head4 Input
{
level => { type => 'string' },
category => { type => 'string' },
parts => { type => 'array' },
fields => { type => 'hashref', optional => 1 },
}
=head4 Output
{ type => 'undef' }
=head2 Detection methods
=over 4
=item is_trace
=item is_debug
=item is_info
=item is_notice
=item is_warning
=item is_error
=item is_critical
=item is_alert
=item is_emergency
=back
if($adapter->is_debug()) { ... }
Return 1 if a message at that level would be logged by the wrapped
L<Log::Abstraction> instance's level threshold, otherwise 0. Each calls the
L<Log::Abstraction> method of the same name (C<is_warning> calls
C<is_warn>). As in Log::Abstraction, C<is_trace> equals C<is_debug>.
=head3 API SPECIFICATION
=head4 Input
{} (no arguments)
=head4 Output
{ type => 'boolean' }
=cut | |||||
| 317 | ||||||
| 318 | # --------------------------------------------------------------------------- | |||||
| 319 | # Build logging methods for every Log::Any level name. | |||||
| 320 | # Each method delegates to the corresponding Log::Abstraction dispatch method. | |||||
| 321 | # Logging through Log::Any must never kill the module doing the logging, so | |||||
| 322 | # a croak from Log::Abstraction (e.g. croak_on_error) is turned into a carp. | |||||
| 323 | # --------------------------------------------------------------------------- | |||||
| 324 | for my $la_level (keys %LA_TO_METHOD) { | |||||
| 325 | my $method = $LA_TO_METHOD{$la_level}; | |||||
| 326 | no strict 'refs'; | |||||
| 327 | *{$la_level} = sub { | |||||
| 328 | my ($self, $msg) = @_; | |||||
| 329 | # The eval would otherwise reset the calling module's $@ | |||||
| 330 | local ($@, $!); | |||||
| 331 | eval { $self->{_logger}->$method($msg); 1 } or Carp::carp($@); | |||||
| 332 | return; | |||||
| 333 | }; | |||||
| 334 | } | |||||
| 335 | ||||||
| 336 | # --------------------------------------------------------------------------- | |||||
| 337 | # structured -- receive a log call with its structured fields from Log::Any | |||||
| 338 | # | |||||
| 339 | # Purpose: Log::Any calls this instead of the per-level methods when the | |||||
| 340 | # adapter can('structured'), passing the parts as given and the | |||||
| 341 | # merged context and fields as a final hashref (only when there are | |||||
| 342 | # any), so the fields reach Log::Abstraction as data, not text. | |||||
| 343 | # Entry: $self -- the adapter. | |||||
| 344 | # $level -- a Log::Any level name. | |||||
| 345 | # $category -- the Log::Any category (unused). | |||||
| 346 | # @parts -- message parts, then optionally a hashref of fields. | |||||
| 347 | # Exit: Returns nothing. | |||||
| 348 | # Side effects: Logs through the wrapped Log::Abstraction instance. | |||||
| 349 | # --------------------------------------------------------------------------- | |||||
| 350 | sub structured { | |||||
| 351 | my ($self, $level, $category, @parts) = @_; | |||||
| 352 | ||||||
| 353 | my $fields = (@parts && (ref($parts[-1]) eq 'HASH')) ? pop(@parts) : undef; | |||||
| 354 | my $msg = join(' ', grep { defined($_) && length($_) } @parts); | |||||
| 355 | my $method = $LA_TO_METHOD{$level} or return; | |||||
| 356 | ||||||
| 357 | # The eval would otherwise reset the calling module's $@ | |||||
| 358 | local ($@, $!); | |||||
| 359 | eval { $self->{_logger}->$method($msg, ($fields ? $fields : ())); 1 } or Carp::carp($@); | |||||
| 360 | return; | |||||
| 361 | } | |||||
| 362 | ||||||
| 363 | # --------------------------------------------------------------------------- | |||||
| 364 | # Build is_* detection methods for every Log::Any level name. | |||||
| 365 | # Each delegates to the matching Log::Abstraction is_* method (is_warning to | |||||
| 366 | # is_warn), which returns 1 when messages at that level would not be dropped. | |||||
| 367 | # --------------------------------------------------------------------------- | |||||
| 368 | for my $la_level (keys %LA_TO_METHOD) { | |||||
| 369 | my $detector = "is_$LA_TO_METHOD{$la_level}"; | |||||
| 370 | no strict 'refs'; | |||||
| 371 | *{"is_$la_level"} = sub { | |||||
| 372 | my $self = $_[0]; | |||||
| 373 | return $self->{_logger}->$detector(); | |||||
| 374 | }; | |||||
| 375 | } | |||||
| 376 | ||||||
| 377 - 442 | =head1 LIMITATIONS
=over 4
=item B<Message parts are joined with a space>
In structured mode Log::Any passes the message parts, including any
C<prefix>, separately; L</structured> joins them with a single space, so
a prefix is followed by a space.
=item B<Filters disable structured fields>
Log::Any doesn't use L</structured> when the proxy has a C<filter>; the
fields then arrive as text appended to the message, as Log::Any formats them.
=item B<Logging never dies>
A croak from the wrapped C<Log::Abstraction> instance, such as from
C<croak_on_error>, is turned into a C<Carp::carp> so that a module logging
through Log::Any is never killed by its log call.
=item B<Log::Any as optional, not required>
This adapter requires L<Log::Any> at runtime, but Log::Any is listed as a
C<recommends> dependency rather than C<requires>. CPAN clients that do not
install recommended modules will allow this adapter to be installed but not
loaded. Install Log::Any explicitly if you intend to use this adapter.
=back
=head1 AUTHOR
Nigel Horne C<njh@nigelhorne.com>
=head1 SEE ALSO
L<Log::Abstraction>, L<Log::Any>, L<Log::Any::Adapter>
=encoding utf-8
=head1 FORMAL SPECIFICATION
=head2 init
ââ AdapterState ââââââââââââââââââââââââââââââââââââââââââââââ
â _logger : Log::Abstraction
ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
ââ Init ââââââââââââââââââââââââââââââââââââââââââââââââââââââ
â args? : { instance? : Log::Abstraction | logger_args }
â result! : AdapterState
ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
â args?.instance â â
â§ isa(args?.instance, Log::Abstraction)
â â¹ result!._logger = args?.instance
â args?.instance = â
â â¹ result!._logger = Log::Abstraction::new(logger_args)
ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
=head1 COPYRIGHT AND LICENSE
Copyright (C) 2026 Nigel Horne
Usage is subject to the GPL2 licence terms.
If you use it,
please let me know.
=cut | |||||
| 443 | ||||||
| 444 | 1; | |||||