Logger入門
ログの目的
アプリをユーザーに使い続けてもらうためには、バグの修正が重要ですが、時には再現しにくいバグもあります。そんなときに、ログは再現しにくいバグを発見し修正する手がかりになります。ログは OS によりアーカイブされるため、あとでデバイスからログを取得して、分析できます。
ロギングを追加する方法
コード
ロギングの方法は大きく分けて2つあります。1つ目は WWDC20 で発表された Logger APIs を使った、より Swift らしいシンタックスのロギングの方法。2つ目はそれより以前の WWDC16 で発表された os_log
関数を使った C 言語ベースの API を使ったロギングの方法です。
iOS14, tvOS14, watchOS 7, macOS Big Sur以降で利用可能なコード
Logger APIs:
import OSLog
let logger = Logger(subsystem: "com.miharun.logger", category: "Analytics")
private func beginTask() {
let x = 42
let y = "hello"
logger.info("\(x) is an integer and \(y) is a string")
}
Logger同様に文字列補間を受け入れる os_log()
関数(deprecated):
import OSLog
let logger = OSLog(subsystem: "com.miharun.logger", category: "Analytics")
private func beginTask() {
let x = 42
let y = "hello"
os_log(.info, log: logger, "\(x) is an integer and \(y) is a string")
}
それ以前のリリース(iOS10〜13)で利用可能なコード
printfスタイル [1] のフォーマット文字列を受け入れる古い os_log()
関数:
import OSLog
let logger = OSLog(subsystem: "com.miharun.logger", category: "Analytics")
private func beginTask() {
let logger = OSLog(subsystem: "com.miharun.logger", category: "Analytics")
let x = 42
let y = "hello"
os_log(.info, log: logger, "%d is an integer and %s is a string", x, y)
}
ロギングできるデータ型
ロギングできるデータ型は、符号付き・符号なしの Int、Float、Double、Bool、String、NSObject、UnsafeRaw(Buffer)Pointer、配列や辞書のようなCustomStringConvertible
に準拠する値、type(of: c)
やInt.self
のようなメタタイプを含みます。独自の型をログメッセージに表示させるためには、次のように CustomStringConvertible
プロトコルに準拠させるだけでできます [2]。
CustomStringConvertible
プロトコルへの準拠前:
struct Point {
let x: Int, y: Int
}
let p = Point(x: 21, y: 30)
print(p)
// Prints "Point(x: 21, y: 30)"
CustomStringConvertible
プロトコルへの準拠後:
extension Point: CustomStringConvertible {
var description: String {
return "(\(x), \(y))"
}
}
print(p)
// Prints "(21, 30)"
サブシステムとカテゴリ
ログのフィルタリングを容易にするのが、サブシステムに Bundle Identifier を使う方法とカテゴリにクラス名やコンポーネント名を指定する方法です。
import Foundation
import OSLog
let subsystem = Bundle.main.bundleIdentifier ?? "unknown"
let logger = Logger(subsystem: subsystem, category: "Network")
ログハンドル(let logger = Logger(subsystem:category:)
)を複数作ることが大切とされているので、実装時には次のように工夫をするといいでしょう。
import Foundation
import OSLog
let subsystem = Bundle.main.bundleIdentifier ?? "unknown"
// 定義
extension Logger {
static let io = Logger(subsystem: subsystem, category: "I/O")
static let network = Logger(subsystem: subsystem, category: "Network")
}
// 使い方
func didTapButton() {
Logger.io.info("Tapped a button")
}
func fetchData() {
Logger.network.info("Started fetching data")
}
ログを収集する方法
アプリ実行中にログをストリームする方法
アプリがXcodeで起動された場合
アプリが Xcode で起動された場合、Xcode のコンソールで表示できます。たとえば次のログを実行した場合のログの表示を確認していきます。
let logger = Logger(subsystem: subsystem, category: "Analytics")
logger.debug("LogLevel Variable")
logger.info("LogLevel Variable")
logger.notice("LogLevel Variable")
logger.error("LogLevel Variable")
logger.fault("LogLevel Variable")
なお、ログレベルの説明は後述しています。
Xcode15
ログの表示機能が Xcode15 で大きく改善されました。これらの表示機能は、Logger APIs だけでなく、 os_log
関数も同様に機能します。
デフォルトではシンプルにログに出力した文字列だけが表示されますが、下部にフィルターボタンが登場し、表示したいメタデータを選択できます。
メタデータを全選択すると次のような表示になります。さらに、それぞれのログからコードに飛ぶことができます。ログにカーソルを合わせるとまもなく表示される右矢印ボタンをクリックするか、ログ上で右クリックをしてメニューを表示し[Jump To Source]をクリックします。
フィルターのサジェスチョンも有用で、メタデータごとに候補を提案してくれます。
Xcode14以前
Xcode14 以前では、タイムスタンプ、ライブラリ名、PID:ITD、カテゴリ名のメタデータが、ログに出力した文字列と共に表示されるだけです。
デバイスがMacに接続されている場合
デバイスが Mac に接続されている場合、「Console.app」で表示できます。サイドバーで対象デバイスを選択し、フィルターにサブシステム名などを入力して、スタートボタンをクリックします。
アプリ実行後にログを収集する方法
コマンドラインでデバイスからアーカイブログを取得し、コンソールアプリで表示できます。
ログは OS により圧縮された形式でデバイスに保存されます。このログを取得するため、まずデバイスを Mac に接続します。 log collect
コマンドで --device
オプションをつけることで、デバイスのログを取得できます [3] 。
log collect --device
このとき、その他のオプションとして、ログを必要とする開始時刻とアーカイブログを格納するファイル名も指定します。ログを必要とする開始時刻として、通常はバグを最初にみつけた時より数分前にします。
log collect --device --start '2024-11-04 9:41:00' --output sample.logarchive
// Archive successfully written to sample1.logarchive
出力されたアーカイブログ(.logarchive
)をダブルクリックして、コンソールアプリでアーカイブログを開くことができます。コンソールアプリを使うと閲覧やフィルターがしやすいので、活用しましょう。
ログレベル
基本的なログレベルは、Logger APIs と os_log
関数で共通です。
ログレベルの種類
メッセージの重要度を示す5つのログレベルがあります。重要度が低い方から並べると次のとおりです。
- Debug
- Info
- Notice(Default)
- Error
- Fault
ログレベル | 使いどき | コンソールアプリの色 |
---|---|---|
Debug | 有用なメッセージに対して、デバッグ時にだけ使われる。 | なし |
Info | トラブルシューティングに有用だが不可欠ではないメッセージに対して使われる。 | なし |
Notice(Default) | トラブルシューティングに不可欠なメッセージに対して使われる。 | なし |
Error | 実行中に発生するエラーを記録するために使われる。 | 黄色 |
Fault | プログラム内の潜在的なバグが原因で発生する状況を記録するために使われる。プログラムが保持するはずの前提が実行時に破られたことを記録するのにも使われる。 | 赤 |
ログレベルの選び方
ログレベルを選ぶときに考慮すべき重要事項は持続性(persistence)です。持続性とは、アプリの実行が終了した時点でログメッセージをアーカイブしたり取得することが可能かどうかという指標です。たとえば、持続性のないログの場合、アプリの実行中にしかログメッセージをストリーミングできません。ログメッセージに持続性があるか否かは、ログレベルで異なります。
ログレベル | 持続性 | |
---|---|---|
Debug | なし | アプリの実行が完了した後に取得することはできない |
Info | 大半はない。ログ収集コマンドまでの短い時間のログはあり | |
Notice(Default) | あり | アプリの実行が完了した後に取得できる |
Error | あり | アプリの実行が完了した後に取得できる |
Fault | あり | アプリの実行が完了した後に取得できる |
アーカイブできるメッセージの数には容量制限があることに留意してください。容量制限を超過すると、古いメッセージから削除され、閲覧できなくなります。通常、数日間保持されます。エラー・障害レベルのメッセージは通知レベルのメッセージより長く保持されます。
ログレベルの違いによるパフォーマンスの違い
重要度が低いレベルほど速いです。デバッグレベルでのロギングが高速な理由は、デバッグメッセージにまったく持続性がないからです。持続性がないため、デバッグレベルではメッセージ作成のために負荷の高い関数を呼び出しても影響が小さいです。
さらに詳細なログレベルの種類
WWDC20 では上記の5種類のログレベルが説明されていましたが、Logger APIs に関する Xcode の開発者ドキュメントでは、8種類のログレベルが定義されています。
- Trace
- Debug
- Info
- Notice(Default)
- Warning
- Error
- Critical
- Fault
機能的には、Trace は Debug の、Warning は Error の、Critical は Fault のエイリアスです。使いどころをより詳細に分類したい場合に使うといいでしょう。
ログメッセージのフォーマット
Logger APIs では、リッチなデータのフォーマット方法を多数提供しています。ログに出力される生データは理解しづらいことがありますが、データのフォーマットにより、ランタイムコストを払わずにリーダービリティを向上させることができます。これは、iOS14 以降の Logger 同様に文字列補間を受け入れる os_log()
関数でも使うことができます。
logger.log("\(data, format: .hex, align: .right(columns: width))")
例:
// 10進数の値をフォーマットする
let decimalNumber: UInt8 = 42
logger.notice("\(decimalNumber, format: .octal)")
logger.notice("\(decimalNumber, format: .decimal)")
logger.notice("\(decimalNumber, format: .hex)")
// Double型の値をフォーマットする
let doubleNumber: Double = 123456789
logger.notice("\(doubleNumber, format: .exponential)")
logger.notice("\(doubleNumber, format: .fixed(precision: 3))")
// さまざまな桁数の文字列を整列させる
let cardID1 = "ABC123"
let cardID2 = "ABC1234"
let cardID3 = "ABC12345"
let width = 8
logger.notice("\(cardID1, align: .right(columns: width))")
logger.notice("\(cardID2, align: .right(columns: width))")
logger.notice("\(cardID3, align: .right(columns: width))")
個人情報をログで可視化させない
ログは、デバイスへの物理的なアクセス権をもつユーザーであれば誰でも収集できます。そのため、個人情報をログで可視化させることは問題があります。
プライバシーオプションを使用して、メッセージ内の補間された変数を非表示または表示できます。デフォルトでは、数値は表示され、動的な文字列と複雑なオブジェクトは非表示にされます。
また、ユーザーのプライバシーを保護しつつ、特定のアカウントに紐づくログを表示したい場合があります。そのような場合には、 OSLogPrivacy.Mask.hash
[4] を使って、文字列を現在のプロセスに固有のハッシュ値に置き換えることができます。
// 文字列
let accountNumber = "1234567890"
logger.notice("Paid with bank account \(accountNumber)")
let smoothieName = "Apple"
logger.notice("Ordered smoothie \(smoothieName, privacy: .public)")
// 数値
let orderCount = 3
logger.notice("Ordered \(orderCount) smoothies")
let userAge = 23
logger.notice("User's age: \(userAge, privacy: .private)")
// ハッシュマスク
// ユーザーのアカウント番号を隠すが、同一アカウントに対する他のログメッセージとの
// 照合ができるようにハッシュマスクを含める。
logger.notice("Start transaction for account \(accountNumber, privacy: .private(mask: .hash))")
デバイスを Mac に接続して Console.app で表示します [5] 。
参考資料
- 動画
- WWDC23: Debug with structured logging
- WWDC20: Explore logging in Swift
- ドキュメント
-
printf
スタイルのフォーマット文字列とは、C 言語のprintf
関数で用いられる形式のことです。この形式では、プレースホルダーと呼ばれる特殊な記号(%d
、%s
など)が用いられ、これが実際の値と置き換えられます。 ↩︎ -
log collect --help
で他のオプションや日付時刻の有効なフォーマットを確認できます。 ↩︎ -
デバッグ実行中は、開発者が Xcode のコンソールまたは Console.app を通じて全てのログ出力を見ることができます。これにはプライバシー設定が適用されず、ログに含まれる機密情報も表示されます。一方で、デバッグなしでアプリが実行されている場合、実機においては、プライバシー設定が有効化され、特定の情報は隠蔽されて
<private>
と表示されます。
Simulator においては、開発環境の一部と見なされるため、プライバシーの置換が実機と同じようには行われず、Console.app を通じて全てのログ出力を見ることができます。
開発環境ですべてのログ出力を見ることができるのは、問題の特定と解決をより容易にするためだと思われます。 ↩︎
Discussion