Revision history for Log-Abstraction

0.37	Sun Oct  4 09:43:19 PM EDT 2026
	[ Bug Fixes ]
	- journald: a datagram bigger than the system's Unix-socket send
	  buffer failed with "Message too long" (EMSGSIZE) and the message
	  was lost.  The send buffer is now raised where the kernel allows,
	  and on EMSGSIZE the message is truncated to half the size and the
	  send retried, down to 4,096 bytes.  Found by a CPAN smoker whose
	  buffer was under 200,000 bytes, failing t/regressions.t:
	  https://matrix.perl-magpie.org/results/3720262e-c005-11f1-a083-8730a1ea2842
	- Logging no longer changes the caller's $@ or $!, so code such as
	  eval { ... }; if($@) { $log->debug('failed'); die $@ } rethrows the
	  real error rather than an empty one.  The same applies to the
	  Log::Any adapter and to DESTROY.
	- A file path ending in a newline (e.g. "app.log\n") is now rejected
	  like any other control character; before, the newline was silently
	  dropped and a different file written.
	[ Tests ]
	- t/function.t covers every internal helper directly, and checks for
	  memory cycles and that logging leaves $_, $@ and $! alone.
	- New t/journald_emsgsize.t fakes a small send buffer by overriding
	  send().  The override records the datagram rather than sending it,
	  so the test doesn't hit the system's own limit (it failed on macOS,
	  which caps Unix datagrams at 2KB, with ENOBUFS).

0.36	Sat Oct  3 09:35:44 PM EDT 2026
	[ Enhancements ]
	- Structured fields: every logging method accepts a hashref of
	  fields after the message, e.g. $log->info('login', { user_id => 42 }).
	  They are kept as data in the history, array and CODE-ref backends,
	  as a nested "fields" object in format => 'json', and as journal
	  fields in journald; other backends get them appended as logfmt
	  key=value text.
	- The Log::Any adapter implements structured(), so a trailing hashref
	  and the Log::Any context reach Log::Abstraction as fields.
	- New critical(), alert() and emergency() methods, logged at their own
	  levels (syslog crit/alert/emerg, journald PRIORITY 2/1/0).  They
	  behave like error(): croak_on_error, or having no backend, croaks.
	  An object logger without the method, such as Log::Log4perl, is
	  called with fatal(), or error() if it has no fatal().
	- New is_trace, is_info, is_notice, is_warn, is_error, is_critical,
	  is_alert and is_emergency methods alongside is_debug.  The Log::Any
	  adapter's detection methods now call them.
	- New timestamp options: timestamp_format (a strftime pattern, or
	  'iso8601'/'rfc3339'), utc, and timestamp_precision (0-9 digits of
	  fractional seconds, from Time::HiRes).  Patterns may also use %N,
	  %3N etc., and %z/%:z, which now give a numeric offset on every
	  platform.  They apply to %timestamp% and the JSON timestamp.  The
	  time is now read once per message, so all backends show the same
	  time.  Time::HiRes and Time::Local (both core) are new dependencies.
	- File rotation for the path backends: rotate_size (bytes, or with
	  K/M/G), rotate_interval (hourly, daily, weekly, monthly) and
	  rotate_keep (default 5).  Files are rotated to FILE.1, FILE.2 ...
	  before the write that is due.  Since the file is opened for every
	  message, logrotate's default rename-and-create works without
	  copytruncate or SIGHUP; this is now documented and tested.
	- Every backend can have its own 'level' and 'format'.  syslog,
	  journald and sendmail take them as keys; file, fd and array (in a
	  logger hash or at the top level) may be given as a hash, e.g.
	  file => { file => $path, level => 'info', format => 'json' }.  A
	  format replaces the message for array, syslog, journald and email.
	  Invalid values croak in new().
	[ Incompatible Changes ]
	- The Log::Any adapter no longer collapses critical, alert and
	  emergency into error; they go to the new methods, so backends now
	  see the levels 'critical', 'alert' and 'emergency' (CODE callbacks
	  testing for 'error' miss them, and syslog priorities change), and
	  is_critical, is_alert and is_emergency have their own thresholds.
	- A hashref as the last of two or more arguments to a logging method
	  is now taken as structured fields.  It used to be joined into the
	  message as "HASH(0x...)".
	- When a call has structured fields, an object logger (such as the
	  default Log::Log4perl) is passed them as an extra " key=value ..."
	  argument after the message.
	- Log::Abstraction no longer imports Carp, POSIX, Scalar::Util,
	  Sys::Syslog or Email::Sender::Simple functions, so they can no longer
	  be called as methods on a logger ($log->carp, $log->syslog,
	  $log->strftime ...).  Code mocking Log::Abstraction::openlog or
	  ::setlogsock should mock Sys::Syslog::openlog/setlogsock instead.
	- 'level' and 'format' in the journald hash are now that backend's
	  level and format; they were sent to the journal as LEVEL and FORMAT
	  fields.
	[ Bug Fixes ]
	- A top-level 'fd' now counts as a backend in new(), as 'file' and
	  'array' do: Log::Abstraction->new(fd => $fh) no longer also sets up
	  Log::Log4perl and sends every message there too.
	- The release dates in this file are now in ISO-8601 (W3CDTF) form,
	  as CPAN::Changes expects.
	- t/journald.t no longer hangs if a journald send fails: its receiver
	  now reads with MSG_DONTWAIT, so the test fails instead.  This was the
	  hang in RT#181461.

0.35	2026-09-30T21:44:46-04:00
	[ Enhancements ]
	- New max_messages option caps the in-memory message history.

	[ Security ]
	- Format tokens inside a log message are no longer expanded.  Tokens
	  were substituted one at a time with %message% first, so a message
	  containing "%env_FOO%" leaked $ENV{FOO} into file/fd logs.  All
	  tokens are now expanded in a single pass.
	- Line breaks in a message are followed by a tab in text formats, so a
	  message can no longer forge extra log entries.
	- syslog messages are passed through a '%s' format, so '%m' and other
	  '%' sequences in a message are logged literally.

	[ Bug Fixes ]
	- A failed sendmail delivery no longer stops the message reaching the
	  syslog, journald, fd and top-level file/fd backends.
	- level() setter is now case-insensitive, as documented.
	- The syslog sub-hash 'level' may now be a name as well as a number;
	  a name used to warn "isn't numeric" and drop every message.  The
	  sendmail 'level' may likewise be a number, and is case-insensitive.
	- Invalid syslog/sendmail levels, a sendmail hash without 'to', and
	  invalid journald field names now croak in new() rather than
	  silently dropping messages at log time.
	- script_name is now auto-detected for logger => { syslog => {...} },
	  not only for a top-level syslog key.
	- warn()/error() called as a class method on a subclass no longer die
	  with "Can't use string as a HASH ref".
	- carp_on_warn (and the no-backend carp) no longer fires for a warning
	  below the level threshold.
	- A top-level file or fd now counts as a backend, so error() no longer
	  croaks (and warn() no longer carps) on a file-only logger.
	- syslog and sendmail now join message parts without a separator, like
	  every other backend.
	- Unicode messages are written as UTF-8 by the file/fd backends without
	  "Wide character" warnings; an fd with an encoding layer is not
	  double-encoded.  JSON output keys are now sorted.
	- journald: values are sent as UTF-8 with correct binary-framing
	  lengths; a message too big for one datagram is truncated; a missing
	  journald socket carps once rather than on every message.
	- Destroying one instance no longer closes the process-wide syslog
	  connection while other instances still use it.
	- Log::Any adapter: file/line now point at the code calling Log::Any;
	  a non-Log::Abstraction 'instance' croaks instead of being silently
	  replaced; carp_on_warn, croak_on_error, config_file and max_messages
	  are forwarded; a croak while logging is turned into a carp.

	[ Documentation ]
	- Documented that trace shares debug's threshold, the syslog sub-hash
	  keys, Unicode handling, and the full list of accepted level names.
	- Formal specification brought in line with the code.

	[ Housekeeping ]
	- Removed the unused JSON::MaybeXS test dependency.
	- journald binary framing no longer uses pack('Q<'), which dies on
	  perls built without 64-bit integers.
	- Minimum Perl version is now 5.014 (the code needs 5.010, and
	  Readonly::Values::Syslog needs 5.014).
	- Log::Any and the Email::* modules used by the sendmail backend are
	  now proper runtime 'recommends' in META; Log::Any was previously
	  hidden as an unrecognised resource.
	- Documented the Log::Any adapter's generated logging and is_*
	  methods, and fixed its SYNOPSIS.
	- autodie is now loaded with :default rather than :all, since the
	  module never calls system() or exec(); autodie is now declared.
	  IPC::System::Simple is declared as a runtime requirement because
	  Params::Get, loaded via Config::Abstraction, needs it but doesn't
	  declare it.
	- Data::Dumper is now a test dependency only; the tests that use it now
	  load it themselves.  Removed the unused File::Slurp, File::Glob
	  and File::stat test dependencies.  Declared the
	  core modules Socket and File::Basename, and the author-test modules
	  as develop prerequisites.
	- Documented the messages raised while logging (invalid file name,
	  SMTP host/port, unusable logger, delivery failures), and the
	  Log::Abstraction::new() function-call form.
	- t/10-compile.t no longer fails when Log::Any is not installed.
	  Log::Any is only a recommended dependency, so the compile check of
	  Log::Any::Adapter::Abstraction is now skipped when
	  Log::Any::Adapter::Base cannot be loaded.

0.34	2026-09-26T20:47:01-04:00
	[ Bug Fixes ]
	- Bump minimum versions

0.33	2026-07-10T11:03:49-04:00
	[ Architecture ]
	- Added Sub::Private (enforce mode) to mark _log, _high_priority,
	  _validate_file_path, _format_message, _journald_send, and
	  _sanitize_email_header as private via the :Private attribute.
	  Enforcement is active in production; HARNESS_ACTIVE bypass keeps
	  white-box tests functional without special setup.
	- Converted _journald_send from a bare package function to a proper
	  OOP method ($self->_journald_send), consistent with all other
	  private helpers in the class.
	- Fixed caller-depth resolution for warn/error/fatal: file and line
	  reported to CODE-ref callbacks and all file/fd format backends now
	  correctly resolve to the caller's source location.  Previously the
	  extra _high_priority stack frame caused them to point to the module's
	  internal dispatch code.  _format_message now accepts pre-computed
	  caller_file/caller_line parameters rather than calling caller(2)
	  internally; _log computes the correct depth (1 for trace/debug/info/
	  notice; 2 for warn/error) and passes the values down.
	- Removed unreachable UNIVERSAL::isa call in _log's private-method
	  guard; replaced with the idiomatic method form (caller)[0]->isa().
	- Removed spurious File::Basename->import() guard; require + fully-
	  qualified call is sufficient.
	- Added =head1 LIMITATIONS section to Log::Abstraction documenting
	  syslog hash mutation, no-structured-fields, single-threaded email
	  throttle, and OpenTelemetry status.
	- Added =head1 LIMITATIONS section to Log::Any::Adapter::Abstraction
	  documenting nine-to-six level collapse, no-structured-fields, and
	  the recommended-not-required Log::Any dependency.

	[ Bug Fixes ]
	- Fixed warn()/error()/fatal() calls: file and line now correctly
	  identify the caller's source location in all backends (CODE-ref,
	  file, fd, hash-file, hash-fd).  Previously these resolved to
	  Abstraction.pm's internal _high_priority frame.

	[ Tests ]
	- Added t/caller_depth.t: verifies that file/line in CODE-ref
	  callbacks correctly identify the caller's source location for all
	  six log levels (trace, debug, info, notice, warn, error, fatal).

	[ Enhancements ]
	- Added Log::Any::Adapter::Abstraction: route Log::Any-using CPAN modules
	  through Log::Abstraction with a single Log::Any::Adapter->set() call.
	  Covers all nine Log::Any levels (critical/alert/emergency collapse to error).
	- Added format => 'json' magic value: file and fd backends now emit compact
	  JSON objects (timestamp/level/message/file/line, plus class for subclasses)
	  compatible with journald, Loki, Elasticsearch, and Splunk.
	- Added journald sub-backend to the HASH logger: sends structured fields
	  (MESSAGE, PRIORITY, SYSLOG_IDENTIFIER, plus any caller-supplied extras)
	  as a Unix-domain datagram using the systemd native journal protocol.
	  Extra config keys are forwarded as uppercase journald fields.
	  Delivery failures are silent (carp only); the app is never crashed.
	- Added JSON::PP as a runtime dependency (core since 5.014; used by json format).
	- Added Log::Any as a recommended (not required) runtime dependency.

	[ Tests ]
	- Added t/log_any_adapter.t: routing, level mapping, is_* detection, threshold
	  filtering, and adapter-built-from-constructor-args subtests.
	- Added t/json_format.t: JSON line validity, field types (line as integer),
	  class suppression for base class, class inclusion for subclass, all backend
	  variants (scalar-path, fd, hash-file).
	- Added t/journald.t: uses a temporary Unix-domain datagram socket as a fake
	  journald receiver to verify field encoding (text and binary framing),
	  PRIORITY mapping, extra fields, coexistence with other sub-backends, and
	  silent failure on an unreachable socket path.

0.32	2026-06-09T19:34:45-04:00
	[ Bug Fixes ]
	- Fixed SMTP host/port validation in sendmail backend: croak calls were
	  inside the eval{} that catches delivery failures, so bad config was
	  silently swallowed instead of propagating. Validation now runs before
	  the eval so misconfigured host/port croaks immediately to the caller.

0.31	2026-06-08T20:55:28-04:00
	[ Architecture ]
	- Added use autodie qw(:all); file writes wrapped in eval{} to keep
	  logging failures silent (app must not crash when log file is unavailable)
	- Extracted _format_message($self, $level, $str, $use_class) helper,
	  eliminating 4+ copies of the format-token substitution block
	- Extracted _validate_file_path($self, $path) helper, unifying path
	  validation for hash-backend file, scalar-path, and self->{'file'} under
	  one implementation
	- Added Readonly constants for all magic strings and numbers (SMTP defaults,
	  port range, syslog defaults, format strings, validation regexes)
	- Collapsed _high_priority duplicate if/else blocks into one
	- All public void methods now return $self to allow method chaining;
	  level() setter returns undef on invalid input to signal the error
	- Fixed syslog priority mapping: all levels (trace→debug, debug→debug,
	  info→info, notice→notice, warn→warning, error→err) now correctly mapped
	  via %LEVEL_TO_SYSLOG_PRIORITY hash instead of a binary error/warning test
	- Moved Readonly from test-only to runtime dependency (Makefile.PL, cpanfile)

	[ Documentation ]
	- Added Z-notation FORMAL SPECIFICATION to every public method POD
	- Added API Specification (Params::Validate::Strict input and Return::Set
	  output schemas) to every public method POD
	- Added MESSAGES table to every public method POD
	- Added internal-routine comment blocks (purpose, entry, exit, side
	  effects, notes) for _log, _high_priority, _format_message,
	  _validate_file_path, _sanitize_email_header, DESTROY
	- Suppressed %class% for the base Log::Abstraction class (appears empty,
	  as it added no useful information when not subclassed)

	[ Tests ]
	- Added t/locales.t: covers geographic (Locale::Country ISO-code sanity
	  with BAIL_OUT on drift, case-insensitive codes, concurrent instances) and
	  POSIX system-locale (LC_ALL en_US.UTF-8/de_DE.UTF-8/ja_JP.UTF-8 error
	  paths using local $! = ENOENT; my $msg = "$!" pattern)
	- Updated edge_cases.t and function.t to use unified "Invalid file name"
	  error message (standardised via _validate_file_path)

	[ Security ]
	- Fixed path traversal in HASH-backend file logger: path regex now blocks
	  '..' sequences, preventing writes outside the intended directory
	- Fixed open() in HASH-backend file logger to use the untainted $file
	  variable rather than the original $logger->{'file'} value
	- Added path validation (same allowlist + '..' check) to the scalar-string
	  logger path, which previously called open() with no validation at all
	- Validated SMTP host in sendmail backend: only [a-zA-Z0-9.-] characters
	  accepted, preventing header/protocol injection via a crafted hostname
	- Validated SMTP port in sendmail backend: must be an integer 1-65535,
	  preventing SSRF port-scanning via an attacker-controlled config file
	- Fixed %env_*% format expansion in HASH file backend to use /ge with
	  defined-or fallback, consistent with the fd and scalar-path backends

	[ Bug Fixes ]
	- Fixed use Readonly::Values::Syslog version in source to 0.04, matching
	  Makefile.PL and cpanfile (was still 0.03 after the 0.30 bump)

	[ Documentation ]
	- Fixed EXAMPLES POD: sample output class field was "main"; corrected to
	  "Log::Abstraction" (blessed class of the logger object)
	- Fixed EXAMPLES POD: sample output warn-row level field was "warning";
	  corrected to "warn" to match what the module actually passes to callbacks
	- Added note explaining that file/line resolve to the module's internal
	  dispatch for warn/error calls due to the extra _high_priority stack frame
	- Replaced broken combined file+sendmail example (file backend writes
	  native text format, not CSV) with a correct sendmail-only example; added
	  level => 'warn' key to restrict emails to warn-and-above; noted that
	  combining CSV output with email requires both in a single code-ref

	[ Tests ]
	- Simplified $parse_line regex in integration.t: removed dead |"$ alternation
	  subsumed by "(?:,|$)

0.30	2026-05-27T13:09:35-04:00

	[ Enhancements ]
	- Added EXAMPLES POD section: CSV file logging for BI import (code-ref
	  backend writing timestamp/level/class/file/line/message rows, with a
	  combined sendmail+min_interval variant for real-time alerting)

	[ Bug Fixes]
	- Bump minimum Readonly::Values::Syslog to 0.04 to fix is_debug() returning
	  false at trace level on older installations
	  Fixes https://www.cpantesters.org/cpan/report/32b653da-59c4-11f1-ba50-8ade6d8775ea

0.29	2026-05-26T20:36:13-04:00

	[ Enhancements ]
	- Added min_interval throttle to sendmail backend: at most one email per
	  configured interval (seconds); cooldown state stored in _last_email_sent
	  on the object and inherited by clones

	- Bump minimum Test::Mockingbird version to fix https://www.cpantesters.org/cpan/report/f148a3e6-50a2-11f1-8224-a122dd379578

0.28	2026-05-15T20:28:07-04:00

	[ Enhancements ]
	- Added context (ctx) to code ref logger callbacks

	[ Bug Fixes]
	- Fixed carp_on_warn and croak_on_error when logging to an array backend

	- Fixed cloning to a different level (level string now converted to integer)

	- Fixed DESTROY to call Sys::Syslog::closelog() fully-qualified for correct mock interception

	- Fixed warn(undef) and warn(warning => undef) to be silent no-ops

	- Fixed warn(warning => [undef, ...]) to filter undef elements before joining

	- Fixed odd-count plain list in warn/error (_high_priority now wraps get_params in eval)

	- Fixed autovivification of sendmail key via exists() guards on sendmail hash access

	- Fixed %env_foo% format token to expand missing env vars to empty string without warning

	- Fixed ::new() to always construct normally regardless of arguments

	- Removed spurious Carp::croak/carp fallback when array backend is defined

0.27	2026-01-11T09:52:54-05:00
	Easier to read tests
	Added fatal as a synonym for error

0.26	2025-10-15T17:03:58-04:00
	Added testing dashboard on GitHub Pages
	Added croak_on_error

0.25	2025-08-17T20:41:38-04:00
	Added is_debug for consistency with Log::Any
	Use Return::Set to assert return values within specification
	Sanitize the e-mail headers
	Started to add format argument to new()
	Don't close fd that were passed in
	Only load the mail modules when needed

0.24	2025-07-19T16:52:41-04:00
	Added the ability to send an email for higher priority messages
	Only load Log::Log4perl when needed
	Deep-clone the messages array during cloning
	Map notice to info for Log::Log4perl
	Syslog facility is now configurable (default is still local0)
	Connections to the syslog are now persistent

0.23	2025-07-17T14:45:54-04:00
	Better error message when we don't know what to do
	Allow the syslog config to say 'server' instead of 'host' for consistency with CHI

0.22	2025-07-17T08:22:16-04:00
	Error() now sends to syslog like warn()
	Ensure undef isn't passed in the messages array to coderefs

0.21	2025-07-16T16:21:25-04:00
	Added error()

0.20	2025-06-15T21:19:03-04:00
	Fix GitHub#2

0.19	2025-06-10T08:32:28-04:00
	Bump minimum version of Sys::Syslog
	More untaint checking

0.18	2025-06-06T15:52:27-04:00
	Fix level testing

0.17	2025-05-22T14:21:14-04:00
	Added the messages() method

0.16	2025-05-22T07:40:24-04:00
	Handle upper case levels, such as 'INFO'

0.15	2025-05-20T21:10:23-04:00
	No need to create a Log::Log4perl when sending output to a file

0.14	2025-05-20T07:44:19-04:00
	Adjusted minimum version of Config::Abstraction.
		It should be 0.19 in terms of testing but it's best to use the fixes in 0.25
		RT#165420 - ANDK
	Added 'array' to the logger hash
	Introduced 'level' - minimum level to log at, defaults to 'warn'
	Fixed where to log bugs
	Check and untaint the filename

0.13	2025-05-14T08:30:53-04:00
	Use '>' after level rather than ':' to files, like lower level loggers
	Try not to put the name of this package in the logfiles,
		it adds nothing apart from disc usage
	Block setting logger => Log::Abstraction in new()
	Croak if we don't know how to handle a message

0.12	2025-05-09T07:42:35-04:00
	Added the file parameter, so that both file and syslog can now be given,
		and file can be read in from a configuration file
	Added the fd descriptor - a file descriptor to log to
	Added separators between fields in file output

0.11	2025-05-07T15:37:37-04:00
	Fixed calling can() on an unblessed variable
	Honour carp_on_warn when syslog is set

0.10	2025-05-06T20:43:48-04:00
	Added carp_on_warn

0.09	2025-05-06T08:15:36-04:00
	If no logger is given, use Log4perl

0.08	2025-05-05T11:39:58-04:00
	Use Config::Abstraction instead of Config::Auto
	Guess the value if script_name if it's not given

0.07	2025-03-24T08:36:01-04:00
	Handle "Socket operation on non-socket" on Solaris
	Calling new on an object now returns a clone rather than setting the defaults in the new object

0.06	2025-03-12T13:23:24-04:00
	Don't put spaces between elements of an array

0.05	2025-03-11T14:10:10-04:00
	Handle being passed a reference to an array

0.04	2025-03-10T09:39:46-04:00
	Renamed from Log-YetAnother to Log-Abstraction
	Added config_file argument to new()

0.03	2025-03-08T08:40:00-05:00
	Fix passing an array to warn()

0.02	2025-03-06T17:09:32-05:00
	Improved argument checking to new()

0.01	2025-03-06T15:42:04-05:00
        First draft
