Skip to content

Perfect-Logger 4.0.0

Latest

Choose a tag to compare

@taplin taplin released this 15 Oct 18:04
733b69f

Perfect-Logger 4.0.0 is the first tagged release of the Swift 6 rewrite. It replaces 3.3.0 (2018). The package is now a thin layer over apple/swift-log: a Perfect-style LogFile façade, a FileLogHandler and a RemoteLogHandler, wired up with one PerfectLogger.bootstrap(...) call. The Perfect-CURL and PerfectLib dependencies are gone.

.package(url: "https://github.com/PerfectlySoft/Perfect-Logger.git", from: "4.0.0"),

Requirements

  • Swift 6.2 or later (swift-tools-version: 6.2, Swift 6 language mode).
  • macOS 12 or later, or Linux. CI covers Swift 6.2 and 6.4 on Ubuntu 24.04 and Xcode 26.0.1 and 26.6 on macOS, in debug and release, with warnings as errors.
  • swift-log 1.12.0 or later (from:).

Highlights

  • swift-log backend. Libraries keep logging against a plain Logger; the app decides where it goes. PerfectLogger.bootstrap(console:file:fileOptions:remoteServer:remoteToken:level:metadataProvider:) combines stdout, file and remote handlers.
  • LogFile keeps the 3.x surface: LogFile.info/warning/error/critical/debug/terminal(_:eventid:) still return the event id for correlation, and LogPriority and LogOptions keep their cases.
  • The file line format is unchanged from 3.x ([LEVEL] [eventid] [timestamp] message), including even padding and [EMERG] for terminal.
  • Metadata in file lines. Handler metadata, a swift-log metadata provider, call metadata and a logged error: are merged as swift-log's StreamLogHandler does. With the new LogOptions.metadata (part of .default), keys other than eventid are appended after the message as sorted, escaped key=value pairs; lines without them are byte-for-byte the same.
  • A logger that doesn't take the process down. File writes use O_APPEND and write(2) and drop the line on an I/O error; a FIFO with no reader is refused instead of hanging; a broken pipe gives EPIPE, not SIGPIPE. The remote handler uses a bounded queue and a hard 10-second limit per POST.
  • Works on Linux (Swift 6.2 and 6.4, arm64 and x86_64).

Breaking and behaviour changes

  • Destinations are configured once with PerfectLogger.bootstrap(...), not with LogFile.location, LogFile.options, LogFile.even and LogFile.threshold, and the logFile:/evenIdents: arguments on LogFile calls are gone. Upgrade: call bootstrap(file:fileOptions:level:) at startup, or build a FileLogHandler(label:path:options:even:).
  • RemoteLogger is replaced by RemoteLogHandler, and the JSON body changed. 3.x posted appuuid, eventid, loglevel and detail; 4.0 posts level, label, source, eventid, message, timestamp (ISO 8601, UTC) and, when present, metadata. The endpoint is still <server>/api/v1/log/<token>. Upgrade: pass remoteServer:/remoteToken: to bootstrap, and update a 3.x collector to read the new fields.
  • No separate console echo. bootstrap(console: true) adds swift-log's stdout handler, whose format differs from 3.x's console lines. Upgrade: none, unless you parse console output.
  • Remote events are queued and can be dropped. Each collector gets one queue of at most 1,000 events (RemoteLogHandler.maxPendingEvents), sent one at a time; a POST that hasn't finished after 10 seconds is cancelled and the next event goes. Events beyond that, and events still queued at exit (including after LogFile.terminal), are lost. Upgrade: none; use the file handler for anything that must be kept.
  • Misconfiguration warns on standard error. A remoteServer without a remoteToken, a server that isn't an http(s) URL without a query or fragment, or a log file that can't be opened (including a directory, a socket or a FIFO with no reader) is reported once on stderr and that destination is skipped. Before, it was dropped silently, or the first log call hung on the FIFO. Upgrade: none.
  • Metadata values are escaped in file lines. \, ", newline, CR and tab are backslash-escaped, other control characters become \u{XX}, and empty values or values with a space, = or " are double-quoted, as are such keys. Nested dictionaries are written with sorted keys. Upgrade: if you parse the suffix, unquote and unescape.
  • LogOptions.default.rawValue is now 15 (was 7), because .default includes .metadata. Upgrade: only if you stored or compared the raw value; LogOptions(rawValue: 7) is the old default.
  • LogFile.logger modified before bootstrap is rebuilt. If it still has the perfect.logfile label (for example after LogFile.logger.logLevel = .debug), bootstrap rebuilds it on the new backend with the same level and metadata. A logger you assign with another label is kept. Upgrade: none.
  • Additive API: bootstrap and both handler initializers gained a defaulted metadataProvider: parameter, and there are new LogOptions.metadata and RemoteLogHandler.maxPendingEvents. LogOptions.default now includes .metadata. Upgrade: none; existing calls compile unchanged. Use [.priority, .eventId, .timestamp] if you never want metadata in the file.

Fixes

Against the pre-release main (f2c99dc), all in #10:

  • Linux: the package didn't build ("cannot find 'URLRequest' in scope"); RemoteLogHandler now imports FoundationNetworking.
  • Crash on a full disk: FileLogHandler used seekToEndOfFile()/write(_ Data), which trap on I/O errors. Logging to /dev/full in a swift:6.4-noble container died with "'try!' expression unexpectedly raised an error … No space left on device". Writes now go through write(2) with EINTR retry, a failed line is dropped, and after a partial write the handler tries to end the line.
  • FIFO hang: a log path naming a FIFO with no reader blocked the first log call in open(2) forever. The file is now opened with O_NONBLOCK, so it fails with ENXIO and is skipped with a warning. Directories, sockets and block devices are refused up front.
  • SIGPIPE: logging to a pipe or FIFO whose reader had gone away killed the process (exit 141). Darwin sets F_SETNOSIGPIPE; on Linux SIGPIPE is blocked on the writing thread and consumed, so the write fails with EPIPE.
  • Log injection: a metadata value containing a newline could write a forged line such as [CRITICAL] [forged-id] … admin password changed, and a key or value with a space or = blurred the pairs. Keys and values are now escaped and quoted.
  • Remote timeout: URLRequest.timeoutInterval is an idle timeout (and Linux's URLSession.shared used 60 s), so a collector that trickled a byte every 3 seconds stalled the queue forever on both platforms. Each POST is now raced against a 10-second timer and cancelled when it loses; the default transport also uses an ephemeral session with 10-second request and resource timeouts. Against a test server, the old code sent 1 of 3 events in 34 s; the new code sends one every 10 s on macOS and on Linux (Swift 6.2 and 6.4), for a server that trickles and for one that never answers.
  • Deadlock: the LogFile.logger getter created the default logger while holding its lock, so a LoggingSystem factory that used LogFile deadlocked. It now builds outside the lock, and a re-entrant call gets a no-op logger. A logger still being built on the old backend when bootstrap runs is discarded rather than cached.
  • Locale-dependent timestamps: the formatter followed the user's locale and calendar. A th_TH user got year 2544, ja_JP@calendar=japanese got 0013, and ar_SA got Arabic-Indic digits and an Arabic "GMT". It's now pinned to en_US_POSIX and Gregorian, and locked for use across threads.
  • Metadata ignored: both handlers ignored their metadataProvider and every metadata key but eventid.
  • LogFile.terminal wrote [CRITICAL] instead of 3.x's [EMERG]. It now tags the event with perfect.priority=terminal, a namespaced key, so user priority metadata can't trigger it.
  • Data race on LogFile.logger: it was a nonisolated(unsafe) static var, and ThreadSanitizer reported a race when it was replaced while other tasks logged. It's now lock-protected.
  • LogFile used before bootstrap stayed on swift-log's default stderr handler for the life of the process, including after LogFile.logger.logLevel = …. bootstrap now rebinds or rebuilds the default LogFile.logger.
  • Unbounded remote fan-out: RemoteLogHandler started a detached Task and a URLSession request for every log line, with no limit and no timeout. It also built a new ISO8601DateFormatter per event and didn't percent-encode the token. Collector URLs with a query or fragment are now rejected.
  • swift-log floor: from: "1.5.0" let consumers resolve swift-log versions without LogEvent (before 1.11.0), which failed to build ("cannot find type 'LogEvent' in scope"). Versions before 1.12.0 also lack LogEvent.error, which the handlers now use, so the floor is 1.12.0.
  • LogFile.debug evaluated its message even when debug was filtered out.
  • CI: new GitHub Actions workflow (Linux Swift 6.2/6.4 and macOS Xcode 26.0.1/26.6, debug and release, warnings as errors, contents: read, 30-minute timeouts).
  • README: install line, requirements, behaviour and a 3.x migration section.

Verified: debug and release builds with warnings as errors on Xcode 27 (Swift 6.4) and Xcode 26.3 (Swift 6.2.4), and on Linux with swift:6.2-noble and swift:6.4-noble (arm64) and swift:6.4-noble on amd64; the 33 tests pass everywhere, and a ThreadSanitizer run on macOS (Xcode 27) is clean. A review stress harness (pre-bootstrap level, FIFO, broken pipe, injection, re-entrant factory, concurrent swaps) passes on macOS and Linux. A scratch app using PerfectLogger.bootstrap(console:file:level:), LogFile.info/warning/error(_:eventid:) and LogPriority.level, as PerfectTemplate does, built with warnings as errors and wrote the expected lines.

Known limitations

  • The remote token is part of the URL path (as in 3.x), so it can show up in proxy and server access logs. Use HTTPS.
  • Remote delivery is best effort: no retries, no flush at exit, and drops once 1,000 events are queued. A custom transport that ignores cancellation keeps running after its 10 seconds (the queue moves on regardless).
  • The file handler can't stop everything: exceeding RLIMIT_FSIZE raises SIGXFSZ, and a write to a FIFO or device that blocks (a full pipe, say) blocks the logging thread.
  • The message itself isn't escaped, only metadata; don't log untrusted text with newlines as the message if lines must stay one per event.
  • LogFile.logger.logLevel = x is a read then a write; racing it against LogFile.logger = y can lose one of them.
  • A LoggingSystem factory that logs through LogFile while LogFile's logger is being created has that message dropped. The re-entrancy guard covers the same thread only, so a factory must not block waiting on another thread that uses LogFile.
  • A file path that can't be opened when LogFile's logger is created (for example a FIFO with no reader yet) stays disabled for that logger, so LogFile never writes to it.
  • FileLogHandler keeps its file descriptor open for the life of the process. Rotating by renaming the file (rather than copytruncate) keeps writing to the renamed file.
  • .trace and .notice are written as [DEBUG] and [INFO], since LogPriority has no equivalents.
  • LoggingSystem.bootstrap can only run once per process, so PerfectLogger.bootstrap can't be called twice.

Upgrade notes

  • From 3.x: see Breaking changes. Replace the LogFile.location/options/threshold and RemoteLogger settings with one PerfectLogger.bootstrap(...) call at startup.
  • From the pre-release main: nothing to change. Expect key=value suffixes in file lines if you attach metadata, and a stderr warning if a destination is misconfigured.

Pull requests

  • #9: Swift 6 modernization, rebuilt on apple/swift-log.
  • #10: 4.0.0 preparation (CI, Linux build, crash-safe file sink, metadata, races, remote handler).

Legacy

The pre-Swift-6 code (3.3.0) is on the legacy branch. Stay on 3.x if you can't move to Swift 6.2.