Getting Started Quickly With iOS Logging

Previously, we’ve covered how to manage logging for many different languages. We’ve discussed C#, Java, Python, Ruby, Go, JavaScript, PHP, and Swift. And finally, we’ve documented a few libraries and platforms, like Log4J, NodeJS, Spring Boot, and Rails.

Now we’re going to add iOS to the list. Apple’s mobile platform is ubiquitous, with iPhones, iPads, and iPad minis coming in first or second in phone and tablet sales.

I’ll start with an example of logging manually with iOS at first. Then, I’ll go into the details of how and why logging matters. Finally, I’ll move on to Apple’s Unified Logger and how you can use it to watch your application and isolate issues and problems.

The code examples will be in Swift, but you can easily translate the samples to your language if you’re an Objective-C user.

Let’s get to work!

Apple_with_Scalyr_colors_signifying_ios_logging

The Simplest iOS Logging That Could Possibly Work

Create an iOS Project

Let’s start by creating a new project in Xcode. If you don’t already have it installed, go to the App Store and install it from there.

Next, start Xcode and select Create a new Xcode Project from the first dialog window.

I’m going to keep the application simple in this tutorial so we can focus on logging and logging best practices. So select iOS and Single View App and then click Next.

Now you can give your project a name. The values you use for this page aren’t important. Just make sure that Swift is set as the project language.

Now we have a project to work with.

Select ViewController.swift in your IDE.

import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
    }

}

Here you see the Swift method that’s run when your application’s main view loads. Since we selected a single view application, that means that’s the only view.

Log Messages to the Console

Let’s start with the absolute simplest log message there is. Add a single line to viewDidLoad.

override func viewDidLoad() {
    super.viewDidLoad()
    print ("Application view has loaded")
}

Next, run the application. The easiest way is to click the run button in the title bar of the editor.

You’ll see the iPhone simulator start. If it’s the first time you’ve run the app, it might take a few minutes to load.

XCode sends our log message to the console.

Console messages aren’t very useful in a mobile app. You can see them when you’re running your code in the simulator, so they’re useful as quick and dirty messages in the IDE. But the debugger is probably a better option.

So rather than print to the console, we’ll save logging information to a text file.

Log Messages to a File

Swift has several ways to write a string to a file. But since the purpose here is to show the easiest way, we’re going to take advantage of its ability to write to any URL.

    override func viewDidLoad() {
        super.viewDidLoad()
        
        let documents = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]
        let documentsURL = URL(fileURLWithPath: documents)
        let fileURL = documentsURL.appendingPathComponent("application.log")
        
        let text = "View has loaded."
        
        do {
            try text.write(to: fileURL, atomically: false, encoding: .utf8)
        } catch {
            print ("Unexpected error writing to log: : \(error).")
        }
    }

Rerun the application and then look at our improvised log file. Unfortunately, viewing the file is easier said than done. Writing to a file in Swift is easy; managing files in iOS is not. If we were running our sample code on a device, we could find the log file using iTunes. But since we’re using the simulator, we’re going to have to do some spelunking with the Finder.

First, open a folder in Finder. Use Finder’s Go To Folder option.

This opens a folder named Devices. Find the newest folder inside Devices.

This is the filesystem of the last iOS simulator run. Open it, and then expand the Data folder. Next, expand the Application folder inside data. Find the newest application folder.

Finding Your Log File

Open this folder and then expand the Documents folder. There is the log!

The file preview shows us our single log message.

You started this extended process by declaring where your log file will be. As you’ll see below, this is important and something all components need to know—unless there’s a framework that manages log locations for you.

Next, you converted that filename to a URL. All Swift strings can write themselves to a URL, so you’re taking advantage of that. Finally, you created a string and wrote it to the URL.

This works, and you have a semi-permanent record for our log messages. But’s it difficult to manage these logs since iOS doesn’t lend itself to managing files.

You also have to figure out how to log an error when the application can’t log to the text file since the write method throws. It seems like a logging system that’s built into the platform would be a better option than managing the files yourself.

There’s also a potential impact on performance. What’s the cost of having every log message open a URL and write itself? How well will that scale for hundreds or thousands of log messages? Is it thread-safe? What happens when the application wants to log more than one message at the same time?

Now, let’s look at a better system for iOS logging.

What Is Application Logging?

Before we continue, let’s take a quick look at the process of logging and how best to manage it.

I’m sure you’ve already dealt with logging at least once and have an idea of what it is. But it’s still worth agreeing on a definition so we can evaluate a logging framework in terms of what we need it to do. Back in the first article in this series, we defined application logging like this:

Application logging involves recording information about your application’s runtime behavior to a more persistent medium.

Let’s start with that definition.

Logging entails recording runtime behavior. We document application events in a log by adding them to a store, in the order that they happen, with a timestamp.

We store logs in a persistent medium. Application events occur too quickly to follow them as they happen. So we want to be able to go back and review them. Persistent storage makes it possible to track down an error and maintain an audit trail. The medium can be a disk, a relational database, or a search engine.

So that’s what logging is. How can you do iOS logging more effectively, and why would you want to? These are important questions. Let’s get back to work.

Apple’s Unified Logging System

With macOS Sierra and iOS 10, Apple introduced the new unified logging system. While they haven’t deprecated the legacy logging system yet, the new logger is the direction Apple wants us to head in.

The unified logger has built-in security and performance features that make it more desirable than the legacy system. And it also has a quirk that many developers and administrators dislike: it stores messages in memory and a proprietary data store. There are no human-readable log files. You can only read messages with Console.app or the command-line log tool.

Let’s add the unified logger to our application and see how it works.

Let’s start by removing the code for manipulating files and just replacing it with os_log(), the unified logger. We need to import the os package and add the call to os_log.

import UIKit
import os

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        
        let text = "View has loaded."
        os_log(text)
    }
}

This code doesn’t build.

We can only pass a static string to os_log. To do so, we’ll change how our string is defined.

import UIKit
import os

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        
        let text:StaticString = "View has loaded."
        os_log(text)
    }
}

This builds and runs. But where’s the log message?

Apple’s Console Application

Run the Console application. You can find it in Applications/Utilities or by opening Spotlight and searching for it.

When it opens, you’ll see a list of devices on the left-hand side. Select the entry that corresponds to the device you’re using in the simulator.

I’m using the iPhone 8 Plus simulator here.

Next, search for the text View has loaded in the search bar, as shown below.

There’s our log message.

The unified logger takes care of the log file location by storing all logs in the same system and indexing them. We don’t have to worry about where they belong, how to open them, and when to close them.

So what do we need to concern ourselves with when designing log messages for an iOS application?

Why Log?

Is there such a thing as the perfect application? Is it possible to write code that always reacts predictably to unexpected events? Can you design and implement an app that anticipates every customer’s needs?

No.

So you need a way to keep an eye on your code. Despite your best efforts, your code has bugs, and you need a window that lets you see what’s going on in there.

When we’re working on our development systems, we can find and fix problems quickly. You have a debugger, and you have an iOS simulator that lets you emulate every current device. By adding print statements and logs while you code, you can know what your application does while you test. You can watch your application in a perfect walled garden and throw every problem you can think of at it.

Debugging vs. the Real World

But the code is going to head out into the real world. It’s going to encounter things you didn’t think of. An evil user is going to try to twist it into things you didn’t anticipate. It will face conditions you could never imagine. And then, when it finally fails, you’ll need a way to figure out what happened.

This is where logs come in.

You can use logs for things other than finding bugs, too. Even an application that works has opportunities for improvement. You can use the logs to detect patterns of behavior that point to new features. Maybe you’ll find a resource that should be cached in memory instead of retrieved from a remote service for each use. Perhaps you’ll find a difficult to support feature that’s rarely used.

And, of course, the usefulness of logs doesn’t stop there. Sometimes we need an “extra copy” of transactional data, such as credit card charges or other financial transactions.

We need visibility into our application’s runtime behavior, and we get it with logging.

How Should You Log?

We took a quick look at Apple’s Unified Logging System. It fulfills the requirement for persistently storing logs in a central location. We can find them with a text search via the console application or with a command-line tool. But there were a lot of log messages in that Console app. You were able to find what you were looking for because you knew the exact text of the log message. That’s not usually the case.

This problem leads to a more general question: what information should be logged?

Most logging systems include at least the following information in each log entry:

  • Timestamp: When the event described in the log entry happened, the unified logger takes care of this for us.
  • Event context: This is useful information about the event. “It worked” or “It’s broken” might be useful or entertaining during a debugging session. “Failed to connect to database at 192.9.168.3:5000” is more useful in production.
  • Severity level: Logs tend to have a level that puts them in context compared to other entries. The unified logger defines default, info, debug, error, and fault. We’ll take a closer look at the logging levels below.

So the unified logger takes care of two out of three essential aspects of logging for us. It’s up to you to manage the content of our messages effectively.

These are only the basics when it comes to logging best practices. For instance, Scalyr has an article about logging best practices here. There’s also an excellent cheat sheet here.

What Should You Log?

The focus of this tutorial is iOS logging, but we’re using Apple’s Unified Logger. It supports a variety of platforms and application types. Useful log messages for a tablet or phone application aren’t necessarily the same as those for laptops or servers.

For example, a mobile game developer is interested in troubleshooting round-trip message latency and rendering performance. A server application is interested in storage space and processing resources. So, before you release your application, take some time to think about what messages are going to be useful.

Customizing the Unified Logging System

Adding a Log Level

So let’s use these principles to make our log messages more effective. You can already log information to the unified logger with a single line of code. What more can you do?

First, we’ll add context to our messages with log levels. Change the call to os_log by adding a few more arguments, and update the log message text to suit it.

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        
        let text:StaticString = "THIS IS AN ERROR."
        os_log(text, log: OSLog.default, type: .error)
    }
}

Build and run and take a look at the console application.

The logger tagged the message as an error for us because you added the type argument to the call. Let’s take a brief look at the unified logger’s logging levels.

Supported Logging Levels

The second argument to os_log was an OSLogType. This argument tags the message with the corresponding level. We used .error, but the unified logger also supports .info, .debug, or .fault. If you omit the type argument, the logger uses the default level, notice, which doesn’t provide any context at all. So, it’s best to explicitly specify a level.

Here’s a list of levels supported by the unified logger.

  • default (notice): The default level. This level provides no additional context and should be avoided.
  • info: Informational messages that don’t indicate an error or failure.
  • debug: Debug-level messages intended for use in a development environment.
  • error: Error messages for reporting errors and failures.
  • fault: Fatal messages that indicate a serious problem.

Log Instances

You called os_log with a single argument to log messages in the first exercise. When you call it with just a message, it uses a default logger instance. But when you call it with an instance and a log type, you’re calling a different underlying function.

Let’s take a closer look at our log message in the console application. Expand the details at the bottom of the console application.

Since you used the default logger instance, the details are sparse. The message lacks subsystem and category information.

To populate these fields, you need a logger instance that’s initialized with information about the application.

override func viewDidLoad() {
    super.viewDidLoad()
        
    let log = OSLog.init(subsystem: "com.scalyr.iOS-Logger", category: "Logging Tutorial")
        
    let text:StaticString = "THIS IS AN ERROR."
    os_log(text, log: log, type: .error)
}

You’re creating a logger with OSLog.init. The name of the subsystem is the bundle identifier. The category is an arbitrary name for grouping log messages.

Next, you need to pass the logger instance to os_log instead of the default instance you used earlier.

Run the code, and you’ll find a log message, set to the error level, in the console app. Click on it to see the details.

Now, send the log message as a fault.

os_log(text, log: log, type: .fault)

Run this version, and check the console application.

You can see the details and the fault log level.

Log Contents

You can set up a log with a subsystem and category, and you can control the level of each message as you send it. Finally, the last thing to look at is the log message contents.

Even though you’re limited to static strings in the log message, you can still use C-style formatting with Swift strings.

So you can use the log strings as a formatter and pass in non-static values as arguments.

Let’s give one a try.

let message = "Hello, World!"
os_log("This is my log message: %@", log: log, type: .error, message)

Run this from Xcode, and you’ll see the log message.

You can log Swift’s scalar values too.

let message = 42 os_log("The answer is: %d", log: log, type: .fault, message)

But what we see from inside Xcode isn’t always what happens in production. The unified logger has built-in protection for values that are classified as secrets.

Redacting Secrets

Let’s change the code to print a string in the log message.

let message = "a secret"
os_log("The answer is: %@", log: log, type: .fault, message)    }

Run this in the simulator, and you’ll see “The answer is a secret” in the console application.

Now, build the application and run it on a phone or tablet. You’ll need to connect a device, deploy the app on it, and start it there, not from inside Xcode.

The unified logger redacts the string if the application runs outside of a debugger. This is the default behavior for string messages. It isn’t for scalar values.

If you don’t want a string to be redacted, use the {public} notation.

os_log("The answer is: %{public}@", log: log, type: .error, message)

If you want a scalar value redacted, use the {private} notation.

os_log("The answer is: %{private}d", log: log, type: .error, 42)

This built-in security feature helps protect critical information. As tempting as it might be to mark everything public, resist the urge.

Advanced iOS Logging

We’ve already covered a quick start with iOS logging, lets talk about an advanced logging concept before we wrap up.

Signposts

Earlier, we mentioned the types of log messages that might be useful for a phone or tablet app. Two of those, round trip messages and rendering are important for responsive applications. Logging how long they take could be very useful for identifying problems and improving performance.

But timing and logs can be tricky. When do you set the timer? Before you log the message or after? How does stopping to log the message affect your timing? Apple built signposts into the unified logger to solve these problems.

With signposts, you use the logging API to set a start and a finish for an operation. So, if you want to log the precise time it takes to request a resource from a server and process the result, you set a signpost, perform the operation, and then log the result.

let log = OSLog.init(subsystem: "com.scalyr.iOS-Logger", category: "Logging Tutorial")  let signpostID = OSSignpostID(log: log) os_signpost( type: .begin, log: log, name: "Request Resource", signpostID: signpostID) requestData();  os_signpost(type: .end,log: log, name: "Recevied Resource", signpostID: signpostID) 

Apple takes care of all the timing issues for you, so your log ends up with accurate and useful timestamps.

What Now?

We’ve covered the hows and the whys of logging on iOS with the unified logger. The unified logger is a comprehensive system that makes logging easy for an application developer, and it’s a better option than trying to roll your own logs with print or write statements.

Even though we covered a lot of ground in this tutorial, there’s a lot more to know about logging. Start here, and don’t forget our cheat sheet.

Apple’s documentation on the unified logger is here for developers.

Scalyr’s log aggregation tools help you aggregate, process, search and visualize your logs. This makes it easier to find what you need in the vast ocean of log entries produced by your growing applications.

So now that you know the fundamentals, get started with logging in your Swift applications today!