NAME
Log::Abstraction - Logging Abstraction Layer
VERSION
0.41
SYNOPSIS
use Log::Abstraction;
# The default level is 'warning'; 'trace' lets every example through
my $logger = Log::Abstraction->new(logger => 'logfile.log', level => 'trace');
$logger->debug('This is a debug message');
$logger->info('This is an info message');
$logger->notice('This is a notice message');
$logger->trace('This is a trace message');
$logger->warn({ warning => 'This is a warning message' });
# Structured fields
$logger->info('User logged in', { user_id => 42 });
DESCRIPTION
The Log::Abstraction class provides a flexible logging layer on top of different types of loggers, including code references, arrays, file paths, and objects. It also supports logging to syslog if configured.
Unicode
Messages may be character strings containing any Unicode text. The file, fd and scalar-path backends write character strings as UTF-8 (unless an fd handle already has a :utf8 or :encoding layer, in which case the handle does the encoding), and format => 'json' output is UTF-8 too. journald fields are sent as UTF-8. Byte strings are written unchanged.
Structured fields
Every logging method accepts a hash reference of structured fields after the message:
$logger->info('User logged in', { user_id => 42, ip => $ip });
$logger->warn('Slow query', { ms => 1250 });
A hash reference is taken as fields only when it is the last of two or more arguments, so warn({ warning => ... }) keeps its meaning, and a lone hash reference is still a message. An empty hash reference is ignored. The fields are copied, so changing the hash afterwards doesn't change what was logged. They are kept out of the message and go to each backend as follows:
"messages",
arrayand an ARRAYlogger-- afieldskey in the entry, alongsidelevelandmessage.a CODE
logger-- afieldskey in the hashref it is called with.format => 'json'-- a nestedfieldsobject. Objects are stringified; other references are kept as JSON data.journald-- journal fields. Each name is upper-cased, characters other thanA-Z,0-9and_become_, leading underscores are removed and it is cut to 64 characters; a field left with no name is dropped. Fields override the extra keys in thejournaldhash, but neverMESSAGE,PRIORITYorSYSLOG_IDENTIFIER.text formats (
file,fd, a scalarlogger),syslog,sendmailand object loggers -- appended to the message as logfmt-stylekey=valuepairs in key order, e.g.User logged in ip=10.0.0.1 user_id=42. Characters other than[\w.-]in a key become_. A value that is empty or contains white space,",=or\is double-quoted, with"and\escaped and control characters written as\n,\r,\tor\xNN. Objects are stringified and other references written as JSON. An object logger is passed the pairs as an extra argument after the message.
When logging through Log::Any, a hash reference at the end of the call, together with the proxy's context, arrives here as fields; see "structured" in Log::Any::Adapter::Abstraction.
Redaction
The redact option removes secrets before a message reaches the history or any backend. Each match of any of its patterns is replaced with [REDACTED]:
my $log = Log::Abstraction->new(
logger => '/var/log/myapp.log',
redact => [qr/password=\S+/, qr/\b\d{4}(?:[ -]?\d{4}){3}\b/],
);
$log->warn('Login failed: user=fred password=hunter2');
# Login failed: user=fred [REDACTED]
To keep the start of a match, end it with \K: qr/password=\K\S+/ logs password=[REDACTED]. The patterns run over the whole message, after its arguments are joined, so a match may span them, as in $log->info('password=', $password); a CODE or object logger then gets the joined message as one argument. The text given to carp or croak (see "warn" and "error") is redacted too.
"Structured fields" are redacted as well: string values, including those inside plain hashes and arrays, and objects whose stringification matches (they become the redacted string). Field names aren't redacted, nor are the format string, %env_*% values or ctx.
Redaction is applied only to messages that pass the logger's level, so it costs nothing for messages that are dropped. A pattern that can match the empty string, such as qr/x*/, is rejected by "new".
Email digests
min_interval limits the sendmail backend to one email per interval, dropping the messages in between. With digest, they are held instead, and sent ahead of the next message in the first email after the interval:
my $log = Log::Abstraction->new(
logger => {
file => '/var/log/myapp.log',
sendmail => {
to => 'ops@example.com',
level => 'error',
min_interval => 300,
digest => 1,
format => '%level%> [%timestamp%] %message%',
},
},
);
Each message is one line of the email, oldest first, as its own email body would be: formatted with the sendmail format if there is one (as above, so that each line has its time), else the message. At most digest_max messages (default 100) are held; later ones are only counted, and the email says ... and N more messages after the held ones. Messages held when an email fails to send stay held, and the new one is held with them.
Nothing is sent on a timer: held messages go out with the next email, when "flush" is called, or when the logger is destroyed (which calls "flush"). A clone made with "new" starts with none held.
Per-backend level and format
Every backend can have its own level and format, as well as the logger's. syslog, journald and sendmail are hashes already, so they take them as keys. file, fd and array, inside a logger hash or at the top level, and log_any and log_dispatch, may be given as a hash holding the destination under the backend's own name:
my $log = Log::Abstraction->new(
level => 'debug',
logger => {
file => { file => '/var/log/myapp.log', format => 'json' },
fd => { fd => \*STDERR, level => 'warning' },
array => { array => \@recent, level => 'info' },
syslog => { level => 'error', format => '%level%: %message%' },
},
);
level-- the backend only gets messages at this level or more severe. A level name or a syslog number (0-7). The logger'slevelis applied first, so a backend's level can only narrow it: set the logger'slevelto the most verbose any backend wants.format-- a format string, orjson, as for the logger's "format", which it overrides. Forfileandfdit is the line written. For the others, which by default get the message as it is, it replaces the message: thearrayentry'smessage, the text sent to syslog, Log::Any or Log::Dispatch, the journal'sMESSAGEfield and the email body.
The plain forms (file => $path, fd => $handle, array => \@array, log_any => $category, log_dispatch => $dispatcher) still work and have no level or format of their own. A blessed handle object is a destination, not the hash form.
Forwarding to Log::Any and Log::Dispatch
Log::Any::Adapter::Abstraction brings messages from Log::Any into Log::Abstraction. The log_any and log_dispatch backends go the other way, sending messages on to an application's existing Log::Any or Log::Dispatch set-up, alongside any other backends:
my $log = Log::Abstraction->new(
level => 'debug',
logger => {
log_any => 'MyApp', # a category, or a Log::Any logger
log_dispatch => $dispatcher, # a Log::Dispatch object
file => '/var/log/myapp.log',
},
);
Levels keep their names, except that warn becomes warning, and Log::Dispatch, which has nothing below debug, gets trace messages as debug. Log::Any gets "Structured fields" as its trailing hashref, so a structured adapter receives them as data; Log::Dispatch has no fields, so they are appended to the message as text. With its own format (see "Per-backend level and format") a backend gets the formatted line instead.
A Log::Any category name is looked up with Log::Any->get_logger(category => $name) (which needs Log::Any installed) when the first message is sent, and follows later Log::Any::Adapter->set() calls. If Log::Any or Log::Dispatch dies, Carp::carp is called and the other backends still get the message.
A Log::Any::Proxy or Log::Dispatch object given as the logger itself is used in the same way, rather than as an object whose method is called for each level: neither has a method for every level, and both would join the arguments with spaces.
A message that reaches a Log::Abstraction while it is being forwarded (e.g. through Log::Any::Adapter::Abstraction) is logged by that logger's other backends but not forwarded again, so a set-up that points back at itself doesn't loop.
File rotation
Log files written by path (a scalar logger, a file key in a logger hash, and the top-level file) can be rotated by size, by time, or both:
my $log = Log::Abstraction->new(
file => '/var/log/myapp.log',
rotate_size => '10M',
rotate_interval => 'daily',
rotate_keep => 7,
);
Before each write the file is checked, and if it is due it is renamed to myapp.log.1, the old .1 to .2 and so on, the oldest beyond rotate_keep being deleted; the line then goes to a new myapp.log. A rotation that fails (e.g. for lack of permission) is ignored, and the line is still written. File handles passed as fd aren't rotated.
Rotation isn't coordinated between processes: if several processes log to the same file, use logrotate instead.
logrotate
The file is kept open between messages, but before each write the logger checks that the path still names the same file (device and inode), so logrotate's default (rename the file and let the application create a new one) works without copytruncate, and without sending the process a SIGHUP: the next message is written to a new file.
A postrotate script that sends the process a SIGHUP works too: except on Windows, the first time a logger opens a file, Log::Abstraction sets $SIG{HUP} to a handler that makes every logger open its files again on the next write. A handler that was already set (a code reference or a sub name) is called after it. This means a SIGHUP no longer ends the program, as it does by default, while a logger that has opened a file exists. When the last of them is destroyed, the handler that was there before is put back. A handler that the program sets later replaces Log::Abstraction's (and isn't changed when the loggers are destroyed); it can call "reopen" itself.
In case a SIGHUP isn't sent, every file is also closed and opened again every 5 minutes, by default. reopen_interval changes how often, makes SIGHUP the only way (-1), or closes the file after every message (0), as before version 0.40.
On Windows an open file can't be renamed or deleted, so a file the logger is writing can only be rotated by the logger itself (which closes it first), or after calling "reopen", and then only until the next message.
METHODS
new
my $logger = Log::Abstraction->new(%args);
my $logger = Log::Abstraction->new(\%args);
my $logger = Log::Abstraction->new($file_path);
# Clone with optional overrides
my $clone = $logger->new(level => 'debug');
Creates a new Log::Abstraction instance, or clones an existing one when called on an object. It may also be called as a plain function, Log::Abstraction::new(%args), which behaves like Log::Abstraction->new(%args).
Arguments
carp_on_warnIf set to 1, and no
loggeris given, callCarp::carponwarn(). Also causeserror()tocarpifcroak_on_erroris not set.croak_on_errorIf set to 1, and no
loggeris given, callCarp::croakonerror().config_filePath to a configuration file (YAML, XML, INI, etc.) whose contents are merged with the constructor arguments. On non-Windows systems the class can also be configured via environment variables prefixed with
"Log::Abstraction::". For example:export Log::Abstraction::script_name=fooctxArbitrary context value passed through to CODE-ref logger callbacks as
$args->{ctx}.formatFormat string for the file, fd and scalar-path backends; a backend's own
format(see "Per-backend level and format") overrides it for that backend. Unset or an empty string means the default,%level%> [%timestamp%] %class% %callstack% %message%. Tokens expanded at log time:%callstack% caller file and line number %class% blessed class of the logger object %level% upper-cased level name %message% the joined log message %timestamp% the time of the call; YYYY-MM-DD HH:MM:SS local time by default (see timestamp_format, timestamp_precision and utc) %env_FOO% value of $ENV{FOO}, or empty string if unsetTokens are only expanded in the format string itself, never in the text of the message. Each line break in a message is followed by a tab, so a continuation line can't be mistaken for a new log entry.
The special value
"json"(not a format string but a magic keyword) switches all file and fd backends to emit one compact JSON object per log line:{"timestamp":"...","level":"info","message":"...","file":"...","line":42}This format is compatible with log aggregators such as journald, Loki, Elasticsearch, and Splunk.
classis included when the logger is a subclass ofLog::Abstraction, andfieldswhen the call has "Structured fields". Keys are emitted in sorted order.Security note: because a format may contain
%env_*%tokens, which expand to environment variables, avoid granting untrusted sources write access to config files that setformator any backend'sformat(see "Per-backend level and format").levelMinimum level at which to emit log entries. Defaults to
"warning". Valid values (case-insensitive):trace,debug,info/informational,notice,warn/warning,error/err,crit/critical/fatal,alert,emerg/emergency/panic.traceanddebugare the same threshold (see "LIMITATIONS"). It may also be an array reference, whose first element is used, as some configuration-file formats produce.max_messagesThe most entries to keep in the in-memory history returned by "messages"; when it is full, the oldest entry is discarded. Must be a non-negative integer;
0keeps no history at all. Unlimited by default, which in a long-running process means the history grows without bound.loggerOne of:
A code reference -- called with a hashref
{ class, file, line, level, message, ctx, fields }(ctxandfieldsonly when there are any)An object -- method matching the level name is called on it; a Log::Any logger or a Log::Dispatch object is used as the
log_anyorlog_dispatchbackend (see "Forwarding to Log::Any and Log::Dispatch")A hash reference -- may contain
file,array,fd,syslog,journald,sendmail,log_anyand/orlog_dispatchkeys, each of which may have its ownlevelandformat(see "Per-backend level and format")An array reference --
{ level, message }hashrefs are pushed onto it, with afieldskey when the call has "Structured fields"A scalar string -- treated as a file path to append to
When not supplied, Log::Log4perl is initialised as the default backend.
The
sendmailsub-hash supports:host,port,to,from,subject,level,format,min_interval,digest,digest_max,async.tois required. Withformat, the email body is the formatted line rather than the message.levelmay be a level name or a syslog number (0-7); without it, every message is emailed. At most one email is sent permin_intervalseconds per instance; the messages in between are dropped, unlessdigestis set, which sends them with the next email (see "Email digests"). If delivery fails,Carp::carpis called and the other backends still receive the message.Sending an email waits for the SMTP server, which can take seconds. With
async => 1, the email is sent by a child process (made withfork), and the log call returns at once. Thehostandportare still checked first. The logger can't know whether the child succeeded: the child carps a failure itself, and the message (and any digest sent with it) is lost rather than held. Emails sent close together are sent at the same time, so they may arrive in any order. The finished children are reaped the next time an email is sent and when a logger is destroyed; a program that reaps every child itself (e.g.waitpid(-1, ...)in a$SIG{CHLD}handler) may see them. On Windows, whoseforkis emulated with threads, whenforkfails, and for the email sent when perl exits, the email is sent in the log call as withoutasync.The
syslogsub-hash supports the keys below. The message is passed tosyslog()through a%sformat, so%sequences in it, such as%m, are logged literally.facility-- the syslog facility (default:local0)level-- only messages at this level or more severe are sent; a level name or a syslog number (0-7)format-- format the message with this (see "format") before sending it; by default the message is sent as it ishost(or its aliasserver), and any other "setlogsock" in Sys::Syslog option -- passed tosetlogsock()
The
journaldsub-hash sends each message as a single datagram to the systemd journal using the journald native protocol. Supported keys:socket-- path to the journald socket (default: /run/systemd/journal/socket)identifier-- value for theSYSLOG_IDENTIFIERfield (default: basename of$0)level-- only messages at this level or more severe are sent; a level name or a syslog number (0-7)format-- theMESSAGEfield is the message formatted with this (see "format"); by default it is the message as it isany other key -- included verbatim as an uppercase journald field name. The upper-cased name must contain only
A-Z,0-9and_, and must not start with_;new()croaks otherwise.
The
PRIORITYfield is set automatically from the log level (0=emerg...7=debug). A message too large for one datagram (about 200KB) is truncated and[truncated]appended. Delivery failures are silent apart from a singleCarp::carp(repeated only after a later send has succeeded); the application is never crashed by a journald error. The socket is opened for the first message and reused for the next ones; after a failed send it is closed, and the next message opens a new one. Each datagram is addressed to the socket path, so delivery continues after journald restarts.redactPatterns to remove from every message before it is logged: a
qr//, a string (compiled as a regular expression; a config file can't hold aqr//), or an array reference of them. See "Redaction".reopen_intervalLog files written by path are kept open between messages. Every this many seconds (default 300, i.e. 5 minutes) each one is closed and opened again on its next write, in case a rotation tool changed the file without renaming it and without sending a
SIGHUP(see "logrotate").0closes the file after every message, so it is never held open, and noSIGHUPhandler is installed for it.-1never reopens the file because of time: only aSIGHUP, "reopen", or the file being renamed or deleted does. (Windows has noSIGHUP.)rotate_intervalRotate log files by time:
hourly,daily,weekly(weeks start on Monday) ormonthly, case-insensitive. Before each write, a file whose last-modified time is in an earlier period than now (in local time, or UTC withutc) is rotated, so a file not written to for a while rotates on the next write. See "File rotation".rotate_keepHow many rotated files to keep, FILE.1 to FILE.n (default 5). With
0, a file due for rotation is deleted instead.rotate_sizeRotate log files that have reached this size: a number of bytes, optionally followed by
K,MorG(powers of 1024), e.g.10M. See "File rotation".script_nameScript name reported to syslog. Auto-detected from
$0if not supplied.timestamp_formatHow
%timestamp%, and thetimestampkey offormat => 'json', are written. Either a "strftime" in POSIX pattern (default%Y-%m-%d %H:%M:%S) or one of these names (case-insensitive):iso8601, rfc3339 2026-10-03T20:14:23-04:00, or 2026-10-04T00:14:23Z with utcThe pattern may also use:
%N fractional seconds, 9 digits (nanoseconds) %3N fractional seconds, 3 digits (milliseconds); any width 1-9 %z UTC offset as +hhmm (on every platform, unlike some strftimes) %:z UTC offset as +hh:mm, as RFC 3339 needs %Z the time-zone name; "UTC" when utc is set %% a literal %Fractional seconds come from Time::HiRes and are truncated, not rounded; digits beyond the system clock's resolution (usually microseconds) are noise. The timestamp is taken once per message, so every backend shows the same time.
timestamp_precisionThe number of fractional-second digits, 0-9 (default 0), added after the seconds (each
%S) of whichevertimestamp_formatis in use:Log::Abstraction->new(timestamp_format => 'rfc3339', timestamp_precision => 3, utc => 1); # 2026-10-04T00:14:24.094ZutcIf true, timestamps are in UTC rather than local time.
verboseWhen using the default Log::Log4perl backend, raises the logging level to DEBUG when set to a true value.
Returns
A blessed Log::Abstraction object.
Side Effects
Loads File::Basename if syslog is configured (either at the top level or in a logger hash) and script_name is not supplied. Loads Log::Log4perl if no backend (logger, file, fd or array) is specified.
Builds one Log::Abstraction::Backend object for each place messages go to. The arguments are not changed: in particular, the syslog hash keeps its facility, level and format keys, and a server key is not renamed host (before 0.40, the first message did both).
Example
my $logger = Log::Abstraction->new(
level => 'debug',
logger => \@messages,
);
my $clone = $logger->new(level => 'info');
API SPECIFICATION
Input
{
carp_on_warn => { type => 'boolean', optional => 1 },
config_file => { type => 'string', optional => 1 },
croak_on_error => { type => 'boolean', optional => 1 },
ctx => { optional => 1 },
format => { type => 'string', optional => 1 },
level => { type => 'string', regex => qr/^(trace|debug|info(?:rmational)?|notice|warn(?:ing)?|err(?:or)?|crit(?:ical)?|fatal|alert|emerg(?:ency)?|panic)$/i, optional => 1 },
logger => { optional => 1 },
max_messages => { type => 'integer', min => 0, optional => 1 },
redact => { optional => 1 }, # regex, string, or arrayref of them
reopen_interval => { type => 'integer', min => -1, optional => 1 },
rotate_interval => { type => 'string', regex => qr/^(hourly|daily|weekly|monthly)$/i, optional => 1 },
rotate_keep => { type => 'integer', min => 0, optional => 1 },
rotate_size => { type => 'string', regex => qr/^\s*[1-9]\d*\s*[kmg]?b?\s*$/i, optional => 1 },
script_name => { type => 'string', optional => 1 },
timestamp_format => { type => 'string', min => 1, optional => 1 },
timestamp_precision => { type => 'integer', min => 0, max => 9, optional => 1 },
utc => { type => 'boolean', optional => 1 },
verbose => { type => 'boolean', optional => 1 },
}
Output
{ type => 'object', class => 'Log::Abstraction' }
MESSAGES
Error Meaning / Action
---------------------------------------- -----------------------------------------
"<class>: <path>: File not readable" config_file path exists but is unreadable.
Check file permissions.
"<class>: Can't load configuration Config::Abstraction could not parse the
from <path>" file. Check syntax and format.
"<class>: syslog needs to know the syslog backend requested but script_name
script name" could not be determined. Pass it explicitly.
"<class>: attempt to encapsulate logger => Log::Abstraction would create
Log::Abstraction as a logging class, a needless forwarding loop. Use a
that would add a needless indirection" different backend.
"<class>: invalid syslog level '<l>'" level value is not a recognised syslog
level name. Use trace/debug/info/notice/
warn/warning/error.
"<class>: max_messages must be a max_messages is negative or not a number.
non-negative integer, not '<v>'"
"<class>: rotate_size must be a rotate_size is not, e.g., 1048576, 512K,
positive number of bytes, optionally 10M or 1G.
with K, M or G, not '<v>'"
"<class>: rotate_interval must be rotate_interval is not one of those names.
hourly, daily, weekly or monthly,
not '<v>'"
"<class>: rotate_keep must be a rotate_keep is negative or not a number.
non-negative integer, not '<v>'"
"<class>: reopen_interval must be a reopen_interval is not a whole number
number of seconds, 0 or -1, not '<v>'" of seconds, 0 (close after every
message) or -1 (only on SIGHUP).
"<class>: timestamp_format must be a timestamp_format is undef, empty or a
non-empty string" reference.
"<class>: timestamp_precision must be timestamp_precision is not a whole
an integer from 0 to 9, not '<v>'" number of digits from 0 to 9.
"<class>: redact patterns must be A redact entry is a reference other than
regular expressions or non-empty a qr//, or an empty string.
strings"
"<class>: invalid redact pattern '<p>': A redact string is not a valid regular
<error>" expression.
"<class>: redact pattern <p> matches the The pattern can match nothing at all
empty string" (e.g. qr/x*/), which would put a marker
between every character.
"<class>: invalid <backend> level '<l>'" A backend's 'level' (file, fd, array,
sendmail, journald) is neither a level
name nor 0-7. (A bad syslog 'level'
gives "invalid syslog level", as above.)
"<class>: the <backend> format must be A backend's 'format' is undef, empty or
a non-empty string" a reference.
"<class>: the <backend> hash needs a The hash form of file, fd, array, log_any
'<backend>' key" or log_dispatch has no destination (e.g.
file => { level => 'info' } without a
'file' key).
"<class>: the sendmail backend needs The sendmail sub-hash has no 'to' key.
a 'to' address"
"<class>: sendmail digest_max must be a digest_max is zero, negative or not a
positive integer, not '<v>'" number.
"<class>: invalid journald field name An extra journald key, upper-cased, is not
'<k>'" [A-Z0-9_] or starts with '_'.
"<class>: the logger hash has none of The logger hash has nowhere to send
the keys array, fd, file, journald, messages. (Before 0.40, the first
log_any, log_dispatch, sendmail, message croaked "Don't know how to deal
syslog" with the <level> message".)
"<class>: the logger can't be a <TYPE> logger is a reference of an unsupported
reference" type, e.g. a SCALAR or GLOB reference.
"<class>: log_any must be a Log::Any log_any is neither a Log::Any logger
logger or a category name" (Log::Any::Proxy) nor a non-empty string.
"<class>: log_any needs Log::Any to be log_any is a category name but Log::Any
installed" can't be loaded.
"<class>: log_dispatch must be a log_dispatch is not an object with a
Log::Dispatch object" log() method.
The following are not raised by new() but later, by the logging methods (trace, debug, info, notice, warn, error, fatal, critical, alert, emergency), when a message that passes the level threshold reaches the backend concerned. Croaks are configuration errors; delivery failures only carp, because a logging failure must never crash the application.
Croak Meaning / Action
---------------------------------------- -----------------------------------------
"<class>: Invalid file name: <path>" A file path (logger string, 'file' key or
logger hash 'file') contains one of
< > | * ? ; ! ` $ " or a control
character, or contains '..'.
"<class>: Invalid SMTP host: <host>" The sendmail 'host' contains characters
other than A-Z a-z 0-9 . -
"<class>: Invalid SMTP port: <port>" The sendmail 'port' is not an integer in
1-65535.
"<class>: <object class> doesn't know An object logger has no method for this
how to deal with the <level> message" level. (notice falls back to info.)
Carp Meaning / Action
---------------------------------------- -----------------------------------------
"Failed to send email: <error>" SMTP delivery failed. The other backends
still receive the message.
"<class>: syslog failed: <error>" Sys::Syslog::syslog() died.
"<class>: journald send failed: <error>" The journald socket could not be reached.
Given once, then not again until a send
succeeds.
"<class>: Log::Any failed: <error>" Log::Any (or its adapter) died. The other
"<class>: Log::Dispatch failed: <error>" backends still receive the message.
PSEUDOCODE
FUNCTION new(class_or_obj, args...)
Parse args:
IF single non-hash scalar
THEN store as logger shorthand
ELSE extract named params via Params::Get
IF config_file present:
CROAK if file is not readable
Load via Config::Abstraction, merge into args (constructor args win)
Restore caller-supplied array ref that config merge would have dropped
IF called on a blessed instance (clone form):
CROAK on an invalid timestamp_format or timestamp_precision
CROAK on an invalid rotate_size, rotate_interval, rotate_keep or
reopen_interval, and normalise rotate_size to bytes
CROAK on an invalid redact pattern; compile redact to one regex
shallow-clone self merged with override args
validate and store new level integer if level given in args
copy message history list
start the clone with no held email digest, no open files, and not
counted as a user of the SIGHUP handler
count the clone as a user of an open syslog connection
build the clone's own backends
RETURN clone
IF syslog requested (top level or in a logger hash) and script_name
not supplied:
auto-detect script name via File::Basename
CROAK if still undefined
IF logger arg is a Log::Abstraction object:
CROAK (would create a needless forwarding loop)
IF no logger AND no file AND no fd AND no array:
load Log::Log4perl, easy_init at DEBUG or ERROR per verbose flag
store Log4perl logger as the backend
Normalise and validate level:
IF level is an arrayref, take first element
lc() the level string
CROAK if not in syslog_values lookup
default to $DEFAULT_LEVEL if not supplied
CROAK if max_messages is given and is not a non-negative integer
CROAK if timestamp_format is empty or not a string, or
timestamp_precision is not an integer 0-9
CROAK if rotate_size is not a positive size, rotate_interval is not
hourly/daily/weekly/monthly, or rotate_keep or reopen_interval is not
a non-negative integer; normalise rotate_size to bytes
CROAK if a redact pattern is not a regex or non-empty string, doesn't
compile, or matches the empty string; compile redact to one regex
FOR each backend (top-level file/fd/array, and the logger hash's
file/fd/array/syslog/sendmail/journald/log_any/log_dispatch):
IF log_any: CROAK unless a Log::Any::Proxy, or a category name with
Log::Any installed
IF log_dispatch: CROAK unless an object with a log() method
SKIP the rest unless given as a hash
CROAK if a file/fd/array/log_any/log_dispatch hash lacks its
destination key (its own name)
CROAK if its 'level' is not a level name or 0-7
CROAK if its 'format' is undef, empty or not a string
IF logger is a hash:
CROAK if a sendmail sub-hash has no 'to' address, or a digest_max
that isn't a positive integer
CROAK if an extra journald key is not a valid journald field name
CROAK if it has none of the backend keys
self = bless { messages => [], merged args, level => numeric } as class
self->{_backends} = _build_backends() (CROAK on an unsupported logger)
RETURN self
END FUNCTION
level
my $current = $logger->level();
$logger->level('debug');
Get or set the minimum logging level. When setting, returns $self to allow method chaining. When getting, returns the current level as an integer (per the syslog numeric scale; lower numbers are higher priority).
Arguments
$level(optional)A level name string:
trace,debug,info,notice,warn/warning, orerror. Case-insensitive. Omit to perform a pure get.
Returns
In getter mode: an integer in the range 0 (emergency) to 7 (debug/trace).
In setter mode: $self (to allow chaining), or undef, after a Carp::carp, if the level name is not recognised; the level is then unchanged. A false argument (undef, '' or 0) is a get, not a set, so levels are set by name.
Side Effects
When setting, updates $self->{level}.
Example
$logger->level('debug');
my $n = $logger->level(); # e.g. 7
# Method chaining
$logger->level('info')->info('Now at info level');
API SPECIFICATION
Input
{
level => { type => 'string', regex => qr/^(trace|debug|info(?:rmational)?|notice|warn(?:ing)?|err(?:or)?|crit(?:ical)?|fatal|alert|emerg(?:ency)?|panic)$/i, optional => 1 },
}
Output
Getter: { type => 'integer', min => 0, max => 7 }
Setter: { type => 'object', class => 'Log::Abstraction' }
MESSAGES
Warning Meaning / Action
---------------------------------------- ------------------------------------------
"<class>: invalid syslog level '<l>'" The supplied level name is not recognised.
Use trace/debug/info/notice/warn/error.
PSEUDOCODE
FUNCTION level(self, level?)
IF level argument supplied:
CARP and RETURN undef if level is not a recognised syslog name
Store syslog_values{level} in self->{'level'}
RETURN self (allows method chaining)
ELSE (getter mode):
RETURN self->{'level'} (current numeric threshold)
END FUNCTION
Level detection methods
- is_trace
- is_debug
- is_info
- is_notice
- is_warn
- is_error
- is_critical
- is_alert
- is_emergency
if($logger->is_debug()) { ... }
Each returns a true value when a message logged with the method of the same name (is_warn for warn()) would pass the logger's level threshold, so that expensive message-building can be skipped. They follow the current threshold, including changes made with "level". As with the levels themselves, is_trace equals is_debug. Provided for compatibility with Log::Any.
Arguments
None.
Returns
1 if messages at that level would be emitted; 0 otherwise.
Example
if($logger->is_debug()) {
$logger->debug('Expensive diagnostic: ' . Dumper(\%state));
}
$logger->level('warning');
$logger->is_warn(); # 1
$logger->is_info(); # 0
API SPECIFICATION
Input
{} (no arguments)
Output
{ type => 'boolean' }
messages
my $aref = $logger->messages();
Returns a reference to a shallow copy of all messages emitted through this logger since it was created (or since the last clone).
Arguments
None.
Returns
An array reference of hashrefs, each with keys level (string) and message (string), and fields (hashref) when the message was logged with "Structured fields".
Side Effects
None. The returned array is a copy; modifying it does not affect the internal history.
Example
$logger->info('hello');
my $msgs = $logger->messages();
# $msgs->[0] = { level => 'info', message => 'hello' }
API SPECIFICATION
Input
{} (no arguments)
Output
{ type => 'arrayref', element_type => { level => 'string', message => 'string', fields => 'hashref?' } }
flush
$logger->flush();
Sends, now, the messages that a sendmail backend with digest is holding back because of min_interval (see "Email digests"), whether or not the interval has passed. Does nothing if none are held. Called automatically when the logger is destroyed.
Arguments
None.
Returns
The logger, for method chaining.
Side Effects
May send an email, which starts the min_interval interval again. A delivery failure is carped and the messages stay held. With async, the email is sent by a child process and the messages are no longer held: a failure is carped by the child, and the messages are lost. Croaks if the sendmail host or port is invalid.
Example
$logger->error('disk full'); # emailed
$logger->error('disk still full'); # held: within min_interval
$logger->flush(); # emailed now
API SPECIFICATION
Input
{} (no arguments)
Output
{ type => 'object', class => 'Log::Abstraction' }
reopen
$logger->reopen();
Closes the log files the logger holds open, so that the next message opens them again. Log files written by path (see "File rotation") are kept open between messages; call this after something outside the program has renamed, moved or truncated them. It isn't usually needed: a file that has been renamed or deleted is noticed on the next write, a SIGHUP reopens every logger's files (except on Windows, see "logrotate"), and every file is reopened when reopen_interval has passed (unless it is -1). File handles passed as fd are not closed. It does nothing if no files are open.
A file is also closed when writing to it fails (e.g. the disk is full), so that the next message opens it again.
A program that sets its own $SIG{HUP} after logging has started replaces the module's handler, and can call this from its own:
$SIG{HUP} = sub { $logger->reopen(); reload_config() };
Arguments
None.
Returns
The logger, for method chaining.
Side Effects
Closes the logger's log files.
API SPECIFICATION
Input
{} (no arguments)
Output
{ type => 'object', class => 'Log::Abstraction' }
trace
$logger->trace(@messages);
$logger->trace(\@messages);
Logs a message at trace level. syslog has no priority below debug, so trace shares debug's threshold: trace messages are emitted whenever debug messages are, and are sent to syslog and journald as debug. The message is dropped silently when the configured level is above debug.
Arguments
@messagesOne or more strings, or a single array reference. All elements are joined without a separator before storage. May be followed by a hashref of "Structured fields".
Returns
$self, to allow method chaining.
Side Effects
Appends to the internal message history and dispatches to configured backends.
Example
$logger->trace('entering sub foo, args=', join(',', @args));
# Chaining
$logger->trace('start')->debug('details')->info('summary');
API SPECIFICATION
Input
{ messages => { type => [ 'arrayref', 'scalar' ] } }
Output
{ type => 'object', class => 'Log::Abstraction' }
MESSAGES
Croaks if the configured backend is misconfigured, and carps if delivery fails; see the second table under "new"'s MESSAGES.
debug
$logger->debug(@messages);
$logger->debug(\@messages);
Logs a message at debug level.
Arguments
@messagesOne or more strings, or a single array reference, optionally followed by a hashref of "Structured fields".
Returns
$self, to allow method chaining.
Side Effects
Appends to the internal message history and dispatches to configured backends.
Example
$logger->debug('Query took ', $elapsed, 'ms');
API SPECIFICATION
Input
{ messages => { type => [ 'arrayref', 'scalar' ] } }
Output
{ type => 'object', class => 'Log::Abstraction' }
MESSAGES
Croaks if the configured backend is misconfigured, and carps if delivery fails; see the second table under "new"'s MESSAGES.
info
$logger->info(@messages);
$logger->info(\@messages);
Logs a message at info level.
Arguments
@messagesOne or more strings, or a single array reference, optionally followed by a hashref of "Structured fields".
Returns
$self, to allow method chaining.
Side Effects
Appends to the internal message history and dispatches to configured backends.
Example
$logger->info('Server started on port ', $port);
API SPECIFICATION
Input
{ messages => { type => [ 'arrayref', 'scalar' ] } }
Output
{ type => 'object', class => 'Log::Abstraction' }
MESSAGES
Croaks if the configured backend is misconfigured, and carps if delivery fails; see the second table under "new"'s MESSAGES.
notice
$logger->notice(@messages);
$logger->notice(\@messages);
Logs a message at notice level (higher priority than info, lower than warn).
Arguments
@messagesOne or more strings, or a single array reference, optionally followed by a hashref of "Structured fields".
Returns
$self, to allow method chaining.
Side Effects
Appends to the internal message history and dispatches to configured backends.
Example
$logger->notice('Configuration reloaded');
API SPECIFICATION
Input
{ messages => { type => [ 'arrayref', 'scalar' ] } }
Output
{ type => 'object', class => 'Log::Abstraction' }
MESSAGES
Croaks if the configured backend is misconfigured, and carps if delivery fails; see the second table under "new"'s MESSAGES.
warn
$logger->warn(@messages);
$logger->warn(\@messages);
$logger->warn(warning => $text);
$logger->warn({ warning => $text });
$logger->warn(warning => \@parts);
$logger->warn($text, \%fields);
Logs a warning message. Also dispatches to syslog and/or email backends when those are configured. Falls back to Carp::carp when no backend (logger, array, file or fd) is set. The Carp::carp (whether from carp_on_warn or the fallback) only happens when the message passes the level threshold.
Called as a class method (Log::Abstraction->warn(...), or on a subclass), it calls Carp::carp directly.
A warn() call with an empty or all-undef argument list is a silent no-op.
Arguments
@messagesA plain list of strings joined without separator, or a named
warningparameter whose value may be a string or an array reference of strings. Either form may be followed by a hashref of "Structured fields", e.g.warn('Slow query', { ms => 1250 }).
Returns
$self, to allow method chaining.
Side Effects
Appends to internal message history. Writes to all configured backends. May call Carp::carp if carp_on_warn is set or no backend is active.
Example
$logger->warn('Disk usage is high');
$logger->warn(warning => 'Connection reset', ' retrying');
$logger->warn({ warning => ['Part A', 'Part B'] });
API SPECIFICATION
Input
# Named form
{ warning => { type => [ 'scalar', 'arrayref' ] } }
# Plain-list form
{ messages => { type => 'arrayref' } }
Output
{ type => 'object', class => 'Log::Abstraction' }
MESSAGES
(the warning text itself) Carped if carp_on_warn is set, or if no
backend (logger, array, file or fd) is
configured, provided the warning passes
the level threshold. Also carped when
called as a class method.
Backend misconfiguration and delivery failures are reported as described in the second table under "new"'s MESSAGES.
error
$logger->error(@messages);
$logger->error(warning => $text);
$logger->error($text, \%fields);
Logs an error-level message. Behaves identically to warn() but at the error level, which triggers Carp::croak if croak_on_error is set or no backend (logger, array, file or fd) is set. Called as a class method, it calls Carp::croak directly.
Arguments
Same argument forms as warn().
Returns
$self, to allow method chaining. Note: if croak_on_error is set, the method never returns -- execution unwinds via Carp::croak.
Side Effects
Same as warn() plus optional Carp::croak escalation.
Example
$logger->error('Fatal: database unavailable');
API SPECIFICATION
Input
{ warning => { type => [ 'scalar', 'arrayref' ], optional => 1 } }
Output
{ type => 'object', class => 'Log::Abstraction' }
MESSAGES
Croak Meaning / Action
---------------------------------------- ------------------------------------------
(the error message text itself) croak_on_error is set, or no backend
(logger, array, file or fd) is
configured, or error() was called as a
class method. The call stack is unwound.
(the error message text itself), as a carp_on_warn is set and croak_on_error
carp is not.
Backend misconfiguration and delivery failures are reported as described in the second table under "new"'s MESSAGES.
fatal
$logger->fatal(@messages);
Synonym for error(). Provided for compatibility with logging frameworks that use fatal as the highest-severity level name.
Arguments
Same as error().
Returns
$self.
Side Effects
Same as error().
Example
$logger->fatal('Unrecoverable state; aborting');
API SPECIFICATION
Input
{ warning => { type => [ 'scalar', 'arrayref' ], optional => 1 } }
Output
{ type => 'object', class => 'Log::Abstraction' }
MESSAGES
Same as error().
Methods above error
- critical
- alert
- emergency
$logger->critical(@messages);
$logger->alert(warning => $text);
$logger->emergency($text, \%fields);
Log a message at a level more severe than error:
Method Level syslog Priority
---------- ---------- ------- --------
critical critical crit 2
alert alert alert 1
emergency emergency emerg 0
Arguments
critical, alert and emergency take the same argument forms as warn().
Returns
$self, to allow method chaining (unless they croak; see below).
Side Effects
These behave like error(), at a more severe level: croak_on_error, or having no backend, makes them Carp::croak, and carp_on_warn makes them Carp::carp. The level string passed to backends is the method name (critical, alert or emergency, upper-cased in text formats); syslog gets crit, alert or emerg, and journald PRIORITY 2, 1 or 0. An object logger without the method (such as Log::Log4perl) is called with fatal, or error if it has no fatal either.
Example
$logger->critical('Disk 95% full', { mount => '/var' });
$logger->alert('Primary database unreachable');
$logger->emergency('Data corruption detected; shutting down');
API SPECIFICATION
Input
{ warning => { type => [ 'scalar', 'arrayref' ], optional => 1 } }
Output
{ type => 'object', class => 'Log::Abstraction' }
MESSAGES
Same as error().
EXAMPLES
CSV file logging for BI import
The code-reference backend gives you full control over the output format. The example below writes every message at trace level and above as a CSV row to a file, producing output that can be loaded directly into a spreadsheet or BI tool (Tableau, Power BI, Metabase, etc.).
Each row contains: timestamp, level, class, file, line, message.
use Log::Abstraction;
my $csv_file = 'app_events.csv';
# Write the header row once (skip if the file already exists and has data).
unless (-s $csv_file) {
open my $fh, '>', $csv_file or die "Cannot open $csv_file: $!";
print $fh qq{timestamp,level,class,file,line,message\n};
close $fh;
}
# Helper: quote a single CSV field (escapes embedded double-quotes).
my $csv_field = sub {
my $v = defined $_[0] ? $_[0] : '';
$v =~ s/"/""/g;
return qq{"$v"};
};
my $logger = Log::Abstraction->new(
level => 'trace', # capture everything from trace upwards
logger => sub {
my $args = $_[0];
my $timestamp = POSIX::strftime('%Y-%m-%dT%H:%M:%SZ', gmtime);
my $message = join(' ', @{ $args->{message} // [] });
open my $fh, '>>', $csv_file or return;
print $fh join(',',
$csv_field->($timestamp),
$csv_field->($args->{level}),
$csv_field->($args->{class}),
$csv_field->($args->{file}),
$csv_field->($args->{line}),
$csv_field->($message),
), "\n";
close $fh;
},
);
$logger->trace('application started');
$logger->info('user logged in', { user => 'alice' });
$logger->warn({ warning => 'disk usage above 80%' });
The resulting app_events.csv looks like:
timestamp,level,class,file,line,message
"2026-05-27T14:00:00Z","trace","Log::Abstraction","app.pl","42","application started"
"2026-05-27T14:00:01Z","info","Log::Abstraction","app.pl","43","user logged in"
"2026-05-27T14:00:02Z","warn","Log::Abstraction","Log/Abstraction.pm","820","disk usage above 80%"
Note: class is always Log::Abstraction (or the subclass name if you subclass the module). For trace, debug, info, and notice calls, file and line resolve to the caller's source location. For warn and error calls the extra _high_priority stack frame shifts the resolution one level inward, so file and line point into the module rather than the calling script.
For production use, consider replacing the manual $csv_field quoting with Text::CSV for correct handling of embedded newlines and other edge cases.
If you also want real-time alerting on critical events, add the email logic directly inside the code-ref callback -- test $args->{level} and call your mailer for warn / error messages while still writing the CSV row for every message.
Alternatively, use the sendmail hash-ref backend on its own (without the code-ref) and add a level key to restrict emails to warn-and-above:
my $logger = Log::Abstraction->new(
level => 'warn',
logger => {
sendmail => {
host => 'smtp.example.com',
to => 'ops@example.com',
from => 'logger@example.com',
subject => 'Application alert',
level => 'warn', # only email at warn level and above
min_interval => 300, # at most one alert email per 5 minutes
},
},
);
Note: the sendmail backend writes the module's standard text format, not CSV. To produce CSV rows and send email alerts from the same logger, embed both the CSV-write and the mail-send logic inside a single code-ref callback as described above.
LIMITATIONS
- trace is the same threshold as debug
-
syslog has no priority below debug, so
traceanddebugshare one threshold: a logger atdebuglevel also emitstracemessages, andtracecan't be filtered separately. - Unbounded message history by default
-
Every logged message is kept in the history returned by "messages". In a long-running process (a daemon, or under mod_perl) set
max_messagesto stop it growing without bound. -
openlog()andcloselog()act on the whole process, so every instance logging to syslog shares one connection, opened with thescript_nameof the first. It is closed when the last such instance is destroyed. - Structured fields are text in most backends
-
Only the history, array, CODE-ref, JSON, journald and Log::Any backends keep "Structured fields" as data. Text formats, syslog, email, Log::Dispatch and object loggers get them as
key=valuetext appended to the message, and a customformathas no token for them on their own. - Process-wide state, and no thread safety
-
Some state belongs to the whole process, not to a logger: the syslog connection (above), the
$SIG{HUP}handler (see "logrotate"), and the list ofasyncemail children still to be reaped. The per-logger state (open log files, themin_intervalthrottle anddigest, the first-open syslog flag) is stored on the object without locking. Under Perl ithreads or other concurrency models, objects shared between threads are not safe. - Asynchronous email uses fork
-
The
sendmailasyncoption sends each email from a child made withfork. That may not suit every program: under mod_perl the child is a copy of the Apache process; with ithreads only the calling thread is copied; and in an event loop (AnyEvent, IO::Async, Mojo::IOLoop) the child is reaped by this module, not by the loop's child watcher. A program that reaps every child itself (e.g.waitpid(-1, ...)in a$SIG{CHLD}handler) sees these children too. The child's success or failure isn't reported back: a failure is carped by the child and the message is lost. Withoutasyncthe email is sent in the log call. - OpenTelemetry not yet supported
-
The OTel Logs SDK for Perl is incomplete; see the TODO block at the top of lib/Log/Abstraction.pm for a full status report and the list of blockers. Monitor https://metacpan.org/pod/OpenTelemetry::SDK for progress.
- Log::Log4perl is a de-facto required dependency
-
When no
logger,file,fdorarraybackend is configured,new()loads Log::Log4perl and uses it as the default backend, so it is a required dependency even for applications that never use it.
AUTHOR
Nigel Horne njh@nigelhorne.com
SEE ALSO
Log::Any and Log::Any::Adapter::Abstraction
Route messages from any
Log::Any-using CPAN module throughLog::Abstractionwith a singleLog::Any::Adapter->set()call, or send messages on to Log::Any with thelog_anybackend.-
The classes that send messages to each kind of backend.
-
Send messages on to a
Log::Dispatchobject with thelog_dispatchbackend (see "Forwarding to Log::Any and Log::Dispatch").
SUPPORT
This module is provided as-is without any warranty.
Please report any bugs or feature requests to bug-log-abstraction at rt.cpan.org, or through the web interface at http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Log-Abstraction. I will be notified, and then you'll automatically be notified of progress on your bug as I make changes.
You can find documentation for this module with the perldoc command.
perldoc Log::Abstraction
You can also look for information at:
MetaCPAN
RT: CPAN's request tracker
CPAN Testers' Matrix
CPAN Testers Dependencies
FORMAL SPECIFICATION
new
FIELDS == STRING ⇸ VALUE structured fields (see Structured fields)
ENTRY == { level : STRING; message : STRING; fields : FIELDS }
entry(l, m, f) == {level ↦ l, message ↦ ρ(m)} ∪ (if f = ∅ then ∅ else {fields ↦ ρ(f)})
ρ(x) == if redact = ∅ then x
else x with each match of redact replaced by '[REDACTED]'
(in strings, recursively in plain hashes and arrays)
┌─ LogState ──────────────────────────────────────────────────
│ level : ℤ
│ messages : seq ENTRY
│ max_messages : ℕ ∪ {∞}
│ logger : LOGGER
│ redact : REGEX ∪ {∅}
├─────────────────────────────────────────────────────────────
│ 0 ≤ level ≤ 7
│ #messages ≤ max_messages
└─────────────────────────────────────────────────────────────
┌─ New ───────────────────────────────────────────────────────
│ args? : Args
│ result! : LogState
├─────────────────────────────────────────────────────────────
│ result!.level = syslog_values(args?.level ∨ 'warning')
│ result!.messages = ⟨⟩
│ result!.max_messages = args?.max_messages ∨ ∞
│ result!.redact = ⋃ args?.redact {one regex matching any; ∅ if none}
│ args?.redact ≠ ∅ ⟹ ¬('' ∈ L(result!.redact))
│ args?.logger ≠ ∅ ⟹ result!.logger = args?.logger
│ args?.logger = ∅ ∧ args?.file = ∅ ∧ args?.fd = ∅ ∧ args?.array = ∅
│ ⟹ result!.logger = Log4perl
└─────────────────────────────────────────────────────────────
Clone operation (called on an existing object):
┌─ Clone ─────────────────────────────────────────────────────
│ ΔLogState
│ overrides? : Args
├─────────────────────────────────────────────────────────────
│ result!.level = syslog_values(overrides?.level ∨ level)
│ result!.messages = messages {new sequence; entries shared}
│ result!.logger = overrides?.logger ∨ logger
└─────────────────────────────────────────────────────────────
level
┌─ LevelGet ─────────────────────────────────────────────────
│ ΞLogState
│ result! : ℤ
├─────────────────────────────────────────────────────────────
│ result! = level
│ 0 ≤ result! ∧ result! ≤ 7
└─────────────────────────────────────────────────────────────
┌─ LevelSet ─────────────────────────────────────────────────
│ ΔLogState
│ new_level? : STRING
├─────────────────────────────────────────────────────────────
│ new_level? ∈ dom(syslog_values)
│ level' = syslog_values(new_level?)
└─────────────────────────────────────────────────────────────
┌─ LevelSetInvalid ──────────────────────────────────────────
│ ΞLogState
│ new_level? : STRING
│ result! : undef
├─────────────────────────────────────────────────────────────
│ new_level? ≠ ''
│ new_level? ¬in; dom(syslog_values)
│ carp("invalid syslog level")
└─────────────────────────────────────────────────────────────
level(new_level?) ≡ LevelSet ∨ LevelSetInvalid
is_trace, is_debug, is_info, is_notice, is_warn, is_error, is_critical, is_alert, is_emergency
┌─ IsLevel ──────────────────────────────────────────────────
│ ΞLogState
│ lvl? : LEVEL
│ result! : BOOLEAN
├─────────────────────────────────────────────────────────────
│ result! = (level ≥ syslog_values(lvl?))
└─────────────────────────────────────────────────────────────
is_<lvl> ≡ IsLevel[lvl? := lvl]
messages
┌─ Messages ─────────────────────────────────────────────────
│ ΞLogState
│ result! : seq ENTRY
├─────────────────────────────────────────────────────────────
│ result! = messages
└─────────────────────────────────────────────────────────────
flush
┌─ Flush ────────────────────────────────────────────────────
│ ΔLogState
│ result! : LogState
├─────────────────────────────────────────────────────────────
│ digest ≠ ⟨⟩ ∧ (sent(digest) ∨ async ∧ forked) ⟹
│ digest' = ⟨⟩ ∧ last_email_sent' = now
│ digest ≠ ⟨⟩ ∧ ¬sent(digest) ∧ ¬(async ∧ forked) ⟹ digest' = digest
│ digest = ⟨⟩ ⟹ digest' = digest
│ messages' = messages
│ result! = self
└─────────────────────────────────────────────────────────────
reopen
INTERVAL == ℤ reopen_interval: seconds, 0 or -1
HANDLE == { fh; dev; ino; sighup : ℕ; opened : ℕ }
┌─ OpenFiles ─────────────────────────────────────────────────
│ handles : PATH ⇸ HANDLE
│ reopen_interval : INTERVAL
├─────────────────────────────────────────────────────────────
│ reopen_interval ≥ -1
│ reopen_interval = 0 ⟹ handles = ∅ (between log calls)
└─────────────────────────────────────────────────────────────
A handle h on path p is used for the next write iff
stat(p) = (h.dev, h.ino) ∧ h.sighup = sighup_count
∧ (reopen_interval = -1 ∨ now - h.opened < reopen_interval)
and is otherwise closed and p opened again.
┌─ Reopen ────────────────────────────────────────────────────
│ ΔOpenFiles
│ result! : LogState
├─────────────────────────────────────────────────────────────
│ handles' = ∅
│ reopen_interval' = reopen_interval
│ result! = self
└─────────────────────────────────────────────────────────────
trace
┌─ Trace ────────────────────────────────────────────────────
│ ΔLogState
│ msg? : seq STRING
│ fields? : FIELDS
├─────────────────────────────────────────────────────────────
│ syslog_values('trace') ≤ level
│ messages' = messages ⌢ ⟨entry('trace', ⊕(msg?), fields?)⟩
└─────────────────────────────────────────────────────────────
debug
┌─ Debug ────────────────────────────────────────────────────
│ ΔLogState
│ msg? : seq STRING
│ fields? : FIELDS
├─────────────────────────────────────────────────────────────
│ syslog_values('debug') ≤ level
│ messages' = messages ⌢ ⟨entry('debug', ⊕(msg?), fields?)⟩
└─────────────────────────────────────────────────────────────
info
┌─ Info ─────────────────────────────────────────────────────
│ ΔLogState
│ msg? : seq STRING
│ fields? : FIELDS
├─────────────────────────────────────────────────────────────
│ syslog_values('info') ≤ level
│ messages' = messages ⌢ ⟨entry('info', ⊕(msg?), fields?)⟩
└─────────────────────────────────────────────────────────────
notice
┌─ Notice ───────────────────────────────────────────────────
│ ΔLogState
│ msg? : seq STRING
│ fields? : FIELDS
├─────────────────────────────────────────────────────────────
│ syslog_values('notice') ≤ level
│ messages' = messages ⌢ ⟨entry('notice', ⊕(msg?), fields?)⟩
└─────────────────────────────────────────────────────────────
warn
┌─ Warn ─────────────────────────────────────────────────────
│ ΔLogState
│ msg? : seq STRING | { warning : STRING | seq STRING }
│ fields? : FIELDS
├─────────────────────────────────────────────────────────────
│ msg? ≠ ∅ ∧ join(msg?) ≠ ''
│ syslog_values('warn') ≤ level
│ messages' = messages ⌢ ⟨entry('warn', join(msg?), fields?)⟩
│ (carp_on_warn ∨ no_backend) ⟹ carp(join(msg?))
└─────────────────────────────────────────────────────────────
no_backend ≡ logger = ∅ ∧ array = ∅ ∧ file = ∅ ∧ fd = ∅
Called as a class method (no LogState): carp(join(msg?)), and
messages is not touched.
error
┌─ Error ────────────────────────────────────────────────────
│ ΔLogState
│ msg? : seq STRING | { warning : STRING | seq STRING }
│ fields? : FIELDS
├─────────────────────────────────────────────────────────────
│ msg? ≠ ∅ ∧ join(msg?) ≠ ''
│ syslog_values('error') ≤ level
│ messages' = messages ⌢ ⟨entry('error', join(msg?), fields?)⟩
│ (croak_on_error ∨ no_backend) ⟹ execution_continues = false
└─────────────────────────────────────────────────────────────
Called as a class method (no LogState): croak(join(msg?)).
fatal
fatal ≡ error (identical operation schema)
critical, alert, emergency
The Error schema, with 'error' replaced by 'critical', 'alert' or
'emergency' respectively.
In every logging schema, when #messages' would exceed max_messages
the oldest entries are dropped: messages' = the last max_messages
entries. fields? is a hashref given after the message (see
Structured fields); fields? = ∅ when there is none.
COPYRIGHT AND LICENSE
Copyright (C) 2025-2026 Nigel Horne
Usage is subject to the GPL2 licence terms. If you use it, please let me know.