Change the OSLog level that is written to Xcode console of a dependent library

I'd like to give control to the app developer that uses my library to select which level of logs they'd like to see from my lib (e.g. do they want to see all debug messages or just errors).

I know there are filtering controls that Xcode gives us, but I'm wondering if there is a way to pull this off with code. Ideally the user callsite would look like MyLib(logLevel: .info).

And then I would pass that info level somehow to OSLog. Today, I create my logger like this:

let myLogger = Logger(
    subsystem: Bundle.main.bundleIdentifier ?? "UnknownApp",
    category: "MyLibrary"
)

As far as I can tell, there is nothing I can then pass to my myLogger instance to configure the threshold level. I'm imagining an interface like:

myLogger.logLevel(.warning)
// Later...
myLogger.debug("You won't see this")
myLogger.error("But you will see this")

Does OSLog and friends give us any ability to do this out of the box, or are we building little wrappers around OSLog to accomplish this?

Thank you, Lou

Answered by DTS Engineer in 827797022

You can’t associate a level with a Logger object, but it is possible to log at a user-supplied level:

import os.log

class MyLib {

    init(logLevel: OSLogType) {
        self.log = Logger(subsystem: "com.example.Test774931", category: "general")
        self.logLevel = logLevel
    }
    let log: Logger
    let logLevel: OSLogType
    
    func someMethod() {
        log.log(level: self.logLevel, "will start some method")
        … do stuff …
        log.log(level: self.logLevel, "did finish some method")
    }
}

Is that sufficient?

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

You can’t associate a level with a Logger object, but it is possible to log at a user-supplied level:

import os.log

class MyLib {

    init(logLevel: OSLogType) {
        self.log = Logger(subsystem: "com.example.Test774931", category: "general")
        self.logLevel = logLevel
    }
    let log: Logger
    let logLevel: OSLogType
    
    func someMethod() {
        log.log(level: self.logLevel, "will start some method")
        … do stuff …
        log.log(level: self.logLevel, "did finish some method")
    }
}

Is that sufficient?

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Thanks Quinn the Eskimo!

I don't like that quite as much, because then all logs from my lib will be at the same level (the one passed in by the user of the lib).

The something-is-very-broken logs would be drowned out in a sea of debug logs. For example, there are debug logs that emit on each audio buffer coming from the mic, containing the sample count in the buffer. These are frequent and only meant to be 'turned on' when I'm trying to debug an issue.

I'll share what I ended up doing. It makes log lines a little cumbersome for me, but thankfully users of my lib don't have to deal with that.

import OSLog

public enum AIProxyLogLevel: Int {
    case debug
    case info
    case warning
    case error
    case critical

    func isAtOrAboveThresholdLevel(_ threshold: AIProxyLogLevel) -> Bool {
        return self.rawValue >= threshold.rawValue
    }
}

internal var aiproxyCallerDesiredLogLevel = AIProxyLogLevel.warning
internal let aiproxyLogger = Logger(
    subsystem: Bundle.main.bundleIdentifier ?? "UnknownApp",
    category: "AIProxy"
)

// Why not create a wrapper around OSLog instead of forcing log callsites to include an `if ll(<level>)` check?
// Because I like the Xcode log feature that links to the source location of the log.
// If you create a wrapper, even one that is inlined, the Xcode source feature always links to the wrapper location.
@inline(__always)
internal func ll(_ logLevel: AIProxyLogLevel) -> Bool {
    return logLevel.isAtOrAboveThresholdLevel(aiproxyCallerDesiredLogLevel)
}

And then my lib adds log lines like so:

if ll(.warning) { aiproxyLogger.warning("this is a warning log") }
if ll(.debug) { aiproxyLogger.debug("this is a debug log") }
...

And I expose a simple interface to the user of the lib that internally sets aiproxyCallerDesiredLogLevel.

It's a little unorthodox, but it's getting the job done for me, and Xcode's link-to-log-source functionality still works.

Big fan of your work. Every time I stumble on a great forum or technical note thread it always has your name on it.

ps. long live macnetworkprog!

Lou

Accepted Answer

How about the following?

internal func logif(_ logLevel: AIProxyLogLevel) -> Logger? {
    if logLevel.isAtOrAboveThresholdLevel(aiproxyCallerDesiredLogLevel) { aiproxyLogger } else { nil }
}

logif(.warning)?.warning("this is a warning log")
Written by lzell2 in 827813022
ps. long live macnetworkprog!

Huzzah!

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Well I don't mind that one bit. Thank you!

Adopted in https://github.com/lzell/AIProxySwift/pull/113

Change the OSLog level that is written to Xcode console of a dependent library
 
 
Q