Created
September 10, 2018 22:15
-
-
Save Ponyboy47/f8213da3018fac99daf3d27d67f36903 to your computer and use it in GitHub Desktop.
Proof of Concept of a Swift Logger
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
import Dispatch | |
// Special queue for flushing the logs | |
private let loggingQueue = DispatchQueue(label: "com.logging.queue", qos: .utility) | |
// The application-wide logger object | |
public var logger: Logger = GlobalLogger.global | |
// A global logger which flushes data using the loggingQueue | |
public final class GlobalLogger: LogHandler { | |
public static let global: GlobalLogger = GlobalLogger() | |
// A buffer of logs to flush | |
public private(set) var buffer: [LogMessage] = [] | |
// Whether or not the logger is currently flushing data | |
public private(set) var isFlushing: Bool = false | |
// The handler used to flush the data (should only ever be nil for the GlobalLogger) | |
public private(set) var handler: LogHandler! | |
// The context contains information about the log level and the destination where logs are sent | |
public var context: LogContext = GlobalLogContext() | |
// Adds a log message to the buffer and flushes it if it's not currently flushing logs | |
private func add(message: LogMessage) { | |
buffer.append(message) | |
if !isFlushing { | |
flush() | |
} | |
} | |
// Write an array of items to a single log message using the specified level, separator, and line terminator | |
public func write(_ items: [Any], separator: String = " ", terminator: String = "\n", level: LogLevel) { | |
guard level >= self.level else { return } | |
// A basic log message format, should be more customizable and contain items like the date, process, etc... | |
let messageData = "[\(level)] \(context)\(items.map({ return "\($0)" }).joined(separator: separator))\(terminator)".data(using: .utf8)! | |
let message = LogMessage(data: messageData, destination: context.destination) | |
self.add(message: message) | |
} | |
public func write(message: LogMessage) { | |
self.add(message: message) | |
} | |
// Flushes the buffer of log messages | |
public func flush() { | |
isFlushing = true | |
defer { isFlushing = false } | |
while !buffer.isEmpty { | |
let message = loggingQueue.sync { return buffer.removeFirst() } | |
loggingQueue.async { | |
do { | |
try message.destination.write(data: message.data) | |
} catch { | |
self.buffer.append(message) | |
} | |
} | |
} | |
} | |
} | |
public protocol Logger { | |
var context: LogContext { get set } | |
var handler: LogHandler! { get } | |
func write(_ items: [Any], separator: String, terminator: String, level: LogLevel) | |
} | |
public extension Logger { | |
public static var global: GlobalLogger { return GlobalLogger.global } | |
public var global: GlobalLogger { return Self.global } | |
public var handler: LogHandler! { return Self.global } | |
public var level: LogLevel { | |
get { return context.level } | |
set { context.level = newValue } | |
} | |
public var destination: LogDestination { | |
get { return context.destination } | |
set { context.destination = newValue } | |
} | |
public func write(_ items: [Any], separator: String = " ", terminator: String = "\n", level: LogLevel) { | |
guard level >= self.level else { return } | |
let messageData = "[\(level)] \(context)\(items.map({ return "\($0)" }).joined(separator: separator))\(terminator)".data(using: .utf8)! | |
let message = LogMessage(data: messageData, destination: context.destination) | |
handler.write(message: message) | |
} | |
public func trace(_ items: Any..., separator: String = " ", terminator: String = "\n") { | |
write(items, separator: separator, terminator: terminator, level: .trace) | |
} | |
public func debug(_ items: Any..., separator: String = " ", terminator: String = "\n") { | |
write(items, separator: separator, terminator: terminator, level: .debug) | |
} | |
public func info(_ items: Any..., separator: String = " ", terminator: String = "\n") { | |
write(items, separator: separator, terminator: terminator, level: .info) | |
} | |
public func warn(_ items: Any..., separator: String = " ", terminator: String = "\n") { | |
write(items, separator: separator, terminator: terminator, level: .warn) | |
} | |
public func error(_ items: Any..., separator: String = " ", terminator: String = "\n") { | |
write(items, separator: separator, terminator: terminator, level: .error) | |
} | |
public func fatal(_ items: Any..., separator: String = " ", terminator: String = "\n") { | |
write(items, separator: separator, terminator: terminator, level: .fatal) | |
} | |
} | |
public protocol LogHandler: Logger { | |
// The buffer of logs that need to be flushed | |
var buffer: [LogMessage] { get } | |
// Whether or not the handler is currently flushing the buffer | |
var isFlushing: Bool { get } | |
// These functions should not be called directly | |
func write(message: LogMessage) | |
func flush() | |
} | |
public struct LogMessage { | |
// The data that will be written | |
public let data: Data | |
public let destination: LogDestination | |
} | |
public protocol LogDestination { | |
func write(data: Data) throws | |
} | |
public protocol LogContext: CustomStringConvertible { | |
var destination: LogDestination { get set } | |
var level: LogLevel { get set } | |
} | |
// Log levels determine the severity of a message as well as whether or not it will be | |
// written (based on the context.level of the logger) | |
public struct LogLevel: ExpressibleByIntegerLiteral, Comparable, CustomStringConvertible { | |
public typealias IntegerLiteralType = UInt32 | |
private var rawValue: UInt32 | |
public var description: String { | |
switch self { | |
case .trace: return "trace" | |
case .debug: return "debug" | |
case .info: return "info" | |
case .warn: return "warn" | |
case .error: return "error" | |
case .fatal: return "fatal" | |
default: return "custom(\(rawValue))" | |
} | |
} | |
public static let trace: LogLevel = 0 | |
public static let debug: LogLevel = 64 // 2^6 | |
public static let info: LogLevel = 4096 // 2^12 | |
public static let warn: LogLevel = 524288 // 2^19 | |
public static let error: LogLevel = 33554432 // 2^25 | |
public static let fatal: LogLevel = 4294967295 // 2^32 | |
public static func custom(_ value: UInt32) -> LogLevel { return LogLevel(integerLiteral: value) } | |
public init(integerLiteral value: UInt32) { | |
rawValue = value | |
} | |
public static func == (lhs: LogLevel, rhs: LogLevel) -> Bool { | |
return lhs.rawValue == rhs.rawValue | |
} | |
public static func < (lhs: LogLevel, rhs: LogLevel) -> Bool { | |
return lhs.rawValue < rhs.rawValue | |
} | |
} | |
// The global context writes to stdout and has an empty description | |
public struct GlobalLogContext: LogContext { | |
public var destination: LogDestination = FileLogDestination.stdout | |
public var level: LogLevel = .info | |
public private(set) var description: String = "" | |
public init() {} | |
} | |
// Writing logs to a file | |
public class FileLogDestination: LogDestination { | |
// Need an open file handle for writing Data | |
private var openFileHandle: FileHandle | |
// stdout is technically a file | |
public static let stdout = FileLogDestination(handle: .standardOutput) | |
// Allow specifying a path to open | |
public convenience init(path: URL) throws { | |
self.init(handle: try FileHandle(forWritingTo: path)) | |
} | |
// File descriptor must be opened with write permissions | |
public init(handle: FileHandle) { | |
openFileHandle = handle | |
} | |
public func write(data: Data) { | |
openFileHandle.write(data) | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foudation | |
import XCTest | |
@testable import Logger | |
final class LoggerTests: XCTestCase { | |
func testGlobal() { | |
// Won't be printed | |
logger.trace("Trace world") | |
logger.debug("Debug world") | |
// Will be printed | |
logger.info("Info world") | |
logger.warn("Warn world") | |
logger.error("Error world") | |
logger.fatal("Fatal world") | |
} | |
func testTraceGlobal() { | |
// Won't be printed | |
logger.trace("Trace World 1") | |
logger.level = .trace | |
// Will be printed | |
logger.trace("Trace World 2") | |
logger.level = .info | |
} | |
func testCustomLogger() { | |
struct WebContext: LogContext { | |
var destination: LogDestination = FileLogDestination.stdout | |
var level: LogLevel = .info | |
// Use a calculated var so that each message will have a unique requestID | |
var requestID: UUID { return UUID() } | |
var description: String { | |
return "[\(requestID)]: " | |
} | |
init() {} | |
} | |
struct WebLogger: Logger { | |
var context: LogContext = WebContext() | |
} | |
var webLogger = WebLogger() | |
// Give the web logger a different context level so that we can demonstrate the difference between the global logger | |
webLogger.level = .trace | |
// Logger level should restrict this from being printed | |
logger.global.trace("Trace world") | |
// These should all be printed (and each will have a unique UUID) | |
webLogger.trace("Trace web") | |
webLogger.debug("Debug web") | |
webLogger.info("Info web") | |
webLogger.warn("Warn web") | |
webLogger.error("Error web") | |
webLogger.fatal("Fatal web") | |
} | |
static var allTests = [ | |
("testGlobal", testGlobal), | |
("testTraceGlobal", testTraceGlobal), | |
("testCustomLogger", testCustomLogger), | |
] | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment