Pulse

Appleプラットフォーム向けの強力なロギングシステム。ログとネットワークリクエストの記録・検査機能を提供し、リアルタイム表示・共有が可能。開発・デバッグ・本番監視での幅広い用途に対応する現代的なソリューション。

ロギングSwiftiOSネットワークデバッグ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) {
        // 実装: レポートの保存と共有
    }
}