Photo by Andrew Neel on Unsplash

Flexible logging strategy for iOS app

Bogusław Parol
Schibsted Tech Polska
4 min readSep 15, 2019

--

One of the most important thing that helps to reach high level of app quality is getting as much information from our apps users as it’s possible.

There is a bunch of great logging libraries out there, every one of them has some pros and cons. There is also os.log provided by Apple, that lets us to read logs in very good manner using Console application or stream it directly to Terminal. You can find tons of great articles describing how to use all of them, so I will not focus on that in this post.

Instead, I would like to share my way of providing flexible logging to different target systems using different levels . So, let’s get it started.

TLDR; use dependency injection and build a facade-like function to dispatch log message to different targets. 😎

First of all we need to think about which target systems we will support. Even though we want to build flexible, target-independent logging experience, it’s good to think for a moment what kind of information every possible target will need. In my projects usually I support:

  • logging to XCode console that helps me resolving issues while debugging
  • logging to os.log library that lets me and any other interested team member (i.e. tester) to read logs directly from Console app
  • logging to Crashlytics or similar QA cloud platform

Actually, first two are interconnected — logging using os.log will cause your log messages to appear in XCode console as well.

os.log uses concept of subsystems and categories, both help us to organize our logs and read and filter them efficiently. We can borrow concept of categories from Apple and use it in our solution. Let’s assume for now that we will use three categories:

  • general — a default category used if we don’t want to use any specific category,
  • viewCycle — for logging events like loading new screen or removing one
  • memoryManagement — used for logging creation and destruction of important objects (this category is very helpful if we want to detect memory related issues).

Add as many categories as you need, i.e. networking seems to me as a good candidate. Given that, we can create enum that will store our categories:

Next, we need to define log levels according to our needs. The most common levels used by different platforms are: debug, info, warning, and error. We can extend this set with fault level, which is used by Apple for system-level or multi-process errors only:

We used Int based enum that can be compared in order to manage log level efficiently. Next, we can define protocol LogHandler, that different target systems will implement:

Now, we need a mechanism for dispatching log messages to particular targets. We use dependency injection for defining handlers list, so we can write tests for the logger further.

Thanks to filter{$0.supportedLevel ≥ level} line and the way how we defined our LogLevel enum, logs will be delivered to appropriate handlers. For instance, if we want to log message with info level that has rawValue equal 3, only handlers with supported log level that have rawValue ≥ 3 will print that message (info, debug, verbose).

Let’s now try to implement handler for logging to os.log library. The logging is implemented by OSLog class there. To use the most of its capabilities, we can use our categories. Firstly we need to create OSLog instance for every category that we have. Convenient way for doing this is extending the class. Also, we would need to map our custom LogLevel to Apple’s OSLogType:

Having this, creating log handler for os.log is trivial:

Of course we don’t need to add all available arguments to the format string like I did above, however, because the same message will be printed directly to the XCode console, that seem to be less inappropriate.

In order to provide easy usage of our logger and being able to pass implicitly details like filename, function name, line and column further, we can wrap it into a function that defines bunch of default arguments:

Now adding new log handlers for any other targets is super easy. We just need to inject appropriate handlers to our Logger somewhere at the start of the app 🎉

If this solution looks interesting to you, you can check the demo project available at Github.

Please let me if you have any comments or suggestions, just send me an email at boguslaw.parol@gmail.com

Thanks for reading! 🙇 🙂

--

--