CocoaLumberjack: Logging on Steroids
Logging is one of the most useful instruments to inspect, understand, and debug iOS and OS X applications. You are probably familiar with the NSLog function provided by the Foundation framework, but have you ever felt the need for something more powerful? CocoaLumberjack is an open source library created and maintained by Robbie Hanson. CocoaLumberjack takes logging to a whole new level, and in this tutorial I will show you how to setup and use CocoaLumberjack in an iOS application.
Logging? Who Needs Logging?
Logging diagnostic information to a console, file, or remote server is widely used in almost any type of software development. It is one of the simplest forms of debugging, which is probably why it is so widespread. It is the first tool that I use when I am debugging or trying to understand a complex piece of logic regardless of the language. It is easy, fast, and comes with very little overhead.
Why should you use CocoaLumberjack if all it does is send pieces of data to the console or a file? One reason is that CocoaLumberjack is (mostly) faster than the NSLog function that the Foundation framework provides us with. Thanks to a number of convenient macros provided by CocoaLumberjack, switching from NSLog to CocoaLumberjack is as easy as replacing your NSLog with DDLog statements.
Another benefit of CocoaLumberjack is that one log statement can be sent to multiple loggers (console, file, remote database, etc.). You can configure CocoaLumberjack in such a way that it behaves differently depending on the build configuration (Debug, Release, etc.). There is much more that CocoaLumberjack can do for you so let me show you how to get started with this nifty library.
Step 1: Setting Up CocoaLumberjack
Create a new project in Xcode by selecting the Single View Application template from the list of available templates (figure 1). Name your application Logging, enter a company identifier, set iPhone for the device family, and then check Use Automatic Reference Counting. The rest of the checkboxes can be left unchecked for this project (figure 2). Tell Xcode where you want to save the project and hit the Createbutton.
![CocoaLumberjack: Logging on Steroids: Choosing a Project Template - Figure 1](http://static.oschina.net/uploads/img/201604/05184839_pSqv.png)
![CocoaLumberjack: Logging on Steroids: Configuring the New Project - Figure 2](http://static.oschina.net/uploads/img/201604/05184840_YOrB.png)
Adding the CocoaLumberjack library to your project is as easy as downloading the latest version from GitHub, extracting the archive, and dragging the folder named Lumberjack into your project. The core files are DDLog.h/.m, DDASLLogger.h/.m, DDTTYLogger.h/.m, and DDFileLogger.h/.m. The other files in the folder are stubs for more advanced uses of CocoaLumberjack, which I won't cover in this tutorial. You can ignore or delete these files.
If you take a peak inside DDLog.h and DDLog.m, you may be surprised by the number of lines of code in these files. As I said, CocoaLumberjack has a lot of really useful features. CocoaLumberjack is more powerful than NSLog because it takes advantage of multi-threading, Grand Central Dispatch, and the power of the Objective-C runtime.
You will also notice that there are a surprising number of macros defined in DDLog.h. We won't use the majority of these macros. The macros that we will use in this tutorial are DDLogError, DDLogWarn, DDLogInfo, and DDLogVerbose. They all perform the same task, but each macro is associated with a log level. I will talk more about log levels in a few moments.
Before we start using CocoaLumberjack, it is a good idea to add an import statement to the project's precompiled header file. Open Logging-Prefix.pch and add an import statement for DDLog.h. This ensure that the macros defined in DDLog.h are available throughout the project.
01
02
03
04
05
06
07
08
09
10
11
12
|
#import <Availability.h>
#ifndef __IPHONE_4_0
#warning "This project uses features only available in iOS SDK 4.0 and later."
#endif
#ifdef __OBJC__
#import <UIKit/UIKit.h>
#import <Foundation/Foundation.h>
#import "DDLog.h"
#endif
|
Step 2: Adding a Logger
Configuring CocoaLumberjack is easy. First, however, we need to import several classes of the CocoaLumberjack library. At the top of MTAppDelegate.m, add an import statement for DDASLLogger.h, DDTTYLogger.h, and DDFileLogger.h (see below). The first two classes are in charge of sending log messages to the Console application (Console.app) and Xcode's Console. The DDFileLogger class takes care of writing log messages to a file on disk.
1
2
3
4
5
6
|
#import "MTAppDelegate.h"
#import "DDASLLogger.h"
#import "DDTTYLogger.h"
#import "DDFileLogger.h"
#import "MTViewController.h"
|
In the application delegate's application:didFinishLaunchingWithOptions: method, we add two loggers as shown below. Both DDASLLogger and DDTTYLogger are singletons as you may have noticed. With this setup, we mimic the behavior of the NSLog function, that is, log messages are sent to the Console application (Console.app) and Xcode's Console.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
|
- (BOOL)application:(UIApplication*)applicationdidFinishLaunchingWithOptions:(NSDictionary*)launchOptions {
// Configure CocoaLumberjack
[DDLogaddLogger:[DDASLLoggersharedInstance]];
[DDLogaddLogger:[DDTTYLoggersharedInstance]];
// Initialize View Controller
self.viewController= [[MTViewControlleralloc]initWithNibName:@"MTViewController"bundle:nil];
// Initialize Window
self.window= [[UIWindowalloc]initWithFrame:[[UIScreenmainScreen]bounds]];
// Configure Window
[self.windowsetRootViewController:self.viewController];
[self.windowmakeKeyAndVisible];
returnYES;
}
|
This is all that we have to do to get started with CocoaLumberjack. You can test this out by adding the following log statements to the viewDidLoad method of the MTViewController class. Build and run the project in the iOS Simulator to see if everything works as expected.
1
2
3
4
5
6
7
8
|
- (void)viewDidLoad {
[superviewDidLoad];
DDLogError(@"This is an error.");
DDLogWarn(@"This is a warning.");
DDLogInfo(@"This is just a message.");
DDLogVerbose(@"This is a verbose message.");
}
|
Did you also run into a compiler error? The compiler error reads Use of undeclared identifier 'ddLogLevel'. It seems that we need to declare ddLogLevel before we can make use of CocoaLumberjack. This is actually a feature of CocoaLumberjack. By declaring and dynamically assigning a value to ddLogLevel we can configure CocoaLumberjack in such a way that log statements are executed based on the build configuration. To understand what I mean, amend the precompiled header file of our project (Logging-Prefix.pch) as shown below.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
|
#import <Availability.h>
#ifndef __IPHONE_4_0
#warning "This project uses features only available in iOS SDK 4.0 and later."
#endif
#ifdef __OBJC__
#import <UIKit/UIKit.h>
#import <Foundation/Foundation.h>
#import "DDLog.h"
#endif
#ifdef DEBUG
staticconstintddLogLevel =LOG_LEVEL_VERBOSE;
#else
staticconstintddLogLevel =LOG_LEVEL_ERROR;
#endif
|
By default, CocoaLumberjack defines four log levels, (1) error, (2) warning, (3) info, and (4) verbose. Defining log levels is very common in logging libraries (e.g., log4j and log4php). By assigning a log level to a log statement, it can be categorized, which is very useful as you will see in a moment. In the precompiled header file, we declare ddLogLevel and assign a value to it. The value of ddLogLevel determines which log statements are executed and which are ignored. In other words, if the build configuration is equal to Debug(read: if the preprocessor macro DEBUG is defined), then ddLogLevel is equal to LOG_LEVEL_VERBOSE, the highest log level. This means that every log statement will be executed. However, if the build configuration is not equal to Debug, then only log statements with a log level of error are executed. It is important to know that the log levels are ordered as you can see in DDLog.h where they are defined.
Why is this useful? This provides a very easy mechanism to control what is being logged based on the build configuration. You can try this out by changing the current active scheme in Xcode. Stop the application and click the active scheme named Logging on the right of the stop button (figure 3). Select Edit Scheme... from the menu and click Run Logging on the left (figure 4). Under the Info tab, set the Build Configuration to Release (figure 4). With this option, you select the build configuration that Xcode should use when the application runs in the iOS Simulator.
![CocoaLumberjack: Logging on Steroids: Selecting the Active Scheme in Xcode - Figure 3](http://static.oschina.net/uploads/img/201604/05184842_Gl3h.png)
![CocoaLumberjack: Logging on Steroids: Editing the Active Scheme in Xcode - Figure 4](http://static.oschina.net/uploads/img/201604/05184843_YeOV.png)
If you now build and run your project in the iOS Simulator, you should only see log statements with a log level of error printed to Xcode's Console. All log statements with a log level higher than error are ignored. Keep in mind that the DEBUG preprocessor macro is named CONFIGURATION_DEBUG in Xcode 3. You can read more about this on the CocoaLumberjack's Wiki.
Step 3: Logging to a File
Logging to a file is a piece of cake with CocoaLumberjack. Not only is it easy to set up, CocoaLumberjack comes with a number of useful options, such as limiting the file size of log files and setting a rolling frequency. You can even tell CocoaLumberjack to remove old log files as new log files are created. Let me show you how this works.
Revisit the application delegate's application:didFinishLaunchingWithOptions: method and update its implementation as shown below. After initializing an instance of DDFileLogger, we configure it by (1) setting the maximum file size of each log file (in bytes), (2) setting the rolling frequency to 24 hours, and (3) setting the maximum number of log files that should be kept to seven. Don't forget to add the file logger as we did earlier.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
- (BOOL)application:(UIApplication*)applicationdidFinishLaunchingWithOptions:(NSDictionary*)launchOptions {
// Configure CocoaLumberjack
[DDLogaddLogger:[DDASLLoggersharedInstance]];
[DDLogaddLogger:[DDTTYLoggersharedInstance]];
// Initialize File Logger
DDFileLogger*fileLogger = [[DDFileLoggeralloc]init];
// Configure File Logger
[fileLoggersetMaximumFileSize:(1024*1024)];
[fileLoggersetRollingFrequency:(3600.0*24.0)];
[[fileLoggerlogFileManager]setMaximumNumberOfLogFiles:7];
[DDLogaddLogger:fileLogger];
// Initialize View Controller
self.viewController= [[MTViewControlleralloc]initWithNibName:@"MTViewController"bundle:nil];
// Initialize Window
self.window= [[UIWindowalloc]initWithFrame:[[UIScreenmainScreen]bounds]];
// Configure Window
[self.windowsetRootViewController:self.viewController];
[self.windowmakeKeyAndVisible];
returnYES;
}
|
Before you build and run the project, open the Finder and browse to the following location, ~/Library/Application Support/iPhone Simulator//Applications//Library/Caches/. As you can see, the path might be slightly different depending on which version of the iOS Simulator you are using. Run the application in the iOS Simulator and inspect the contents of the Caches directory. It should now have a folder named Logs containing one text file named log-XXXXXX.txt. The last six characters of the file name are unique to prevent log files from being overwritten. It is possible to specify the location where the log files are stored. Keep in mind that he Caches directory can be emptied by the operating system at any time. If you want to store your application's log files in a safer location, then I suggest storing them in the application's Documents directory.
Bonus: Colors
Even though colors seem like nothing more than eye candy, every developer knows how important colors are when working in a code editor. With CocoaLumberjack, you can add color to Xcode's Console. Robbie Hanson, the creator of CocoaLumberjack, also contributed to an Xcode plugin named Xcode Colors. CocoaLumberjack works very well with Xcode Colors. Download the latest version of Xcode Colors, extract the archive, and put its contents in Xcode's plug-ins folder (located at ~/Library/Application Support/Developer/Shared/Xcode/Plug-ins/), and restart Xcode. Note that it might be necessary to manually create the plug-ins folder if it isn't present.
To enable colors in Xcode's Console, head back to the application:didFinishLaunchingWithOptions:method and tell the shared instance of the TTYLogger class to enable colors (see below). CocoaLumberjack uses default colors if you don't specify a color for a specific log level. Overriding the default color settings is easy as shown below. Run the application in the iOS Simulator and inspect Xcode's Console window to see the result (figure 5).
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
- (BOOL)application:(UIApplication*)applicationdidFinishLaunchingWithOptions:(NSDictionary*)launchOptions {
// Configure CocoaLumberjack
[DDLogaddLogger:[DDASLLoggersharedInstance]];
[DDLogaddLogger:[DDTTYLoggersharedInstance]];
// Enable Colors
[[DDTTYLoggersharedInstance]setColorsEnabled:YES];
[[DDTTYLoggersharedInstance]setForegroundColor:[UIColorgreenColor]backgroundColor:nilforFlag:LOG_FLAG_INFO];
// Initialize File Logger
DDFileLogger*fileLogger = [[DDFileLoggeralloc]init];
// Configure File Logger
[fileLoggersetMaximumFileSize:(1024*1024)];
[fileLoggersetRollingFrequency:(3600.0*24.0)];
[[fileLoggerlogFileManager]setMaximumNumberOfLogFiles:7];
[DDLogaddLogger:fileLogger];
// Initialize View Controller
self.viewController= [[MTViewControlleralloc]initWithNibName:@"MTViewController"bundle:nil];
// Initialize Window
self.window= [[UIWindowalloc]initWithFrame:[[UIScreenmainScreen]bounds]];
// Configure Window
[self.windowsetRootViewController:self.viewController];
[self.windowmakeKeyAndVisible];
returnYES;
}
|
![CocoaLumberjack: Logging on Steroids: Adding Color to Xcode's Console - Figure 5](http://static.oschina.net/uploads/img/201604/05184844_UkZq.png)
I already mentioned that CocoaLumberjack defines four log levels by default. It is possible, however, to define custom log levels. I won't discuss custom log levels in this tutorial, but if you want to know more about this feature, then I suggest that you read the article about custom log levels on CocoaLumberjack's Wiki.
Combining colors with custom log levels results in a very powerful tool to collect data and debug an application. Keep in mind that CocoaLumberjack has a lot more to offer than what I have shown in this short tutorial. With CocoaLumberjack, you can create custom loggers as well as custom formatters. Custom loggers are especially useful if you want to log to a database or send log files to a remote server at regular time intervals. CocoaLumberjack really is a powerful library that has become an indispensable tool in my toolbox.
Conclusion
Logging application data and diagnostic information to the console or a file can be very useful when debugging problems both during development and production. Having a solid logging solution in place is therefore essential. Along with many other developers, I have created custom logging solutions for many projects, but CocoaLumberjack is an ideal replacement and it has a lot more to offer.