ストリーミングはほとんどのブラウザと
Developerアプリで視聴できます。
-
Swiftにおけるロギング
Swiftの統合ロギングAPIの最新バージョンをご確認いただけます。Appでプライバシーの保護を実現し、イベントやエラーのログを取る方法を学びましょう。パーフォーマンスを落とすことなく、データフォーマット用のパワフル、かつ読みやすいオプションをご利用いただけます。また、App内の予期せぬ動作を認識しデバッグするのに役立つ、ログメッセージの収集および処理方法についてお伝えします。
リソース
関連ビデオ
WWDC23
WWDC20
-
ダウンロード
こんにちは WWDCへようこそ “Swiftでロギングしてみる” 私はAppleのエンジニア ラヴィ・カンダダイ・マダヴァンです 今回はos_logとも呼ばれる Appleの統合ロギング APIを使って― アプリケーションのデバッグを より簡単にする方法を紹介します アクセス権を持つデバイスから ログを収集する方法を説明し アプリケーションの問題を把握して修正するのに 有効なツールのデモを行います
ログ呼び出しのパフォーマンスの管理方法と フォーマッティングオプションでログメッセージ のリーダビリティを向上させる方法も紹介します バグを修正することは大切です 信頼でき バグがほとんど生じない高品質の アプリケーションをユーザは期待しています 小さくともバグが存在すると ユーザ体験が低下しかねません 他に比べて修正が困難なバグもあります なかなか見つけられないバグは大抵 開発中に再現が難しいバグです 再現しにくいバグを検出し修正するのに ログは有用なツールです バグを再現することなく バグを把握するための手がかりを提供します 私が開発しているアプリケーションの 再現しにくいバグを見てみましょう その後 このバグを把握・修正できるように ロギングを追加する方法を説明します
Frutaというアプリケーションを使えば ユーザはスムージーを購入できます スムージーを閲覧し スムージーをタップすれば それを購入できます
最近 私はこのアプリケーションに “ギフトカード”機能を追加し― ギフトカードを閲覧するための タブを作成しました Apple Payを使用して購入するには カードをタップします スクロールして最後のカードに到達すると アプリケーション内ではサーバーと通信し 追加のカードの読み込みが始まります カードをタップして1枚選ぶと アプリケーション内の カードの読み込みや通信が停止します しかし 戻って引き続き 追加のカードを閲覧することもできます 大抵 このように機能しますが 残念ながら時折バグが見られることもあります アプリケーションがカードを読み込んでいる時に カードをタップすると エラーで読み込みが失敗することもあります
このエラーは私の開発機の近くで生じないので もどかしいです
非常にまれにしか生じないエラーなので デバッガーで再現できません アプリケーションにロギングを追加すれば このようなエラーを再現せずとも把握できます Xcode 12では統合ログに使用する 新しいSwift APIを導入しています アプリケーション実行時に発生する 重要なイベントをこれらのAPIで記録できます ログはオペレーティングシステムにより アーカイブされるため 後でデバイスから取得できます これらの新しいAPIは非常に効率的なので アプリケーションの動作遅延を招くことなく 広範囲で使えます
たった3つの簡単なステップで アプリケーションにロギングを追加できます まず 新しいロギングAPIを定義する “OS”モジュールをインポートします 次に Logger型のインスタンスを作成し サブシステムとカテゴリを渡します これらはLoggerで記録される各メッセージに 添付されます サブシステムは通常 アプリケーションから送られた メッセージを識別できるバンドル識別子です このカテゴリを使えば プログラムの各部分から 送信されるメッセージを詳細に区別できます ここではLoggerの“ギフトカード”カテゴリを 使っています
最後にLoggerインスタンスのメソッドを呼び出し コード内の適切な場所にロギングを追加します データがサーバーからアプリケーションに ダウンロードされるたびにログを追加しています
Loggerで文字列補間を使えば実行時間データを ログメッセージに追加できます 例えば ここでログメッセージに タスク識別子を追加していきます これは印刷関数を呼び出すのと似ています
ただし ログメッセージには 重要な相違点があります 印刷と違い ログメッセージはよく遅延するため 完全には文字列に変換されません そのかわりに コンパイラとロギングライブラリが連携し ロギングデータの型を利用するログメッセージを 大幅に最適化して表示させます 表示が最適化された場合は ログメッセージが実際に表示される場合にのみ 文字列に変換する代償を払う必要があります ログメッセージには さまざまなデータ型が含まれる可能性があります IntやDoubleなどの数値タイプや Objective Cオブジェクト― SwiftのCustomStringConvertibleプロトコルに 準拠する型をロギングできます つまり独自の型をログメッセージに追加するには CustomStringConvertibleに 準拠させるだけでいいのです 実行時間データを ログメッセージに追加する場合― 文字列やオブジェクトなどの数値以外の型が― ログで編集するように 初期設定されている点に注意してください これが行われるのは アプリケーションが出荷され ユーザのデバイスで実行された時に― ログに個人情報が 表示されないようにするためです
例えば文字列で表されるユーザの銀行口座番号が 含まれているメッセージをロギングした場合― 出力ログでは 口座番号は個人情報として編集されます
ただし 機密情報を扱わないデータの場合は ログで可視化できます 実行時間データをロギングする場合は 値“.public”を― オプションのパラメータ個人情報に渡します
これで ログでデータの内容が 表示されるようになります ここにスムージーの名前が1つあります プライバシーについての詳細は のちほど紹介します
アプリケーションでロギングされたメッセージは オペレーティングシステムにより 圧縮された形式でデバイスに保存されます Macの“log collect”コマンドで これらのログを取得できます まず デバイスをMacに接続します 次に デバイスオプションを指定して 端末から“log collect”コマンドを実行します
ログが必要なタイミングを 開始時刻として指定します 通常は バグを最初に確認した数分前を 指定します アーカイブログを格納するための ファイル名も指定します アーカイブログをダブルクリックして コンソールアプリケーションで アーカイブログを開きます このアプリケーションを使えば ログの閲覧や フィルタリングがしやすくなります Fruta アプリケーションで再現が困難なバグを ロギングを使って把握する方法を見てみましょう “ギフトカード”ビューのソースコードに ロギングを追加しました
私はOSモジュールをインポートし ロギングAPIを開いて バンドル識別子と“ギフトカード”カテゴリを 持つLoggerを作成しました このビューで実行されたイベントを記録するため ロギング機能を追加しました 例えばアプリケーションを使って サーバーと通信するタスクを開始すると タスクを一意に識別するUUIDが ロギングされるようになりました 識別子には機密情報が含まれていないので ログで可視化するように公開しました ログ収集を使って ログアーカイブから Fruta アプリケーションのログを抽出しました では これを コンソールアプリケーションで開いてみます ここには たくさんのログエントリがあります システム内の全プロセスを通じてロギングされた メッセージが含まれているからです アプリケーションの“検索”や “フィルター”機能で確認したいログを絞ります まず サブシステムでフィルタリングします この例では アプリケーションのバンドル識別子です そして Fruta アプリケーションからの メッセージのみを表示するように絞ります
右上の検索フィールドをクリックし サブシステムを入力します
そしてドロップダウン リストから サブシステムを選択します
アプリケーションのログのみをスクロールして エラーに対応するメッセージを探します
私のアプリケーションでは 多数ロギングしているので エントリがまだたくさんありすぎて 他の問題を把握しづらくなっています 本当に必要なのは 問題を絞り込む方法です ロギングされたタスク識別子の ソリューションを利用します 問題が生じたタスクのタスクIDで絞り込み 問題に関連するログのみを表示します タスク識別子を別のキーワードとして 検索フィールドに追加し 実行してみます
今 存在しているログに目を通し エラーを把握します
最初のエントリは 追加のギフトカードをフェッチするタスクが アプリケーションで開始していることを示します そしてネットワークエラーのために タスク完了となり タイムアウト後に再試行するため 待機中であることが分かります 次のエントリは その間に― タスクを停止しようとするギフトカードを ユーザが選んだことを示しています しかしこの時点でアクティブなタスクがないため アプリケーション自体が矛盾した状態で検出され 失敗します これで実際に生じた問題を再現できます カードを1枚選択することによって ネットワークエラーにより停止したタイミングで 追加のギフトカードを読み込むタスクを 止めようとしました これで バグを再現しにくい理由が分かりました イベントとネットワークエラーの タイミングに関係しているのです ログのおかげで バグを把握し 修正することができます
“log collect”で アプリケーション実行後に ログを収集できることを確認しました アプリケーション実行中に ログをストリーミングすることもできます デバイスがMacに接続されている場合は コンソールアプリケーションで発生した ログメッセージをストリーミングできます アプリケーションがXcodeで起動された場合は Xcodeのコンソールにも表示されます これは“printf”のデバッグのかわりとして― 簡単に絞り込みができ より構造化された出力ができるので便利です
コンソールアプリケーションで ログを参照していた時に “failure”というメッセージが 強調表示されていました これは 私がエラーログレベルで ロギングしたためです ロギングAPIでは メッセージの重要度を示す 5つのログレベルがあります
重要度が低い方から順に並べると “デバッグ”“情報” 初期設定の“通知”“エラー” “障害”です
デバッグレベルは有用なメッセージに対して デバッグ時に限り使われます 情報レベルは トラブルシューティングエラーに 有用ではあるが 不可欠ではないメッセージに使われます 通知レベルは トラブルシューティングに 不可欠なメッセージに使われます エラーレベルは 実行中に発生するエラーを 記録するために使われます 障害レベルが最も深刻です プログラム内の潜在的なバグが原因で 発生する状況を記録するために使われます プログラムが保持するはずの前提が実行時に 破られたことを記録するのにも使われます エラー・障害レベルはコンソール アプリケーション内で黄色と赤で表示されます
Loggerの型には 各ログレベルのメソッドがあります 例えばデバッグ メッセージをロギングするには Loggerでデバッグ関数を呼び出します ログレベルを選ぶ際に 考慮すべき重要事項は 持続性です つまりアプリケーションの実行が終了した時点で ログメッセージをアーカイブしたり 取得することが可能かどうかです 持続性のないログの場合 アプリケーションの 実行中にしかストリーミングできません メッセージに持続性があるか否かは ログレベルで異なります メソッドの重要度が高いほど 持続性も高くなります
デバッグレベルのメッセージは 持続性がありません つまり アプリケーションの実行が完了した後に 取得することはできないのです 情報レベルのメッセージの大半は 持続性がありませんが ログ収集コマンドまでの短い時間に 作成されたメッセージは例外です 他の各レベルでロギングされたメッセージは 継続性があり 後で取得できます ただし アーカイブするメッセージの数には 容量制限があります 制限を超過すると 古いメッセージから削除され 閲覧できなくなります エラー・障害レベルのメッセージは 通知レベルのメッセージより長く保持されます 通常 メッセージは数日間 保持されますが デバイスの空き容量によって異なります
ログレベルもパフォーマンスに影響します ロギングでは一般的に オーバーヘッドが低くなりますが ログレベルでは互いに 相対的に異なるパフォーマンスが保持されます 重要度が低いレベルほど 速いのです 障害レベルの場合は最も遅く デバッグレベルの場合は最も速いです
デバッグレベルでのロギングが高速な理由は デバッグメッセージが全く保持されないからです ログがストリーミングされていない場合 これらは破棄されます さらにSwiftコンパイラでは 高度な最適化により デバッグメッセージが破棄された場合 メッセージ作成のコードが実行すらされません つまり デバッグレベルでは 詳細なメッセージをロギングし メッセージ作成のために 負荷の高い関数を呼び出すことができます その代償をユーザが払うことはありません
タスク識別子などの実行時間データを含む Fruta アプリケーションを使い― ログ内のメッセージがデバッグに さらに有用になったことを説明しました ただし数値や文字列などの生データは 把握や解釈が難しい場合があります 実行時間を犠牲にせずにリーダビリティを 向上させるデータのフォーマット方法を ロギングAPIは多数 提供しています Fruta アプリケーションに戻り ログメッセージのフォーマッティングを活用して デバッグする方法を確認しましょう ギフトカードビューで パフォーマンス上の問題が発生しています カードの読み込みに 非常に時間がかかる場合があります ギフトカードの読み込みに 複数のサーバーが使われています どのサーバーが選択されているかが パフォーマンスの問題に関与しているのでしょう これを調べるために ロギングを追加し サーバーとの通信に関する統計情報を収集します
タスクごとに タスク識別子 フェッチされたギフトカードの識別子― 要求を処理したサーバー タスク完了までの合計期間をロギングします
次にMacにiPhoneを接続し Xcodeからアプリケーションを実行し Xcodeのコンソールでログを表示します
残念ながら ログが整列していないため 非常に把握しづらい状況です よってフォーマッティングオプションを使って この見た目を改善します
まずギフトカード識別子を固定幅にするため カード識別子で保持可能な最大文字数を 表示します
今回は高い精度を必要としていないので 小数点以下2桁の時間に丸めます
アプリケーションを再起動し ログを再度表示してみます
これでログが読みやすくなりました きちんと整列しているので オプションキーを押したまま“列選択”を使用し フィールドをコピーします
これらをNumbersに貼り付け データを可視化します
このグラフを見れば 遅延タスクはすべて サーバー3で処理したことが明らかなので メンテナンスのために サーバーをオフラインに設定します
要するにデータフォーマットに オプションの “format”と“align”パラメータを使います ロギングAPIによるデータフォーマットでは ログ呼び出しの手間が省けるので データを最大限 見やすく把握しやすくするのに フォーマッティングを好きに使えます ロギングAPIの数多くのフォーマッティング オプションのうち 一部を説明したにすぎません
Xcodeのコード補完を使って あらゆるオプションを確認できます 例えば16進数 8進数 指数などの フォーマット番号です ログ内のデータの可視性を管理するには プライバシーオプションを使用します ロギングデータの個人情報を 慎重に取り扱うことは とても重要です なぜならアプリケーションが出荷され ユーザの手元に届いた後も ロギングが常に行われるからです デバイスとそのパスコードへの物理的な アクセス権を持つユーザがログを収集できます ログのメッセージで個人情報を公開として マークしなければ ログでの露出を防げます
実際にそれを行わずに パブリックを使用するのと同じ利点が 等値保持ハッシュを使えば 得ることができます こうすればデータの内容を明らかにしなくても ログのフィルタリングに有用な ロギング値が一致するタイミングを把握できます 例えばマスクパラメータを .privateプライバシーオプションに渡せば 顧客の銀行口座番号をハッシュで記録できます 口座番号を伏せたまま 2つのログメッセージで 同じ口座が参照されたタイミングが把握できます
ご紹介したLogger APIは iOS 14で利用可能です 以前のリリースを対象とする アプリケーションの場合は printfスタイルのフォーマット文字列を 受け入れるos_log関数を使いましょう このリリース以降は Logger同様に 文字列補間をos_log関数に渡すこともできます 要約すると 新しいSwiftロギングAPIを使えば 問題のデバッグが可能です それ以外の方法では 把握や修正を行うことは ほぼ不可能です バグを再現せずとも開発デバイスからログを 取得して ログにドリルダウンできるからです ロギングAPIは 高いパフォーマンスと リッチなフォーマッティングを兼ね備えています したがって 情報メッセージをロギングでき ナレッジではエンドユーザに対して アプリケーションを遅延させることがありません ありがとうございました
-
-
2:44 - Example illustrating how to add logging to your app in three simple steps
// Add logging to your app in three simple steps import os let logger = Logger(subsystem: "com.example.Fruta", category: "giftcards") func beginTask(url: URL, handler: (Data) -> Void) { launchTask(with: url) { handler($0) } logger.log("Started a task") }
-
3:32 - An example code that logs a message with run-time data
// Add runtime data to the log messsage using string interpolation import os let logger = Logger(subsystem: "com.example.Fruta", category: "giftcards") func beginTask(url: URL, handler: (Data) -> Void) { launchTask(with: url) { handler($0) } logger.log("Started a task \(taskId)") }
-
4:28 - Example illustrating why nonnumeric types are redacted in the logs by default
logger.log("Paid with bank account \(accountNumber)")
-
5:01 - Code that shows how to mark public data so that it is displayed in the logs
logger.log("Ordered smoothie \(smoothieName, privacy: .public)")
-
6:03 - Code shown during first demo
import SwiftUI import os let logger = Logger(subsystem: "com.example.Fruta", category: "giftcards") struct GiftCardView: View { // Denotes whether there is an active task for loading gift cards. @State private var taskRunning: Bool = false // A UUID that uniquely identifies a task. @State private var currentTaskID: UUID = UUID() // An unrecoverable error seen during execution. @State private var error: Error? = nil // A model that stores information about gift cards. @ObservedObject var model: GiftCardModel var body: some View { // Display a list of gifts which can be tapped on and scrolled through. GiftCardList(model: model, taskRunning: $taskRunning, currentTaskID: $currentTaskID, error: $error, downloadAction: beginTask, stopAction: endTask) .navigationTitle("Gift Cards") } // Start a task to download gift cards from a server. func beginTask(serverURL: URL, cardDownloadHandler: @escaping (Data) -> Void) { logger.log("Starting a new task for loading cards \(currentTaskID, privacy: .public)") launchTask(with: serverURL) { cardDownloadHandler($0) } } // Stop the currently running task for downloading cards from a server. func endTask() { guard taskRunning else { logger.fault("Task \(currentTaskID, privacy: .public) is not runinng, cannot be stopped!") error = TaskError.noActiveTask return } taskRunning = false logger.log("Task \(currentTaskID, privacy: .public) interrupted") } // Start a URLSession dataTask with the given URL. func launchTask(with url: URL, handler: @escaping (Data) -> Void) { guard error == nil else { return } taskRunning = true let task = URLSession.shared.dataTask(with: url) { data, response, error in if let error = error { self.error = ConnectionError.other(error) } if let data = data { handler(data) } } task.resume() } }
-
11:51 - Illustration of how debug-level logging will not evaluate the code that constructs log message
logger.debug("\(slowFunction(data))")
-
import SwiftUI import os let statisticsLogger = Logger(subsystem: "com.example.Fruta", category: "statistics") // Log statistics about communication with a server. func logStatistics(taskID: UUID, giftCardID: String, serverID: Int, seconds: Double) { statisticsLogger.log("\(taskID) \(giftCardID, align: .left(columns: GiftCard.maxIDLength)) \(serverID) \(seconds, format: .fixed(precision: 2))") }
-
15:00 - Example of formatting log messages
logger.log("\(data, format: .hex, align: .right(columns: width))")
-
logger.log("Paid with bank account: \(accountNumber, privacy: .private(mask: .hash))")
-
-
特定のトピックをお探しの場合は、上にトピックを入力すると、関連するトピックにすばやく移動できます。
クエリの送信中にエラーが発生しました。インターネット接続を確認して、もう一度お試しください。