SwiftのOSLogでログを取得するときにログ内にログレベルを埋め込む方法と、OSLogStoreでログレベルが記載されているログだけを抽出する方法

# 概要

以前、[OSLogの記事](https://qiita.com/KenUehara/items/aedfcd19f10879c936c0)を書きましたが、こちらの記事で記述した方法だとログ本体にログレベルが付与されないため、ログを分析する基盤に送付したとしても分析することが難しいという点に気付きました。


当記事では、
- OSLogを使用して取得したログにログレベルを自動で付与する
- OSLogStoreでそれらのログを取得するときにログレベルが付与されたログのみを抽出する

という内容について解説します。

- - -
# 詳細

## OSLogを使用して取得したログにログレベルを自動で付与する

OSLogには`Trace,Debug,Info,Notice,Warning,Error,Critical,Fault`の8種類のログがあるので、それぞれのログに自動的にログレベルを付与するために、[こちらの方の記事](https://kumonosu.cloudsquare.jp/program/swift/post-4640/)を参考にして、以下のようなextensionを作成しました。


```Swift
extension Logger {
    static var level = OSLogType.info

    init(label: String) {
        self.init(subsystem: "", category: label)
    }

    func trace(
        _ message: String = "", function: String = #function, line: Int = #line,
        file: String = #file
    ) {
        self.log(level: .debug, "[TRACE][\(file):\(function):\(line)] \(message)")
    }

    func debug(
        _ message: String = "", function: String = #function, line: Int = #line,
        file: String = #file
    ) {
        self.log(level: .debug, "[DEBUG][\(file):\(function):\(line)] \(message)")
    }

    func info(
        _ message: String = "", function: String = #function, line: Int = #line,
        file: String = #file
    ) {
        self.log(level: .info, "[INFO][\(file):\(function):\(line)] \(message)")
    }

    func notice(
        _ message: String = "", function: String = #function, line: Int = #line,
        file: String = #file
    ) {
        self.log(level: .debug, "[NOTICE][\(file):\(function):\(line)] \(message)")
    }

    func warning(
        _ message: String = "", function: String = #function, line: Int = #line,
        file: String = #file
    ) {
        self.log(level: .error, "[WARNING][\(file):\(function):\(line)] \(message)")
    }

    func error(
        _ message: String = "", function: String = #function, line: Int = #line,
        file: String = #file
    ) {
        self.log(level: .error, "[ERROR][\(file):\(function):\(line)] \(message)")
    }

    func critical(
        _ message: String = "", function: String = #function, line: Int = #line,
        file: String = #file
    ) {
        self.log(level: .fault, "[CRITICAL][\(file):\(function):\(line)] \(message)")
    }

    func fault(
        _ message: String = "", function: String = #function, line: Int = #line,
        file: String = #file
    ) {
        self.log(level: .fault, "[FAULT][\(file):\(function):\(line)] \(message)")
    }
}
```


このextensionにより、自動的に`ログレベル`、`呼び出し元の関数`、`呼び出し元の関数のファイル内の行`が自動でログに付与されるようになります。

実例として、SwiftUIのボタンをタップしたときに全ての種類のログが実行されるようにしてみます。

```Swift
Button("ログ出力") {
            AppLogger.shared.logger.trace("trace:\(Date())")
            AppLogger.shared.logger.debug("debug:\(Date())")
            AppLogger.shared.logger.info("info:\(Date())")
            AppLogger.shared.logger.notice("notice:\(Date())")
            AppLogger.shared.logger.warning("warning:\(Date())")
            AppLogger.shared.logger.error("error:\(Date())")
            AppLogger.shared.logger.critical("critical:\(Date())")
            AppLogger.shared.logger.fault("fault:\(Date())")
        }
```


このボタンをタップすると、以下のような形式でログが出力されます。

```
[TRACE][...ContentView.swift:body:25] trace:2024-01-24 08:55:24 +0000
[DEBUG][...ContentView.swift:body:25] debug:2024-01-24 08:55:24 +0000
[INFO][...ContentView.swift:body:26] info:2024-01-24 08:55:24 +0000
[NOTICE][...ContentView.swift:body:27] notice:2024-01-24 08:55:24 +0000
[WARNING][...ContentView.swift:body:28] warning:2024-01-24 08:55:24 +0000
[ERROR][...ContentView.swift:body:29] error:2024-01-24 08:55:24 +0000
[CRITICAL][...ContentView.swift:body:30] critical:2024-01-24 08:55:24 +0000
[FAULT][...ContentView.swift:body:31] fault:2024-01-24 08:55:24 +0000
```

これで目的だったログレベルが自動で付与されるようになりました。

## OSLogStoreでそれらのログを取得するときにログレベルが付与されたログのみを抽出する
次に、抽出処理です。
先述のログを外部のログ分析基盤(Cloudwatch Logsなど)に送付する場合、OSLogStoreを使用することになると思いますが、[こちらの記事](https://qiita.com/KenUehara/items/c7339b9c01b9ed4c31e1)で解説している通り、OSLogStoreで蓄積されたログを取得すると、iPhone本体のログなどが混ざるため、送付するログとしては適切ではありません。

そこで、ログレベルが付与されているログだけを抽出するために以下のような関数を用意しました。

```swift
func exportLog(startDate: Date, userName: String) async {
            do {
                let store = try OSLogStore(scope: .currentProcessIdentifier)
                let logLevels = [
                    "[INFO]", "[TRACE]", "[DEBUG]", "[WARNING]", "[ERROR]",
                    "[CRITICAL]", "[FAULT]",
                ]
                
                let position = store.position(date: startDate)
                let enumerator = try store.__entriesEnumerator(
                    options: [.reverse], position: position, predicate: nil)

                enumerator.forEach { element in
                    if let logEntry = element as? OSLogEntry {
                        let message = logEntry.composedMessage
                        for logLevel in logLevels {
                            if message.starts(with: logLevel) {
                                logMessages += message + "\n"
                            }
                        }
                    }
                }
            // ここに送信処理を実装
            } catch {
                print("Error exporting logs: \(error)")
            }
        }
```

この関数を使用することによって、OSLogのログの内、ログレベルが埋め込まれているログのみを抽出することが可能です。


```text:OSLogのログ
...
CF Read: domain = com.apple.Accessibility, preference = SpeechSettingsDisabledByManagedConfiguration, appID = (null) result = (null) (-1 - empty, 0 - false, 1 - true)
CF Read: domain = com.apple.Accessibility, preference = QuickSpeak, appID = (null) result = (null) (-1 - empty, 0 - false, 1 - true)
Evaluating dispatch of UIEvent: 0x600003914000; type: 0; subtype: 0; backing type: 11; shouldSend: 1; ignoreInteractionEvents: 0, systemGestureStateChange: 0
Sending UIEvent type: 0; subtype: 0; to windows: 1
Sending UIEvent type: 0; subtype: 0; to window: <UIWindow: 0x108214410>; contextId: 0xF00E590D
Evaluating dispatch of UIEvent: 0x600003914000; type: 0; subtype: 0; backing type: 11; shouldSend: 0; ignoreInteractionEvents: 0, systemGestureStateChange: 1
Evaluating dispatch of UIEvent: 0x600003914000; type: 0; subtype: 0; backing type: 11; shouldSend: 1; ignoreInteractionEvents: 0, systemGestureStateChange: 0
Sending UIEvent type: 0; subtype: 0; to windows: 1
Sending UIEvent type: 0; subtype: 0; to window: <UIWindow: 0x108214410>; contextId: 0xF00E590D
[TRACE][body:24] trace:2024-01-23 10:03:26 +0000
[DEBUG][body:25] debug:2024-01-23 10:03:26 +0000
[INFO][body:26] info:2024-01-23 10:03:26 +0000
[NOTICE][body:27] notice:2024-01-23 10:03:26 +0000
[WARNING][body:28] warning:2024-01-23 10:03:26 +0000
[ERROR][body:29] error:2024-01-23 10:03:26 +0000
[CRITICAL][body:30] critical:2024-01-23 10:03:26 +0000
[FAULT][body:31] fault:2024-01-23 10:03:26 +0000
....
```


```text:関数を使用して特定のログのみを抽出した結果(一部ディレクトリを省略しています)
[TRACE][...ContentView.swift:body:25] trace:2024-01-24 08:55:24 +0000
[DEBUG][...ContentView.swift:body:25] debug:2024-01-24 08:55:24 +0000
[INFO][...ContentView.swift:body:26] info:2024-01-24 08:55:24 +0000
[NOTICE][...ContentView.swift:body:27] notice:2024-01-24 08:55:24 +0000
[WARNING][...ContentView.swift:body:28] warning:2024-01-24 08:55:24 +0000
[ERROR][...ContentView.swift:body:29] error:2024-01-24 08:55:24 +0000
[CRITICAL][...ContentView.swift:body:30] critical:2024-01-24 08:55:24 +0000
[FAULT][...ContentView.swift:body:31] fault:2024-01-24 08:55:24 +0000
```

以上です。