Developing a Tiny Logger in Swift

Sauvik Dolui
7 min readMay 12, 2017

--

Basic Usages of the Logger

Debugging an application is always a troublous job for any developer. Time to find out the errors and fix them may be huge depending upon the quality of code base you are working with. The very first thing is that you find the error as quickly as possible. Now suppose you are maintaining a project done by a senior who has recently switched to a new company. As a new bee to the project, one of the options you have is to replicate the error test case and follow the logs on console. This is the easiest way if the developer of that module has written down the error logs on console. Once you find the error message you can start tracing from there.

The Problems

As an iOS developer, I face this promlem a lot. Poor quality of code appears in the following ways

// 1. ELSE not implemented
if my_condition {
do_stuff()
}
// 2. ELSE not implemented when IF LET FAILS
if let sweet_optional = self.sweetOptional else {
do_stuff()
}
// 3. Missing event log when GUARD LET FAILS
guard let sweet_optional = self.sweetOptional else {
return
}
// 4. Missing DEFAULT log when NOT MATCH IN SWITCHswitch validityStatus {
case .valid:
do_valid_stuff()
case .invalid:
do_invalid_stuff()
default:
()
}

From senior developer’s perspective it’s a offense as these example codes are error prone. Only God can help you when you are debugging an error on the last night of your project’s deadline.

The Solution:

Once you start believing that you should handle these scenarios, you might be thinking to rewrite the codes in the following ways using print() or debugPrint()

// 1. ELSE not implemented
if my_condition {
do_stuff()
} else {
print("‼️OMG: my_condition failed")
}
// 2. ELSE not implemented when IF LET FAILS
if let sweet_optional = self.sweetOptional else {
do_stuff()
} else {
print("⚠️OMG: self.sweetOptional is nil")
}
// 3. Missing event log when GUARD LET FAILS
guard let sweet_optional = self.sweetOptional else {
debugPrint("⚠️OMG: self.sweetOptional is nil")
return
}
// 4. Missing DEFAULT log when NO MATCH IN SWITCH
switch validityStatus {
case .valid:
do_valid_stuff()
case .invalid:
do_invalid_stuff()
default:
debugPrint("🔥OMG: No Case matched")
}

Considering the error handling part, now it looks a little bit nice.

Problem with print() and debugPrint():

You can see I am using print and debugPrint to log the error events which may lead us to some performance issues. print() can slow down the execution as swift compiler does not optimize code in most optimal way. The same is also applicable even when you are using debugPrint().

The DEBUG Compiler Flag:

Using the following syntax we can avoid the issues with the performance.

#if DEBUG
print("print under DEBUG")
debugPrint("debugPrint under DEBUG")
#endif

We Need More:

Well our aim was not just to print our message, we will be printing the message with the following meta data

  1. Type of the message (Error, Debug, Info, Severe Error, Verbose)
  2. The source of the message (File, Line Number, Column Number)
  3. Time of the event.

Special Literal in Swift:

Image Source: https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/Expressions.html

These are special string literals whose values are changed depending upon the position they appear in your source file.

Let’s Start Coding

Let’s try to do some experiments with these special literals

I have added the a print log in a View Controller’s viewDidLoad() function.

override func viewDidLoad() {
super.viewDidLoad()
print(#file)
}
// OUTPUT /Users/sauvikdolui/Documents/iOS/Practice/OpenSource/GitHub/2017/SwiftLogger/SwiftLogger/ViewController.swift

Just have a look at the output from Xcode’s console. See it’s the file path 😀. We can extract the file name easily. Let’s try to see what the other literals do by adding three more print logs.

override func viewDidLoad() {
super.viewDidLoad()
print(#file)
print(#function)
print(#line)
print(#column)

}
// OUTPUT
viewDidLoad()
17
15

See, what we have on console, there are three new information. There we can see

  1. The function name where the #function literal appeared.
  2. The line number on which the #line literal was logged.
  3. Even the column number (I won’t count that 🤒) for the #column literal.

Okay, we have enough information to track a log, the file name, the function, the line number, even the column from where it appeared. So we need to give a big applause to these tiny special string literals👏👏. If we can smartly use them, they can help us to resolve issues.

Let’s wrap them up: The Log Class

Now we are going to create a tiny logger utility class to log debug messages on console. Each message will contain the following peaces of information

  1. Date and time of the log.
  2. Type of the log (This will be interesting 😇).
  3. File name from where the log message appears.
  4. The line number and even the
  5. Column number.

I will love to name the class as Logger.

class Log {
// May I help you?
}

Helpers for the Logger

1. A date formatter and an extension:

The following snippet will help us to extract the date and of the log.

class Log {   // 1. The date formatter
static var dateFormat = "yyyy-MM-dd hh:mm:ssSSS" // Use your own
static var dateFormatter: DateFormatter {
let formatter = DateFormatter()
formatter.dateFormat = dateFormat
formatter.locale = Locale.current
formatter.timeZone = TimeZone.current
return formatter
}

}
// 2. The Date to String extension
extension Date {
func toString() -> String {
return Logger.dateFormatter.string(from: self as Date)
}
}

2. Types of log

We can broadly classify log types into the following categories.

  1. Error
  2. Information
  3. Debug
  4. Verbose
  5. Warning
  6. Severe

The following enum outside side of Logger will help us to trace that.

enum LogEvent: String {
case e = "[‼️]" // error
case i = "[ℹ️]" // info
case d = "[💬]" // debug
case v = "[🔬]" // verbose
case w = "[⚠️]" // warning
case s = "[🔥]" // severe
}
class Log {
// ...
}
// 2. The Date to String extension
extension Date {
//...
}

3. Overriding Swift.print()

Leaving `print()` statements in your production code base may introduce some security vulnerabilities. Xcode 8 had introduced a new type of Swift compilation flag called Active Compilation Conditions, by default which contains a DEBUG flag.

Active Compilation Conditions was introduced with Xcode 8

We can use this complier flag to conditionally compile debug code only. So, we can write our own version of print() in global scope and wrap the Swift.print() within this DEBUG compiler flag. This helps us to remove the risk of loggin some debug info on console by even when we forget to remove them from production codebase.

/// Wrapping Swift.print() within DEBUG flag 
func print(_ object: Any) {
// Only allowing in DEBUG mode
#if DEBUG
Swift.print(object)
#endif

}

4. File Name Extraction

The following function takes the resposibility to extract the file name from the file path.

class Log {   // ...   private class func sourceFileName(filePath: String) -> String {
let components = filePath.components(separatedBy: "/")
return components.isEmpty ? "" : components.last!
}

}

5. The Final Touch

class Log {
// ...
// 1.
class func e( _ object: Any,// 1
filename: String = #file, // 2
line: Int = #line, // 3
column: Int = #column, // 4
funcName: String = #function) {
if isLoggingEnabled {
print("\(Date().toString()) \(LogEvent.e.rawValue)[\(sourceFileName(filePath: filename))]:\(line) \(column) \(funcname) -> \(object)")
}
}
}

Let’s move one by one

  1. object: This will be the debug object which is to be printed on the debug console. See there is not any default value of this parameter. So you will be supplying this object.
  2. fileName: The file name from where the log will appear. We are capturing the default value by using #file. If we do not supply any value, it will simply copy the path of the file from where log() is getting called.
  3. line: The line number of the log message. Default value will simply be the line number from where log() is executed.
  4. column: The same will happen for this parameter too.
  5. funcName: The default value of this parameter is the signature of the function from where the log function is getting called.

So following the same way we can define

// 2.
class func d( _ object: Any,... // for debug
// 3.
class func i( _ object: Any,... // for info
// 4.
class func v( _ object: Any,... // for verbose
// 5.
class func w( _ object: Any,... // for warning
// 6.
class func s( _ object: Any,... // for severe

How to use?

You can simple use it in the following ways

Let’s try to use it in some realtime scenarios

// 1. Handling ELSE
if my_condition {
do_stuff()
} else {
Log.e(message: "my_condition failed") // Error
}
// 2. Handling ELSE when IF LET FAILS
if let sweet_optional = self.sweetOptional else {
do_stuff()
} else {
Log.d(message: "sweetOptional is nil") // Debug
}
// 3. Handling GUARD LET failures
guard let sweet_optional = self.sweetOptional else {
Log.s(message: "sweetOptional is nil") // Severe
fatalError()
}
// 4. Handling DEFAULT case IN SWITCH
switch validityStatus {
case .valid:
do_valid_stuff()
case .invalid:
do_invalid_stuff()
default:
Log.i(message: "No Case matched") // Info
}

Where to go from here?

You will find the full source code of this blog on GitHub. Please don’t forget to recommend or bookmark this blog if you like it 😎.

Promotions:

Don’t forget to read my previous blogs😏.

1. Network reachability status monitoring on iOS

  1. Part 1.
  2. Part 2.

2. A Smart Way to Manage Colour Schemes for iOS Application Development.

3. Handling Fonts in iOS Development, a Simpler Way.

4. Making a Stateful TableView for iOS.

5. A few git tricks & tips.

--

--