CocoaLumberjack
macOS、iOS、tvOS、watchOS向けの高速でシンプルかつ強力で柔軟なロギングフレームワーク。NSLogよりも桁違いに高速で、アプリケーション起動時の1行設定で利用可能。豊富なカスタマイズオプションと拡張性を提供。
ライブラリ
CocoaLumberjack
概要
CocoaLumberjackは、macOS、iOS、tvOS、watchOS向けの高速でシンプルかつ強力で柔軟なロギングフレームワークです。NSLogよりも桁違いに高速で、アプリケーション起動時の1行設定で利用可能な優れた開発体験を提供。豊富なカスタマイズオプションと拡張性により、高度なロギング要件にも対応可能。15年以上の開発実績と成熟したAPIにより、iOS・macOSアプリケーション開発における信頼性の高いロギングソリューションとして、Swift・Objective-C両言語でのサポートを実現しています。
詳細
CocoaLumberjack 2025年版では、Swiftコミュニティで人気を維持する老舗ライブラリとして確固たる地位を保持しています。既存プロジェクトでの継続使用と、柔軟性を重視する開発者による新規採用が見られる一方、Apple公式のOSLogの普及により、新規プロジェクトでの採用は減少傾向。しかし、カスタマイズ性の高さ、豊富な出力先オプション、ファイルローテーション機能、Swift Package Manager対応により、複雑なロギング要件を持つプロジェクトでは依然として優位性を保持。マルチプラットフォーム対応(macOS、iOS、tvOS、watchOS、visionOS)と高いパフォーマンスにより、プロフェッショナルなiOSアプリケーション開発において重要な選択肢として評価されています。
主な特徴
- 高速パフォーマンス: NSLogより桁違いに高速な非同期ログ処理
- 柔軟な設定: 複数のロガーと出力先への同時書き込み対応
- Swift統合: SwiftとObjective-C両方での完全サポート
- 豊富な出力先: ファイル、コンソール、システムログ、カスタムログ等
- Apple全プラットフォーム: iOS、macOS、tvOS、watchOS、visionOS対応
- カスタマイズ性: カスタムフォーマッター、フィルター、ログ処理の実装可能
メリット・デメリット
メリット
- NSLogと比較して大幅な性能向上と効率的なリソース使用
- 1行の初期化コードで即座に利用開始可能な簡単セットアップ
- ファイルローテーション、圧縮、自動クリーンアップ等の高度な機能
- CocoaPods、Carthage、Swift Package Manager等の多様なインストール方法
- 15年以上の開発実績による安定性と信頼性
- 豊富なドキュメントと活発なコミュニティサポート
デメリット
- Apple公式OSLogの普及により新規採用機会の減少
- 小規模アプリではオーバースペックな場合がある
- サードパーティライブラリとしての依存関係追加
- Swift-logバックエンドとしての使用時の設定複雑性
- iOS11未満、macOS 10.13未満のサポート終了
- プライバシー・データ収集の責任がアプリ開発者に委ねられる
参考ページ
書き方の例
インストールと基本セットアップ
// Swift Package Manager (推奨)
// File -> Add Package Dependencies...
// https://github.com/CocoaLumberjack/CocoaLumberjack.git
// CocoaPods の場合
// pod 'CocoaLumberjack/Swift'
// Carthage の場合
// github "CocoaLumberjack/CocoaLumberjack"
import CocoaLumberjack
class AppDelegate: UIApplicationDelegate {
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// 基本的な初期化(1行設定)
DDLog.add(DDOSLogger.sharedInstance) // OSログ(Apple統合ログ)
DDLog.add(DDTTYLogger.sharedInstance!) // Xcodeコンソール
// ログレベル設定
#if DEBUG
DDLog.logLevel = .all
#else
DDLog.logLevel = .warning
#endif
// 基本的なログ出力
DDLogVerbose("アプリケーション起動 - 詳細")
DDLogDebug("アプリケーション起動 - デバッグ")
DDLogInfo("アプリケーション起動 - 情報")
DDLogWarn("アプリケーション起動 - 警告")
DDLogError("アプリケーション起動 - エラー")
return true
}
}
// SwiftUI での設定例
@main
struct MyApp: App {
init() {
setupLogging()
}
private func setupLogging() {
// DDOSLogger: Apple 統合ログシステム
DDLog.add(DDOSLogger.sharedInstance)
// DDTTYLogger: 開発環境コンソール出力
if let ttyLogger = DDTTYLogger.sharedInstance {
DDLog.add(ttyLogger)
// カラー出力設定
ttyLogger.colorsEnabled = true
ttyLogger.setForegroundColor(.red, backgroundColor: nil, for: .error)
ttyLogger.setForegroundColor(.orange, backgroundColor: nil, for: .warning)
ttyLogger.setForegroundColor(.green, backgroundColor: nil, for: .info)
}
#if DEBUG
DDLog.logLevel = .all
#else
DDLog.logLevel = .info
#endif
}
var body: some Scene {
WindowGroup {
ContentView()
.onAppear {
DDLogInfo("SwiftUIアプリケーション表示開始")
}
}
}
}
基本的なログレベルと出力先設定
import CocoaLumberjack
// ログレベル定義
extension DDLogLevel {
static var appLogLevel: DDLogLevel {
#if DEBUG
return .all
#elseif STAGING
return .info
#else
return .warning
#endif
}
}
class LoggingManager {
static let shared = LoggingManager()
private init() {
setupLoggers()
}
private func setupLoggers() {
// グローバルログレベル設定
DDLog.logLevel = DDLogLevel.appLogLevel
// 1. OSログ(システム統合)
let osLogger = DDOSLogger.sharedInstance
osLogger.logFormatter = CustomOSLogFormatter()
DDLog.add(osLogger)
// 2. コンソールログ(開発環境)
if let ttyLogger = DDTTYLogger.sharedInstance {
ttyLogger.logFormatter = CustomConsoleFormatter()
ttyLogger.colorsEnabled = true
setupConsoleColors(ttyLogger)
DDLog.add(ttyLogger)
}
// 3. ファイルログ(本番・デバッグ用)
let fileLogger = DDFileLogger()
fileLogger.rollingFrequency = 60 * 60 * 24 // 24時間
fileLogger.logFileManager.maximumNumberOfLogFiles = 7 // 1週間分
fileLogger.maximumFileSize = 1024 * 1024 * 10 // 10MB
fileLogger.logFormatter = CustomFileFormatter()
DDLog.add(fileLogger)
DDLogInfo("ログシステム初期化完了")
DDLogInfo("ファイルログパス: \(fileLogger.logFileManager.logsDirectory)")
}
private func setupConsoleColors(_ logger: DDTTYLogger) {
logger.setForegroundColor(.white, backgroundColor: .red, for: .error)
logger.setForegroundColor(.orange, backgroundColor: nil, for: .warning)
logger.setForegroundColor(.green, backgroundColor: nil, for: .info)
logger.setForegroundColor(.lightGray, backgroundColor: nil, for: .debug)
logger.setForegroundColor(.darkGray, backgroundColor: nil, for: .verbose)
}
// 動的ログレベル変更
func setLogLevel(_ level: DDLogLevel) {
DDLog.logLevel = level
DDLogInfo("ログレベル変更: \(level)")
}
// ログファイル管理
func getLogFiles() -> [String] {
guard let fileLogger = DDLog.allLoggers.first(where: { $0 is DDFileLogger }) as? DDFileLogger else {
return []
}
return fileLogger.logFileManager.sortedLogFilePaths
}
func exportLogFiles() -> URL? {
let logFiles = getLogFiles()
guard !logFiles.isEmpty else { return nil }
// ログファイルをZIP形式でエクスポート
let documentsPath = FileManager.default.urls(for: .documentDirectory,
in: .userDomainMask).first!
let exportURL = documentsPath.appendingPathComponent("app_logs_\(Date()).zip")
// 実際のZIP作成処理は省略
DDLogInfo("ログファイルエクスポート: \(exportURL)")
return exportURL
}
}
// カスタムフォーマッター実装
class CustomConsoleFormatter: NSObject, DDLogFormatter {
let dateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "HH:mm:ss.SSS"
return formatter
}()
func format(message logMessage: DDLogMessage) -> String? {
let timestamp = dateFormatter.string(from: logMessage.timestamp)
let logLevel = logLevelString(logMessage.level)
let fileName = (logMessage.fileName as NSString).lastPathComponent
let function = logMessage.function ?? "unknown"
let line = logMessage.line
return "\(timestamp) [\(logLevel)] \(fileName):\(line) \(function) - \(logMessage.message)"
}
private func logLevelString(_ level: DDLogLevel) -> String {
switch level {
case .error: return "ERROR"
case .warning: return "WARN "
case .info: return "INFO "
case .debug: return "DEBUG"
case .verbose: return "TRACE"
default: return "UNKNOWN"
}
}
}
class CustomFileFormatter: NSObject, DDLogFormatter {
let dateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS"
return formatter
}()
func format(message logMessage: DDLogMessage) -> String? {
let timestamp = dateFormatter.string(from: logMessage.timestamp)
let logLevel = logLevelString(logMessage.level)
let fileName = (logMessage.fileName as NSString).lastPathComponent
let function = logMessage.function ?? "unknown"
let line = logMessage.line
return "\(timestamp) [\(logLevel)] [\(fileName):\(line)] \(function) - \(logMessage.message)"
}
private func logLevelString(_ level: DDLogLevel) -> String {
switch level {
case .error: return "ERROR"
case .warning: return "WARN "
case .info: return "INFO "
case .debug: return "DEBUG"
case .verbose: return "TRACE"
default: return "UNKNOWN"
}
}
}
class CustomOSLogFormatter: NSObject, DDLogFormatter {
func format(message logMessage: DDLogMessage) -> String? {
let fileName = (logMessage.fileName as NSString).lastPathComponent
let function = logMessage.function ?? "unknown"
let line = logMessage.line
return "[\(fileName):\(line)] \(function) - \(logMessage.message)"
}
}
// 使用例
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// ログマネージャー初期化
_ = LoggingManager.shared
DDLogInfo("ViewControllerロード完了")
// 各種ログレベルのテスト
testLogging()
}
private func testLogging() {
DDLogVerbose("詳細なトレース情報")
DDLogDebug("デバッグ情報: viewDidLoad")
DDLogInfo("ユーザーアクション: 画面表示")
DDLogWarn("警告: 非推奨APIの使用")
DDLogError("エラー: ネットワーク接続失敗")
// パラメータ付きログ
let userId = 12345
let actionName = "button_tap"
DDLogInfo("ユーザーアクション - UserID: \(userId), Action: \(actionName)")
}
}
高度なカスタマイズとフィルタリング
import CocoaLumberjack
// カスタムログコンテキスト定義
let LogContextUserActions = 1000
let LogContextNetworking = 2000
let LogContextDatabase = 3000
let LogContextAuthentication = 4000
// コンテキスト別ログマクロ
func DDLogUserAction(_ message: @autoclosure () -> String,
level: DDLogLevel = .info,
context: Int = LogContextUserActions,
file: StaticString = #file,
function: StaticString = #function,
line: UInt = #line) {
_DDLogMessage(message(), level: level, flag: DDLogFlag(level),
context: context, file: file, function: function, line: line)
}
func DDLogNetwork(_ message: @autoclosure () -> String,
level: DDLogLevel = .debug,
context: Int = LogContextNetworking,
file: StaticString = #file,
function: StaticString = #function,
line: UInt = #line) {
_DDLogMessage(message(), level: level, flag: DDLogFlag(level),
context: context, file: file, function: function, line: line)
}
func DDLogDatabase(_ message: @autoclosure () -> String,
level: DDLogLevel = .debug,
context: Int = LogContextDatabase,
file: StaticString = #file,
function: StaticString = #function,
line: UInt = #line) {
_DDLogMessage(message(), level: level, flag: DDLogFlag(level),
context: context, file: file, function: function, line: line)
}
// 高度なログマネージャー
class AdvancedLoggingManager {
static let shared = AdvancedLoggingManager()
private var userActionsLogger: DDFileLogger?
private var networkLogger: DDFileLogger?
private var databaseLogger: DDFileLogger?
private init() {
setupAdvancedLoggers()
}
private func setupAdvancedLoggers() {
// 基本ロガー設定
setupBasicLoggers()
// コンテキスト別ファイルロガー
setupContextualFileLoggers()
// ネットワーク専用ロガー
setupNetworkLogger()
DDLogInfo("高度なログシステム初期化完了")
}
private func setupBasicLoggers() {
// OSログ
let osLogger = DDOSLogger.sharedInstance
osLogger.logFormatter = AdvancedOSLogFormatter()
DDLog.add(osLogger, with: .all)
// コンソールログ
if let ttyLogger = DDTTYLogger.sharedInstance {
ttyLogger.logFormatter = AdvancedConsoleFormatter()
ttyLogger.colorsEnabled = true
setupAdvancedColors(ttyLogger)
DDLog.add(ttyLogger, with: .all)
}
}
private func setupContextualFileLoggers() {
// ユーザーアクション専用ログ
userActionsLogger = createContextualLogger(
filename: "user_actions",
context: LogContextUserActions
)
// データベース専用ログ
databaseLogger = createContextualLogger(
filename: "database",
context: LogContextDatabase
)
}
private func createContextualLogger(filename: String, context: Int) -> DDFileLogger {
let fileLogger = DDFileLogger()
// ファイル名とパス設定
let documentsPath = FileManager.default.urls(for: .documentDirectory,
in: .userDomainMask).first!
let logsDirectory = documentsPath.appendingPathComponent("Logs/\(filename)")
let logFileManager = DDLogFileManagerDefault(logsDirectory: logsDirectory.path)
fileLogger.logFileManager = logFileManager
// ローテーション設定
fileLogger.rollingFrequency = 60 * 60 * 24 // 24時間
fileLogger.logFileManager.maximumNumberOfLogFiles = 14 // 2週間分
fileLogger.maximumFileSize = 1024 * 1024 * 5 // 5MB
// カスタムフォーマッター
fileLogger.logFormatter = ContextualFileFormatter(context: context)
// コンテキストフィルター適用
DDLog.add(fileLogger, with: DDLogLevel.all, context: context)
return fileLogger
}
private func setupNetworkLogger() {
networkLogger = DDFileLogger()
// ネットワーク専用ディレクトリ
let documentsPath = FileManager.default.urls(for: .documentDirectory,
in: .userDomainMask).first!
let networkLogsDirectory = documentsPath.appendingPathComponent("Logs/network")
let logFileManager = DDLogFileManagerDefault(logsDirectory: networkLogsDirectory.path)
networkLogger?.logFileManager = logFileManager
// 頻繁なローテーション(ネットワークログは量が多いため)
networkLogger?.rollingFrequency = 60 * 60 * 12 // 12時間
networkLogger?.logFileManager.maximumNumberOfLogFiles = 28 // 2週間分
networkLogger?.maximumFileSize = 1024 * 1024 * 2 // 2MB
networkLogger?.logFormatter = NetworkLogFormatter()
if let logger = networkLogger {
DDLog.add(logger, with: .all, context: LogContextNetworking)
}
}
private func setupAdvancedColors(_ logger: DDTTYLogger) {
// エラーレベル
logger.setForegroundColor(.white, backgroundColor: .red, for: .error)
// 警告レベル
logger.setForegroundColor(.black, backgroundColor: .yellow, for: .warning)
// 情報レベル
logger.setForegroundColor(.blue, backgroundColor: nil, for: .info)
// デバッグレベル
logger.setForegroundColor(.green, backgroundColor: nil, for: .debug)
// 詳細レベル
logger.setForegroundColor(.lightGray, backgroundColor: nil, for: .verbose)
}
// ログ分析メソッド
func analyzeLogFiles() -> LogAnalysisResult {
var result = LogAnalysisResult()
// ユーザーアクションログ分析
if let userLogger = userActionsLogger {
result.userActionsCount = countLogEntries(in: userLogger)
}
// ネットワークログ分析
if let netLogger = networkLogger {
result.networkRequestsCount = countLogEntries(in: netLogger)
}
// データベースログ分析
if let dbLogger = databaseLogger {
result.databaseOperationsCount = countLogEntries(in: dbLogger)
}
return result
}
private func countLogEntries(in logger: DDFileLogger) -> Int {
let logFiles = logger.logFileManager.sortedLogFilePaths
var totalLines = 0
for filePath in logFiles {
do {
let content = try String(contentsOfFile: filePath)
totalLines += content.components(separatedBy: .newlines).count
} catch {
DDLogError("ログファイル読み込みエラー: \(error)")
}
}
return totalLines
}
}
// 高度なフォーマッター実装
class AdvancedConsoleFormatter: NSObject, DDLogFormatter {
let dateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "HH:mm:ss.SSS"
return formatter
}()
func format(message logMessage: DDLogMessage) -> String? {
let timestamp = dateFormatter.string(from: logMessage.timestamp)
let logLevel = logLevelEmoji(logMessage.level)
let context = contextString(logMessage.context)
let fileName = (logMessage.fileName as NSString).lastPathComponent
let function = logMessage.function ?? "unknown"
let line = logMessage.line
return "\(timestamp) \(logLevel) [\(context)] \(fileName):\(line) \(function) - \(logMessage.message)"
}
private func logLevelEmoji(_ level: DDLogLevel) -> String {
switch level {
case .error: return "🔴"
case .warning: return "🟡"
case .info: return "🔵"
case .debug: return "🟢"
case .verbose: return "⚪"
default: return "⚫"
}
}
private func contextString(_ context: Int) -> String {
switch context {
case LogContextUserActions: return "USER"
case LogContextNetworking: return "NET"
case LogContextDatabase: return "DB"
case LogContextAuthentication: return "AUTH"
default: return "GENERAL"
}
}
}
class ContextualFileFormatter: NSObject, DDLogFormatter {
private let context: Int
private let dateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS"
return formatter
}()
init(context: Int) {
self.context = context
super.init()
}
func format(message logMessage: DDLogMessage) -> String? {
guard logMessage.context == context else { return nil }
let timestamp = dateFormatter.string(from: logMessage.timestamp)
let logLevel = logLevelString(logMessage.level)
let fileName = (logMessage.fileName as NSString).lastPathComponent
let function = logMessage.function ?? "unknown"
let line = logMessage.line
return "\(timestamp) [\(logLevel)] [\(fileName):\(line)] \(function) - \(logMessage.message)"
}
private func logLevelString(_ level: DDLogLevel) -> String {
switch level {
case .error: return "ERROR"
case .warning: return "WARN "
case .info: return "INFO "
case .debug: return "DEBUG"
case .verbose: return "TRACE"
default: return "UNKNOWN"
}
}
}
class NetworkLogFormatter: NSObject, DDLogFormatter {
let dateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS"
return formatter
}()
func format(message logMessage: DDLogMessage) -> String? {
let timestamp = dateFormatter.string(from: logMessage.timestamp)
let logLevel = logLevelString(logMessage.level)
return "\(timestamp) [\(logLevel)] [NETWORK] \(logMessage.message)"
}
private func logLevelString(_ level: DDLogLevel) -> String {
switch level {
case .error: return "ERROR"
case .warning: return "WARN "
case .info: return "INFO "
case .debug: return "DEBUG"
case .verbose: return "TRACE"
default: return "UNKNOWN"
}
}
}
class AdvancedOSLogFormatter: NSObject, DDLogFormatter {
func format(message logMessage: DDLogMessage) -> String? {
let context = contextString(logMessage.context)
let fileName = (logMessage.fileName as NSString).lastPathComponent
let function = logMessage.function ?? "unknown"
let line = logMessage.line
return "[\(context)] [\(fileName):\(line)] \(function) - \(logMessage.message)"
}
private func contextString(_ context: Int) -> String {
switch context {
case LogContextUserActions: return "USER"
case LogContextNetworking: return "NET"
case LogContextDatabase: return "DB"
case LogContextAuthentication: return "AUTH"
default: return "GENERAL"
}
}
}
// ログ分析結果構造体
struct LogAnalysisResult {
var userActionsCount: Int = 0
var networkRequestsCount: Int = 0
var databaseOperationsCount: Int = 0
var totalLogFiles: Int = 0
var totalLogSize: Int64 = 0
var summary: String {
return """
ログ分析結果:
- ユーザーアクション: \(userActionsCount) 件
- ネットワークリクエスト: \(networkRequestsCount) 件
- データベース操作: \(databaseOperationsCount) 件
- 総ログファイル数: \(totalLogFiles)
- 総ログサイズ: \(ByteCountFormatter.string(fromByteCount: totalLogSize, countStyle: .file))
"""
}
}
// 使用例
class AdvancedLoggingViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// 高度なログマネージャー初期化
_ = AdvancedLoggingManager.shared
// 各種コンテキストでのログテスト
testContextualLogging()
}
private func testContextualLogging() {
// ユーザーアクションログ
DDLogUserAction("ボタンタップ - 設定画面へ遷移")
DDLogUserAction("スワイプジェスチャー検出")
// ネットワークログ
DDLogNetwork("HTTP GET /api/users - 開始")
DDLogNetwork("HTTP GET /api/users - 200 OK - 250ms")
// データベースログ
DDLogDatabase("SQLite SELECT FROM users WHERE id = 123")
DDLogDatabase("Core Data save context - 成功")
// 一般ログ
DDLogInfo("画面表示完了")
}
@IBAction func analyzeLogsButtonTapped(_ sender: UIButton) {
let analysisResult = AdvancedLoggingManager.shared.analyzeLogFiles()
let alert = UIAlertController(
title: "ログ分析結果",
message: analysisResult.summary,
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "OK", style: .default))
present(alert, animated: true)
DDLogUserAction("ログ分析結果表示")
}
}
パフォーマンス監視とクラッシュレポート
import CocoaLumberjack
import Foundation
// パフォーマンス監視ロガー
class PerformanceLogger {
static let shared = PerformanceLogger()
private let performanceQueue = DispatchQueue(label: "com.app.performance.logging",
qos: .utility)
private var performanceMetrics: [String: PerformanceMetric] = [:]
private let metricsLock = NSLock()
private init() {
setupPerformanceLogging()
}
private func setupPerformanceLogging() {
// パフォーマンス専用ファイルロガー
let perfLogger = DDFileLogger()
let documentsPath = FileManager.default.urls(for: .documentDirectory,
in: .userDomainMask).first!
let perfLogsDirectory = documentsPath.appendingPathComponent("Logs/performance")
let logFileManager = DDLogFileManagerDefault(logsDirectory: perfLogsDirectory.path)
perfLogger.logFileManager = logFileManager
perfLogger.rollingFrequency = 60 * 60 * 24 // 24時間
perfLogger.logFileManager.maximumNumberOfLogFiles = 7 // 1週間分
perfLogger.maximumFileSize = 1024 * 1024 * 10 // 10MB
perfLogger.logFormatter = PerformanceLogFormatter()
DDLog.add(perfLogger, with: .info, context: LogContextPerformance)
DDLogInfo("パフォーマンス監視ロガー初期化完了")
}
// メソッド実行時間測定
func measureExecutionTime<T>(
operationName: String,
operation: () throws -> T
) rethrows -> T {
let startTime = CFAbsoluteTimeGetCurrent()
let result = try operation()
let executionTime = CFAbsoluteTimeGetCurrent() - startTime
logPerformance(operationName: operationName,
executionTime: executionTime * 1000) // ms
return result
}
// 非同期処理時間測定
func measureAsyncExecutionTime<T>(
operationName: String,
operation: (@escaping (T) -> Void) -> Void,
completion: @escaping (T, TimeInterval) -> Void
) {
let startTime = CFAbsoluteTimeGetCurrent()
operation { result in
let executionTime = CFAbsoluteTimeGetCurrent() - startTime
self.logPerformance(operationName: operationName,
executionTime: executionTime * 1000)
completion(result, executionTime)
}
}
// メトリクス記録
func recordMetric(name: String, value: Double, unit: String = "") {
metricsLock.lock()
defer { metricsLock.unlock() }
if var metric = performanceMetrics[name] {
metric.addValue(value)
performanceMetrics[name] = metric
} else {
performanceMetrics[name] = PerformanceMetric(name: name, unit: unit, value: value)
}
DDLogPerformance("メトリクス記録: \(name) = \(value) \(unit)")
}
// メモリ使用量監視
func logMemoryUsage(operation: String) {
let memoryInfo = getMemoryUsage()
recordMetric(name: "memory_used_mb", value: memoryInfo.used)
recordMetric(name: "memory_available_mb", value: memoryInfo.available)
DDLogPerformance("メモリ使用量 [\(operation)]: 使用中 \(String(format: "%.1f", memoryInfo.used))MB / 利用可能 \(String(format: "%.1f", memoryInfo.available))MB")
// メモリ警告しきい値チェック
if memoryInfo.usagePercentage > 85.0 {
DDLogWarn("高メモリ使用率警告: \(String(format: "%.1f", memoryInfo.usagePercentage))%")
}
}
// CPU使用率監視
func logCPUUsage(operation: String) {
let cpuUsage = getCPUUsage()
recordMetric(name: "cpu_usage_percent", value: cpuUsage)
DDLogPerformance("CPU使用率 [\(operation)]: \(String(format: "%.1f", cpuUsage))%")
// CPU使用率警告
if cpuUsage > 80.0 {
DDLogWarn("高CPU使用率警告: \(String(format: "%.1f", cpuUsage))%")
}
}
// パフォーマンス統計レポート
func generatePerformanceReport() -> String {
metricsLock.lock()
defer { metricsLock.unlock() }
var report = "=== パフォーマンスレポート ===\n"
for (name, metric) in performanceMetrics.sorted(by: { $0.key < $1.key }) {
report += "\(name): 平均 \(String(format: "%.2f", metric.average)) \(metric.unit) "
report += "(最小: \(String(format: "%.2f", metric.minimum)), "
report += "最大: \(String(format: "%.2f", metric.maximum)), "
report += "回数: \(metric.count))\n"
}
DDLogInfo("パフォーマンスレポート生成")
return report
}
private func logPerformance(operationName: String, executionTime: TimeInterval) {
performanceQueue.async {
self.recordMetric(name: "\(operationName)_time_ms",
value: executionTime, unit: "ms")
DDLogPerformance("実行時間測定: \(operationName) - \(String(format: "%.2f", executionTime))ms")
// 遅い処理の警告
if executionTime > 1000 { // 1秒以上
DDLogWarn("低速処理検出: \(operationName) - \(String(format: "%.2f", executionTime))ms")
}
}
}
private func getMemoryUsage() -> (used: Double, available: Double, usagePercentage: Double) {
var info = mach_task_basic_info()
var count = mach_msg_type_number_t(MemoryLayout<mach_task_basic_info>.size)/4
let result: kern_return_t = withUnsafeMutablePointer(to: &info) {
$0.withMemoryRebound(to: integer_t.self, capacity: 1) {
task_info(mach_task_self_, task_flavor_t(MACH_TASK_BASIC_INFO), $0, &count)
}
}
if result == KERN_SUCCESS {
let usedMB = Double(info.resident_size) / 1024.0 / 1024.0
let availableMB = Double(ProcessInfo.processInfo.physicalMemory) / 1024.0 / 1024.0
let usagePercentage = (usedMB / availableMB) * 100.0
return (used: usedMB, available: availableMB, usagePercentage: usagePercentage)
} else {
return (used: 0, available: 0, usagePercentage: 0)
}
}
private func getCPUUsage() -> Double {
var info = processor_info_array_t.allocate(capacity: 1)
var numCpuInfo: mach_msg_type_number_t = 0
var numCpus: natural_t = 0
let result = host_processor_info(mach_host_self(), PROCESSOR_CPU_LOAD_INFO,
&numCpus, &info, &numCpuInfo)
if result == KERN_SUCCESS {
defer { info.deallocate() }
let cpuLoadInfo = info.bindMemory(to: processor_cpu_load_info.self, capacity: Int(numCpus))
var totalTicks: UInt32 = 0
var idleTicks: UInt32 = 0
for i in 0..<Int(numCpus) {
totalTicks += cpuLoadInfo[i].cpu_ticks.0 + cpuLoadInfo[i].cpu_ticks.1 +
cpuLoadInfo[i].cpu_ticks.2 + cpuLoadInfo[i].cpu_ticks.3
idleTicks += cpuLoadInfo[i].cpu_ticks.2 // CPU_STATE_IDLE
}
let usage = Double(totalTicks - idleTicks) / Double(totalTicks) * 100.0
return usage
}
return 0.0
}
}
// パフォーマンスメトリクス構造体
struct PerformanceMetric {
let name: String
let unit: String
private var values: [Double] = []
init(name: String, unit: String, value: Double) {
self.name = name
self.unit = unit
self.values = [value]
}
mutating func addValue(_ value: Double) {
values.append(value)
}
var average: Double {
guard !values.isEmpty else { return 0 }
return values.reduce(0, +) / Double(values.count)
}
var minimum: Double {
return values.min() ?? 0
}
var maximum: Double {
return values.max() ?? 0
}
var count: Int {
return values.count
}
}
// クラッシュレポートロガー
class CrashReportLogger {
static let shared = CrashReportLogger()
private init() {
setupCrashLogging()
setupUncaughtExceptionHandler()
}
private func setupCrashLogging() {
// クラッシュ専用ファイルロガー
let crashLogger = DDFileLogger()
let documentsPath = FileManager.default.urls(for: .documentDirectory,
in: .userDomainMask).first!
let crashLogsDirectory = documentsPath.appendingPathComponent("Logs/crashes")
let logFileManager = DDLogFileManagerDefault(logsDirectory: crashLogsDirectory.path)
crashLogger.logFileManager = logFileManager
crashLogger.rollingFrequency = 0 // クラッシュ時のみローテーション
crashLogger.logFileManager.maximumNumberOfLogFiles = 50 // 50個のクラッシュレポート保持
crashLogger.maximumFileSize = 1024 * 1024 * 5 // 5MB
crashLogger.logFormatter = CrashLogFormatter()
DDLog.add(crashLogger, with: .error, context: LogContextCrash)
}
private func setupUncaughtExceptionHandler() {
NSSetUncaughtExceptionHandler { exception in
CrashReportLogger.shared.logCrash(exception: exception)
}
// Fatal signal handler
signal(SIGABRT) { signal in
CrashReportLogger.shared.logFatalSignal(signal: signal)
}
signal(SIGILL) { signal in
CrashReportLogger.shared.logFatalSignal(signal: signal)
}
signal(SIGSEGV) { signal in
CrashReportLogger.shared.logFatalSignal(signal: signal)
}
signal(SIGFPE) { signal in
CrashReportLogger.shared.logFatalSignal(signal: signal)
}
signal(SIGBUS) { signal in
CrashReportLogger.shared.logFatalSignal(signal: signal)
}
}
private func logCrash(exception: NSException) {
let crashReport = generateCrashReport(exception: exception)
DDLogCrash("=== CRASH REPORT ===")
DDLogCrash(crashReport)
DDLogCrash("=== END CRASH REPORT ===")
// クラッシュログを即座にフラッシュ
DDLog.flushLog()
}
private func logFatalSignal(signal: Int32) {
let crashReport = generateSignalCrashReport(signal: signal)
DDLogCrash("=== FATAL SIGNAL REPORT ===")
DDLogCrash(crashReport)
DDLogCrash("=== END FATAL SIGNAL REPORT ===")
// クラッシュログを即座にフラッシュ
DDLog.flushLog()
}
private func generateCrashReport(exception: NSException) -> String {
var report = ""
// 基本情報
report += "Exception Name: \(exception.name)\n"
report += "Reason: \(exception.reason ?? "Unknown")\n"
report += "User Info: \(exception.userInfo ?? [:])\n"
// デバイス情報
report += "\n=== Device Information ===\n"
report += getDeviceInformation()
// アプリ情報
report += "\n=== Application Information ===\n"
report += getApplicationInformation()
// スタックトレース
report += "\n=== Stack Trace ===\n"
if let callStack = exception.callStackSymbols {
for (index, symbol) in callStack.enumerated() {
report += "\(index): \(symbol)\n"
}
}
// メモリ情報
report += "\n=== Memory Information ===\n"
let memoryInfo = PerformanceLogger.shared.getMemoryUsage()
report += "Used Memory: \(String(format: "%.1f", memoryInfo.used))MB\n"
report += "Available Memory: \(String(format: "%.1f", memoryInfo.available))MB\n"
report += "Memory Usage: \(String(format: "%.1f", memoryInfo.usagePercentage))%\n"
return report
}
private func generateSignalCrashReport(signal: Int32) -> String {
var report = ""
report += "Fatal Signal: \(signal) (\(signalName(signal)))\n"
// デバイス・アプリ情報
report += "\n=== Device Information ===\n"
report += getDeviceInformation()
report += "\n=== Application Information ===\n"
report += getApplicationInformation()
// スタックトレース(可能な場合)
report += "\n=== Stack Trace ===\n"
let symbols = Thread.callStackSymbols
for (index, symbol) in symbols.enumerated() {
report += "\(index): \(symbol)\n"
}
return report
}
private func getDeviceInformation() -> String {
var info = ""
let device = UIDevice.current
info += "Device: \(device.model)\n"
info += "System Name: \(device.systemName)\n"
info += "System Version: \(device.systemVersion)\n"
info += "Identifier: \(device.identifierForVendor?.uuidString ?? "Unknown")\n"
// プロセッサ情報
var size = 0
sysctlbyname("hw.machine", nil, &size, nil, 0)
var machine = [CChar](repeating: 0, count: size)
sysctlbyname("hw.machine", &machine, &size, nil, 0)
info += "Hardware: \(String(cString: machine))\n"
return info
}
private func getApplicationInformation() -> String {
var info = ""
let bundle = Bundle.main
info += "App Name: \(bundle.object(forInfoDictionaryKey: "CFBundleName") ?? "Unknown")\n"
info += "App Version: \(bundle.object(forInfoDictionaryKey: "CFBundleShortVersionString") ?? "Unknown")\n"
info += "Build Number: \(bundle.object(forInfoDictionaryKey: "CFBundleVersion") ?? "Unknown")\n"
info += "Bundle ID: \(bundle.bundleIdentifier ?? "Unknown")\n"
// アプリ実行時間
let processInfo = ProcessInfo.processInfo
info += "Process Start Time: \(processInfo.processName)\n"
info += "System Uptime: \(processInfo.systemUptime) seconds\n"
return info
}
private func signalName(_ signal: Int32) -> String {
switch signal {
case SIGABRT: return "SIGABRT"
case SIGILL: return "SIGILL"
case SIGSEGV: return "SIGSEGV"
case SIGFPE: return "SIGFPE"
case SIGBUS: return "SIGBUS"
default: return "UNKNOWN"
}
}
}
// 専用ログレベルとマクロ
let LogContextPerformance = 5000
let LogContextCrash = 6000
func DDLogPerformance(_ message: @autoclosure () -> String,
level: DDLogLevel = .info,
context: Int = LogContextPerformance,
file: StaticString = #file,
function: StaticString = #function,
line: UInt = #line) {
_DDLogMessage(message(), level: level, flag: DDLogFlag(level),
context: context, file: file, function: function, line: line)
}
func DDLogCrash(_ message: @autoclosure () -> String,
level: DDLogLevel = .error,
context: Int = LogContextCrash,
file: StaticString = #file,
function: StaticString = #function,
line: UInt = #line) {
_DDLogMessage(message(), level: level, flag: DDLogFlag(level),
context: context, file: file, function: function, line: line)
}
// カスタムフォーマッター
class PerformanceLogFormatter: NSObject, DDLogFormatter {
let dateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS"
return formatter
}()
func format(message logMessage: DDLogMessage) -> String? {
let timestamp = dateFormatter.string(from: logMessage.timestamp)
let logLevel = logLevelString(logMessage.level)
return "\(timestamp) [\(logLevel)] [PERFORMANCE] \(logMessage.message)"
}
private func logLevelString(_ level: DDLogLevel) -> String {
switch level {
case .error: return "ERROR"
case .warning: return "WARN "
case .info: return "INFO "
case .debug: return "DEBUG"
case .verbose: return "TRACE"
default: return "UNKNOWN"
}
}
}
class CrashLogFormatter: NSObject, DDLogFormatter {
let dateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS"
return formatter
}()
func format(message logMessage: DDLogMessage) -> String? {
let timestamp = dateFormatter.string(from: logMessage.timestamp)
return "\(timestamp) [CRASH] \(logMessage.message)"
}
}
// 使用例
class PerformanceMonitoringViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// パフォーマンス監視とクラッシュレポート初期化
_ = PerformanceLogger.shared
_ = CrashReportLogger.shared
// メモリとCPU監視開始
startPerformanceMonitoring()
DDLogInfo("パフォーマンス監視画面ロード完了")
}
private func startPerformanceMonitoring() {
// 定期的なメモリ・CPU使用率監視
Timer.scheduledTimer(withTimeInterval: 30.0, repeats: true) { _ in
PerformanceLogger.shared.logMemoryUsage(operation: "periodic_check")
PerformanceLogger.shared.logCPUUsage(operation: "periodic_check")
}
}
@IBAction func performHeavyOperationButtonTapped(_ sender: UIButton) {
PerformanceLogger.shared.measureExecutionTime(operationName: "heavy_calculation") {
// 重い計算処理のシミュレーション
var result = 0.0
for i in 0..<1_000_000 {
result += sin(Double(i))
}
DDLogDebug("重い計算完了: \(result)")
}
DDLogUserAction("重い処理実行ボタンタップ")
}
@IBAction func generateCrashButtonTapped(_ sender: UIButton) {
// クラッシュテスト(デバッグ専用)
#if DEBUG
fatalError("テスト用クラッシュ")
#endif
}
@IBAction func showPerformanceReportButtonTapped(_ sender: UIButton) {
let report = PerformanceLogger.shared.generatePerformanceReport()
let alert = UIAlertController(
title: "パフォーマンスレポート",
message: report,
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "OK", style: .default))
present(alert, animated: true)
DDLogUserAction("パフォーマンスレポート表示")
}
}