File Coverage

File:blib/lib/Log/Any/Adapter/Abstraction.pm
Coverage:100.0%

linestmtbrancondsubtimecode
1package 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
9use Carp ();
10use Log::Abstraction;
11use Scalar::Util ();
12
13our $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# ---------------------------------------------------------------------------
153my %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# ---------------------------------------------------------------------------
177sub 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# ---------------------------------------------------------------------------
324for 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# ---------------------------------------------------------------------------
350sub 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# ---------------------------------------------------------------------------
368for 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
4441;