Skip to content

Prepare 4.0.0: CI, Linux build, crash-safe file sink, metadata, race fixes - #10

Merged
taplin merged 7 commits into
mainfrom
release-4.0.0
Oct 15, 2026
Merged

taplin merged 7 commits into
mainfrom
release-4.0.0

Conversation

@taplin

@taplin taplin commented Oct 15, 2026

Copy link
Copy Markdown
Contributor

Summary

Prepares Perfect-Logger for 4.0.0, the first release of the Swift 6 rewrite on apple/swift-log (3.3.0 is the 2018 code).

  • CI: Linux Swift 6.2/6.4 (noble) and macOS Xcode 26.0.1/26.6, debug and release, warnings as errors.
  • swift-log floor 1.12.0: the handlers need LogEvent (1.11) and LogEvent.error (1.12); from: "1.5.0" could not build.
  • Linux build: RemoteLogHandler now imports FoundationNetworking.
  • File sink: opened with O_APPEND|O_CREAT|O_CLOEXEC, one write(2) per line; I/O errors (e.g. a full disk) drop the line instead of crashing. FIFOs open non-blocking (no hang without a reader), directories/sockets/block devices are refused, SIGPIPE is suppressed.
  • Timestamps: en_US_POSIX + Gregorian, so output no longer depends on the host locale.
  • Metadata: handler, metadataProvider, call and error metadata are merged; non-eventid keys are appended as escaped, sorted key=value pairs (new LogOptions.metadata, in .default). Lines carrying only eventid are unchanged.
  • Races: LogFile.logger is lock-protected; bootstrap rebinds the default logger (including one used or tuned before bootstrap) without a stale-logger race; a backend factory that logs no longer deadlocks.
  • Remote handler: one bounded queue per collector (1,000 events, newest dropped), each POST bounded by a hard 10 s timeout, token percent-encoded, invalid URLs warned about once.
  • LogFile.terminal writes [EMERG] again; LogFile.debug's autoclosure is lazy.
  • README rewritten for 4.0.0 with a 3.x migration section.

The public API is source-compatible with main; changes are additive (metadataProvider: parameters, LogOptions.metadata, RemoteLogHandler.maxPendingEvents). Behaviour changes for the release notes: LogOptions.default.rawValue is now 15, and the remote JSON body differs from 3.x (already the case on main).

Verification

  • 33 tests pass: Xcode 27 (Swift 6.4) and Xcode 26.3 (Swift 6.2.4), debug and release with warnings as errors; TSan clean.
  • Linux swift:6.2-noble and swift:6.4-noble on arm64, and swift:6.4-noble amd64, debug and release.
  • Three adversarial review rounds with stress programs (full disk, FIFO with/without a reader, broken pipe, metadata injection, hung and trickling collectors, re-entrant factory, bootstrap races); the final round's verdict was push.

🤖 Generated with Claude Code

taplin and others added 7 commits October 15, 2026 11:07
…lease

Mirrors Perfect-CRUD's workflow minus the iOS job (the package declares
macOS only). Warnings are errors; release builds enable testing for
@testable imports.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The handlers implement LogHandler.log(event:), and LogEvent only exists
from swift-log 1.11.0 (LogEvent.error from 1.12.0). With the old
from: "1.5.0" floor a consumer resolving swift-log 1.6-1.10 failed to
build with "cannot find type 'LogEvent' in scope".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d races

Linux
- RemoteLogHandler imports FoundationNetworking when available; main did
  not build on Linux ("cannot find 'URLRequest' in scope").

FileLogHandler
- FileSink opens the file with O_APPEND|O_CREAT|O_CLOEXEC and writes each
  line with write(2), retrying EINTR and dropping the line on any other
  error. The legacy seekToEndOfFile()/write(Data) trap on I/O errors
  (on Linux: "'try!' expression unexpectedly raised ... No space left on
  device" when writing to /dev/full). A path that cannot be opened prints
  one warning on stderr instead of failing silently.
- The timestamp formatter is pinned to en_US_POSIX and the Gregorian
  calendar (a th_TH user got year 2544, ar_SA got Arabic-Indic digits)
  and is guarded by a lock.
- Metadata from the handler, its metadataProvider, the call and the
  call's error are merged as swift-log's StreamLogHandler does. New
  LogOptions.metadata (in .default) appends keys other than eventid after
  the message as sorted key=value pairs; lines with only an eventid are
  byte-for-byte unchanged, and .none still writes the bare message.
- LogFile.terminal tags its event priority=terminal so the file shows
  [EMERG] as 3.x did, instead of [CRITICAL].

RemoteLogHandler
- Events go through one bounded queue per collector URL (1000 pending,
  then dropped) drained by a single task, instead of an unbounded
  detached Task and URLSession call per log line. Requests time out
  after 10 s.
- The token is percent-encoded into the path; an empty server/token or a
  non-http(s) URL disables the handler with one stderr warning.
- Merges metadataProvider and call metadata; sends extra keys as an
  optional "metadata" object. The JSON keys are otherwise unchanged.

LogFile / bootstrap
- LogFile.logger is lock-protected instead of nonisolated(unsafe) (TSan
  reported a data race when swapping it while logging).
- bootstrap rebinds LogFile's default logger, so LogFile calls made
  before bootstrapping no longer pin it to swift-log's default backend.
- bootstrap gains a metadataProvider: parameter (default nil) and warns
  instead of silently dropping the remote handler when remoteToken is
  missing.
- LogFile.debug no longer evaluates its autoclosure when filtered.

All public API changes are additive (new defaulted parameters,
LogOptions.metadata, RemoteLogHandler.maxPendingEvents).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tion

Install from: "4.0.0"; Swift 6.2+, macOS 12+ and Linux; swift-log
1.12.0+. Documents the metadata suffix, POSIX timestamps, append-mode
writes, the bounded remote queue and its JSON body, the stderr warnings
for bad file/remote configuration, and how 3.x call sites map to 4.0
(including the changed remote JSON body).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…File rebind and re-entrancy

Remote
- Each POST is raced against a timer and abandoned (its task cancelled)
  after 10 s whatever the transport does. URLRequest.timeoutInterval is an
  idle timeout and Linux's URLSession.shared ignored it, so a collector
  that trickled one byte every 3 s stalled the queue forever. The default
  transport also uses an ephemeral session with 10 s request and
  resource timeouts.
- Collector base URLs with a query or fragment are rejected.

File sink
- stat() the path first: directories, sockets and block devices are
  refused with one warning. The file is opened with O_NONBLOCK (cleared
  afterwards), so a FIFO with no reader is refused (ENXIO) instead of
  hanging the first log call.
- Writing to a pipe whose reader left gave SIGPIPE (exit 141). Darwin
  now sets F_SETNOSIGPIPE; Linux blocks SIGPIPE on the writing thread for
  non-regular files and consumes the pending signal, so the write fails
  with EPIPE.
- After a partial write the sink tries to write "\n" so the next line
  doesn't glue onto the broken one.

Metadata
- Keys and values in the file suffix are escaped (\\ " \n \r \t, other
  controls as \u{XX}) and quoted when empty or containing space, '=' or
  '"', so a value can no longer forge a log line. Nested dictionaries
  are rendered with sorted keys (also in the remote JSON).
- The terminal marker key is now "perfect.priority", so user "priority"
  metadata can't turn a line into [EMERG].

LogFile
- The getter builds the default Logger outside the lock and publishes it
  only if nothing replaced it meanwhile; a LoggingSystem factory that
  itself uses LogFile deadlocked. A re-entrant build on the same thread
  gets a no-op logger instead of recursing.
- bootstrap rebuilds an assigned logger that still has the default label
  (what `LogFile.logger.logLevel = x` before bootstrap leaves) on the new
  backend, keeping logLevel and handler metadata; before, it stayed on
  the stderr handler forever. Other assigned loggers are kept.

Tests: line-format tests use /dev/null instead of leaving temp files;
the bounded-queue test is deterministic (gate-blocked transport); new
tests for the hard timeout, cancellation, FIFO, broken pipe, directory,
escaping, nested sorting, marker collision, URL rejection and rebind.
CI: drop the Swift 6.2 miscompile comment copied from Perfect-CRUD.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Xcode 26.3 (Swift 6.2.4) failed the test build with "unable to
type-check this expression in reasonable time" on the bounded-queue
test's array comparison.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…trap

rebindDefaultLogger returned early when no logger was stored, without
bumping the generation. A first LogFile use that was still building its
logger on the pre-bootstrap backend then published it after bootstrap,
and LogFile stayed on that backend (reviewer's rebind-race: 34/600
runs pinned on macOS, 180/400 on Linux 6.4, 251/400 on 6.2; now 0).
The rebind now invalidates in-flight builds in that case too.

The getter's body is factored into resolveLogger(make:) so a test can
rebind mid-build deterministically (it fails without the fix).

Document that the re-entrancy guard is same-thread only (a factory must
not block on another thread that uses LogFile), that LogFile calls made
from inside the factory are dropped, and that a path that can't be
opened when LogFile's logger is created, such as a FIFO with no reader,
stays disabled for that logger.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@taplin
taplin merged commit 733b69f into main Oct 15, 2026
16 checks passed
@taplin
taplin deleted the release-4.0.0 branch October 15, 2026 19:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant