Skip to content

Instantly share code, notes, and snippets.

@JetForMe
Created December 29, 2022 06:25
Show Gist options
  • Save JetForMe/6e37527ff184677bfd707aed996cc9d6 to your computer and use it in GitHub Desktop.
Save JetForMe/6e37527ff184677bfd707aed996cc9d6 to your computer and use it in GitHub Desktop.
DynamicallyConfigurableLogHandler.swift
//
// 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