19

In Xcode 12 / iOS 14, OSLog gained support for string interpolation (yay!). But it's still not possible to attach hooks to easily log to other channels, such as Crashlytics.

So I figured I'll just make a simple wrapper and pass on the parameters. However, there seems to be some magic happening regarding string interpolation.

The new Logger class provided, it takes a OSLogMessage as parameter and can be used as follows:

let someVar = "some var"
let logger = Logger(subsystem: "com.my.app", category: "UI")

logger.error("some message")
logger.error("some message with default var: \(someVar)")
logger.error("some message with private var: \(someVar, privacy: .private)")
logger.error("some message with private var: \(someVar, privacy: .private(mask: .hash))")
logger.error("some message with public var: \(someVar, privacy: .public)")

Wrapping the new Logger struct

So lets just wrap this in a struct:

struct MyLogger {
    let logger = Logger(subsystem: "com.my.app", category: "UI")

    func error(_ message: OSLogMessage) {
        logger.error(message)
    }
}

Same signature, but unfortunately, the compiler won't allow this:

ERROR: Argument must be a string interpolation

Furthermore, trying to call my struct also causes a weirdly specific compiler error:

let logger = MyLogger()
let value = "value"
logger.error("Some log message \(value, privacy: .public)")

Yields:

String interpolation cannot be used in this context; if you are calling an os_log function, try a different overload

Directly calling os_log(_: OSLogMessage) instead of the new struct gives the same result.

Is there a way to work around this? Am I missing something?

Kevin R
  • 8,230
  • 4
  • 41
  • 46
  • 2
    You cannot wrap a Logger call. The argument must be a string literal, not a passed value. – matt Jul 01 '20 at 12:30
  • @matt Thanks, can you tell me why isn't it possible to pass on a string literal? To me it seems a bit unclear that me function seems to be taking a defined type as a parameter, but not really? Also, is there another way to work around this? – Kevin R Jul 02 '20 at 09:10
  • 1
    Once you pass it on, it is no longer a literal! “Pass on” and “literal” are opposites. – matt Jul 02 '20 at 12:31
  • Thanks @Matt, this definition wasn't clear to me. Is this an intended restriction? Seems to really limit the usage here :(. Means you can't even build a helper function to return a log message? Also is this behaviour is imposed because `OSLogMessage` implements `ExpressibleByStringLiteral`? Do you know any good resources to read more about this? The docs seem quite limited. – Kevin R Jul 03 '20 at 10:09
  • 1
    The docs on unified logging are not "limited". They are atrocious! Well, I shouldn't exaggerate; they are _way_ better than they used to be. — What I suggest is just watching the WWDC videos over the years, since about 2016 IIRC. Apple has always warned not to prematurely interpolate with `os_log`. The whole point is that the interpolated values are not even evaluated until runtime, _after_ we have left the app process and gone into logging-land. That way, expensive interpolations are postponed, and the app itself is not slowed. The new Logger enforces this by requiring a literal. – matt Jul 03 '20 at 14:04
  • 7
    The annoying part here is if we wanted to inject #file and #line, it's being fully prevented. So os_log is not useful if we want to log file/line/function names in the logs. Apple might be enforcing it, but they're also making their logger basically useless for most developers. And let's not even start about the ability to write to a file. I guess we'll see the major OSS loggers re-implementing the string interpolation that Apple added to os_log. – Claus Jørgensen Jul 14 '20 at 18:04

2 Answers2

12

From the Apple Forums:

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.

mrtnrst
  • 41
  • 6
Shady Mostafa
  • 815
  • 10
  • 15
2

I just use something like this to workaround the limitation:

struct MyLogger {
    let logger = Logger(subsystem: "com.my.app", category: "UI")

    func error(_ message: String) {
        logger.error("\(message)")
    }
}
maremoto007
  • 104
  • 1
  • 5
  • 1
    This will break the log entries - all messages will simply be cut from there https://developer.apple.com/documentation/os/oslogprivacy/3578096-private – Kruperfone Apr 01 '22 at 14:06