Error Handling: Loggings

Error Handling: Loggings

Have you ever been in a situation where your manager approaches you and mentions that there is a critical bug on production, and upon hearing the details, you’ve no idea what’s going on “out there”?

You open up Xcode, try to reproduce the issue, and you can’t seem to find what he’s talking about? You try to trace the code, try to “manually” test harder, but to no avail?

Then it hits ya, you ask a friend, and he asks, “Did you check the logs?”, and you be like…

What?

Logs

Like a dear diary, with their colorful way of expressing how your day went, the same thing in Software Development, the only difference is, they sound robotic, they’re labeled with file names, line numbers, sometimes method names They’re colored somehow, but when the color is red, you know it’s going to be a looong day So why are they important? Well, they are some sorts of hints to when an issue takes place

Story Time!

Like our previous post, the marketing team decided to launch a campaign on Facebook, and suddenly all the products they’re referring to in the ads are not accessible through the ad, you spend some time trying to replicate the issue, but the deeplinking does work on your machine, what seems to be the problem? Turns out Bob, the marketing guy, had a typo in the deeplink URL he used, and that wasn’t obvious when debugging, it took a while figuring this out, but the campaign is saved, and you live to fight another bug, but what would it be like if you had “Logs”?

Another Dimension where you had Logs

So, day 1 in the campaign, Bob presses the confirm button, users start to interact with the ad, and 1 day later, the issue is later reported to your manager, and you learn of it As a smart engineer, you already had logs installed, you find a bunch of red-colored messages saying… coudln't unwrap yourapp:/product_details?id=123 as a URL for handling it. and you managed to discover that there is an issue with the deeplink you’re receiving from the marketing guy, you ask them to edit it to the correct format, and the day is saved

Dr. Strange

Back to your own dimension after work, you went to do your laundry and you met Dr. Strange there

Turns out, all that multiverse madness didn’t give him enough time to do his laundry and now he have a chance to, you both strike a conversation and somehow, you got to let him show you how did you do in other realities, you find that in one reality you had logs installed and that helped ya solve the bug quickly without much stress that you had today, now you want to install one too, and that’s what we will do today!

Time to add a diary system

So, let’s first define some structure and focus on a system we already have in place, the print statement, it would be nice if we could add more info to it, like severity, its place in the code, maybe some emojis since the console doesn’t support colors, so let’s define one

public final class SystemLogger {
    public static let main: SystemLogger = .init()
    private init() {}

    public func info(_ message: String,
                               metaData: [String: Encodable]? = nil,
                               file: String = #file,
                               function: String = #function,
                               line: UInt = #line) {
        #if DEBUG
            print(message.withPrefix("ℹ️ "), "Details: \(file), \(function), \(line)")
        #endif
    }

    ...
}

Well, now we have more control, and it makes sense that the logger be accessible through a singleton, but only problem is, we only have access to those logs during development 🤔 Turns out we can add another Logger that we can utilize, like SwiftyBeaver, so we go on, and replicate what we did…

public final class SwiftyBeaverLogger {
    public static let main: SwiftyBeaverLogger = .init()
    private init() {}

    public func info(_ message: String,
                               metaData: [String: Encodable]? = nil,
                               file: String = #file,
                               function: String = #function,
                               line: UInt = #line) {
        ...
    }

    ...
}

During working with our two new loggers, you found that you wanted to log to both at the same time, after putting in some thoughts, you noticed that both implementations are too close to Strategy pattern, where they just differ in how they do their work, so you decide to add a protocol, to be later used inside a Manager class where you call it and the manager invokes all the strategies (Loggers)

public protocol LogEngine {
    func info(message: String,
              metaData: [String: Encodable]?,
              file: String = #file,
              function: String = #function,
              line: UInt = #line)
    func warn(message: String,
              metaData: [String: Encodable]?,
              file: String = #file,
              function: String = #function,
              line: UInt = #line)
    func error(message: String,
              metaData: [String: Encodable]?,
              file: String = #file,
              function: String = #function,
              line: UInt = #line)
}

You conform the Loggers to the LogEngine protocol

public final class SystemLogger: LogEngine { ... }
public final class SwiftyBeaverLogger: LogEngine { ... }
And add the `LoggersManager`
public enum LoggersManager {
    private static var engines: [LogEngine] {
        [SystemLogger.main,
        SwiftyBeaverLogger.main]
    }

    public static func info(message: String,
              metaData: [String: Encodable]?,
              file: String = #file,
              function: String = #function,
              line: UInt = #line) {
        engines.forEach { 
            $0.info(
                message: message,
                  metaData: metaData,
                  file: file,
                  function: function,
                  line: line)
 }
    }

    public static func warn(message: String,
              metaData: [String: Encodable]?,
              file: String = #file,
              function: String = #function,
              line: UInt = #line) {
        engines.forEach { 
            $0.warn(
                message: message,
                  metaData: metaData,
                  file: file,
                  function: function,
                  line: line)
         }
    }

    public static func error(message: String,
              metaData: [String: Encodable]?,
              file: String = #file,
              function: String = #function,
              line: UInt = #line) {
        engines.forEach { 
            $0.error(
                message: message,
                  metaData: metaData,
                  file: file,
                  function: function,
                  line: line)
        }
    }
}

So back to our previous example, we can now do some edits

func handle(deeplink: String) {
  guard let deeplinkUrl = URL(string: deeplink) else { 
//    debugPrint("coudln't unwrap \(deeplink) as a URL for handling it.")
    LoggersManager.error("coudln't unwrap \(deeplink) as a URL for handling it.")
    return
  }
  ...
}

And now, we have something like this

image.png

Conclusion

You decide to persuade Dr. Strange to watch other dimensions and, actually, you found another one where issues are monitored, and reported way before it may reach the users (or Bob’s mischievous-evilly-cursed-little-fingers), next time we will discuss the importance of having Error Monitoring tools like Sentry, and how you can use them to not just monitor errors and logs, but also gain more insights on the user’s environment which will help in having better Error Handling