PrettyLog is a small Swift Package that makes logging in the Xcode Console a little bit more beautiful.
dependencies: [
.package(url: "https://github.com/bennokress/PrettyLog", .upToNextMajor(from: "2.0.0"))
]
PrettyLog v2 introduces several new features while maintaining backward compatibility except for one small breaking change. Here's what's new and what you need to update:
Three new log levels have been added with environment-specific semantics. In our team at work this has been established to make clear which level is appropriate for a log message.
- 🟤
.xcode
- identical to.debug
- 🔵
.staging
- identical to.verbose
- 🟢
.production
- identical to.info
Additionally Key Event Logging was added which is meant to always log without sensitive data, perhaps even to another system, to gain insights and generate statistics.
- 📊
.keyEvent
- Highest priority for critical events (📊)
All logging methods except logK
now support an optional sensitiveMessages
parameter for data that should only be logged in certain environments (configurable for each LogTarget
):
// Before (1.x)
logI("Login", "User ID: \(userID), category: .user)
// After (2.0) - with sensitive data support we could for example log the password to the console, but not in production
logI("Login", "User ID: \(userID), sensitiveMessages: "Password: \(password)", category: .user)
PrettyLog is now officially compatible with Swift 6.1.
With the new sensitive data, an additional parameter is needed to define a LogTarget
that defines if the target should log or omit sensitive information.
public protocol LogTarget {
/// If `false`, Strings passed to the `log` method as sensitive data will be discarded.
var canLogSensitiveInformation: Bool { get }
// …
}
- All existing
logD()
,logV()
,logI()
,logW()
,logE()
calls continue to work unchanged, but can be adjusted to the new semantic notation - Global log method definitions from the "Import once" pattern work as before, but can be expanded to support the new functionalities
With PrettyLog you can make your print statements prettier and more useful. Just replace this ...
print("User tapped Continue Button")
... with this:
logD("User tapped Continue Button")
So far so good. You have a lot more options with PrettyLog though ...
logD("A debug log")
logV("A verbose log")
logI("An info log")
logW("A warning log")
logE("An error log")
Instead of Debug, Verbose and Info, you can also use Xcode, Staging and Production, as well as the new Key Event for Statistics Generation:
logX("A log only to Xcode is basically a debug log")
logS("A log up to the Staging Environment is basically a verbose log")
logP("A log up to Production is basically an info log")
logK("And this is a Key Event Log for statistics")
logV("Got an API Response ...", category: .service)
logV("Saving Token from API: \(token)", category: .storage)
logI("User tapped Continue Button", category: .user)
logE("Username and Password did not match", category: .manager)
logW("Or create your own Category ...", category: .custom("Announcement"))
But wouldn't it be nice to log that a token was saved in production, but omit the token itself there while still logging it in development? This is possible with v2 of PrettyLog:
logV("Got an API Response ...", category: .service)
logI("Saving Token from API", sensitiveMessages: token, category: .storage)
logI("User tapped Continue Button", category: .user)
logE("Username and Password did not match", category: .manager)
logW("Or create your own Category ...", category: .custom("Announcement"))
logV(service, "Got an API Response ...", category: .service)
logW(screen, element, "Username too long", joinedBy: " → ", category: .manager)
let error = NetworkError.notFound
let exception = NSException(name: .portTimeoutException, reason: nil)
log(error, category: .service)
log(exception, category: .service)
Included in PrettyLog are the categories that I use routinely. Those may differ from what is useful in your project, so I made it easy for you to define your own categories. Simply extend LogCategory
like this:
extension LogCategory {
/// This custom category can be used like all the predefined ones: logV("Running Unit Tests ...", category: .todo)
static var todo: LogCategory { .custom("To Do") }
}
PrettyLog contains a few default Log Levels, but that doesn't mean that you are limited to them. To define your own level extend LogLevel
like this:
extension LogLevel {
/// This custom level can be used like all the predefined ones, it has to be called with the universal `log` method though: `log("The login method is not yet implemented", category: .todo, as: .todo)
static var todo: LogLevel { .custom(emoji: "🟣", priority: 200) }
}
If you want to use your custom Log Level in line with all the predefined ones, you might want to define a global method like logT
for it somewhere in your code:
/// Log messages in the provided order with TODO level
/// - Parameters:
/// - messages: One or more strings and string-convertible objects to include in the log statement
/// - sensitiveMessages: One or more strings and string-convertible objects to include in the log statement if the target allows sensitive content
/// - separator: The separator between messages (defaults to `-`)
/// - Attention: No log will be created, if `messages` and `sensitiveMessages` are both empty or consist only of `nil`-elements.
public func logT(_ messages: String?..., sensitiveMessages: String?..., joinedBy separator: String = " - ") {
PrettyLogProxy.log(messages, sensitiveMessages: sensitiveMessages, joinedBy: separator, as: .todo, category: .todo)
}
See the section 'Import once' in the Integration part of this README for a well suited place to define this.
PrettyLog makes it easy to send your log messages to different destinations with Log Targets. Predefined in the package is ConsoleLog
which sends statements of all Log Levels to the Xcode console.
In real world apps you might want to log to Backends and Web Services or locally using different local solutions than the plain old print
. That's where the LogTarget
protocol comes in.
The updated LogTarget
protocol in v2.0 requires you to implement:
import Foundation
import PrettyLog
struct Console: LogTarget {
var canLogSensitiveInformation: Bool { !App.shared.isProductionVersion }
var logPriorityRange: ClosedRange<LogLevel>? {
App.shared.isDeveloperVersion ? .allowAll : .allowNone
}
/// Create the log statement with a consistent design.
/// - Parameters:
/// - level: The log level is responsible for the emoji displayed in the log statement.
/// - message: The message is printed to the right of the log level emoji.
/// - category: The category is printed to the left of the log level emoji.
func createLog(_ level: LogLevel, message: String, category: LogCategory) {
print("\(prefix(level: level, category: category)) \(message)")
}
// MARK: Private Helpers
private var currentTimestamp: String {
let formatter = DateFormatter()
formatter.dateFormat = "HH:mm:ss.SSS"
return formatter.string(from: Date())
}
private func prefix(level: LogLevel, category: LogCategory) -> String {
"\(currentTimestamp) \(category.truncatedOrPadded(to: 20)) \(level.emoji)"
}
}
When using PrettyLog you have basically two options:
Since you probably log throughout your app, importing PrettyLog in every single file might become cumbersome, but it's an option.
import Foundation
import PrettyLog
struct MyModel {
func doStuff() {
logV("Doing some stuff ...")
}
}
Another option is to create a Swift file somewhere in your app that serves as a proxy to PrettyLog. This enables you to import PrettyLog and define the log methods globally once.
import Foundation
import PrettyLog
// TODO: Decide, if you want to use logD or logX, logV or logS, as well as logI or logP below
/// Log a statement with DEBUG level.
/// - Parameters:
/// - messages: One or more strings and string-convertible objects to include in the log statement
/// - sensitiveMessages: One or more strings and string-convertible objects to include in the log statement if the target allows sensitive content
/// - separator: The separator between messages (defaults to `-`)
/// - category: The category of the log message (defaults to `.uncategorized`)
/// - Attention: No log will be created, if `messages` and `sensitiveMessages` are both empty or consist only of `nil`-elements!
func logD(_ messages: String?..., sensitiveMessages: String?..., joinedBy separator: String = " - ", category: LogCategory = .uncategorized) {
PrettyLogProxy.logD(messages, sensitiveMessages: sensitiveMessages, joinedBy: separator, category: category)
}
/// Log a statement with VERBOSE level.
/// - Parameters:
/// - messages: One or more strings and string-convertible objects to include in the log statement
/// - sensitiveMessages: One or more strings and string-convertible objects to include in the log statement if the target allows sensitive content
/// - separator: The separator between messages (defaults to `-`)
/// - category: The category of the log message (defaults to `.uncategorized`)
/// - Attention: No log will be created, if `messages` and `sensitiveMessages` are both empty or consist only of `nil`-elements!
func logV(_ messages: String?..., sensitiveMessages: String?..., joinedBy separator: String = " - ", category: LogCategory = .uncategorized) {
PrettyLogProxy.logV(messages, sensitiveMessages: sensitiveMessages, joinedBy: separator, category: category)
}
/// Log a statement with INFO level.
/// - Parameters:
/// - messages: One or more strings and string-convertible objects to include in the log statement
/// - sensitiveMessages: One or more strings and string-convertible objects to include in the log statement if the target allows sensitive content
/// - separator: The separator between messages (defaults to `-`)
/// - category: The category of the log message (defaults to `.uncategorized`)
/// - Attention: No log will be created, if `messages` and `sensitiveMessages` are both empty or consist only of `nil`-elements!
func logI(_ messages: String?..., sensitiveMessages: String?..., joinedBy separator: String = " - ", category: LogCategory = .uncategorized) {
PrettyLogProxy.logI(messages, sensitiveMessages: sensitiveMessages, joinedBy: separator, category: category)
}
/// Log a statement with WARNING level.
/// - Parameters:
/// - messages: One or more strings and string-convertible objects to include in the log statement
/// - sensitiveMessages: One or more strings and string-convertible objects to include in the log statement if the target allows sensitive content
/// - separator: The separator between messages (defaults to `-`)
/// - category: The category of the log message (defaults to `.uncategorized`)
/// - Attention: No log will be created, if `messages` and `sensitiveMessages` are both empty or consist only of `nil`-elements!
func logW(_ messages: String?..., sensitiveMessages: String?..., joinedBy separator: String = " - ", category: LogCategory = .uncategorized) {
PrettyLogProxy.logW(messages, sensitiveMessages: sensitiveMessages, joinedBy: separator, category: category)
}
/// Log a statement with ERROR level.
/// - Parameters:
/// - messages: One or more strings and string-convertible objects to include in the log statement
/// - sensitiveMessages: One or more strings and string-convertible objects to include in the log statement if the target allows sensitive content
/// - separator: The separator between messages (defaults to `-`)
/// - category: The category of the log message (defaults to `.uncategorized`)
/// - Attention: No log will be created, if `messages` and `sensitiveMessages` are both empty or consist only of `nil`-elements!
func logE(_ messages: String?..., sensitiveMessages: String?..., joinedBy separator: String = " - ", category: LogCategory = .uncategorized) {
PrettyLogProxy.logE(messages, sensitiveMessages: sensitiveMessages, joinedBy: separator, category: category)
}
/// Log a Key Event.
/// - Parameters:
/// - messages: One or more strings and string-convertible objects to include in the log statement
/// - separator: The separator between messages (defaults to `-`)
/// - category: The category of the log message (defaults to `.uncategorized`)
/// - Attention: No log will be created, if `messages` is empty or consist only of `nil`-elements!
func logK(_ messages: String?..., joinedBy separator: String = " - ", category: LogCategory = .uncategorized) {
PrettyLogProxy.logK(messages, joinedBy: separator, category: category)
}
/// Log an `Error`.
/// - Parameters:
/// - error: The error to log
/// - category: The category of the log message (defaults to `.uncategorized`)
/// - Attention: No log will be created, if `error` is `nil`.
func log(_ error: Error?, category: LogCategory = .uncategorized) {
PrettyLogProxy.log(error, category: category)
}
/// Log a `NSException`.
/// - Parameters:
/// - exception: The exception to log
/// - category: The category of the log statement (defaults to `.uncategorized`)
/// - Attention: No log will be created, if `exception` is `nil`.
func log(_ exception: NSException?, category: LogCategory = .uncategorized) {
PrettyLogProxy.log(exception, category: category)
}
// TODO: Add custom global log methods if needed -> for example: if you have custom LogLevel and LogCategory `.todo`, you could define `logT for that.
// /// Log a statement with TODO level.
// /// - Parameters:
// /// - messages: One or more strings and string-convertible objects to include in the log statement
// /// - sensitiveMessages: One or more strings and string-convertible objects to include in the log statement if the target allows sensitive content
// /// - separator: The separator between messages (defaults to `-`)
// /// - Attention: No log will be created, if `messages` and `sensitiveMessages` are both empty or consist only of `nil`-elements!
// public func logT(_ messages: String?..., sensitiveMessages: String?..., joinedBy separator: String = " - ") {
// PrettyLogProxy.log(messages, sensitiveMessages: sensitiveMessages, joinedBy: separator, as: .todo, category: .todo)
// }
👨🏻💻 Benno Kress
- Website: bennokress.de
- Mastodon: @benno@iosdev.space
Contributions, issues and feature requests are welcome!
Feel free to check issues page.
Copyright © Benno Kress.
This project is MIT licensed.
This README was generated with ❤️ by readme-md-generator