CocoaLumberjack
Fast, simple, yet powerful and flexible logging framework for macOS, iOS, tvOS, and watchOS. Orders of magnitude faster than NSLog, available with single-line configuration at application launch. Provides rich customization options and extensibility.
Library
CocoaLumberjack
Overview
CocoaLumberjack is a fast, simple, yet powerful and flexible logging framework for macOS, iOS, tvOS, and watchOS. It delivers orders of magnitude better performance than NSLog and provides an excellent development experience with single-line configuration at application launch. With rich customization options and extensibility, it handles advanced logging requirements. Through over 15 years of development experience and mature APIs, it realizes a reliable logging solution for iOS and macOS application development, supporting both Swift and Objective-C languages.
Details
CocoaLumberjack 2025 edition maintains its solid position as a veteran library popular in the Swift community. While continued use in existing projects and new adoption by developers prioritizing flexibility are observed, new project adoption is declining due to the proliferation of Apple's official OSLog. However, it maintains advantages in projects with complex logging requirements through high customizability, rich output destination options, file rotation capabilities, and Swift Package Manager support. With multi-platform support (macOS, iOS, tvOS, watchOS, visionOS) and high performance, it is valued as an important choice in professional iOS application development.
Key Features
- High-speed performance: Orders of magnitude faster asynchronous log processing than NSLog
- Flexible configuration: Support for simultaneous writing to multiple loggers and output destinations
- Swift integration: Complete support for both Swift and Objective-C
- Rich output destinations: Files, console, system log, custom logs, etc.
- Apple platform support: Compatible with iOS, macOS, tvOS, watchOS, visionOS
- High customizability: Implementation support for custom formatters, filters, and log processing
Pros and Cons
Pros
- Significant performance improvement and efficient resource usage compared to NSLog
- Simple setup with immediate availability through single-line initialization code
- Advanced features like file rotation, compression, and automatic cleanup
- Various installation methods including CocoaPods, Carthage, and Swift Package Manager
- Stability and reliability through over 15 years of development experience
- Rich documentation and active community support
Cons
- Decreased new adoption opportunities due to Apple's official OSLog proliferation
- May be overkill for small-scale applications
- Addition of third-party library dependencies
- Configuration complexity when used as Swift-log backend
- End of support for iOS11 and earlier, macOS 10.13 and earlier
- Privacy and data collection responsibility delegated to app developers
Reference Pages
- CocoaLumberjack GitHub Repository
- CocoaLumberjack Official Documentation
- CocoaLumberjack Getting Started
Usage Examples
Installation and Basic Setup
// Swift Package Manager (Recommended)
// File -> Add Package Dependencies...
// https://github.com/CocoaLumberjack/CocoaLumberjack.git
// For CocoaPods
// pod 'CocoaLumberjack/Swift'
// For Carthage
// github "CocoaLumberjack/CocoaLumberjack"
import CocoaLumberjack
class AppDelegate: UIApplicationDelegate {
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Basic initialization (single-line setup)
DDLog.add(DDOSLogger.sharedInstance) // OS log (Apple unified logging)
DDLog.add(DDTTYLogger.sharedInstance!) // Xcode console
// Log level setup
#if DEBUG
DDLog.logLevel = .all
#else
DDLog.logLevel = .warning
#endif
// Basic log output
DDLogVerbose("Application started - verbose")
DDLogDebug("Application started - debug")
DDLogInfo("Application started - info")
DDLogWarn("Application started - warning")
DDLogError("Application started - error")
return true
}
}
// SwiftUI setup example
@main
struct MyApp: App {
init() {
setupLogging()
}
private func setupLogging() {
// DDOSLogger: Apple unified logging system
DDLog.add(DDOSLogger.sharedInstance)
// DDTTYLogger: Development environment console output
if let ttyLogger = DDTTYLogger.sharedInstance {
DDLog.add(ttyLogger)
// Color output setup
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 application display started")
}
}
}
}
Basic Log Levels and Output Destination Setup
import CocoaLumberjack
// Log level definition
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() {
// Global log level setup
DDLog.logLevel = DDLogLevel.appLogLevel
// 1. OS log (system integration)
let osLogger = DDOSLogger.sharedInstance
osLogger.logFormatter = CustomOSLogFormatter()
DDLog.add(osLogger)
// 2. Console log (development environment)
if let ttyLogger = DDTTYLogger.sharedInstance {
ttyLogger.logFormatter = CustomConsoleFormatter()
ttyLogger.colorsEnabled = true
setupConsoleColors(ttyLogger)
DDLog.add(ttyLogger)
}
// 3. File log (production/debug)
let fileLogger = DDFileLogger()
fileLogger.rollingFrequency = 60 * 60 * 24 // 24 hours
fileLogger.logFileManager.maximumNumberOfLogFiles = 7 // 1 week
fileLogger.maximumFileSize = 1024 * 1024 * 10 // 10MB
fileLogger.logFormatter = CustomFileFormatter()
DDLog.add(fileLogger)
DDLogInfo("Logging system initialization completed")
DDLogInfo("File log path: \(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)
}
// Dynamic log level change
func setLogLevel(_ level: DDLogLevel) {
DDLog.logLevel = level
DDLogInfo("Log level changed: \(level)")
}
// Log file management
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 }
// Export log files in ZIP format
let documentsPath = FileManager.default.urls(for: .documentDirectory,
in: .userDomainMask).first!
let exportURL = documentsPath.appendingPathComponent("app_logs_\(Date()).zip")
// Actual ZIP creation process omitted
DDLogInfo("Log file export: \(exportURL)")
return exportURL
}
}
// Custom formatter implementation
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)"
}
}
// Usage example
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Log manager initialization
_ = LoggingManager.shared
DDLogInfo("ViewController load completed")
// Test various log levels
testLogging()
}
private func testLogging() {
DDLogVerbose("Detailed trace information")
DDLogDebug("Debug information: viewDidLoad")
DDLogInfo("User action: screen display")
DDLogWarn("Warning: deprecated API usage")
DDLogError("Error: network connection failed")
// Parameterized logging
let userId = 12345
let actionName = "button_tap"
DDLogInfo("User action - UserID: \(userId), Action: \(actionName)")
}
}
Advanced Customization and Filtering
import CocoaLumberjack
// Custom log context definition
let LogContextUserActions = 1000
let LogContextNetworking = 2000
let LogContextDatabase = 3000
let LogContextAuthentication = 4000
// Context-specific log macros
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)
}
// Advanced logging manager
class AdvancedLoggingManager {
static let shared = AdvancedLoggingManager()
private var userActionsLogger: DDFileLogger?
private var networkLogger: DDFileLogger?
private var databaseLogger: DDFileLogger?
private init() {
setupAdvancedLoggers()
}
private func setupAdvancedLoggers() {
// Basic logger setup
setupBasicLoggers()
// Contextual file loggers
setupContextualFileLoggers()
// Network-specific logger
setupNetworkLogger()
DDLogInfo("Advanced logging system initialization completed")
}
private func setupBasicLoggers() {
// OS log
let osLogger = DDOSLogger.sharedInstance
osLogger.logFormatter = AdvancedOSLogFormatter()
DDLog.add(osLogger, with: .all)
// Console log
if let ttyLogger = DDTTYLogger.sharedInstance {
ttyLogger.logFormatter = AdvancedConsoleFormatter()
ttyLogger.colorsEnabled = true
setupAdvancedColors(ttyLogger)
DDLog.add(ttyLogger, with: .all)
}
}
private func setupContextualFileLoggers() {
// User action dedicated log
userActionsLogger = createContextualLogger(
filename: "user_actions",
context: LogContextUserActions
)
// Database dedicated log
databaseLogger = createContextualLogger(
filename: "database",
context: LogContextDatabase
)
}
private func createContextualLogger(filename: String, context: Int) -> DDFileLogger {
let fileLogger = DDFileLogger()
// File name and path setup
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
// Rotation setup
fileLogger.rollingFrequency = 60 * 60 * 24 // 24 hours
fileLogger.logFileManager.maximumNumberOfLogFiles = 14 // 2 weeks
fileLogger.maximumFileSize = 1024 * 1024 * 5 // 5MB
// Custom formatter
fileLogger.logFormatter = ContextualFileFormatter(context: context)
// Apply context filter
DDLog.add(fileLogger, with: DDLogLevel.all, context: context)
return fileLogger
}
private func setupNetworkLogger() {
networkLogger = DDFileLogger()
// Network-specific directory
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
// Frequent rotation (high volume network logs)
networkLogger?.rollingFrequency = 60 * 60 * 12 // 12 hours
networkLogger?.logFileManager.maximumNumberOfLogFiles = 28 // 2 weeks
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) {
// Error level
logger.setForegroundColor(.white, backgroundColor: .red, for: .error)
// Warning level
logger.setForegroundColor(.black, backgroundColor: .yellow, for: .warning)
// Info level
logger.setForegroundColor(.blue, backgroundColor: nil, for: .info)
// Debug level
logger.setForegroundColor(.green, backgroundColor: nil, for: .debug)
// Verbose level
logger.setForegroundColor(.lightGray, backgroundColor: nil, for: .verbose)
}
// Log analysis methods
func analyzeLogFiles() -> LogAnalysisResult {
var result = LogAnalysisResult()
// User action log analysis
if let userLogger = userActionsLogger {
result.userActionsCount = countLogEntries(in: userLogger)
}
// Network log analysis
if let netLogger = networkLogger {
result.networkRequestsCount = countLogEntries(in: netLogger)
}
// Database log analysis
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("Log file read error: \(error)")
}
}
return totalLines
}
}
// Advanced formatter implementation
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"
}
}
}
// Log analysis result structure
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 """
Log Analysis Results:
- User Actions: \(userActionsCount) entries
- Network Requests: \(networkRequestsCount) entries
- Database Operations: \(databaseOperationsCount) entries
- Total Log Files: \(totalLogFiles)
- Total Log Size: \(ByteCountFormatter.string(fromByteCount: totalLogSize, countStyle: .file))
"""
}
}
// Usage example
class AdvancedLoggingViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Advanced logging manager initialization
_ = AdvancedLoggingManager.shared
// Test contextual logging
testContextualLogging()
}
private func testContextualLogging() {
// User action logs
DDLogUserAction("Button tap - navigate to settings")
DDLogUserAction("Swipe gesture detected")
// Network logs
DDLogNetwork("HTTP GET /api/users - started")
DDLogNetwork("HTTP GET /api/users - 200 OK - 250ms")
// Database logs
DDLogDatabase("SQLite SELECT FROM users WHERE id = 123")
DDLogDatabase("Core Data save context - success")
// General logs
DDLogInfo("Screen display completed")
}
@IBAction func analyzeLogsButtonTapped(_ sender: UIButton) {
let analysisResult = AdvancedLoggingManager.shared.analyzeLogFiles()
let alert = UIAlertController(
title: "Log Analysis Results",
message: analysisResult.summary,
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "OK", style: .default))
present(alert, animated: true)
DDLogUserAction("Log analysis results displayed")
}
}
Performance Monitoring and Crash Reporting
import CocoaLumberjack
import Foundation
// Performance monitoring logger
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() {
// Performance-specific file logger
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 hours
perfLogger.logFileManager.maximumNumberOfLogFiles = 7 // 1 week
perfLogger.maximumFileSize = 1024 * 1024 * 10 // 10MB
perfLogger.logFormatter = PerformanceLogFormatter()
DDLog.add(perfLogger, with: .info, context: LogContextPerformance)
DDLogInfo("Performance monitoring logger initialization completed")
}
// Method execution time measurement
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
}
// Asynchronous processing time measurement
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)
}
}
// Metrics recording
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("Metric recorded: \(name) = \(value) \(unit)")
}
// Memory usage monitoring
func logMemoryUsage(operation: String) {
let memoryInfo = getMemoryUsage()
recordMetric(name: "memory_used_mb", value: memoryInfo.used)
recordMetric(name: "memory_available_mb", value: memoryInfo.available)
DDLogPerformance("Memory usage [\(operation)]: Used \(String(format: "%.1f", memoryInfo.used))MB / Available \(String(format: "%.1f", memoryInfo.available))MB")
// Memory warning threshold check
if memoryInfo.usagePercentage > 85.0 {
DDLogWarn("High memory usage warning: \(String(format: "%.1f", memoryInfo.usagePercentage))%")
}
}
// CPU usage monitoring
func logCPUUsage(operation: String) {
let cpuUsage = getCPUUsage()
recordMetric(name: "cpu_usage_percent", value: cpuUsage)
DDLogPerformance("CPU usage [\(operation)]: \(String(format: "%.1f", cpuUsage))%")
// CPU usage warning
if cpuUsage > 80.0 {
DDLogWarn("High CPU usage warning: \(String(format: "%.1f", cpuUsage))%")
}
}
// Performance statistics report
func generatePerformanceReport() -> String {
metricsLock.lock()
defer { metricsLock.unlock() }
var report = "=== Performance Report ===\n"
for (name, metric) in performanceMetrics.sorted(by: { $0.key < $1.key }) {
report += "\(name): avg \(String(format: "%.2f", metric.average)) \(metric.unit) "
report += "(min: \(String(format: "%.2f", metric.minimum)), "
report += "max: \(String(format: "%.2f", metric.maximum)), "
report += "count: \(metric.count))\n"
}
DDLogInfo("Performance report generated")
return report
}
private func logPerformance(operationName: String, executionTime: TimeInterval) {
performanceQueue.async {
self.recordMetric(name: "\(operationName)_time_ms",
value: executionTime, unit: "ms")
DDLogPerformance("Execution time measurement: \(operationName) - \(String(format: "%.2f", executionTime))ms")
// Slow processing warning
if executionTime > 1000 { // 1 second or more
DDLogWarn("Slow processing detected: \(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
}
}
// Performance metrics structure
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
}
}
// Crash report logger
class CrashReportLogger {
static let shared = CrashReportLogger()
private init() {
setupCrashLogging()
setupUncaughtExceptionHandler()
}
private func setupCrashLogging() {
// Crash-specific file logger
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 // Rotate only on crash
crashLogger.logFileManager.maximumNumberOfLogFiles = 50 // Keep 50 crash reports
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 ===")
// Flush crash log immediately
DDLog.flushLog()
}
private func logFatalSignal(signal: Int32) {
let crashReport = generateSignalCrashReport(signal: signal)
DDLogCrash("=== FATAL SIGNAL REPORT ===")
DDLogCrash(crashReport)
DDLogCrash("=== END FATAL SIGNAL REPORT ===")
// Flush crash log immediately
DDLog.flushLog()
}
private func generateCrashReport(exception: NSException) -> String {
var report = ""
// Basic information
report += "Exception Name: \(exception.name)\n"
report += "Reason: \(exception.reason ?? "Unknown")\n"
report += "User Info: \(exception.userInfo ?? [:])\n"
// Device information
report += "\n=== Device Information ===\n"
report += getDeviceInformation()
// Application information
report += "\n=== Application Information ===\n"
report += getApplicationInformation()
// Stack trace
report += "\n=== Stack Trace ===\n"
if let callStack = exception.callStackSymbols {
for (index, symbol) in callStack.enumerated() {
report += "\(index): \(symbol)\n"
}
}
// Memory information
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"
// Device and application information
report += "\n=== Device Information ===\n"
report += getDeviceInformation()
report += "\n=== Application Information ===\n"
report += getApplicationInformation()
// Stack trace (if possible)
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"
// Processor information
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"
// Application runtime
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"
}
}
}
// Dedicated log levels and macros
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)
}
// Custom formatters
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)"
}
}
// Usage example
class PerformanceMonitoringViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Performance monitoring and crash reporting initialization
_ = PerformanceLogger.shared
_ = CrashReportLogger.shared
// Start memory and CPU monitoring
startPerformanceMonitoring()
DDLogInfo("Performance monitoring screen load completed")
}
private func startPerformanceMonitoring() {
// Periodic memory and CPU usage monitoring
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") {
// Heavy calculation simulation
var result = 0.0
for i in 0..<1_000_000 {
result += sin(Double(i))
}
DDLogDebug("Heavy calculation completed: \(result)")
}
DDLogUserAction("Heavy operation button tapped")
}
@IBAction func generateCrashButtonTapped(_ sender: UIButton) {
// Crash test (debug only)
#if DEBUG
fatalError("Test crash")
#endif
}
@IBAction func showPerformanceReportButtonTapped(_ sender: UIButton) {
let report = PerformanceLogger.shared.generatePerformanceReport()
let alert = UIAlertController(
title: "Performance Report",
message: report,
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "OK", style: .default))
present(alert, animated: true)
DDLogUserAction("Performance report displayed")
}
}