Repository navigation
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. LogFilekeeps the 3.x surface:LogFile.info/warning/error/critical/debug/terminal(_:eventid:)still return the event id for correlation, andLogPriorityandLogOptionskeep their cases.- The file line format is unchanged from 3.x (
[LEVEL] [eventid] [timestamp] message), includingevenpadding and[EMERG]forterminal. - Metadata in file lines. Handler metadata, a swift-log metadata provider, call metadata and a logged
error:are merged as swift-log'sStreamLogHandlerdoes. With the newLogOptions.metadata(part of.default), keys other thaneventidare appended after the message as sorted, escapedkey=valuepairs; lines without them are byte-for-byte the same. - A logger that doesn't take the process down. File writes use
O_APPENDandwrite(2)and drop the line on an I/O error; a FIFO with no reader is refused instead of hanging; a broken pipe givesEPIPE, notSIGPIPE. 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 withLogFile.location,LogFile.options,LogFile.evenandLogFile.threshold, and thelogFile:/evenIdents:arguments onLogFilecalls are gone. Upgrade: callbootstrap(file:fileOptions:level:)at startup, or build aFileLogHandler(label:path:options:even:). RemoteLoggeris replaced byRemoteLogHandler, and the JSON body changed. 3.x postedappuuid,eventid,loglevelanddetail; 4.0 postslevel,label,source,eventid,message,timestamp(ISO 8601, UTC) and, when present,metadata. The endpoint is still<server>/api/v1/log/<token>. Upgrade: passremoteServer:/remoteToken:tobootstrap, 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 afterLogFile.terminal), are lost. Upgrade: none; use the file handler for anything that must be kept. - Misconfiguration warns on standard error. A
remoteServerwithout aremoteToken, 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.rawValueis now 15 (was 7), because.defaultincludes.metadata. Upgrade: only if you stored or compared the raw value;LogOptions(rawValue: 7)is the old default.LogFile.loggermodified before bootstrap is rebuilt. If it still has theperfect.logfilelabel (for example afterLogFile.logger.logLevel = .debug),bootstraprebuilds it on the new backend with the same level and metadata. A logger you assign with another label is kept. Upgrade: none.- Additive API:
bootstrapand both handler initializers gained a defaultedmetadataProvider:parameter, and there are newLogOptions.metadataandRemoteLogHandler.maxPendingEvents.LogOptions.defaultnow 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");
RemoteLogHandlernow imports FoundationNetworking. - Crash on a full disk:
FileLogHandlerusedseekToEndOfFile()/write(_ Data), which trap on I/O errors. Logging to/dev/fullin a swift:6.4-noble container died with "'try!' expression unexpectedly raised an error … No space left on device". Writes now go throughwrite(2)withEINTRretry, 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 withO_NONBLOCK, so it fails withENXIOand 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 LinuxSIGPIPEis blocked on the writing thread and consumed, so the write fails withEPIPE. - 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.timeoutIntervalis an idle timeout (and Linux'sURLSession.sharedused 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.loggergetter created the default logger while holding its lock, so aLoggingSystemfactory that usedLogFiledeadlocked. 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 whenbootstrapruns is discarded rather than cached. - Locale-dependent timestamps: the formatter followed the user's locale and calendar. A
th_THuser got year 2544,ja_JP@calendar=japanesegot 0013, andar_SAgot Arabic-Indic digits and an Arabic "GMT". It's now pinned toen_US_POSIXand Gregorian, and locked for use across threads. - Metadata ignored: both handlers ignored their
metadataProviderand every metadata key buteventid. LogFile.terminalwrote[CRITICAL]instead of 3.x's[EMERG]. It now tags the event withperfect.priority=terminal, a namespaced key, so userprioritymetadata can't trigger it.- Data race on
LogFile.logger: it was anonisolated(unsafe)static var, and ThreadSanitizer reported a race when it was replaced while other tasks logged. It's now lock-protected. LogFileused beforebootstrapstayed on swift-log's default stderr handler for the life of the process, including afterLogFile.logger.logLevel = ….bootstrapnow rebinds or rebuilds the defaultLogFile.logger.- Unbounded remote fan-out:
RemoteLogHandlerstarted a detachedTaskand aURLSessionrequest for every log line, with no limit and no timeout. It also built a newISO8601DateFormatterper 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 withoutLogEvent(before 1.11.0), which failed to build ("cannot find type 'LogEvent' in scope"). Versions before 1.12.0 also lackLogEvent.error, which the handlers now use, so the floor is 1.12.0. LogFile.debugevaluated 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_FSIZEraisesSIGXFSZ, 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 = xis a read then a write; racing it againstLogFile.logger = ycan lose one of them.- A
LoggingSystemfactory that logs throughLogFilewhileLogFile'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 usesLogFile. - 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, soLogFilenever writes to it. FileLogHandlerkeeps its file descriptor open for the life of the process. Rotating by renaming the file (rather thancopytruncate) keeps writing to the renamed file..traceand.noticeare written as[DEBUG]and[INFO], sinceLogPriorityhas no equivalents.LoggingSystem.bootstrapcan only run once per process, soPerfectLogger.bootstrapcan't be called twice.
Upgrade notes
- From 3.x: see Breaking changes. Replace the
LogFile.location/options/thresholdandRemoteLoggersettings with onePerfectLogger.bootstrap(...)call at startup. - From the pre-release
main: nothing to change. Expectkey=valuesuffixes 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.