Menu

Take Control of your Logs with Swift

Read it later:

Like it or not, Swift seems to be here to stay, and become the primary language for Cocoa applications on both iOS and OSX. Like probably most of you, I’ve developed a list of best practices and Objective-C code snippets over the years. Porting these components to Swift it’s a nice occasion to dig deeper into the language one step at a time, and it’s doubtless easier than porting a full application in one shot.

I’m starting with log file management, in particular I’ll address these two basic needs:

  • write your logs to a custom file destination,
  • use different log levels such as info, debug, warning, and so on.

Writing to a custom destination

The simplest way to log things in Cocoa is the NSLog() function, that writes all entries to the application standard error stream (stderr). When an OSX application is compiled for release, all these messages go to the “catch-all” system log, while during development they are catched by XCode and displayed in the debug area.

But what if I want my application to have its own log file called ~/Library/Logs/MyApp.log? The trick here is: redirect the standard error stream of the application to a custom file path. In Objective-C you would do it in the main.m file, before launching NSApplicationMain(). In Swift I wrote a function named SetCustomLogFilename(), packaged in a LogUtils.swift library file. I can call the function inside the AppDelegate class of an OSX application, by simply overriding the init() method, just like this:

//  AppDelegate.swift

import Cocoa
import Foundation

@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {

    @IBOutlet weak var window: NSWindow!

    var appInfo: Dictionary<NSObject,AnyObject>
    var appName: String!

    override init() {

        // Init local parameters
        self.appInfo = CFBundleGetInfoDictionary(CFBundleGetMainBundle()) as Dictionary
        self.appName = appInfo["CFBundleName"] as! String

        // Init parent
        super.init()

        // Other init below...

        SetCustomLogFilename(self.appName)

    }

	// Other methods here...

}

In the example above I first fetch the application name from the Info dictionary, then I call the default init() and finally set the log file by calling SetCustomLogFilename(self.appName). The actual SetCustomLogFilename() function can be implemented like this:

//  LogUtils.swift

// Redirect log to /Library/Logs/<name>.log
func SetCustomLogFilename(name: String) {

    // Search log directory path
    if let logDirectory: NSURL = NSFileManager.defaultManager().URLForDirectory(NSSearchPathDirectory.LibraryDirectory, inDomain: NSSearchPathDomainMask.UserDomainMask, appropriateForURL: nil, create: true, error: nil)?.URLByAppendingPathComponent("Logs/") {

        // Calculate full log file path
        if let logFilePath = logDirectory.URLByAppendingPathComponent(String(format:"%@.log", name)) as NSURL! {

            // Save STDERR
            var stderr = NSFileHandle.fileHandleWithStandardError()
            original_stderr = dup(stderr.fileDescriptor)

            // Create an empty log file at path, NSFileHandle doesn't do it!
            if !NSFileManager.defaultManager().isWritableFileAtPath(logFilePath.path!) {
                "".writeToFile(logFilePath.path!, atomically: true, encoding: NSUTF8StringEncoding, error: nil)
            }


            if let logFileHandle = NSFileHandle(forWritingAtPath: logFilePath.path!) {

                // (Try to) Redirect STDERR to log file
                var err:Int32? = dup2(logFileHandle.fileDescriptor, stderr.fileDescriptor)

                // Something went wrong
                if (err == -1) {
                    ErrorLog(String(format:"Could not redirect stderr, error %d", errno))
                }
            }
        }
    }
}

Firstly I compose the full path for the destination log file. Then I obtain the handle of the stderr stream using NSFileHandle and create a backup copy of it with dup(). A this point we need a file handle for our log file, I’m using NSFileHandle again here, but first I have to create the empty file since NSFileHandle won’t do it for me. With the new handle in place we can call dup2() to redirect the stderr stream to our new file. If the returned value is -1 there is an error that we log to stderr.

This solution is not only more professional, it’s also useful when monitoring your application, because you can see only relevant messages without noise. Moreover, if your application has a feedback/crash reporter, you can send these messages to your issue management platform.

But wait: what is that ErrorLog() function? I’ll come to it in a minute.

Different log entries

When you use NSLog() to write your messages, you log everything everytime. Wouldn’t it be nice if you could different log levels (eg. DEBUG, INFO, WARNING, ERROR) and, for example, automatically disable debug messages when compiling your app for distribution? In Objective-C you could extend NSLog() by defining a couple of macros in a common header file:

// YourApp-Prefix.pch

#ifdef DEBUG
#	define DebugLog(format, ...) NSLog(@"<Debug>: " format @" [" __FILE__ @":%i]", ##__VA_ARGS__, __LINE__)
#	ifdef TRACE_LOG
#		define DebugTrace(format, ...) NSLog(@"<Trace>: " format @" [" __FILE__ @":%i]", ##__VA_ARGS__, __LINE__)
#	else
#		define DebugTrace(format, ...)
#	endif
#	define InfoLog(format, ...) NSLog(@"<Info> " format @" [" __FILE__ @":%i]", ##__VA_ARGS__, __LINE__)
#	define WarningLog(format, ...) NSLog(@"<Warning> " format @" [" __FILE__ @":%i]", ##__VA_ARGS__, __LINE__)
#	define ErrorLog(format, ...) NSLog(@"<Error> " format @" [" __FILE__ @":%i]", ##__VA_ARGS__, __LINE__)
#else
#	define DebugLog(format, ...)
#	define DebugTrace(format, ...)
#	define InfoLog(format, ...) NSLog(@"<Info>: " format, ##__VA_ARGS__)
#	define WarningLog(format, ...) NSLog(@"<Warning>: " format, ##__VA_ARGS__)
#	define ErrorLog(format, ...) NSLog(@"<Error>: " format, ##__VA_ARGS__)
#endif

In Swift it’s a little bit tricky, you can still use “#if/#else/#endif” preprocessor macros (although more constrained), but you don’t have the same freedom with variadic functions. So I solved this way:

#if DEBUG
    func DebugLog(message: String, file: String = __FILE__, line: Int = __LINE__) {
        return { NSLog("<Debug>: " + message + " [" + file + ":%i]", line) }()
    }

    func InfoLog(message: String, file: String = __FILE__, line: Int = __LINE__) {
        return { NSLog("<Info>: " + message + " [" + file + ":%i]", line) }()
    }

    func WarningLog(message: String, file: String = __FILE__, line: Int = __LINE__) {
        return { NSLog("<Warning>: " + message + " [" + file + ":%i]", line) }()
    }

    func ErrorLog(message: String, file: String = __FILE__, line: Int = __LINE__) {
        return { NSLog("<Error>: " + message + " [" + file + ":%i]", line) }()
    }
#else
    func DebugLog(message: String, file: String = __FILE__, line: Int = __LINE__) {
    }

    func InfoLog(message: String, file: String = __FILE__, line: Int = __LINE__) {
        return { NSLog("<Info>: " + message) }()
    }

    func WarningLog(message: String, file: String = __FILE__, line: Int = __LINE__) {
        return { NSLog("<Warning>: " + message) }()
    }

    func ErrorLog(message: String, file: String = __FILE__, line: Int = __LINE__) {
        return { NSLog("<Error>: " + message) }()
    }
#endif

In this case I’m defining some wrapper functions around NSLog(), each function can be called with a formatted string parameter, like this:

ErrorLog(String(format:"Could not redirect stderr, error %d", errno))

It’s not as immediate as the Objective-C version but it’s still simple and pretty clean, you can grab the code on GitHub and adapt it to your needs. This solution works for me so far, if you need a more featured log library you can use the more powerful XCGLogger written by Dave Wood.

And you, what solution or framework do you yse for your applications? Feel free to share your thoughts in the comments below.