Reputation: 5125
Apple recommends using OSLog
`Logger. It is promoted on WWDC al least since 2020 (for example see WWDC 2023 Session 10226 Debug with structured logging).
It is supposed to be super efficient and so on.
It greatest limitation is that:
iOS has very limited facilities for reading the system log. Currently, an iOS app can only read entries created by that specific process, using .currentProcessIdentifier scope. This is annoying if, say, the app crashed and you want to know what it was doing before the crash. What you need is a way to get all log entries written by your app (r. 57880434).
Source: Your Friend the System Log
To let users send logs for a certain period even in case of crashes I couldn't find a better solution but to duplicate logging to a set of files with rotation. If Logger
had some kind of delegate that would look like the best option to do some extra work at right time, but unfortunately it doesn't.
For interoperability I decided to create a logger that has exactly the same function signatures as the system one OSLog
Logger
.
Since Logger
is a struct
I can't inherit it and override its functions to inject file logging along with inherited system logging.
So I decided to wrap Logger
into another struct
HybrydLogger
and just replicate original functions with their signatures like this:
public func error(_ message: OSLogMessage) {
osLogger.error("\(message.description)")
fileLogger.log(level: .error, message.description)
}
There are only a few methods to reimplement, such as (debug
, fault
, and similar).
But I stumbled upon a limitation which seems to be intentional.
When I call HybridLogger.shared.error("Configuration failed: \(res, privacy: .public)")
, I get error: "String interpolation cannot be used in this context; if you are calling an os_log
function, try a different overload."
Looks like it has something to deal with the note in the doc.
You don’t create instances of OSLogMessage directly. Instead, the system creates them for you when writing messages to the unified logging system using a Logger.
From the Swift forum:
The logging APIs use special compiler features to evaluate the privacy level at compile time. As the diagnostic says, you must use a static (i.e., known at compile time) method or property of ‘OSLogPrivacy’; it can’t be a variable that’s evaluated at run time. The implication is that you can’t create your own wrapper for these APIs without using compiler-internal features.
UPD: So the question is: what are the options to accomplish the task of persistent logging while keeping the advantages of Apple "structured logging".
Upvotes: -2
Views: 65
Reputation: 5125
The best option I came up with for now is to replace OSLog
Logger
with Apple's (open source) SwiftLog which allows for multiplexing different loggers.
By combining LoggingOSLog, which logs using OSLog
and XCGLogger
that writes to a file with rotation as MultiplexLogHandler
s for the SwiftLog logger I can get the task done.
import Logging
import LoggingOSLog
import XCGLogger
import FileLogging
let xcgLogger: XCGLogger = {
let log = XCGLogger(identifier: "advancedLogger", includeDefaultDestinations: false)
// Setup `xcgLogger` in a way that it logs only to a file and doesn't duplicate
// LoggingOSLog output to console.
}
...
LoggingSystem.bootstrap { label in
let handlers:[LogHandler] = [
LoggingOSLog(label: label, logLevel: .debug),
XCGLoggerHandler(label: label, logger: xcgLogger),
]
return MultiplexLogHandler(handlers)
}
...
The advantages of such approach:
import OSLog
with import Logging
. Use Logger
as before, just without extra formatting (i.e. `.privacy). There is a way to support same string interpolation capabilities though (see below)..logarchive
logs on device. You can retrieve them if you have physical access to the device and process in Console, Instruments, Xcode. Those logs are aligned with other entries logged by other systems.Disadvantages:
Jump to code
does jump to the code, but not to the Logger
's line but always to the same os_log()
line in LoggingOSLog
which is logical. Native OSLog
OSLogMessage has some compiler related magic that serves for that, and that magic is not easy to replicate..debug()
, .error()
and other similar function signatures are slightly different (the don't include complex string interpolation features like .public
specifier, for example) – you'll need to change the code related to Logger
through all the project a bit, if you previously adopted OSLog
logging. That is not easy to overcome without modifying SwiftLog
:A version of Apple's SwiftLog that adds some improved formatting for app development and includes OSLog-ish string interpolation.
If it was up to me to decide I would just introduce two separate loggers and would accept the fact that I need two lines of code (one for the system logger and one for the file logger) per logging operation. Maybe it's a job for Swift macro but it may have the same limitations as wrapping os_log()
into your own function.
On desktops people try to solve this task by making their own loggers: Enhance macOS logging with syslog-ng’s native macOS system() source).
Upvotes: 0