OSLog in Swift macro doesn't persist original file/line number

314 Views Asked by At

I need to log to OSLog and into a file in parallel (due to OSLogStore not being able to provide old logs).

In Objective-C I can accomplish this with a macro using __FILE__ and __LINE__ in the macro implementation which still reference the position in the original code. I tried to create a Swift macro to make this work from Swift. However, log() takes the file and line number of the macro definition file instead of the position in the calling code. So when I click on the metadata link, I'm taken to the macro instead of the calling location.

Is there any solution to that? #file and #line are correctly set in the macro but there’s no way to specify file and line number to the log() function.

3

There are 3 best solutions below

1
killobatt On

I had a similar problem, solved it by dumping OSLogStore contents to file once a while.

0
Claus Jørgensen On

Unfortunately there's no way for you to do this*. Apple in their infinitive wisdom didn't think anyone would ever do such a thing.

The recommendations from the Apple forums by their resident inhouse Eskimo is that you write a Swift Macro to wrap it, basically writing two lines of logging for every log statement.

* Strictly speaking not true. You could wrap the C APIs yourself, but that's generally not recommended

1
iUrii On

The Xcode's compiler looks at locations of OSLog logger's calls inside your code and points this info to the metadata hence you need to have the different calls for different lines of your code.

You can use an approach with the closure which contains a call to the logger that being passed to your log function:

func writeToFile(_ message: String, file: String, line: Int) {
  let fileName =  NSString(string: file).lastPathComponent
  print("<\(fileName):\(line)> \(message)")
}

func log(_ message: String, file: String = #file, line: Int = #line, writeToLog: (String) -> Void) {
  writeToLog(message)
  writeToFile(message, file: file, line: line)
}

Now you can call your log function in the following manner:

let logger = Logger(subsystem: "com.logger.Test", category: "Test")

log("a") { logger.log("\($0)") }
log("b") { logger.log("\($0)") }

Xcode's Debug Console outputs:

<AppDelegate.swift:35> a
<AppDelegate.swift:36> b
a
Timestamp: 2023-11-26 15:55:08.177017+02:00 | .../AppDelegate.swift 35:23
b
Timestamp: 2023-11-26 15:55:08.177078+02:00 | .../AppDelegate.swift 36:23

To eliminate unnecessary repeated code you can make #log swift macro:

@freestanding(expression)
public macro log(_ message: String) = #externalMacro(module: "MyMacroMacros", type: "LogMacro")

public struct LogMacro: ExpressionMacro {
  public static func expansion(of node: some FreestandingMacroExpansionSyntax, in context: some MacroExpansionContext) throws -> ExprSyntax {
    guard let message = node.argumentList.first?.expression.description else {
      fatalError("No message")
    }
    return "log(\(raw: message)) { logger.log(\"\\($0)\") }"
  }
}

Your code converts to:

import MyMacro

...

#log("a")
#log("b")