Name

Test::Log::Abstraction - Capture log output in tests and assert on it

Version

0.002.0

Synopsis

Check What Your Code Logged

use Test::Most;
use Test::Log::Abstraction;

# Give the test logger to the code that you are testing
my $logger = Test::Log::Abstraction->new();
my $obj = Some::Class->new(logger => $logger);

$obj->do_something();

# Each of these is one TAP test
$logger->like(qr/updated/, 'do_something() logs that it updated');
$logger->has_level('error', 'an error was logged');
$logger->unlike(qr/fatal/, 'nothing fatal was logged');
is($logger->count(), 3, 'three messages were logged');

done_testing();

Check That Nothing Was Logged

my $logger = Test::Log::Abstraction->new();
Some::Class->new(logger => $logger)->run();
$logger->empty('a normal run logs nothing');

Test Several Steps With One Logger

my $logger = Test::Log::Abstraction->new();
my $obj = Some::Class->new(logger => $logger);

$obj->load('good.csv');
$logger->empty('good file: no messages');

$logger->clear();    # forget the messages from the first step
$obj->load('bad.csv');
$logger->has_level('warn', 'bad file: a warning');

Look at the Messages Yourself

foreach my $entry (@{ $logger->messages() }) {
    print "$entry->{level}: $entry->{message}\n";
}

# Structured fields, from a call such as
# $logger->info('user logged in', { user => 'alice' })
is($logger->messages()->[0]->{fields}->{user}, 'alice', 'user field');

Control What Is Printed While the Test Runs

# Print nothing (the messages are still captured)
my $quiet = Test::Log::Abstraction->new(diag => 'none');

# Print everything
my $loud = Test::Log::Abstraction->new(verbose => 1);

# Print only errors and more serious messages
my $errors = Test::Log::Abstraction->new(diag => 'error');

Test Code That Checks the Log Level

# The code under test does: if($logger->is_debug()) { ... }
my $logger = Test::Log::Abstraction->new(level => 'warning');
ok(!$logger->is_debug(), 'debug output is turned off');

Get This Module's Own Messages in Another Language

my $logger = Test::Log::Abstraction->new(lang => 'de');    # German
my $french = Test::Log::Abstraction->new(country => 'FR');    # French

Description

What This Module Is

Some code writes log messages through a logger object. In production that object is usually a Log::Abstraction logger. In a test, you give the code a Test::Log::Abstraction object instead.

This object does not write the messages to a file. It keeps them in a list in memory. After the code has run, your test can check the list: was a message logged, at which level, and what did it say?

It never writes to disk, and it does not load any logging backend.

Which Methods It Has

Which Messages Are Printed

Every message is always stored. This section is only about which messages are also printed in the test output, as TAP comments (lines that start with #).

By default, warning and more serious levels are printed. So if a test causes a warning by accident, you see it. trace, debug, info and notice messages are not printed.

Verbose mode prints every message. Verbose mode is on when you pass verbose => 1 to new(). If you do not pass verbose, it is on when the environment variable TEST_VERBOSE is true (prove -v sets it), or when VERBOSE is true.

To choose the levels, use the diag option of new():

Put exactly: a message is printed when any one of these is true, and not otherwise:

So a misspelt level (caught by "AUTOLOAD") is printed only by rules 1 to 3; and 'none' or an empty list means only rule 1 can apply.

When a test method fails, the messages that explain the failure are printed under it. So you can see why it failed without running the test again.

Global Variables Are Left Alone

No method changes $@, $! or $_, and none of them touches an alarm() timer. So you can log, or test the log, inside an error handler, and $@ still holds the error afterwards. (A method that stops with an error does set $@, as every Perl error does.)

Levels and How Serious They Are

Each level has a number. A lower number means a more serious message. These are the syslog numbers.

0  emergency, emerg, panic
1  alert
2  critical, crit, fatal
3  error, err
4  warning, warn
5  notice
6  info, informational
7  debug, trace

Language of This Module's Messages

This module has its own messages: error messages, warnings, and the text that explains a failed test. They can be in English (en), German (de), French (fr) or Simplified Chinese (zh).

The language is chosen like this. The first rule that gives an answer is used:

The default is English, not the language of your computer. This is on purpose: the test output is then the same on every computer.

If a message has no translation, the English message is used. You can add or change messages with the i18n option of new().

This only changes this module's own messages. The messages that your code logs are never changed.

Default Settings

The defaults for new() are in the hash %Test::Log::Abstraction::config. It has the same keys as the options of new(). You can change it in a test, or fill it with Object::Configure:

$Test::Log::Abstraction::config{'diag'} = 'none';

A change only affects loggers that are created after it.

You can also pass the result of Object::Configure straight to new(). Settings in environment variables named Test__Log__Abstraction__KEY then override the arguments; the extra keys that Object::Configure adds (such as its own logger) are ignored:

# In the shell: export Test__Log__Abstraction__diag=none
my $params = Object::Configure::configure('Test::Log::Abstraction', { level => 'error' });
my $logger = Test::Log::Abstraction->new($params);

Common Pitfalls

Encoding

This module never changes the text that your code logs. It stores each message exactly as it was given.

Methods

New

Create a new test logger.

Purpose

Make a logger that stores every message it is given, so that your test can check the messages later.

Args

All options are optional. Give them as a list (key => value) or as one hash reference.

Other options are allowed and ignored. (A Log::Abstraction configuration hash can be passed unchanged.) If you pass an odd number of arguments, they are all ignored.

Three Ways to Call It

Returns

The new logger object.

Side Effects

Reads %ENV to decide on verbose mode, and, with lang => 'auto', to choose the language. Nothing else changes. A clone does not change the original logger.

Example

# The usual way
my $logger = Test::Log::Abstraction->new();

# Store everything, print nothing
my $quiet = Test::Log::Abstraction->new(diag => 'none');

# A hash reference works too; messages in German
my $german = Test::Log::Abstraction->new({ country => 'DE' });

# A clone that prints everything; $logger is not changed
my $loud = $logger->new(diag => 'all');

Api Specification

Input
{
    verbose => { type => 'scalar', optional => 1 },
    diag => { type => ['string', 'arrayref'], optional => 1 },
    level => { type => 'string', optional => 1 },
    lang => { type => 'string', optional => 1, matches => qr/\A(?:(?i:auto)|[A-Za-z]{2,3}(?:[_.\@-][\w.\@-]*+)?)\z/ },
    country => { type => 'string', optional => 1, matches => qr/\A[A-Za-z]{2}\z/ },
    i18n => { type => 'hashref', optional => 1 },
}

Domains (valid / invalid / boundaries):

verbose   valid: any plain value, Perl truth decides ('0', '' and undef
          are off; '00', '0.0' and ' ' are on); absent: from
          TEST_VERBOSE or VERBOSE.  Invalid: any reference.
diag      valid: 'all' or 'none' (any case); a level name (any case),
          which prints severity 0 (emergency) up to that level's
          number, 7 (trace) being every level; an array reference of
          level names, [] printing nothing and duplicates counting
          once; absent or undef: $config{diag}.  Invalid: any other
          string, including '' and numbers such as '4'; a list
          element that is not a level name (or is 'all'); a hash,
          code or scalar reference.
level     valid: the 16 level names, any case, setting 0 (emergency)
          to 7 (trace); absent or undef: $config{level}.  Invalid:
          '', numbers (even 0 to 7), any other name.
lang      valid: 'auto' (any case); 2 or 3 ASCII letters, then
          optionally one of _ . @ - and anything else ('de',
          'eng', 'de_DE.UTF-8', 'zh-Hant').  A code with no catalogue
          ('ja', 'eng') gives 'en'.  Invalid: 1 letter, 4 or more
          letters, '', digits, a non-ASCII letter in the code, any
          whitespace or newline.
country   valid: exactly 2 ASCII letters, any case; one with no
          mapping ('JP') gives 'en'.  Invalid: 1 or 3 letters, '',
          digits, non-ASCII letters.
i18n      valid: a hash reference (even empty).  Invalid: any other
          type.

An "invalid argument" explanation, and a level or diag name repeated in an error, is at most 200 characters; one longer is cut to 200 and ends with "...". Control characters in it are shown as \xNN.

Output
{ type => 'object', isa => 'Test::Log::Abstraction' }

Messages

All of these stop the program (croak).

Message                         Meaning                       What to do
------------------------------  ----------------------------  ------------------------------
invalid diag level 'X'          X is not a level name         Use a name from the level table
diag must be a level name ...   diag is a hash or code ref    Give a string or an array ref
invalid syslog level 'X'        the level option is unknown   Use a name from the level table
invalid argument: ...           an option has the wrong type  Fix the option that is named
                                or format

Pseudocode

if new() was called on a logger object:
    options = the object's options, replaced by the new options
    build a logger from the options, with a copy of the messages
    copy verbose and level from the object, unless new values were given
else:
    if new() was called as a function, the first argument is an option
    turn the arguments into a hash (ignore an odd-length list)
    check the options
    choose the language, then the diag rule, then the level
return the logger

Trace, Debug, Info, Notice, Warn, Error, Fatal, Critical, Alert, Emergency

Store a message at this level.

Purpose

These are the methods that the code under test calls to log something. There is one method for each Log::Abstraction level, and one for each syslog name: warning, err, crit, emerg, panic and informational. Each method stores the message under the name that was called, and may print it (see "Which messages are printed").

By default:

Args

Any list of values. They are turned into one message like this:

Returns

The logger, as in Log::Abstraction. So you can chain calls: $logger->info('a')->info('b').

Side Effects

Adds one entry to the stored messages (none for a call with no values at warn or above; see "Args"). May print the message. The variables $@ and $! are not changed. So you can log inside an error handler without losing the error.

Example

$logger->warn('something looks wrong');
$logger->warn('file ', $name, ' is empty');          # joined: one message
$logger->info('started', { pid => $$ });             # message + fields
$logger->error({ error => 'cannot open file' });     # hash as the message
$logger->debug(['part 1, ', 'part 2']);              # array of parts

Api Specification

Input
{
    messages => { type => 'arrayref', position => 0, slurp => 1 },
}

Domains: any number of arguments (0 gives an empty message) of any kind and length. Exactly one trailing newline is removed (so "m\n\n" keeps one). A trailing hash after one or more values is fields; an empty one is dropped; a blessed hash is part of the message. Text may be ASCII, decoded text in any script (accents, CJK, emoji and emoji sequences, combining marks, right-to-left text, zero-width characters) or bytes; it is stored with its length unchanged. See "ENCODING" for how it is printed.

Output
{ type => 'object', isa => 'Test::Log::Abstraction' }

Messages

Message                         Meaning                       What to do
------------------------------  ----------------------------  ------------------------------
X() must be called on an        not called on a logger        Call it on a logger object
object, not on the class        (croak)

Is_Trace, Is_Debug, Is_Info, Is_Notice, Is_Warn, Is_Error, Is_Critical, Is_Alert, Is_Emergency

Ask if a level is turned on.

Purpose

Some code only builds a log message if the level is turned on, for example if($logger->is_debug()) { ... }. These methods answer that question, as Log::Abstraction does.

Args

None.

Returns

1 if the level is turned on, otherwise 0. A level is turned on when its number is the same as, or lower than, the logger's level (see "level"). The default level is trace, so all these methods return 1.

Side Effects

None.

Example

$logger->level('warning');
$logger->is_warn();     # 1
$logger->is_error();    # 1 (more serious than warning)
$logger->is_info();     # 0 (less serious than warning)

Api Specification

Input
{}

Domains: no arguments. Each predicate is 1 exactly when its level's number is at or below the logger's level: at level 0 (emergency) only is_emergency is 1; at level 7 (trace) all are 1.

Output
{ type => 'boolean' }

Messages

Message                         Meaning                       What to do
------------------------------  ----------------------------  ------------------------------
is_X() must be called on an     not called on a logger        Call it on a logger object
object, not on the class        (croak)

Autoload

Handle a call to a method that does not exist.

Purpose

Perl calls this when the code under test calls a method that this class does not have - usually a misspelt level, such as wran. Any name works, even an empty one ($logger->$name() with $name = ''). Instead of stopping the test, the message is stored under the name that was called, and a notice is printed. The notice is always printed, whatever the diag setting, so the mistake is not hidden.

Args

The same as a level method.

Returns

The logger.

Side Effects

Adds one entry, with the called name as its level. Prints no method 'name'.

Example

$logger->wran('oops');    # stored at level 'wran'; a notice is printed
is($logger->count('wran'), 1, 'the misspelt call was stored');

Api Specification

Input
{
    messages => { type => 'arrayref', position => 0, slurp => 1 },
}

Domains: any method name, including '', non-ASCII names and very long names. The message is stored under the name in lower case.

Output
{ type => 'object', isa => 'Test::Log::Abstraction' }

Messages

Message                         Meaning                       What to do
------------------------------  ----------------------------  ------------------------------
no method 'X'                   X is not a method or a level  Fix the method name
                                (notice; the test goes on)
X() must be called on an        an unknown method was called  Call it on a logger object
object, not on the class        on the class name (croak)

Messages

Get the stored messages.

Purpose

Let your test look at everything that was logged.

Args

None.

Returns

A reference to a new array. It has one hash reference for each message, oldest first. Each hash has these keys:

The array is a copy, as in Log::Abstraction. Adding or removing items in it does not change the stored messages. But the hashes in it are the stored hashes (see "COMMON PITFALLS").

Side Effects

None.

Example

foreach my $entry (@{ $logger->messages() }) {
    diag("$entry->{level}: $entry->{message}");
}

my $first = $logger->messages()->[0];
is($first->{level}, 'warn', 'the first message is a warning');

Api Specification

Input
{}
Output
{ type => 'arrayref' }

Messages

Message                         Meaning                       What to do
------------------------------  ----------------------------  ------------------------------
messages() must be called on    not called on a logger        Call it on a logger object
an object, not on the class     (croak)

Clear

Delete all stored messages.

Purpose

Start again with an empty list, for example between two steps of a test.

Args

None.

Returns

The logger, so you can chain calls.

Side Effects

All stored messages are deleted. The settings (verbose, level, diag, language) do not change. On a logger with no messages, it does nothing, so it is safe to call before every step.

Example

$logger->clear();
$logger->clear()->empty('nothing logged yet');

Api Specification

Input
{}
Output
{ type => 'object', isa => 'Test::Log::Abstraction' }

Messages

Message                         Meaning                       What to do
------------------------------  ----------------------------  ------------------------------
clear() must be called on an    not called on a logger        Call it on a logger object
object, not on the class        (croak)

Count

Count the stored messages.

Purpose

Check how much was logged, in total or at one level.

Args

Returns

The number of messages: 0 or more.

Side Effects

None.

Example

is($logger->count(), 3, 'three messages in total');
is($logger->count('error'), 1, 'one of them is an error');

Api Specification

Input
{
    level => { type => 'string', optional => 1, position => 0 },
}

Domains: undef counts every message; a string counts messages at that level, any case ('', '0' and unknown names give 0; aliases are separate names). Invalid: any reference.

Output
{ type => 'integer', min => 0 }

Messages

Message                         Meaning                       What to do
------------------------------  ----------------------------  ------------------------------
count() must be called on an    not called on a logger        Call it on a logger object
object, not on the class        (croak)
invalid argument: ...           the level is not a string     Give a level name
                                (croak)

Like

Test that a stored message matches a pattern.

Purpose

The test passes if at least one stored message matches the pattern.

Args

Returns

True if the test passed, false if it failed.

Side Effects

Adds one test result to the TAP output. If the test fails, all stored messages are printed under it (at most 20, then a count of the others).

Example

$logger->like(qr/updated/, 'the update was logged');
$logger->like(qr/^Cannot open/i, 'the open error was logged');

Api Specification

Input
{
    pattern => { type => ['regex', 'string'], position => 0 },
    name => { type => 'string', optional => 1, position => 1 },
}

Domains: pattern - a qr// or a string, which is compiled as a regular expression ('' matches every message). Invalid: undef, any other reference, a string that does not compile or can never match. name - undef, or any string (including '' and non-ASCII text). Invalid: a reference.

Output
{ type => 'boolean' }

Messages

Message                         Meaning                       What to do
------------------------------  ----------------------------  ------------------------------
like() needs a pattern          no pattern was given (croak)  Give a qr// or a string
invalid argument: ...           the pattern is not a qr// or  Give a qr// or a string
                                a string, does not compile,   that is a valid regex
                                or can never match (croak)
N messages were captured:       the test failed; the stored   Compare them with the pattern
                                messages follow (output)
like() must be called on        not called on a logger        Call it on a logger object
an object, not on the class     (croak)

Unlike

Test that no stored message matches a pattern.

Purpose

The test passes if no stored message matches the pattern. It also passes when there are no messages.

Args

Returns

True if the test passed, false if it failed.

Side Effects

Adds one test result to the TAP output. If the test fails, the messages that matched are printed under it.

Example

$logger->unlike(qr/fatal/i, 'nothing fatal was logged');

Api Specification

Input
{
    pattern => { type => ['regex', 'string'], position => 0 },
    name => { type => 'string', optional => 1, position => 1 },
}

Domains: as for "like". Note that '' matches every message, so unlike('') fails whenever anything was logged.

Output
{ type => 'boolean' }

Messages

Message                         Meaning                       What to do
------------------------------  ----------------------------  ------------------------------
unlike() needs a pattern        no pattern was given (croak)  Give a qr// or a string
invalid argument: ...           the pattern is not a qr// or  Give a qr// or a string
                                a string, does not compile,   that is a valid regex
                                or can never match (croak)
N messages matched:             the test failed; the          Look at the listed messages
                                matching messages follow
unlike() must be called on      not called on a logger        Call it on a logger object
an object, not on the class     (croak)

Has_Level

Test that something was logged at a level.

Purpose

The test passes if at least one message was stored at this level.

Args

Returns

True if the test passed, false if it failed.

Side Effects

Adds one test result to the TAP output. If the test fails, all stored messages are printed under it, so you can see which levels were used.

Example

$logger->has_level('error', 'the failure was logged');

Api Specification

Input
{
    level => { type => 'string', position => 0 },
    name => { type => 'string', optional => 1, position => 1 },
}

Domains: level - a string, any case ('' and unknown names match nothing; aliases are separate names). Invalid: undef, any reference. name - as for "like".

Output
{ type => 'boolean' }

Messages

Message                         Meaning                       What to do
------------------------------  ----------------------------  ------------------------------
has_level() needs a level name  no level was given (croak)    Give a level name
invalid argument: ...           the level is not a string     Give a level name
                                (croak)
N messages were captured:       the test failed; the stored   Look at the listed levels
                                messages follow (output)
has_level() must be called on   not called on a logger        Call it on a logger object
an object, not on the class     (croak)

Empty

Test that nothing was logged.

Purpose

The test passes if there are no stored messages. Use it to check that a normal run logs nothing.

Args

Returns

True if the test passed, false if it failed.

Side Effects

Adds one test result to the TAP output. If the test fails, the stored messages are printed under it.

Example

$logger->empty('a normal run logs nothing');

Api Specification

Input
{
    name => { type => 'string', optional => 1, position => 0 },
}

Domains: name - undef, or any string. Invalid: a reference.

Output
{ type => 'boolean' }

Messages

Message                         Meaning                       What to do
------------------------------  ----------------------------  ------------------------------
N messages were captured:       the test failed; the stored   Look at the listed messages
                                messages follow (output)
empty() must be called on       not called on a logger        Call it on a logger object
an object, not on the class     (croak)

Verbose

Get or change verbose mode.

Purpose

In verbose mode, every message is printed, whatever the diag rule says. This helps when you are finding out why a test fails.

Args

Returns

The setting after the call: 1 (on) or 0 (off).

Side Effects

Changes the setting, when you give an argument.

Example

$logger->verbose(1);           # print everything from now on
my $on = $logger->verbose();   # 1

Api Specification

Input
{
    value => { type => 'scalar', optional => 1, position => 0 },
}

Domains: absent - get only; otherwise Perl truth decides ('0', '' and undef are off; '0.0' is on).

Output
{ type => 'integer', min => 0, max => 1 }

Messages

Message                         Meaning                       What to do
------------------------------  ----------------------------  ------------------------------
verbose() must be called on an  not called on a logger        Call it on a logger object
object, not on the class        (croak)

Level

Get or change the logger's level.

Purpose

Code under test may read or change the level, as it can with "level" in Log::Abstraction. The level only changes the answers of the is_* methods. Every message is still stored.

Args

Returns

Side Effects

With a known name, the level changes. With an unknown name, nothing changes and a warning is printed.

Example

$logger->level('error');
print $logger->level(), "\n";    # 3

Api Specification

Input
{
    name => { type => 'string', optional => 1, position => 0 },
}

Domains: absent or undef - get only; one of the 16 level names, any case - set (0 for emergency to 7 for trace). Anything else - '', numbers such as '0' or '8', unknown names - warns and returns undef.

Output
{ type => ['integer', 'object'], optional => 1 }

Messages

Message                         Meaning                       What to do
------------------------------  ----------------------------  ------------------------------
invalid syslog level 'X'        X is not a level name         Use a name from the level table
                                (warning; returns undef)
level() must be called on       not called on a logger        Call it on a logger object
an object, not on the class     (croak)

Flush

Do nothing.

Purpose

In Log::Abstraction, flush() sends e-mail messages that are waiting. The test logger never sends e-mail, but the code under test may still call flush(), so it exists.

Args

None.

Returns

The logger, so you can chain calls.

Side Effects

None.

Example

$logger->flush();

Api Specification

Input
{}
Output
{ type => 'object', isa => 'Test::Log::Abstraction' }

Messages

Message                         Meaning                       What to do
------------------------------  ----------------------------  ------------------------------
flush() must be called on an    not called on a logger        Call it on a logger object
object, not on the class        (croak)

Lang

Get the language of this module's messages.

Purpose

Let a test check which language was chosen from the lang and country options or the environment.

Args

None.

Returns

A language code: en, de, fr, zh, or a code that you gave in the i18n option.

Side Effects

None.

Example

my $lang = Test::Log::Abstraction->new(country => 'FR')->lang();    # 'fr'

Api Specification

Input
{}
Output
{ type => 'string', matches => qr/\A[a-z]{2,3}\z/ }

Messages

Message                         Meaning                       What to do
------------------------------  ----------------------------  ------------------------------
lang() must be called on an     not called on a logger        Call it on a logger object
object, not on the class        (croak)

Methods for Subclasses

These methods are for this class, and classes that inherit from it. A subclass may call them, or replace them. Nothing stops other code calling them, but they are not part of the public interface, and may change.

_Emit

Print one line of test output.

Purpose

Every line that this module prints goes through this method. A subclass can replace it to send the lines somewhere else.

Args

Returns

The logger.

Side Effects

Prints the line as a TAP comment, with "diag" in Test::Builder. This works even if the test did not load Test::More. A Perl character string is encoded as UTF-8 first, unless the output already has an encoding layer (see "ENCODING").

Example

package My::Logger;
use parent -norequire, 'Test::Log::Abstraction';

# Send the lines to STDERR instead of the TAP output
sub _emit {
    my ($self, $text) = @_;
    print STDERR "$text\n";
    return $self;
}

Api Specification

Input
{
    text => { type => 'string', position => 0 },
}
Output
{ type => 'object', isa => 'Test::Log::Abstraction' }

Messages

None. It never fails.

i18n

Make one of this module's messages, in the logger's language.

Purpose

All the text that this module shows to people comes from here. So the text can be translated, or changed, without changing the code.

Args

How a Message Template Works

A template is a string. %{name}s is replaced by the value called name. After the name you can use any sprintf format letter, for example %{count}d or %{ratio}.2f. %% gives one %. A value that is missing becomes the text undef, with no warning.

A template can also be a hash. The keys are gender names (such as male, female) or plural forms (zero, one, two, few, many, other). The values are templates, so they can be hashes too. other is used when nothing else fits. zero is used for a count of 0, if it is there.

{
    zero  => 'no messages',
    one   => '%{count}d message',
    other => '%{count}d messages',
}

Where the Template Is Found

The first one found is used:

If the key is not found anywhere, the key itself is returned.

Returns

The finished message, as a Perl character string.

Side Effects

None.

Example

# Inside a subclass
my $text = $self->i18n('needs_pattern', { method => 'like' });
# "Test::Log::Abstraction: like() needs a pattern"

# Your own message, with gender and plural forms.  i18n() is
# meant for subclasses, so call it from a method of your subclass.
my $logger = My::Logger->new(i18n => {
    en => {
        logged => {
            male => { one => 'He logged %{count}d line', other => 'He logged %{count}d lines' },
            other => 'They logged %{count}d lines',
        },
    },
});
# In a method of My::Logger:
$self->i18n('logged', { gender => 'male', count => 2 });    # 'He logged 2 lines'

Api Specification

Input
{
    key => { type => 'string', position => 0 },
    args => { type => 'hashref', optional => 1, position => 1 },
}

Domains: key - any string; undef renders as '', a reference as its string form, and an unknown key is returned as it is. args - a hash reference; anything else is ignored. count - a number selects a plural form (0 uses 'zero' when present); anything else uses 'other'.

Output
{ type => 'string' }

Messages

None. It never fails, for any key or arguments.

Pseudocode

language = the logger's language (for a class name: the configured one)
template = the first one found in the four places listed above
if no template was found, return the key
while the template is a hash:
    choose by gender, else by plural form, else 'other'
replace each %{name}format with sprintf(format, the value of name)
return the text

Limitations

Diagnostics

Each method lists its messages under MESSAGES. All messages can be translated or changed; see "i18n".

The text after invalid argument: explains what was wrong, and may show the value that was given. Other errors and warnings repeat a name you gave: a level name, a diag name, or a method name. Every value repeated like this is cut to 200 characters, and control characters in it (such as a newline, or the escape character that starts a terminal control sequence) are shown as \xNN. So a hostile value cannot make an error message enormous, add a forged ok line to the output, or control your terminal.

See Also

Log::Abstraction, Test::Builder, Test::Most

Author

Nigel Horne, <njh at nigelhorne.com>

Formal Specification

This section describes each method in the Z notation. You do not need it to use the module. It is here so that the behaviour is exact.

State

[TEXT, NAME, KEY, TEMPLATE, TAG, VALUE]
BOOL ::= true | false
SEVERITY == 0 .. 7
BUILTIN == { en, de, fr, zh }

severity : NAME ⇸ SEVERITY
lower : NAME → NAME
catalogue : TAG ⇸ (KEY ⇸ TEMPLATE)
render : seq VALUE → TEXT
─────────
dom catalogue = BUILTIN
∀ n : dom severity • lower n = n

Entry ≙ [ level : NAME; message : TEXT; fields : TEXT ⇸ VALUE ]

Rule ::= all | none | levels⟨⟨ℙ NAME⟩⟩ | atleast⟨⟨SEVERITY⟩⟩

Logger
  log       : seq Entry
  verbose   : BOOL
  threshold : SEVERITY
  lang      : TAG
  overrides : TAG ⇸ (KEY ⇸ TEMPLATE)
  rule      : Rule
  ─────────
  lang ∈ BUILTIN ∪ dom overrides
  ∀ e : ran log • e.level = lower e.level

ΔLogger ≙ [ Logger; Logger' | overrides' = overrides ∧ rule' = rule ]

printed : Logger × NAME → BOOL
─────────
∀ L : Logger; n : NAME •
  printed (L, n) = true ⇔
    L.verbose = true ∨ L.rule = all
    ∨ (∃ S : ℙ NAME • L.rule = levels S ∧ n ∈ S)
    ∨ (∃ t : SEVERITY • L.rule = atleast t ∧ n ∈ dom severity ∧ severity n ≤ t)

severity is the level table under "Levels and how serious they are". lower is Perl's lc. catalogue is the built-in message table, and overrides is the logger's i18n option; so a logger's language may be one that only overrides has. render turns a log call's arguments into its message text (see the level methods' "Args"). rule is the diag option, and printed says whether a stored message is also printed. ΔLogger means the method may change the log and the settings, but never overrides or rule; ΞLogger means it changes nothing.

New

New
  Logger'
  opts? : NAME ⇸ VALUE
  config : NAME ⇸ VALUE
  env : NAME ⇸ TEXT
  ─────────
  let get ≙ λ k : NAME • (if k ∈ dom opts? then opts? k else config k) •
    lower (get level) ∈ dom severity
    log' = ⟨⟩
    verbose' = (if verbose ∈ dom opts? then opts? verbose
                else env TEST_VERBOSE ∨ env VERBOSE)
    threshold' = severity (lower (get level))
    overrides' = (if i18n ∈ dom opts? then opts? i18n else ∅)
    rule' = rule_of (get diag)
    lang' = language (opts?, config, env, overrides')

Clone
  ΞLogger
  clone! : Logger
  ─────────
  clone!.log = log
  clone!.verbose = verbose
  clone!.threshold = threshold
  clone!.lang = lang

config is "Default settings" and env is %ENV. rule_of turns the diag option into a Rule, and fails (croaks) for anything else. language applies the order under "Language of this module's messages": lang, then country, then (for lang => 'auto') the locale variables in env, then config; the result is the first two or three letters, if BUILTIN or overrides' has that language, and otherwise en.

Clone is $logger->new() with no options. Options that are given replace the matching values, as in New.

Trace, Debug, Info, Notice, Warn, Error, Fatal, Critical, Alert, Emergency

Log
  ΔLogger
  name? : NAME
  args? : seq VALUE
  ─────────
  name? ∈ dom severity
  args? = ⟨⟩ ∧ severity name? ≤ severity warn ⇒ log' = log
  ¬ (args? = ⟨⟩ ∧ severity name? ≤ severity warn) ⇒
      log' = log ⁀ ⟨⟨ level ↦ name?, message ↦ render args? ⟩⟩
  verbose' = verbose ∧ threshold' = threshold ∧ lang' = lang

A stored entry is also printed exactly when printed (θLogger, name?).

Is_Trace, Is_Debug, Is_Info, Is_Notice, Is_Warn, Is_Error, Is_Critical, Is_Alert, Is_Emergency

IsLevel
  ΞLogger
  name? : NAME
  result! : BOOL
  ─────────
  name? ∈ dom severity
  result! = true ⇔ severity name? ≤ threshold

Autoload

Unknown
  ΔLogger
  name? : NAME
  args? : seq VALUE
  ─────────
  name? ∉ dom severity
  log' = log ⁀ ⟨⟨ level ↦ lower name?, message ↦ render args? ⟩⟩
  verbose' = verbose ∧ threshold' = threshold ∧ lang' = lang

The notice no method 'name?' is always printed. A name in another case, such as INFO, is not a method, so it comes here, and is stored as info.

Messages

Messages
  ΞLogger
  result! : seq Entry
  ─────────
  result! = log

Clear

Clear
  ΔLogger
  ─────────
  log' = ⟨⟩
  verbose' = verbose ∧ threshold' = threshold ∧ lang' = lang

Count

Count
  ΞLogger
  level? : NAME
  result! : ℕ
  ─────────
  result! = # (log ↾ { e : Entry | e.level = lower level? })

CountAll
  ΞLogger
  result! : ℕ
  ─────────
  result! = # log

Like

Like
  ΞLogger
  pattern? : ℙ TEXT
  result! : BOOL
  ─────────
  result! = true ⇔ (∃ e : ran log • e.message ∈ pattern?)

Unlike

Unlike
  ΞLogger
  pattern? : ℙ TEXT
  result! : BOOL
  ─────────
  result! = true ⇔ (∀ e : ran log • e.message ∉ pattern?)

Has_Level

HasLevel
  ΞLogger
  level? : NAME
  result! : BOOL
  ─────────
  result! = true ⇔ (∃ e : ran log • e.level = lower level?)

Empty

Empty
  ΞLogger
  result! : BOOL
  ─────────
  result! = true ⇔ log = ⟨⟩

Verbose

SetVerbose
  ΔLogger
  value? : BOOL
  result! : BOOL
  ─────────
  verbose' = value?
  result! = verbose'
  log' = log ∧ threshold' = threshold ∧ lang' = lang

GetVerbose
  ΞLogger
  result! : BOOL
  ─────────
  result! = verbose

Level

SetLevel
  ΔLogger
  name? : NAME
  ─────────
  name? ∈ dom severity ⇒ threshold' = severity name?
  name? ∉ dom severity ⇒ threshold' = threshold
  log' = log ∧ verbose' = verbose ∧ lang' = lang

GetLevel
  ΞLogger
  result! : SEVERITY
  ─────────
  result! = threshold

Flush

Flush
  ΞLogger

Lang

Lang
  ΞLogger
  result! : LANG
  ─────────
  result! = lang

i18n

I18n
  ΞLogger
  key? : KEY
  result! : TEXT
  ─────────
  let find ≙ λ t : TAG •
      { m : (TAG ⇸ (KEY ⇸ TEMPLATE)) | m ∈ { overrides, catalogue }
          ∧ t ∈ dom m ∧ key? ∈ dom (m t) } •
    find lang ≠ ∅ ⇒ result! = fill (first (find lang) lang key?)
    find lang = ∅ ∧ find en ≠ ∅ ⇒ result! = fill (first (find en) en key?)
    find lang = ∅ ∧ find en = ∅ ⇒ result! = key?

fill chooses the gender or plural form and fills in the placeholders. first takes overrides before catalogue when both have the key: the i18n option is searched first in each language.

State Diagram

A logger has two main states: EMPTY (no stored messages) and CAPTURING (one or more stored messages). Two settings, verbose and level, can change in either state; they do not move the logger between states. Methods that only read or test (like, count, is_debug, and so on) never change the state.

              new(%options)
              [check options; choose language,
               diag rule and level]
                    |
                    | invalid option
                    +----------------------> croak, no logger made
                    |
                    v
+-----------------------------------------+
|                 EMPTY                   |<----------------+
|  messages = ()                          |                 |
+-----------------------------------------+                 |
     |       ^                                              |
     |       | clear()                                      |
     +-------+ [nothing to delete; return the logger]       |
     |                                                      |
     | trace() ... emergency(), warning() ... panic()       | clear()
     | [store the message; print it if the diag rule        | [delete all
     |  or verbose allows; $@ and $! are kept.  With no     |  messages;
     |  arguments, warn() and above store nothing and stay  |  return the
     |  in EMPTY (or CAPTURING), as in Log::Abstraction]    |  logger]
     |                                                      |
     | wran() or any unknown method (AUTOLOAD)              |
     | [store under that name; always print                 |
     |  "no method 'wran'"]                                 |
     v                                                      |
+-----------------------------------------+                 |
|               CAPTURING                 |-----------------+
|  messages = (m1, m2, ...)               |
+-----------------------------------------+
     |       ^
     |       | any level method, or an unknown method
     +-------+ [store one more message; maybe print it]

Changes allowed in BOTH states (the state stays the same):

  verbose(1) / verbose(0)   verbose on / off
                            [from now on: print every message / use
                             the diag rule]
  level('error')            level number = 3
                            [the is_* answers change; nothing is
                             hidden]
  level('bogus')            no change [warning; returns undef]

Read-only calls in BOTH states (the state stays the same):

  like, unlike, has_level, empty
                            [one TAP result; on failure, print the
                             messages that explain it]
  count, messages, lang, is_trace ... is_emergency, flush
                            [return a value only]
  like(undef), has_level(undef), a bad argument, or any method
  called on something that is not a logger (the class name, undef,
  a plain reference, or another class's object)
                            [croak; no change]

Copying (the original logger does not change):

  EMPTY     --- $logger->new(%options) ---> a new logger in EMPTY
  CAPTURING --- $logger->new(%options) ---> a new logger in CAPTURING
                [the new logger has copies of the messages, and the
                 same verbose and level, unless new values are given]

End:

  EMPTY or CAPTURING --- the last reference goes away ---> destroyed
                [DESTROY does nothing; nothing is stored or printed]

Copyright 2026 Nigel Horne.

Usage is subject to the GPL2 licence terms. If you use it, please let me know.