Frameworks Need Better Logging
Over the past couple years, I’ve worked with quite a few third-party frameworks. These experiences have highlighted a significant deficiency common in many frameworks; most frameworks suck at logging. More recently, I've had the chance to build a few frameworks from the ground up, so I've used those to explore how logging could be done differently.
There are two common ways to handle logging inside a framework, and both of them stink. The first is “good ol’ fashioned”
print/f(). As a framework does its work, it merrily peppers the console with diagnostic or error information. These messages are usually helpful during development, but this pattern leaves a lot to be desired when customers report bugs and the console messages are absent.
Also Bad: Another Framework
The second is the utilization of a logging framework like CocoaLumberjack. Logging frameworks are great because they offer lots of flexibility, including mechanisms for collecting and sending logs in bug reports. Seems great, right? The framework can push error messages into the log, and my app can bundle those log messages up when it sends in a bug report. Except that this app doesn’t use CocoaLumberjack - it uses NSLogger. What’s worse is that another framework in the app uses GTMLogger. All of the sudden, there are three logging frameworks and they don't work together.
A Simpler Way
Great frameworks should strive for zero dependencies. Since integrating a logging framework into your framework is contrary to that goal, let’s explore a simple approach that gives your framework’s integrators more control of logging in their apps.
The approach below is pretty simple, but after using it a number of times in frameworks I’ve helped build, it has proven quite effective. The example I’ll give is written in Objective-C, but the pattern translates to Swift nicely.
Here are the public parts: a register and unregister function, along with a callback signature. I also prefer to provide basic levels, but try not to get too carried away - keep it simple.
Early in the process lifecycle, the app can register a callback with the framework. This block of code gets called any time the framework produces a logging message. Since a level is passed along, the application can determine how to either map the message to another logging system, or simply discard it.
The internals of the framework's logging are even simpler: a basic C function for producing log messages. This, of course, could be improved to use preprocessor macros to include inline file names, line numbers, and compile-time configuration for log levels. Any time the framework needs to produce a log message, it simply calls the log function:
Here's the basic logging implementation from inside the framework. Obviously, your version may require more safety checking (locks around the callback, etc.) or more sophisticated dispatching to protect certain threads and queues, but this should get you started. Hopefully this gives you some ideas about how you can improve logging in your own frameworks to make integration even easier. If you have questions or want to share some ideas, you can find me on Twitter.
I’m not picking on CocoaLumberjack at all here - I use it in my apps, and you should consider doing so as well. It's a great, battle-tested framework. ↩