Skip to content

Instantly share code, notes, and snippets.

@exorcyze
Created January 4, 2021 16:44
Show Gist options
  • Save exorcyze/57e7fb225daeab83f2d998232b462ac2 to your computer and use it in GitHub Desktop.
Save exorcyze/57e7fb225daeab83f2d998232b462ac2 to your computer and use it in GitHub Desktop.

DebugLog

Simple console debug logging.

This simple library was created to try and address the following concerns:

  • Simple usage - using something like print() is simple enough that many devs will throw them in their code rather than use a "heavier" framework.
  • Enhanced information in the console - File, method, and line number are included in the output automatically, as well as simple time information.
  • Developer control for amount of information that is right for them - simple to change your local file to tailor output toward how you personally develop / debug.

This library was not originally intended to address:

  • Remote logging, or logging to other targets.

Further information on the above items below.

Contents

Basic Usage

let test = "FOO"
log( "TEST: \(test)", type: .base )
// [27:57:604] TestController.testOutput:65 > TEST: FOO

If this is included as a separate module, then you will need to make sure to import DebugLog before usage.

Developer Customizing

One of the purposes was to allow for developers to not always have to worry about their debug statements being removed before checking in their work, but rather be able to keep those important to debugging and learning the flow through a section in the code behind a module flag. Then each developer can determine the what information they do or don't want to see in their console. These local changes would just be able to be discarded ( or stashed ) when pulling latest, so they should be easy to set.

A primary method of developer customizing for level of output by module is In DebugFlag:

public struct DebugFlag {
    public static let items: [DebugModule] = [
        DebugModule(name: "Base", icon: "", show: true, type: .base),
        DebugModule(name: "Error", icon: "🔥", show: true, type: .error),
        DebugModule(name: "Network", icon: "", show: true, type: .network),
        DebugModule(name: "AmEx", icon: "", show: false, type: .amex),
    ]
}

If you want to see output related to AmEx, simply set show: true. Conversely, if you don't need to see network calls, you can turn those off easily.

The other way to fine-tune what you see ( and how ) is in DebugFeature. This allows you to do things like get more granular output for some modules ( Eg: Viewing network request headers ), or to change to having the full file path output instead of just the file name:

public struct DebugFeature {
    /// Turn on to force all output to show ( not yet implemented )
    static let showAllOutput = false
    /// Turn on to show the passed log output before the file/line/time information
    static let outputFirst = false
    /// Turn on to show the filePath/fileName instead of just fileName
    static let showFilePath = false
    /// Turn on to show the headers for data requests in the output
    static let requestHeaders = false
}

Adding Modules

If you are working on a new module / section to the code, you just need two to add two things: Update the DebugFilterType enum to have a value for that section ( Eg: case plaid ), and then add to the items array for that module in DebugFlag

public struct DebugFlag {
    public static let items: [DebugModule] = [
        // ... previous items
        DebugModule(name: "Plaid", icon: "", show: false, type: .plaid),
    ]
}

Debug Feature Usage

Debug features are used to enable / disable certain things. These could be things like console output ( Eg: putting user output before file ), or for hiding additional information logging from modules ( Eg: writing out request headers in networking ). The second scenario is shown below.

fileprivate func makeNetworkCall() {
    let method = "GET"
    let shortPath = "/v1/test-call"
    let headers = ["TOKEN": "1774-7772-3345"]
    
    log("\(method) \(shortPath)", type: .network)
    if DebugFeature.requestHeaders {
        log("HEADERS: \(headers)", type: .network)
    }
}

In the above scenario, the request headers will only get logged out if the associated DebugFeature flag has been enabled.

FAQ

Could this be used for more than Xcode console output? Eg: os_log

Absolutely, and there is already basic support implemented for os_log usage in a development context. But remember the primary intent is to provide a quick, simple method for developers to log information for development and testing purposes - with a heavy eye towards "discoverability". That said, a flag for os_log has been added to allow for expanded usage if desired. It would be very little additional work to allow all output through if the target is os_log.

console

Why the focus on simplicity of usage?

I've seen too many examples of robust logging libraries not being used in favor of simple NSLog / print statements because a developer just wants a quick way to try and track things in a section they're working on through several files associated with a feature. The more barriers you put in the way of usage, the less likely it will get used. All the great features in the world don't matter if they are never used.

What is discoverability, and why?

Discoverability is simply the ability to determine the origin of a console statement. Currently, without discoverability provided in the form of File.method:line if you have an excess of console output that buries statements you are trying to use for debugging, you don't know where to look to remove them. Having too much information diluting the output makes the console virtually worthless as a tool if you have several areas to look at.

What about using the Filter function in the console?

This works great in cases where the console output has the full path in it or some other unique, identifying piece of information. However in the absence of that information, or if you need to see output from multiple modules at once ( Eg: Networking, Routes, and Amex ) then it falls a bit short.

Why does every module have an icon?

Only because it allows me to more easily identify console output in the project that did not come from this library. Otherwise I would tend towards only errors having them to really make them stand out.

Why was this not intended for remote logging?

Traditionally there are extensive frameworks available for this that allows for all aspects of application usage to be tracked for a user session, including crashes. If this level is desired, I would propose that there are several existing, mature frameworks and services that will cover that need far better. But I think it could probably be extended a bit more than it currently is if there was a need for it.

Can we make it so all output goes to OSLog when it's enabled?

It would be easy to modify to make sure that all logging statements to got OSLog when it's enabled, regardless of if their respective module is enabled. I would just say we would want to vote on if that should be default or not, or if it should be a debug feature flag, but it seems like a good modification.

Could this also do insert-feature?

The code itself is very simple, and would be trivial to add functionality to. That said, another goal of this library is to provide lean functionality, so I would encourage dilligent assessment for how much any given feature add would increase complexity as weight against actual potential usage.

Can we add bridging so this can be used in Obj-C?

I was considering this - would it be useful? The notable modules I found that had Obj-C still were PayPal Data Collector, mParticle, and AppBoy.

I have something difficult I'm trying to figure out and would like to see the stack at a point

Got you covered:

logStack()

Have a question that should be addressed here?

Please, let me know!

//
// DebugLog.swift
// Created / Copyright © : Mike Johnson, 2016
//
import Foundation
import os
// MARK: - Module Struct
/// Struct used by DebugFlag.items to track modules
public struct DebugModule {
let name: String
let icon: String
let show: Bool
let type: DebugFilterType
}
// MARK: - Debug feature flags
/// These flags are used to conifigure or
/// enable / disable certain features of logging.
public struct DebugFeature {
/// Turn on to force all output to show ( not yet implemented )
static let showAllOutput = false
/// Turn on to use os_log instead of print. This forces extra information on
/// the front of the output, so this is more useful if you're using Console.app
/// instead of the Xcode console.
static let useOSLog = false
/// Turn on to show the passed log output before the file/line/time information
static let outputFirst = false
/// Turn on to show the filePath/fileName instead of just fileName
static let showFilePath = false
}
// MARK: - Debug filter types
/// Specifies the filter type used by modules to determine
/// if it should currently be logged
public enum DebugFilterType {
case base
case error
case network
case networkHeaders
case analytics
case login
case movieList
}
// MARK: - Debug modules
/// Specifies modules to be output for logging.
/// These should usually always default to false when checked
/// in to reduce noise for other developers.
public struct DebugFlag {
public static let items: [DebugModule] = [
DebugModule(name: "Base", icon: "", show: true, type: .base),
DebugModule(name: "Error", icon: "🔥", show: true, type: .error),
DebugModule(name: "Network", icon: "", show: true, type: .network),
DebugModule(name: "Network Headers", icon: "", show: false, type: .networkHeaders),
DebugModule(name: "Analytics", icon: "", show: true, type: .analytics),
DebugModule(name: "Login", icon: "", show: true, type: .login),
DebugModule(name: "Movie List", icon: "", show: true, type: .movieList),
]
}
// MARK: - Logging functions
/// Outputs a log message to the console with the file, function, and line number. Time is output with minute:second:millisecond.
///
/// - Parameters:
/// - msg: String value to be output to the console
/// - file: Leave blank
/// - fnc: Leave blank
/// - line: Leave blank
///
/// - Returns: None
///
/// log( "\(method) : \(path)", type: .network )
/// // [27:57:604] BaseService.request:65 > GET : /api/v5/MyRequest
public func log(_ msg: String, type: DebugFilterType, file: String = #file, fnc: String = #function, line: Int = #line) {
#if DEBUG
struct LogFormatter { static let formatter = DateFormatter() }
// grab our file name and figure out enabled modules this is included in
let filename = (("\(file)" as NSString).lastPathComponent as NSString).deletingPathExtension
let filtered = DebugFlag.items.filter{ $0.show && $0.type == type }
// leave if this file isn't an enabled module
guard filtered.count > 0 else { return }
// deal with formatting all our information for output
let icon = filtered.first?.icon ?? ""
let source = "\(filename).\(fnc)"
let withoutParams = source.components( separatedBy: "(" )[ 0 ]
LogFormatter.formatter.dateFormat = "mm:ss:SSS"
let timestamp = LogFormatter.formatter.string( from: Date() )
let fileOutput = DebugFeature.showFilePath ? file : withoutParams
// Take care of our output based on debug feature flags.
// This could be modified to use other output targets.
var outString = "\(icon)[\(timestamp)] \(fileOutput):\(line) > \(msg)"
if DebugFeature.outputFirst {
outString = "\(icon)\(msg) > \(fileOutput):\(line) [\(timestamp)]"
}
if DebugFeature.useOSLog {
outString = "\(fileOutput):\(line)"
let osLogType = type == .error ? OSLogType.error : OSLogType.default
let osLog = OSLog(subsystem: filename, category: String(describing: type))
// Not using public for msg here since not currently planning on this being
// used for production logs, so output will show <redacted>. But it is
// being used so File.Method:line is public
os_log(osLogType, log: osLog, "%{public}@ > %@", outString, msg)
} else {
print(outString)
}
#endif
}
/// Used to log a stack trace to the console
///
/// logStack()
public func logStack( file: String = #file, fnc: String = #function, line: Int = #line ) {
log( "STACK DUMP:\n\t" + Thread.callStackSymbols.joined(separator: "\n\t"), type: .error, file: file, fnc: fnc, line: line )
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment