Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 36 additions & 4 deletions Example/Tests/LogTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,55 @@ import XCTest

class LogTests: XCTestCase {

final class Loggers {
static let userInterface = Log("UI", logLevel: .verbose)
static let app = Log("App")
}

class NetworkLog: Log {
static var networkLog = Log("Network")

override class var instance: Log {
return networkLog
}
}

func testLogHandler() {

var loggedStrings: [(level: Log.Level, string: String)] = []
Log.handler = { (level, string) in
Log.globalHandler = { (log, level, string) in
loggedStrings.append((level: level, string: string))
}

Log.logLevel = .warn
var networkStrings: [(level: Log.Level, string: String)] = []
NetworkLog.handler = { (level, string) in
networkStrings.append((level: level, string: string))
}

let localInstance = Log("Local Instance", logLevel: .verbose, useEmoji: true)

Log.logLevel = .info
Log.useEmoji = true
Log.verbose("Ignore me")
Log.error("Don't ignore me!")

NetworkLog.logLevel = .info
NetworkLog.verbose("network verbose")
NetworkLog.debug("network debug")
NetworkLog.info("network info")
NetworkLog.warn("network warning")
NetworkLog.error("network error")

localInstance.warn("Local Instance Warning")
Loggers.userInterface.info("Button Pressed")
Loggers.app.warn("Out of Memory")

// Log messages include the date, which is not conducive to testing,
// so we just check the end of the logged string.
XCTAssertEqual(loggedStrings.count, 1)
XCTAssertEqual(loggedStrings.count, 6)
XCTAssertEqual(loggedStrings[0].level, .error)
XCTAssertTrue(loggedStrings[0].string.hasSuffix("Don't ignore me!"))
XCTAssertTrue(loggedStrings[1].string.hasSuffix("network info"))
XCTAssertEqual(networkStrings.count, 3)
}

}
148 changes: 125 additions & 23 deletions Pod/Classes/Logging/Log.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,26 +7,30 @@
//

import Foundation
import OSLog

/**
* A simple log that outputs to the console via ```print()````
* A simple log that outputs to the console via ```print()```` and also logs to the console with OSLog (if available)
*/
open class Log {

// MARK: Configuration
// MARK: Types

public typealias InstanceLogHandler = ((Level, String) -> Void)
public typealias GlobalLogHandler = ((Log, Level, String) -> Void)

/**
Represents a level of detail to be logged.
*/
public enum Level: Int {
public enum Level: Int, CaseIterable {
case verbose
case debug
case info
case warn
case error
case off

var name: String {
public var name: String {
switch self {
case .verbose: return "Verbose"
case .debug: return "Debug"
Expand All @@ -37,7 +41,7 @@ open class Log {
}
}

var emoji: String {
public var emoji: String {
switch self {
case .verbose: return "📖"
case .debug: return "🐝"
Expand All @@ -49,11 +53,38 @@ open class Log {
}
}

public static private(set) var standard = Log("", logLevel: .off)

/// Static instance used for helper methods.
open class var instance: Log {
return standard
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is possibly still confusing for people using the old

class NetworkLog: Log {} approach, because NetworkLog.instance == Log.instance which means that NetworkLog.logLevel = .off still manipulates Log.logLevel.

I don't know if there's a good alternative approach here because there is no class var storage, so at a certain point you'll always have to reach for a static instance that is shared among all subclasses. So maybe it's best to just not even have any open override points that could potentially point to the same static instance, and just make it extra clear that setting Log.logLevel or NetworkLog.logLevel sets it for all instances and subclasses since they all refer to the same static variable.

Would also maybe suggest naming this singleton shared

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's fair, I was attempting to still allow for a similar class NetworkLog: Log {} approach, but giving a way to override which instance gets used from the static functions. Where I ended up, is a little more verbose.

    class NetworkLog: Log {
        static var networkLog = Log("Network")

        override class var instance: Log {
            return networkLog
        }
    }

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yeah. In that case it would be nice if there was a way to force "holding it properly", and enforce this override for subclasses, but that isn't really possible either

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't found a good way to do that yet :-/


/// Subsystem of the OSLog message when running on iOS 14 or later.
open var subsystem: String {
let bundleName = Bundle.main.object(forInfoDictionaryKey: "CFBundleDisplayName") as? String
return (bundleName ?? "com.rightpoint.swiftilities") + ".log"
Copy link
Author

@jmagnini jmagnini May 26, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

subsystem
A string that identifies the app subsystem in which to record messages. Specify this string using reverse DNS notation — for example, com.yourcompany.subsystem_name. The system uses this information to categorize and filter related log messages, and group related logging settings.

Given the definition from Apple. this seems like it should default to the bundle id of the parent application.

}

/// If this is non-nil, we will call it with the same string that we
/// are going to print to the console. You can use this to pass log
/// messages along to your crash reporter, analytics service, etc.
/// - warning: Be mindful of private user data that might end up in
/// your log statements! Use log levels appropriately
/// to keep private data out of logs that are sent over
/// the Internet.
public static var globalHandler: GlobalLogHandler?

// MARK: Configuration

/// Displayed name of the Log instance, also used as the Category in OSLog messages
public var name: String

/// The log level, defaults to .Off
public static var logLevel: Level = .off
public var logLevel: Level = .off

/// If true, prints emojis to signify log type, defaults to off
public static var useEmoji: Bool = false
public var useEmoji: Bool = false

/// If this is non-nil, we will call it with the same string that we
/// are going to print to the console. You can use this to pass log
Expand All @@ -62,7 +93,15 @@ open class Log {
/// your log statements! Use log levels appropriately
/// to keep private data out of logs that are sent over
/// the Internet.
public static var handler: ((Level, String) -> Void)?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this should be kept around and marked with a deprecated attribute, and shimmed with overridden get {} set {} to the new globalHandler

@available(*, deprecated, renamed: "globalHandler")

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's being kept as an Instance level handler. This allow for more selective handling if there are a bunch of different Log instances floating around.

public var handler: InstanceLogHandler?

// MARK: Initializer

required public init(_ name: String, logLevel: Level = .off, useEmoji: Bool = false) {
self.name = name
self.logLevel = logLevel
self.useEmoji = useEmoji
}

// MARK: Private

Expand All @@ -74,37 +113,100 @@ open class Log {
}()

/// Generic log method
fileprivate static func log<T>(_ object: @autoclosure () -> T, level: Log.Level, _ fileName: String, _ functionName: String, _ line: Int) {
if logLevel.rawValue <= level.rawValue {
let date = Log.dateformatter.string(from: Date())
let components: [String] = fileName.components(separatedBy: "/")
let objectName = components.last ?? "Unknown Object"
let levelString = Log.useEmoji ? level.emoji : "|" + level.name.uppercased() + "|"
let logString = "\(levelString)\(date) \(objectName) \(functionName) line \(line):\n\(object())"
print(logString + "\n")
handler?(level, logString)
internal func log<T>(_ object: @autoclosure () -> T, level: Log.Level, _ fileName: String, _ functionName: String, _ line: Int) {
guard logLevel.rawValue <= level.rawValue else {return}

let date = Log.dateformatter.string(from: Date())
let components: [String] = fileName.components(separatedBy: "/")
let objectName = components.last ?? "Unknown Object"
let levelString = useEmoji ? level.emoji : "|" + level.name.uppercased() + "|"
let logString = "\(levelString)\(date) \(objectName) \(functionName) line \(line):\n\(object())"

if #available(iOS 14.0, *) {
let logger = Logger(subsystem: subsystem, category: name)
let objectString = "\(object())"
let logMessage = "\(objectName) \(functionName) line \(line)"
switch level {
case .verbose:
logger.trace("\(logMessage):\n\(objectString, privacy: .private)")
case .debug:
logger.debug("\(logMessage):\n\(objectString, privacy: .private)")
case .info:
logger.info("\(logMessage):\n\(objectString, privacy: .private)")
case .warn:
logger.warning("\(logMessage):\n\(objectString, privacy: .private)")
case .error:
logger.error("\(logMessage):\n\(objectString, privacy: .private)")
case .off:
break
}
}
else {
let nameString = name.count > 0 ? "[\(name)]]" : ""
print(nameString + logString + "\n")
}
self.handler?(level, logString)
Log.globalHandler?(self, level, logString)
}

// MARK: Log Methods
// MARK: Public

public static func error<T>(_ object: @autoclosure () -> T, _ fileName: String = #file, _ functionName: String = #function, _ line: Int = #line) {
public func error<T>(_ object: @autoclosure () -> T, _ fileName: String = #file, _ functionName: String = #function, _ line: Int = #line) {
log(object(), level:.error, fileName, functionName, line)
}

public static func warn<T>(_ object: @autoclosure () -> T, _ fileName: String = #file, _ functionName: String = #function, _ line: Int = #line) {
public func warn<T>(_ object: @autoclosure () -> T, _ fileName: String = #file, _ functionName: String = #function, _ line: Int = #line) {
log(object(), level:.warn, fileName, functionName, line)
}

public static func info<T>(_ object: @autoclosure () -> T, _ fileName: String = #file, _ functionName: String = #function, _ line: Int = #line) {
public func info<T>(_ object: @autoclosure () -> T, _ fileName: String = #file, _ functionName: String = #function, _ line: Int = #line) {
log(object(), level:.info, fileName, functionName, line)
}

public static func debug<T>(_ object: @autoclosure () -> T, _ fileName: String = #file, _ functionName: String = #function, _ line: Int = #line) {
public func debug<T>(_ object: @autoclosure () -> T, _ fileName: String = #file, _ functionName: String = #function, _ line: Int = #line) {
log(object(), level:.debug, fileName, functionName, line)
}

public static func verbose<T>(_ object: @autoclosure () -> T, _ fileName: String = #file, _ functionName: String = #function, _ line: Int = #line) {
public func verbose<T>(_ object: @autoclosure () -> T, _ fileName: String = #file, _ functionName: String = #function, _ line: Int = #line) {
log(object(), level:.verbose, fileName, functionName, line)
}
}

// MARK: Static Helper Methods
extension Log {

public static var logLevel: Level {
get { instance.logLevel }
set { instance.logLevel = newValue }
}

public static var useEmoji: Bool {
get { instance.useEmoji }
set { instance.useEmoji = newValue }
}

public static var handler: InstanceLogHandler? {
get { instance.handler }
set { instance.handler = newValue }
}
Comment on lines +223 to +226
Copy link
Contributor

@chrisballinger chrisballinger May 26, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh! I see the compatibility shim is down here. Might be good to mark some of these as deprecated/renamed

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we want them deprecated? For apps where a single Log instance is sufficient, I can still see some value keeping these shorthand versions around

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah perhaps not deprecated, but specifically the change related to the renaming of handler and globalHandler maybe. Also this one in particular is a bit confusing because it's an InstanceLogHandler but applied globally

Copy link
Author

@jmagnini jmagnini May 26, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's applied to the shared instance, but not necessarily globally. I agree the name is a little confusing though 🤔
Mainly, this is here for backwards compatibility.


public static func error<T>(_ object: @autoclosure () -> T, _ fileName: String = #file, _ functionName: String = #function, _ line: Int = #line) {
instance.log(object(), level:.error, fileName, functionName, line)
}

public static func warn<T>(_ object: @autoclosure () -> T, _ fileName: String = #file, _ functionName: String = #function, _ line: Int = #line) {
instance.log(object(), level:.warn, fileName, functionName, line)
}

public static func info<T>(_ object: @autoclosure () -> T, _ fileName: String = #file, _ functionName: String = #function, _ line: Int = #line) {
instance.log(object(), level:.info, fileName, functionName, line)
}

public static func debug<T>(_ object: @autoclosure () -> T, _ fileName: String = #file, _ functionName: String = #function, _ line: Int = #line) {
instance.log(object(), level:.debug, fileName, functionName, line)
}

public static func verbose<T>(_ object: @autoclosure () -> T, _ fileName: String = #file, _ functionName: String = #function, _ line: Int = #line) {
instance.log(object(), level:.verbose, fileName, functionName, line)
}
}
28 changes: 26 additions & 2 deletions Pod/Classes/Logging/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,36 @@ Log levels (from highest to lowest priority):
- error
- off

When running on iOS 14 or later, log messages will also be reported to OSLog.

## Advanced functionality

You can include one custom handler that will get called for any string being logged. Assigning another handler will replace the first.
You can include one global custom handler that will get called for any string being logged across all Log instances. Assigning another handler will replace the first.

```swift
Log.globalHandler = { (log, level, string) in
sendToAnalytics((key: level, string: string))
}
```

Log messages may be categorized by defining separate instances of the Log class.

```swift
final class Loggers {
static let userInterface = Log("UI", logLevel: .verbose)
static let app = Log("App")
static let network = Log("Network", logLevel: .error)
}

Loggers.userInterface.info("Button Pressed")
Loggers.network.error("Token Expired")
Loggers.app.warn("Out of Memory")
```

You may also include one custom handler per Log instance. Messages will still be reported to the global handler as well.

```swift
Log.handler = { (level, string) in
Loggers.network.handler = { (level, string) in
sendToAnalytics((key: level, string: string))
}
```
Expand Down