Revision history for Log-Abstraction

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
