Types of logging in Swift

Hello! My name is Vladislav Danielyan, I am an iOS developer in AGIMA. I suggest we talk a little about prints. This is one of the first and most used functions, with which the first steps in development begin for any beginner:

print("Hello world")

The purpose of the article is to save the time of novice developers, to protect them from an endless stream of incomprehensible messages in the console and from the nerves spent searching for “that very” line that explains everything. We will analyze the types of prints and write our own small logger, which can be implemented immediately, in parallel with reading.

Logging is an important tool in a developer's arsenal. It helps organize our messages (of which there can be a huge number over time), provides filtering capabilities, and much more. In this article, we'll look at what Apple tools we have at our disposal. Let's start with Print.

Print – the most basic function that outputs text to the Xcode console. It is often used for debugging and does a good job when you have to solve simple problems. But many have encountered a situation where messages in Print become too bulky or when there are too many of them throughout the application. In these cases, the console turns into a solid wall of hard-to-read text.

Therefore, below we will consider alternatives to Print, but first we will understand the varieties of Print itself, see how Print, DebugPrint and Dump differ from each other, talk about their advantages and disadvantages, and also find out what Logger and OSLog are and write their basic implementation for your project.

Print

Let's start with something simple. This is what our function looks like:

func print(
    _ items: Any...,
    separator: String = " ",
    terminator: String = "\n"
)

items — print content: what we want to print.
separator — a separator, inserted between elements.
terminator — will be inserted after the last element from Items.

Print is perhaps the easiest way to display a message. True, not very detailed. Typically used for light debugging (with small objects). It should not remain in the release code.

We won't dwell on it for long. Just look at a small example:

print(1, 2, separator: ", ", terminator: ".")
// 1, 2.

This is how a regular print works. The green numbers are the text we receive from the function.

DebugPrint

func debugPrint(
    _ items: Any...,
    separator: String = " ",
    terminator: String = "\n"
)

Very similar to a regular print, but differs in that it provides additional information about the printed objects:

print(1...5)
// 1...5

debugPrint(1...5)
// ClosedRange(1...5)

let world = "world"

print("hello", world)
// hello world

debugPrint("hello", world)
// "hello" "world"

It is advisable to use DebugPrint in accordance with its name – for debugging. It will show more useful information about what type of objects we are dealing with. Therefore, for the most part, it can become a replacement for regular Print in work. But I recommend removing Print and its alternatives before release.

Dump

func dump<T>(
    _ value: T,
    name: String? = nil,
    indent: Int = 0,
    maxDepth: Int = .max,
    maxItems: Int = .max
) -> T

Another function for printing messages to the console, but this time with significant differences from the previous ones. Let's find out!

For example, we will use a small object Movie:

struct Movie {
    let name: String
    let rating: Double
    let actors: [String]
}

let movie = Movie(
name: "Звездные войны", 
rating: 5.0, 
actors: ["Лиам Нисон", "Натали Портман"]
)

value – the object we want to print.

dump(movie)
// Output:
▿ StudyProject.Movie
  - name: "Звездные войны"
  - rating: 5.0
  ▿ actors: 2 elements
    - "Лиам Нисон"
    - "Натали Портман"

name — the title with which the object will be printed.

dump(movie, name: "Объект")
// Output:
▿ Объект: StudyProject.Movie
  - name: "Звездные войны"
  - rating: 5.0
  ▿ actors: 2 elements
    - "Лиам Нисон"
    - "Натали Портман"

indent — indent: the larger this parameter, the further to the right the message will be displayed.

dump(movie, indent: 10)
// Output:
          ▿ StudyProject.Movie
            - name: "Звездные войны"
            - rating: 5.0
            ▿ actors: 2 elements
              - "Лиам Нисон"
              - "Натали Портман"

maxDepth — depth, reflects how detailed information about the object will be printed.

dump(movie, maxDepth: 1)
// Output:
▿ StudyProject.Movie
  - name: "Звездные войны"
  - rating: 5.0
  ▹ actors: 2 elements

maxItems — the maximum number of elements with a “full” description.

dump(movie, maxItems: 2)
// Output:
▿ StudyProject.Movie
  - name: "Звездные войны"
    (2 more children)

When working with objects and arrays of objects, Dump performs better than Print and DebugPrint. We get a much more visual result, we can influence the form in which the information will be presented, and get rid of unnecessary “noise” in the console.

OSLog

Our most important logging tool is OSLog. Logs come in several levels with a self-explanatory name:

  • default — used if no other specific level is specified;

  • info — useful, but not critically important information will not be displayed in Console.app (more on this later);

  • debug — debugging messages are also not displayed in Console.app;

  • error — for logging errors, it is highlighted in yellow in the Xcode console;

  • fault – also for logging errors, but already critical ones, in Xcode it is highlighted in red.

Let's write a simple logger that you can use in your project right now. In the future, you can use this information as a foundation and expand its capabilities (save logs to a file, measure the speed of function execution, and much more).

// Используем публичную функцию для ведения логов, в зависимости от ваших потребностей, реализовать можно разными способами
public func log(_ items: Any...,
                type: OSLogType = .default,                
                file: String = #file,
                function: String = #function) {
    formLog(items, type: type, file: file, function: function)
}

For this function, we pass the type, filename, and function name. You can customize it however you like. For example, passing the line number or different names for subsystemto make it easier to filter logs by different modules. Please note that in order to save resources, we run logs in a debug environment using #if DEBUG. Let's see how the function is implemented internally:

private func formLog(_ items: [Any],
                     type: OSLogType,
                     file: String,
                     function: String) {
    #if DEBUG
    // Форматируем название файла(можно пропустить или сделать иначе)
    let lastSlashIndex = (file.lastIndex(of: "/") ?? String.Index(utf16Offset: 0, in: file))
    let nextIndex = file.index(after: lastSlashIndex)
    let filename = file.suffix(from: nextIndex).replacingOccurrences(of: ".swift", with: "")
    
    // subsystem, можно передать извне, чтобы отличать логи по модулям
    let subsystemName = "TestModule"
    
    // создаем лог, в качестве категории используем обработанное название файла
    let log = OSLog(subsystem: subsystemName, category: filename)
    
    // формируем сообщение
    let items = items.map {"\($0)"}.joined(separator: ", ")
    let formattedMessage = [function, items].joined(separator: " | ")
    
    os_log("%{public}s", log: log, type: type, formattedMessage)
    #endif
}

So, we added logs to the application and gave them a readable appearance. What's next? Of course you need to read them. We can see the logs in the Xcode console with different colors depending on the type, but we can get the most benefit from them using the native application for MacOS – Console.

Advantages of the application compared to a regular console:

  • the application is still open,

  • saves logs (including after restarting the application),

  • You can set up convenient filter templates in it to quickly find the necessary records.

Let's run the application on the simulator. Let's open the application and see the following picture:

Select our device and click “Start streaming”.

We immediately see a ton of not very informative logs. To find what we need, we will use filtering in the upper right corner. Enter the name of our application, press Enter, select “Library”. Now we will see our own logs.

The search can be customized: add a search for Subsystem to look at messages from a specific module, filter results by type, etc. You can also save the search and customize the displayed fields, for example, remove “process” and add “subsystem”.

We save the search, configure the display of fields and see the following result:

All our logs are now on the screen. We can view, filter, and share them in text form. Another advantage of logging before prints: the logs are not cleared between reboots of your simulator, so you can compare results and track changes consistently, in real time

Logger

A more recent alternative to OSLog is Logger, available with iOS 14.

Logger can be used as an alternative to OSLog. Using Extensions, you can create several loggers responsible for logging different functionality.

import OSLog

extension Logger {
    private static var subsystem = “com.agima.example”

    /// Логи network слоя
    static let network = Logger(subsystem: subsystem, category: "network")

    /// Логи вашего сервиса
    static let yourService = Logger(subsystem: subsystem, category: "your_service")
}

Logger differs from OSLog in details. These include different levels of logging and the ability to configure logs. Let's take it in order.

Logging levels:

  • notice;

  • info;

  • debug;

  • trace;

  • warning;

  • error;

  • fault;

  • critical.

As we can see, there are more levels here than in OSLog, and some of them differ in meaning. In the Xcode console, by clicking on the icon to the right of the “eye” (in the screenshot below), you can display message metadata. Thus our logs will look like this:

In the Console application, they are read identically to the example from OSLog, so there is no need to rebuild.

Another distinctive feature is the ability to configure the privacy of messages. For example:

Logger.network.info("Пользователь \(username, privacy: .private) добавил товар")

Due to the Privacy setting, the Username will be hidden, but you will not see this when using the simulator.

What to end up using

In conclusion, I would like to note that you should not abuse logs in principle. To avoid negative impact on performance, it is better to enable them in the Debug environment or by toggle for a specific user, if such a need arises. It’s also worth being selective about adding them and not logging unnecessarily, in order to avoid unnecessary “noise” and make the debugging process easier.

But if there is still a need, then the choice of tools should be approached taking into account the tasks facing you:

  • Print and its varieties — for debugging in combination with Breakpoints. We don't leave it in the release code. We select the type of print (Dump, Debug, etc.) depending on what exactly we are debugging.

  • OSLog/Logger. We select a framework for logging based on the minimum version of iOS on the project. Logger is available starting from iOS 14. We try to log only when necessary and do not forget to use it only when debugging.

That's all. We looked at the types of prints with their advantages and disadvantages and remembered the types of logging. And with a minimum of effort they made the process of debugging the application easier and more understandable.

This article can be considered a starting point. For further study, I’ll leave the documentation:

Once you're familiar with it, try experimenting on your own. This is a broad topic and is worthy of deeper study.

If you have any questions, ask in the comments. And if you are interested in mobile development news, subscribe to the telegram channel of my colleague Sasha Vorozhishchev.

What else to read

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *