Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@Ponyboy47
Created September 10, 2018 22:15
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Ponyboy47/f8213da3018fac99daf3d27d67f36903 to your computer and use it in GitHub Desktop.
Save Ponyboy47/f8213da3018fac99daf3d27d67f36903 to your computer and use it in GitHub Desktop.
Proof of Concept of a Swift Logger
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)
}
}
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