SwiftプロジェクトでOSLogStoreを使ってユーザーのログを回収する

# 概要

ネット上にOSLogStore周りの使い方をまとめた記事があまりなかったのでそれらの記事を参考にしながら、iPhoneで実機のログを収集するサンプルを作りました。

## OSLogStoreとは？
Apple側で用意してくれているSwiftプロジェクト内で使用できるLogを取るためのライブラリ。
ユーザーのシステムログを取るのに使える。
公式ドキュメントは[ここ](https://developer.apple.com/documentation/oslog/oslogstore)。

## サンプル
https://github.com/Kuehar/OSLogStore

### イメージ

ボタンをタップした時に報告をするアラートが表示され、

![Simulator Screenshot - iPhone 15 - 2023-10-17 at 01.59.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/412789/904a168f-064e-c600-a716-db7e0a42e7b3.png)

アラートの`OK`をタップしたらログが送信されるようなイメージ。

![Simulator Screenshot - iPhone 15 - 2023-10-17 at 01.59.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/412789/c0e9d79c-0f64-2f34-9b30-22af7a19bc00.png)


今回のソースでは送信時の処理にログファイルが`log.txt`としてiPhoneのアプリ内(ドキュメントディレクトリ)に保存される仕様となっているため、一旦ログの中身を見たい場合には`print(messages)`を追加すること。

```log:ログの出力先情報
Logs exported to: file:///Users/UserName/Library/Developer/CoreSimulator/Devices/358AD377-A8B8-4EDA-92A1-0266704E7597/data/Containers/Data/Application/67A88D6C-ADE9-4636-8DE1-FEC69A137CF7/Documents/logs.txt
```

```log:log.txtの中身(print(messages)実行時)
Using ~iphone resources
main bundle CFBundle 0x600003b082a0 </Users/UserName/Library/Developer/CoreSimulator/Devices/358AD377-A8B8-4EDA-92A1-0266704E7597/data/Containers/Bundle/Application/646B339D-2D47-47E4-A692-D8CD41E9D85F/OSLogStore.app> (executable, loaded) getting handle 0xfffffffffffffffb
[0x600003b10000] activating connection: mach=true listener=false peer=false name=com.apple.cfprefsd.daemon
Initializing connection
Removing all cached process handles
Sending handshake request attempt #1 to server
Creating connection to com.apple.runningboard
[0x600003b14000] activating connection: mach=true listener=false peer=false name=com.apple.runningboard
[0x10dd04080] activating connection: mach=false listener=false peer=false name=(anonymous)
Handshake succeeded
Identity resolved as app<kuehar.OSLogStore((null))>
didChangeInheritances: <RBSInheritanceChangeSet| gained:{(
    <RBSInheritance| environment:(none) name:com.apple.frontboard.visibility origID:86005-86002-137>
)} lost:(null)>
No persisted cache on this platform.
Deactivation reason added: 10; deactivation reasons: 0 -> 1024; animating application lifecycle event: 0
assertion failed: 23A344 21A328: libxpc.dylib + 62124 [D42F682D-2192-3D1C-AA6B-B96242532F0B]: 0x7d
activating monitor for service com.apple.frontboard.open
activating monitor for service com.apple.frontboard.workspace-service
..............................(この後にもログが続く)
```

### ソースファイル

```Swift:ContentView.swift
import SwiftUI
import OSLog

struct ContentView: View {
    @State private var messages:String = ""
    @State private var isLogSendAlertDisplay = false
    
    var body: some View {
        Form {
            Section(header: Text("報告")) {
                Button(action: {
                    exportToLogFile()
                    isLogSendAlertDisplay = true
                }, label: {
                    Text("アプリがクラッシュしたことを報告する")
                })
                .alert("通知", isPresented:$isLogSendAlertDisplay) {
                    Button("OK") {
                        // ログ送付処理をここに書く
                        isLogSendAlertDisplay = false
                    }
                    Button("Cancel",role:.cancel){}
                } message: {
                    Text("クラッシュしたことを開発者に報告します。よろしいですか？")
                }
            }
        }
    }
    /// OSLogStoreを使用し、ユーザーログを抽出する。
    /// その後の処理で`logs.txt`を作成し、ドキュメントディレクトリに保存を行う。
    func exportToLogFile() {
        do {
            let store = try OSLogStore(scope: .currentProcessIdentifier)
            let position = store.position(date: Date().addingTimeInterval(-3600))
            let enumerator = try store.__entriesEnumerator(options: [.reverse], position: position, predicate: nil)
            
            var logString = ""
            messages = ""
            enumerator.forEach { element in
                if let message = (element as? OSLogEntry)?.composedMessage {
                    logString += message + "\n"
                    messages += message + "\n"
                }
            }
            
            // ドキュメントディレクトリのパスを取得
            if let documentDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first {
                let logURL = documentDirectory.appendingPathComponent("logs.txt")
                try logString.write(to: logURL, atomically: true, encoding: .utf8)
                print("Logs exported to: \(logURL)")
            }
        } catch {
            print("Error exporting logs: \(error)")
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

```

`exportToLogFile`の中でOSLogStoreを使用し、ユーザーログを抽出する。
その後の処理で`logs.txt`を作成し、ドキュメントディレクトリに保存を行う。

## プロジェクトに応じて対応すべき内容

`log.txt`の送付処理に関してはプロジェクトによってまちまちな部分なので、今回は記載していない。
この記事の目的はあくまでOSLogStoreを通してユーザーのiPhoneのシステムログを取得するところにあるので、その点に関しては各々で対応するものとしている。