3
\$\begingroup\$

I am writing a logging framework for iOS. The idea is to create a behavior similar to NSLog to print the log message and some metadata to the console.

The following struct shall hold the data of a log entry:

public enum LogLevel: Int {

    case highlight = 4
    case debug = 3
    case info = 2
    case warning = 1
    case error = 0

}

public struct LogEntry {

    let date: Date
    let file: String
    let function: String
    let line: Int
    let logLevel: LogLevel
    let text: String

    var metaText: String {
        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = "HH:mm:ss"
        return "\(dateFormatter.string(from: self.date)): \((self.file as NSString).lastPathComponent):\(self.line) \(self.function): "
    }

}

The struct can then be printed with the following function (simplified):

public class QLog {

    static func log(_ logEntry: LogEntry) {
        print("\(logEntry.metaText)\(logEntry.text)")
    }

}

Of course I want to log from any point in my code like QLogDebug("Test"). So, I added the following global functions (I got the idea from https://github.com/goktugyil/QorumLogs):

public func QLogHighlight<T>(date: Date = Date(), file: String = #file, function: String = #function, line: Int = #line, _ object: T) {
    QLog.log(LogEntry(date: date, file: file, function: function, line: line, logLevel: LogLevel.highlight, text: "\(object)"))
}

public func QLogDebug<T>(date: Date = Date(), file: String = #file, function: String = #function, line: Int = #line, _ object: T) {
    QLog.log(LogEntry(date: date, file: file, function: function, line: line, logLevel: LogLevel.debug, text: "\(object)"))
}

public func QLogInfo<T>(date: Date = Date(), file: String = #file, function: String = #function, line: Int = #line, _ object: T) {
    QLog.log(LogEntry(date: date, file: file, function: function, line: line, logLevel: LogLevel.info, text: "\(object)"))
}

public func QLogWarning<T>(date: Date = Date(), file: String = #file, function: String = #function, line: Int = #line, _ object: T) {
    QLog.log(LogEntry(date: date, file: file, function: function, line: line, logLevel: LogLevel.warning, text: "\(object)"))
}

public func QLogError<T>(date: Date = Date(), file: String = #file, function: String = #function, line: Int = #line, _ object: T) {
    QLog.log(LogEntry(date: date, file: file, function: function, line: line, logLevel: LogLevel.error, text: "\(object)"))
}

Now, this is a bit ugly because the function signatures are very long as well as the initialization of the LogEntry object. Also, all five functions are the same except the function name and the log level.

So, are there any possibilities to shorten / simplify this code? Can I somehow shorten the signatures? Can I define the functions without copy-pasting them?

\$\endgroup\$
2
\$\begingroup\$

All your logging function take a Date as the first argument, with a default value of the current date. Unless there is a special reason why a caller should pass a custom date, you can remove that argument and compute the date inside the logging function:

public func qLogHighlight<T>(file: String = #file, function: String = #function,
                             line: Int = #line, _ object: T) {
    QLog.log(LogEntry(date: Date(), file: file, function: function, line: line,
                      logLevel: .highlight, text: "\(object)"))
}

or even give the LogEntry constructor a default value for the date parameter, so that you can omit it here:

public func qLogHighlight<T>(file: String = #file, function: String = #function,
                             line: Int = #line, _ object: T) {
    QLog.log(LogEntry(file: file, function: function, line: line,
                      logLevel: .highlight, text: "\(object)"))
}

Note also that

  • function names should start with a lowercase letter (compare General Conventions in the Swift API Design Guidelines),
  • it suffices to pass .highlight to the QLog.log() call instead of LogLevel.hightlight, the type is automatically inferred from the context.

If you want five separate logging functions, one for each level, then I do not see more room for shortening. An alternative would be to define just one logging function which takes the log level as an argument:

public func qLog<T>(level: LogLevel, file: String = #file, function: String = #function,
                    line: Int = #line, _ object: T) {
    QLog.log(LogEntry(date: Date(), file: file, function: function, line: line,
                      logLevel: level, text: "\(object)"))
}

This would remove to code duplication completely.

Some more thoughts:

Creating a date formatter is “expensive,” it is better to create it once and reuse it (compare e.g. the “Re-Using Formatter Instances” section in NSFormatter on NSHipster.

This can be done with a static property (which is computed lazily and only once):

public struct LogEntry {

    static var formatter: DateFormatter = {
        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = "HH:mm:ss"
        return dateFormatter
    }()

    ...

}

Instead of bridging to NSString one can use

URL(fileURLWithPath: file).lastPathComponent

to get the last path component of a file path.

Apart from special cases (such as escaping closures), properties can be accessed without using self:

var metaText: String {
    return "\(LogEntry.formatter.string(from: date)): \(URL(fileURLWithPath: file).lastPathComponent):\(line) \(function): "
}
\$\endgroup\$
  • \$\begingroup\$ Thank you very much for this detailed analysis. I totally forgot to remove Date() from the function signature and to shorten the enums. I did not know that creating formatters is so expensive, I will change that immediately. Is it really better to create an URL object from a file path to get the last component? I would have thought that this is more expensive than casting the String to NSString. Regarding the uppercase function names: I wanted to use the same style as NSLog, also starting with an uppercase letter. \$\endgroup\$ – sundance Mar 4 '18 at 15:48
  • \$\begingroup\$ @sundance: I did not test the efficiency of URL vs NSString, I just wanted to mention an alternative. Generally, the Apple APIs move to using URLs instead of strings for handling file paths, for example, Process.currentDirectoryPath has been deprecated in favor of currentDirectoryURL. The fastest way (if that is relevant here) would probably be to find the last dot and print the SubString following it. \$\endgroup\$ – Martin R Mar 4 '18 at 15:57

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.