iOS14から登場したNew Logger API vs OSLog

こんにちは、 ２１日目はiOSエンジニアの paper_and_paper です。

アプリを開発されている皆さんは、自分のデバイスでは再現しないようなエラーや不具合ってどれくらいございますか？

おそらくユーザが数100万人規模になると、ユーザからのお問い合わせや不具合報告だけでもかなり頻度と数が上がってくるかと思います。しかし、実際にフタを空けてみたとしても、最初から手順や条件が確立していたり、高頻度で再現できるものは多くはなく、大半の不具合やエラーは開発端末でも再現しにくいため、何かともどかしい思いをします。

今年のWWDC2020では、そんな再現性が低いときの心強い見方、**ロギング** のAPIが新たに発表されました。今回は、それについて少し触れたいと思います。

## ロギングの役割
ユーザはバグが生じない高品質なアプリケーションを期待します。しかし、ソフトウェアにはどうしてもバグは付き物で、日々開発を進めていると大小様々な不具合と対峙しなければなりません。その中でも一番厄介なのは、すぐに**再現できないバグ**です。

再現が難しいバグを検出し、修正するのにログは有用なツールとなります。バグを再現することなく、バグを把握するための手がかりを提供してくれる。また、ログは再現不可能なバグに対する理解を助けてくれるという意味では他に方法ありません。（もしかすると私が知らないだけかもしれません...）

非常に稀にしか生じないエラーだと、そもそもデバッガーで再現ができません。アプケーションにロギングを追加すれば、エラーを再現せずとも把握できます。

## 従来のロギングは...

```swift
import os.log

let log = OSLog.init(subsystem: "jp.iridge.test.logger", category: "main")
os_log("Start network call", log: log)
```

現在のAppleの標準的な統一ロギングシステムは、iOS10から使用できるようになりました。
詳しくは[Unified Logging and Activity Tracing - WWDC2016](https://developer.apple.com/videos/play/wwdc2016/721/)を参照下さい。

subsystemにはアプリを識別するユニークなバンドルIDを指定し、categoryにはモジュールを指定することができました。

## New logging API in Xcode12
Xcode12では統合ログとして使用できる新しいSwift APIが導入されたことはご存知でしょうか？
![Screen Shot 2020-12-21 at 8.51.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/133774/3f42c040-a0a9-b3a3-1d61-bdc387508200.png)

このAPIでは、従来のロギングよりも以下の点が強調されました。(※OSバージョンはiOS14から)

- アプリケーション実行時に発生する重要なイベントを記録ができる。
- オペレーションシステムによりアーカイブされるため、後でデバイスから取得できる。
- 非常に効率的で、アプリケーションの動作遅延を招くことなく広範囲で使用できる。

### ロギング
新しいロギングAPIを使用するには、まず `os_log` と同様に `import os.log` を記述する必要ですが、syntaxに関しては従来とはかなり異なります。

```swift
import os

// Logger型のインスタンスを生成する
// バンドル識別子と "main"をカテゴリを持つLoggerを作成する
let logger = Logger(subsystem: "jp.iridge.test.logger", category: "main")

func beginTask(url: URL, handler: (Data) -> Void) {
    launchTask(with: url) {
        handler($0)
    }
    logger.log("Started a task")
}
```

`Logger`では文字列補間が非常に良くサポートされており。さらに以前のC言語スタイル寄りだったフォーマットも、よりSwiftっぽいスタイルへと変貌を遂げました。

```swift:New
let count = 0
let logger = Logger(subsystem: "jp.iridge.test.logger", category: "main")
logger.debug(“Start \(count) network call”)
```

```swift:Old
let count = 0
let log = OSLog(subsystem: "jp.iridge.test.logger", category: "main")
os_log("Start %d network call", log: log, type: OSLogType.debug, count)
```

### Logデータの取得

1. Macにデバイスを接続する
2. デバイスオプションを指定して、 `log collect`コマンドを実行する
3. アーカイブログをダブルクリックする （コンソールが起動する）

```bash:コマンド例
$ log collect --device --start '2020-12-21 10:00:00 --output hoge.logarchive' 
```

ログが必要なタイミングを開始時刻として指定します。（通常はバグを最初に確認した数分前を指定すれば良いかと思います） また、もし出力ファイルに任意の名前を付けたい場合、アーカイブログを格納するためのファイル名も指定することもできます。


```bash
$ log -h

usage:
    log <command>

global options:
    -?, --help
    -q, --quiet
    -v, --verbose

commands:
    collect         gather system logs into a log archive
    config          view/change logging system settings
    erase           delete system logging data
    show            view/search system logs
    stream          watch live system logs
    stats           show system logging statistics

further help:
    log help <command>
    log help predicates
```

### ログレベル
メッセージの重要度を表すため、5つのログレベルが用意されています。

```swift
Debug				// デバッグレベルは有用なメッセージに対してデバッグ時に限りなく使われる
Info				// トラブルシューティングエラーに有用である。不可欠ではないメッセージに使われる
Notice (Default)	// トラブルシューティングに不可欠なメッセージ使われる
Error				// 実行中に発生するエラーを記録するために使用される
Fault				// プログラム内の潜在的なバグが原因で発生する状況を記録するために使われる予定
```

iOS14以前の場合だと、OSLogType型の引数`type`をもつ `os_log` を宣言していました。 

```swift:Old
let count = 0
let log = OSLog(subsystem: "jp.iridge.test.logger", category: "main")
os_log("Start %d network call", log: log, type: OSLogType.debug, count)
```

iOS14になると、以下のように先にLogger APIが用意してくれます。
![Screen Shot 2020-12-21 at 9.36.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/133774/58baff93-45ce-ab2e-35fe-3cae934f0ea9.png)


### メッセージの持続性
メッセージに持続性があるか否かはログレベルによって異なります。
メソッドの重要度が高いほど、持続性が高くなります。

```swift
Debug				// 持続性なし
---
Info				// log collect コマンドまでは持続する
---
Notice (Default)	// 
Error				// ストレージの限界まで持続性あり
Fault				// 
```

**Debugレベル**のメッセージは持続性がなく、アプリケーションが終了した後に取得することはできません。**情報レベル**のメッセージの大半は持続性がありませんが、ログ収集コマンドまでの短時間に生成されたメッセージだけは例外となります。**通知レベル、エラーレベル、障害レベル**でロギングされたメッセージは持続性があり、後に取得することができます。もちろんアーカイブするメッセージ数には容量制限があり、制限を超えると古いメッセージから削除されて閲覧ができなります。**エラー・障害メッセージは通知レベルのメッセージよりも長く保持される**ようです。

### 型の範囲
- Int, Doubleなどの数値型
- Objective-C オブジェクト
- `CustomStringConvertible` プロトコルに準拠する任意の型

独自の型を対応させたい場合は `CustomStringConvertible`プロトコルに準拠するだけというのが非常にシンプルですね。

### ログメッセージの可視性
メッセージをロギングする場合、物理的にデバイスにアクセスさえできれば、中身を覗き見ることができてしまいます。そのため、敏感な情報を公開できるかどうかは気にかかるところです。

従来の `os_log` では、以下のようにフォーマットオプションで可視性が設定できて、**デフォルトのままだと動的な値が private** となります。例えば、文字列で表されるユーザの銀行口座番号が含まれているメッセージをロギングした場合、出力ログでは口座番号は個人情報として編集されます。


```swift:Old
let log = OSLog(subsystem: "jp.iridge.test.logger", category: "main")
os_log("Bank account number %{private}s, ", log: log, type: .info, 00011112222)
```

`Logger`でも同様、可視性のオプションが設定できますが、さらにカスタマイズオプションが追加されています。`os_log`と同様に動的な変数は**デフォルトではprivate**となっており、そこで詳細なことも パラメータ `.public` or `.private` で詳細な設定ができます。

```swift:New
logger.log("Bank account number \(XXXXXXXX, privacy: .private)")
```

```swift:New
logger.log("Bank account number \(XXXXXXXX, privacy: .private(mask: .hash))")
```

機密情報を扱わないデータの場合はログで可視化することもできます。実行時間データをロギングする場合は 値 `.public` をオプションパラメータ個人情報に渡します。

```swift:New
logger.log("Ordered smoothies \(smoothieName, privacy: .public)")
```
これでログのデータが表示されるようになります。


以上です。

ありがとうございました。


## References
- [logging - Apple Developer](https://developer.apple.com/documentation/os/logging#2878594)
- [Explore logging in Swift - WWDC2020](https://developer.apple.com/videos/play/wwdc2020/10168/)
- [Unified Logging and Activity Tracing - WWDC2016](https://developer.apple.com/videos/play/wwdc2016/721/)
- [Migrating to Unified Logging: Console and Instruments - Raywenderlich.com](https://www.raywenderlich.com/605079-migrating-to-unified-logging-console-and-instruments)