Pulse
Appleプラットフォーム向けの強力なロギングシステム。ログとネットワークリクエストの記録・検査機能を提供し、リアルタイム表示・共有が可能。開発・デバッグ・本番監視での幅広い用途に対応する現代的なソリューション。
ライブラリ
Pulse
概要
Appleプラットフォーム向けの強力なロギングシステムで、ログとURLSessionネットワークリクエストの記録・検査機能をiOSアプリから直接提供します。SwiftUIで構築されたフレームワークとして、リアルタイム表示・共有が可能な現代的なソリューションです。ログはデバイスにローカル保存され、開発・デバッグ・本番監視での幅広い用途に対応し、QAチームやベータテスターが簡単にログを確認・共有できる環境を実現します。
詳細
Pulse 2025年版は、iOS 17.0以降をサポートする高度なロギングフレームワークとして進化を続けています。URLSessionや依存するフレームワーク(AlamofireやGet)からのイベント記録機能と、PulseUIビューによる直接アプリ統合を特徴とします。コア機能のPulseCoreは全Apple プラットフォーム(iOS、macOS、watchOS、tvOS)で利用可能で、永続的ストレージによるログ管理を提供。Pulse Proという専用macOSアプリとの連携により、リアルタイムログ表示と高度な解析機能を実現します。
主な特徴
- 統合ログシステム: ログとネットワークリクエストの統一管理
- SwiftUI統合: ネイティブUI コンポーネントによる直接アプリ統合
- ローカルストレージ: CoreData ベースの永続的ログ保存
- リアルタイム表示: Pulse Pro連携による即座のログ確認
- プライバシー保護: ログはデバイス内に保持、外部送信なし
- QA支援: デバイス上でのログ確認とバグレポート添付機能
メリット・デメリット
メリット
- iOSアプリに直接統合可能でテストビルドで即座に利用開始
- URLSessionとAlamofire等の主要ネットワークライブラリとの優れた統合性
- QAチームによるデバイス上でのログ確認とバグレポート作成支援
- ローカルストレージによりプライバシーとセキュリティが保護
- Pulse Proとの連携による高度なリアルタイム監視と解析
- SwiftUIネイティブUIによる直感的な操作性
デメリット
- iOS 17.0以降という比較的新しいプラットフォーム要件
- Apple エコシステムに限定されクロスプラットフォーム対応なし
- ネットワーク中心のロギングで汎用ログ管理には追加設定必要
- 大規模企業での統一ログ管理システムとの統合課題
- Pulse Pro の追加コストとmacOS環境の必要性
- 複雑な設定やカスタマイズには学習コストが発生
参考ページ
書き方の例
基本セットアップとSwiftLog統合
import Logging
import Pulse
import PulseLogHandler
// PulseLogHandlerでSwiftLogをブートストラップ
LoggingSystem.bootstrap { label in
return PersistentLogHandler(label: label, store: LoggerStore.shared)
}
// SwiftLog APIを使用したログ記録
let logger = Logger(label: "com.example.component")
logger.info("Something notable happened")
logger.error("An error occurred", metadata: ["error": "\(error)"])
logger.debug("Debug information", metadata: ["userId": "uid-123"])
直接LoggerStore使用
import Pulse
// 直接的なメッセージ保存
LoggerStore.shared.storeMessage(
label: "auth",
level: .debug,
message: "Will login user",
metadata: ["userId": .string("uid-1")]
)
LoggerStore.shared.storeMessage(
label: "network",
level: .info,
message: "API request completed",
metadata: [
"endpoint": .string("/api/users"),
"responseTime": .string("150ms"),
"statusCode": .string("200")
]
)
// エラーログの記録
LoggerStore.shared.storeMessage(
label: "database",
level: .error,
message: "Database connection failed",
metadata: [
"error": .string("Connection timeout"),
"retryCount": .string("3")
]
)
URLSessionProxy によるネットワークロギング
import Pulse
// デバッグビルドでのみPulseを有効化
#if DEBUG
let session: URLSessionProtocol = URLSessionProxy(configuration: .default)
#else
let session: URLSessionProtocol = URLSession(configuration: .default)
#endif
// 通常のURLSession使用と同様の API
let url = URL(string: "https://api.example.com/users")!
let task = session.dataTask(with: url) { data, response, error in
if let error = error {
print("Request failed: \(error)")
return
}
if let data = data {
print("Response data: \(data.count) bytes")
}
}
task.resume()
// より高度なリクエスト例
var request = URLRequest(url: URL(string: "https://api.example.com/posts")!)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("Bearer token123", forHTTPHeaderField: "Authorization")
let postData = """
{
"title": "New Post",
"content": "This is a new blog post",
"authorId": 123
}
""".data(using: .utf8)
request.httpBody = postData
let postTask = session.dataTask(with: request) { data, response, error in
// Pulseが自動的にリクエスト/レスポンスをログに記録
if let httpResponse = response as? HTTPURLResponse {
print("Status code: \(httpResponse.statusCode)")
}
}
postTask.resume()
PulseUI統合
import SwiftUI
import PulseUI
struct ContentView: View {
var body: some View {
NavigationView {
VStack {
Text("My App")
.font(.largeTitle)
NavigationLink(destination: ConsoleView()) {
Text("Open Pulse Console")
.padding()
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(8)
}
}
}
}
}
// 専用のデバッグ画面
struct DebugView: View {
var body: some View {
NavigationView {
List {
NavigationLink("ログコンソール", destination: ConsoleView())
NavigationLink("ネットワーク監視", destination: NetworkInspectorView())
NavigationLink("ログ共有", destination: LogSharingView())
}
.navigationTitle("デバッグツール")
}
}
}
// カスタム設定でのConsoleView
struct CustomConsoleView: View {
var body: some View {
ConsoleView(store: LoggerStore.shared)
.navigationTitle("アプリログ")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("クリア") {
LoggerStore.shared.removeAll()
}
}
}
}
}
アプリケーション統合と設定
import SwiftUI
import Pulse
@main
struct MyApp: App {
init() {
// アプリ起動時にPulseを初期化
#if DEBUG
setupPulseLogging()
#endif
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
private func setupPulseLogging() {
// カスタムLoggerStore設定
let store = LoggerStore.shared
// 古いログの自動削除設定(7日間)
store.configuration.maximumAge = 7 * 24 * 60 * 60 // 7 days in seconds
// ログレベルの設定
store.configuration.minimumLevel = .debug
// 初期ログメッセージ
store.storeMessage(
label: "app.lifecycle",
level: .info,
message: "Application launched",
metadata: [
"version": .string(Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "unknown"),
"build": .string(Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "unknown")
]
)
}
}
// 機能別ログラッパー
class AppLogger {
private static let authLogger = Logger(label: "com.myapp.auth")
private static let networkLogger = Logger(label: "com.myapp.network")
private static let uiLogger = Logger(label: "com.myapp.ui")
static func logAuthentication(_ message: String, metadata: [String: Any] = [:]) {
let pulseMetadata = metadata.mapValues { Logger.MetadataValue.string(String(describing: $0)) }
authLogger.info("\(message)", metadata: pulseMetadata)
}
static func logNetworkRequest(_ url: String, method: String, statusCode: Int? = nil) {
var metadata: [String: Logger.MetadataValue] = [
"url": .string(url),
"method": .string(method)
]
if let statusCode = statusCode {
metadata["statusCode"] = .string("\(statusCode)")
}
networkLogger.info("Network request", metadata: metadata)
}
static func logUIEvent(_ event: String, screen: String) {
uiLogger.debug("UI Event", metadata: [
"event": .string(event),
"screen": .string(screen)
])
}
}
ファイルアップロードとログエクスポート
import Pulse
// ログの共有機能
class LogSharingManager {
static func exportLogs() {
let store = LoggerStore.shared
// ログをファイルとしてエクスポート
let documentsPath = FileManager.default.urls(for: .documentDirectory,
in: .userDomainMask).first!
let logFileURL = documentsPath.appendingPathComponent("app_logs.txt")
// ログを文字列形式で取得
var logContent = "=== アプリケーションログ ===\n"
logContent += "エクスポート日時: \(Date())\n\n"
// 最近のメッセージを取得
let messages = store.allMessages()
for message in messages.suffix(100) { // 最新100件
logContent += "[\(message.createdAt)] \(message.level): \(message.text)\n"
}
do {
try logContent.write(to: logFileURL, atomically: true, encoding: .utf8)
// 共有アクションシートを表示
presentShareSheet(with: logFileURL)
} catch {
print("ログエクスポートエラー: \(error)")
}
}
private static func presentShareSheet(with url: URL) {
let activityVC = UIActivityViewController(activityItems: [url], applicationActivities: nil)
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let window = windowScene.windows.first {
window.rootViewController?.present(activityVC, animated: true)
}
}
}
// QA用のフィードバック機能
class QAFeedbackManager {
static func generateBugReport(description: String) {
let store = LoggerStore.shared
// バグレポート用のメタデータ
let deviceInfo = [
"device": UIDevice.current.model,
"os": UIDevice.current.systemVersion,
"app_version": Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "unknown"
]
// バグレポートログを記録
store.storeMessage(
label: "qa.bug_report",
level: .critical,
message: "Bug Report: \(description)",
metadata: deviceInfo.mapValues { .string($0) }
)
// 最近のログと一緒にレポートを生成
exportLogsForBugReport(description: description, deviceInfo: deviceInfo)
}
private static func exportLogsForBugReport(description: String, deviceInfo: [String: String]) {
// バグレポート専用のログエクスポート実装
let reportContent = generateDetailedReport(description: description, deviceInfo: deviceInfo)
// ファイルとして保存し、共有
saveAndShareReport(content: reportContent)
}
private static func generateDetailedReport(description: String, deviceInfo: [String: String]) -> String {
var report = "=== バグレポート ===\n"
report += "報告日時: \(Date())\n"
report += "説明: \(description)\n\n"
report += "=== デバイス情報 ===\n"
for (key, value) in deviceInfo {
report += "\(key): \(value)\n"
}
report += "\n=== 関連ログ ===\n"
// 最近のエラーや警告レベルのログを含める
let store = LoggerStore.shared
let recentErrorLogs = store.allMessages().filter {
$0.level == .error || $0.level == .critical || $0.level == .warning
}.suffix(20)
for log in recentErrorLogs {
report += "[\(log.createdAt)] \(log.level): \(log.text)\n"
}
return report
}
private static func saveAndShareReport(content: String) {
// 実装: レポートの保存と共有
}
}