Utilizing Apple’s oslog for Enhanced Logging in Xcode

Rohit Saini
7 min read6 days ago

--

Effective logging is crucial for debugging and maintaining applications. With the latest advancements in Xcode and Apple’s oslog, developers can leverage powerful logging features. This article will explore a custom logging implementation using oslog, detailing how different log levels can be used, and how metadata enhances log management.

Introduction to Custom Logger

Below is a custom logging implementation in Swift, designed to categorize and format logs effectively using Apple’s oslog.

Code Overview

import Foundation
import os.log

public enum LogLevel {
case debug
case info
case warning
case error
case custom(CustomLogLevel)
}

public enum CustomLogLevel {
case request
case response
}

// Define an enum for date formatters
public enum LogDateFormatter: String {
case MM_dd_yyyy_HH_mm_ss_SSS = "MM/dd/yyyy HH:mm:ss:SSS"
case MM_dd_yyyy_HH_mm_ss = "MM-dd-yyyy HH:mm:ss"
case E_d_MMM_yyyy_HH_mm_ss_Z = "E, d MMM yyyy HH:mm:ss Z"
case MMM_d_HH_mm_ss_SSSZ = "MMM d, HH:mm:ss:SSSZ"
}

public protocol Logger {
func log<T>(file: String, function: String, line: Int, logLevel: LogLevel, _ object: T)
}

public class DefaultLogger: Logger {
public static let shared = DefaultLogger()
private let logSubsystem = "com.app.logger"
private init() {}

public func log<T>(file: String = #file,
function: String = #function,
line: Int = #line,
logLevel: LogLevel,
_ object: T) {
switch logLevel {
case .debug:
let message = logMessage(object, file: file, function: function, line: line)
os_log("%{public}@", log: .default, type: .debug, message)
case .info:
let message = logMessage(object, file: file, function: function, line: line)
os_log("%{public}@", log: .default, type: .info, message)
case .warning:
let message = logMessage(object, file: file, function: function, line: line)
os_log("%{public}@", log: .default, type: .default, message)
case .error:
let message = logMessage(object, file: file, function: function, line: line)
os_log("%{public}@", log: .default, type: .error, message)
case .custom(let level):
switch level {
case .request:
let request = object as? URLRequest
logRequest(request)
case .response:
guard let (response, data, error) = object as? (HTTPURLResponse?, Data?, Error?) else {
os_log("Invalid object passed for response logging.", log: .default, type: .error)
return
}
logResponse(response, data: data, error: error)
}
}
}
}

extension DefaultLogger {
private func logMessage<T>(_ object: T, file: String, function: String, line: Int) -> String {
let fileString = URL(fileURLWithPath: file).lastPathComponent
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = LogDateFormatter.MMM_d_HH_mm_ss_SSSZ.rawValue
let timestamp = dateFormatter.string(from: Date())
let objectType = String(describing: type(of: object))
let memoryAddress = withUnsafePointer(to: object) { "\($0)" }
let objectValue = String(describing: object)

let heading = "Log Information"
let separator = "──────────────────────────────────────────────────────────────────"

// Calculate the length of objectValue
let lines = objectValue.components(separatedBy: "\n")
let longestLineLength = lines.map { $0.count }.max() ?? 0

// Adjust padding for Value section based on longest line length
let valuePadding = max(50, longestLineLength + 1)

var logMessage = "\(separator)\n"
logMessage += "│ \(heading.padding(toLength: separator.count - 2, withPad: " ", startingAt: 0)) │\n"
logMessage += "\(separator)\n"
logMessage += "│ Timestamp │ \(timestamp.padding(toLength: 50, withPad: " ", startingAt: 0)) │\n"
logMessage += "├─────────────────────────────────────────────────────────────────┤\n"
logMessage += "│ File │ \(fileString.padding(toLength: 50, withPad: " ", startingAt: 0)) │\n"
logMessage += "├─────────────────────────────────────────────────────────────────┤\n"
logMessage += "│ Function │ \(function.padding(toLength: 50, withPad: " ", startingAt: 0)) │\n"
logMessage += "├─────────────────────────────────────────────────────────────────┤\n"
logMessage += "│ Line │ \(String(line).padding(toLength: 50, withPad: " ", startingAt: 0)) │\n"
logMessage += "├─────────────────────────────────────────────────────────────────┤\n"
logMessage += "│ Object Type │ \(objectType.padding(toLength: 50, withPad: " ", startingAt: 0)) │\n"
logMessage += "├─────────────────────────────────────────────────────────────────┤\n"
logMessage += "│ Memory Address │ \(memoryAddress.padding(toLength: 50, withPad: " ", startingAt: 0)) │\n"
logMessage += "├─────────────────────────────────────────────────────────────────┤\n"
logMessage += "│ Value │ \(objectValue) \(String(repeating: " ", count: valuePadding - longestLineLength)) │\n"
logMessage += "\(separator)\n"

return logMessage
}

private func logRequest(_ request: URLRequest?) {
guard let request = request else { return }
os_log("%{public}@", log: .default, type: .info, request.cURL)
}

private func logResponse(_ response: HTTPURLResponse?, data: Data?, error: Error?) {
guard let _ = response else { return }
var logMessage = "Response:"
if let data = data, let jsonString = String(data: data, encoding: .utf8), !jsonString.isEmpty {
logMessage += "\n\(jsonString)"
}
if let error = error {
logMessage += "\nError: \(error.localizedDescription)"
}

os_log("%{public}@", log: .default, type: .info, logMessage)
}
}

Detailed Explanation

1. Enum for Log Levels

The LogLevel enum defines various log levels such as debug, info, warning, error, and custom. The custom case allows for specific custom log levels like request and response.

public enum LogLevel {
case debug
case info
case warning
case error
case custom(CustomLogLevel)
}

public enum CustomLogLevel {
case request
case response
}

2. Date Formatter Enum

The LogDateFormatter enum defines different date formats for log timestamps, enhancing log readability.

public enum LogDateFormatter: String {
case MM_dd_yyyy_HH_mm_ss_SSS = "MM/dd/yyyy HH:mm:ss:SSS"
case MM_dd_yyyy_HH_mm_ss = "MM-dd-yyyy HH:mm:ss"
case E_d_MMM_yyyy_HH_mm_ss_Z = "E, d MMM yyyy HH:mm:ss Z"
case MMM_d_HH_mm_ss_SSSZ = "MMM d, HH:mm:ss:SSSZ"
}

3. Logger Protocol

The Logger protocol defines the log function, which all loggers must implement.

public protocol Logger {
func log<T>(file: String, function: String, line: Int, logLevel: LogLevel, _ object: T)
}

4. Default Logger Implementation

The DefaultLogger class is a singleton that implements the Logger protocol. It uses os_log to log messages at different levels.

public class DefaultLogger: Logger {
public static let shared = DefaultLogger()
private let logSubsystem = "com.app.logger"
private init() {}

public func log<T>(file: String = #file,
function: String = #function,
line: Int = #line,
logLevel: LogLevel,
_ object: T) {
switch logLevel {
case .debug:
let message = logMessage(object, file: file, function: function, line: line)
os_log("%{public}@", log: .default, type: .debug, message)
case .info:
let message = logMessage(object, file: file, function: function, line: line)
os_log("%{public}@", log: .default, type: .info, message)
case .warning:
let message = logMessage(object, file: file, function: function, line: line)
os_log("%{public}@", log: .default, type: .default, message)
case .error:
let message = logMessage(object, file: file, function: function, line: line)
os_log("%{public}@", log: .default, type: .error, message)
case .custom(let level):
switch level {
case .request:
let request = object as? URLRequest
logRequest(request)
case .response:
guard let (response, data, error) = object as? (HTTPURLResponse?, Data?, Error?) else {
os_log("Invalid object passed for response logging.", log: .default, type: .error)
return
}
logResponse(response, data: data, error: error)
}
}
}
}

5. Log Message Formatting

The logMessage function formats the log message with metadata such as file name, function name, line number, object type, memory address, and object value.

private func logMessage<T>(_ object: T, file: String, function: String, line: Int) -> String {
let fileString = URL(fileURLWithPath: file).lastPathComponent
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = LogDateFormatter.MMM_d_HH_mm_ss_SSSZ.rawValue
let timestamp = dateFormatter.string(from: Date())
let objectType = String(describing: type(of: object))
let memoryAddress = withUnsafePointer(to: object) { "\($0)" }
let objectValue = String(describing: object)

let heading = "Log Information"
let separator = "──────────────────────────────────────────────────────────────────"

// Calculate the length of objectValue
let lines = objectValue.components(separatedBy: "\n")
let longestLineLength = lines.map { $0.count }.max() ?? 0

// Adjust padding for Value section based on longest line length
let valuePadding = max(50, longestLineLength + 1)

var logMessage = "\(separator)\n"
logMessage += "│ \(heading.padding(toLength: separator.count - 2, withPad: " ", startingAt: 0)) │\n"
logMessage += "\(separator)\n"
logMessage += "│ Timestamp │ \(timestamp.padding(toLength: 50, withPad: " ", startingAt: 0)) │\n"
logMessage += "├─────────────────────────────────────────────────────────────────┤\n"
logMessage += "│ File │ \(fileString.padding(toLength: 50, withPad: " ", startingAt: 0)) │\n"
logMessage += "├─────────────────────────────────────────────────────────────────┤\n"
logMessage += "│ Function │ \(function.padding(toLength: 50, withPad: " ", startingAt: 0)) │\n"
logMessage += "├─────────────────────────────────────────────────────────────────┤\n"
logMessage += "│ Line │ \(String(line).padding(toLength: 50, withPad: " ", startingAt: 0)) │\n"
logMessage += "├─────────────────────────────────────────────────────────────────┤\n"
logMessage += "│ Object Type │ \(objectType.padding(toLength: 50, withPad: " ", startingAt: 0)) │\n"
logMessage += "├─────────────────────────────────────────────────────────────────┤\n"
logMessage += "│ Memory Address │ \(memoryAddress.padding(toLength: 50, withPad: " ", startingAt: 0)) │\n"
logMessage += "├─────────────────────────────────────────────────────────────────┤\n"
logMessage += "│ Value │ \(objectValue) \(String(repeating: " ", count: valuePadding - longestLineLength)) │\n"
logMessage += "\(separator)\n"

return logMessage
}

6. Custom Logging for Network Requests and Responses

The logRequest and logResponse functions handle custom logging for network requests and responses.

private func logRequest(_ request: URLRequest?) {
guard let request = request else { return }
os_log("%{public}@", log: .default, type: .info, request.cURL)
}

private func logResponse(_ response: HTTPURLResponse?, data: Data?, error: Error?) {
guard let _ = response else { return }
var logMessage = "Response:"
if let data = data, let jsonString = String(data: data, encoding: .utf8), !jsonString.isEmpty {
logMessage += "\n\(jsonString)"
}
if let error = error {
logMessage += "\nError: \(error.localizedDescription)"
}

os_log("%{public}@", log: .default, type: .info, logMessage)
}

Advantages of Using Apple’s oslog

1. Filtering Logs in Xcode Console

By using oslog, developers can filter different types of logs (debug, info, error, warning) directly in the Xcode console, making it easier to focus on specific log levels.

2. Row Structure and Metadata

The latest Xcode displays logs in a row structure, allowing users to copy logs by right-clicking on them. Each log entry can contain metadata, providing detailed context for each log message.

3. Categorizing Logs with Metadata

Logs can be categorized using different types of metadata, such as timestamps, file names, function names, line numbers, object types, and memory addresses. This helps in better organizing and understanding logs.

Conclusion

Implementing a custom logger with oslog enhances the logging capabilities in Swift applications. It provides structured and categorized logs, which are easy to filter and manage in Xcode. By leveraging the features of oslog, developers can significantly improve their debugging and monitoring processes.

--

--

Rohit Saini

कर्म मुझे बांधता नहीं, क्योंकि मुझे कर्म के प्रतिफल की कोई इच्छा नहीं |