Created
December 29, 2022 06:25
-
-
Save JetForMe/6e37527ff184677bfd707aed996cc9d6 to your computer and use it in GitHub Desktop.
DynamicallyConfigurableLogHandler.swift
This file contains 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
// | |
// DynamicallyConfigurableLogHandler.swift | |
// | |
// | |
// Created by Rick Mann on 2022-08-04. | |
// | |
import Logging | |
import Foundation | |
import ConsoleKit | |
import Path | |
/** | |
Swift Logging lacks one of Log4J’s best features: the ability to dynamically | |
turn logging on and off per-logger. Because LogHanlders are structs, this | |
struct references a singleton ``LogHandlerCoordinator`` that manages log message | |
gating. | |
Loggers should be named in reverse-FQDN style (e.g. "com.latencyzero.app.Foo"). | |
Logging is gated in the coordinator by a logging threshold, specified per log name | |
level, up to the coordinator’s root threshold. For example, if the root logging | |
threshold is set to ``.info``, and no other threshold is specifieid, then all log | |
messages of ``.info`` level or higher will be output. | |
If, in addition to the ``.info`` root threshold, a threshold is set on a specific | |
logger name, that threshold will apply to any logger with that name. Note that | |
FQDN-style logger names form a hierarchy, with each dotted component of the name | |
serving as a level in the hierarchy. Thus, a threshold specified for `com.latencyzero` | |
will apply to all loggers whose name begins with `com.latencyzero.`. | |
*/ | |
public | |
struct | |
DynamicallyConfigurableLogHandler: LogHandler | |
{ | |
public | |
init(label inLabel: String) | |
{ | |
self.label = inLabel | |
self.metadata = [:] | |
self.logLevel = .trace | |
} | |
public | |
func | |
log(level: Logger.Level, | |
message: Logger.Message, | |
metadata: Logger.Metadata?, | |
file: String, | |
function: String, | |
line: UInt) | |
{ | |
LogHandlerCoordinator.instance.log(label: self.label, level: level, message: message, metadata: metadata, file: file, function: function, line: line) | |
} | |
public | |
subscript(metadataKey inKey: String) | |
-> Logging.Logger.Metadata.Value? | |
{ | |
get { return self.metadata[inKey] } | |
set { self.metadata[inKey] = newValue } | |
} | |
public let label : String | |
public var metadata : Logger.Metadata | |
public var logLevel : Logger.Level | |
} | |
public | |
class | |
LogHandlerCoordinator | |
{ | |
public static let instance = LogHandlerCoordinator() | |
public | |
init() | |
{ | |
self.df = DateFormatter() | |
self.df.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS" | |
} | |
public | |
func | |
set(rootThreshold inThreshold: Logger.Level) | |
{ | |
self.workQ.async { self.rootThreshold = inThreshold } | |
} | |
public | |
func | |
set(threshold inThreshold: Logger.Level, for inName: String) | |
{ | |
self.workQ.async { self.thresholds[inName] = inThreshold } | |
} | |
public | |
func | |
clearThreshold(for inName: String) | |
{ | |
self.workQ.async { self.thresholds.removeValue(forKey: inName) } | |
} | |
public | |
func | |
log(label: String, | |
level: Logger.Level, | |
message: Logger.Message, | |
metadata: Logger.Metadata?, | |
file: String, | |
function: String, | |
line: UInt) | |
{ | |
let now = Date() | |
self.workQ.async | |
{ | |
self.logInternal(timestamp: now, label: label, level: level, message: message, metadata: metadata, file: file, function: function, line: line) | |
} | |
} | |
func | |
logInternal(timestamp: Date, | |
label inLabel: String, | |
level inLevel: Logger.Level, | |
message: Logger.Message, | |
metadata: Logger.Metadata?, | |
file inFile: String, | |
function: String, | |
line inLine: UInt) | |
{ | |
let threshold = self.threshold(for: inLabel) | |
if inLevel >= threshold | |
{ | |
var ct: ConsoleText = "" | |
// Timestamp… | |
if let ts = self.df.string(for: timestamp) | |
{ | |
ct += ts.consoleText() | |
ct += " " | |
} | |
// Logger… | |
let logger = "\(truncate(fqdn: inLabel, toLength: 25)) " | |
ct += logger.consoleText() | |
// Log level… | |
ct += "[\(inLevel.name.padded(to: 8))] ".consoleText(inLevel.style) | |
// File & line… | |
let file = Self.limit(path: inFile, separator: "/", length: 30).padded(to: 30, justification: .left) | |
let line = "\(inLine)".padded(to: 5, justification: .right) | |
let fl = "(\(file):\(line)) " | |
ct += fl.consoleText(color: .custom(r: 0xcc, g: 0xcc, b: 0xcc)) | |
ct += message.description.consoleText() | |
self.console.output(ct) | |
} | |
} | |
/** | |
Abbreviates and truncates an FQDN-style string to fit in the given | |
length, and pads. | |
*/ | |
func | |
truncate(fqdn inName: String, toLength inLength: Int) | |
-> String | |
{ | |
var name = inName | |
if name.count <= inLength | |
{ | |
return name.padded(to: inLength) | |
} | |
var excess = name.count - inLength | |
let components = name.split(separator: ".") | |
var newComponents = [String]() | |
for comp in components[..<(components.count - 1)] | |
{ | |
if excess > 0 | |
{ | |
newComponents.append(String(comp.first!)) | |
excess -= comp.count - 1 | |
} | |
else | |
{ | |
newComponents.append(String(comp)) | |
} | |
} | |
newComponents.append(String(components.last!)) // Always append the full last component | |
name = newComponents.joined(separator: ".") | |
// Now truncate to max width… | |
if name.count > inLength | |
{ | |
name = "\(name.prefix(inLength - 1))…" | |
} | |
else if name.count < inLength | |
{ | |
name = name.padded(to: inLength) | |
} | |
// while name.count > inLength | |
// { | |
// if let firstDot = name.firstIndex(of: ".") | |
// { | |
// let firstComponent = name[..<firstDot] | |
// if firstComponent.count > 1 | |
// { | |
// name.[..<firstDot] = String(firstComponent.first!) | |
// } | |
// } | |
return name | |
} | |
/** | |
Returns the effective threshold for the specified logger name. | |
TODO: This function should be fast as possible; might be worth | |
finding a way to cache thresholds. | |
*/ | |
func | |
threshold(for inName: String) | |
-> Logger.Level | |
{ | |
var name = inName | |
var threshold = self.thresholds[name] | |
while threshold == nil && name.count > 0 | |
{ | |
if let lastDot = name.lastIndex(of: ".") | |
{ | |
name = String(name[..<lastDot]) | |
threshold = self.thresholds[name] | |
} | |
else | |
{ | |
break | |
} | |
} | |
return threshold ?? self.rootThreshold | |
} | |
/** | |
Abbreviate leading path elements as needed to limit the length. | |
*/ | |
static | |
func | |
limit(path inPath: String, separator inSep: Character = ".", length inLength: Int = 0) | |
-> String | |
{ | |
var comps = inPath.split(separator: inSep) | |
var length = inPath.count | |
for (idx, comp) in comps.enumerated() | |
{ | |
// If the string is too long, shorting path components… | |
if length > inLength | |
{ | |
// If it's not the last component, append the abbreviation and continue… | |
if idx < comps.count - 1 | |
{ | |
comps[idx] = comp.prefix(1) | |
length -= comp.count - 1 | |
} | |
} | |
else | |
{ | |
break | |
} | |
} | |
// If inPath started with the separator, prepend it… | |
var result = "" | |
if inPath.prefix(1) == String(inSep) | |
{ | |
result = String(inSep) | |
} | |
// Build and return the result… | |
result.append(comps.joined(separator: String(inSep))) | |
return result | |
} | |
var workQ = DispatchQueue(label: "LogHandlerCoordinator") | |
var thresholds = [String : Logger.Level]() | |
var rootThreshold : Logger.Level = .trace | |
let console : Console = Terminal() | |
let df : DateFormatter | |
} | |
public | |
extension | |
String | |
{ | |
enum | |
Justification | |
{ | |
case left | |
case right | |
case center | |
} | |
/** | |
*/ | |
func | |
padded(to inLength: Int, justification inJust: Justification = .left) | |
-> String | |
{ | |
guard | |
self.count < inLength | |
else | |
{ | |
return self | |
} | |
let pad = String(repeating: " ", count: inLength - self.count) | |
if inJust == .left | |
{ | |
return self + pad | |
} | |
else if inJust == .right | |
{ | |
return pad + self | |
} | |
else | |
{ | |
let halfPad = pad.count / 2 | |
let leftPad = String(repeating: " ", count: halfPad) | |
let rightPad = String(repeating: " ", count: pad.count - halfPad) | |
return leftPad + self + rightPad | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment