230 lines
7.4 KiB
Swift
230 lines
7.4 KiB
Swift
import Foundation
|
|
import Logging
|
|
|
|
public enum CodexBarLog {
|
|
public enum Destination: Sendable {
|
|
case stderr
|
|
case discard
|
|
case oslog(subsystem: String)
|
|
}
|
|
|
|
public enum Level: String, CaseIterable, Identifiable, Sendable {
|
|
case trace
|
|
case verbose
|
|
case debug
|
|
case info
|
|
case warning
|
|
case error
|
|
case critical
|
|
|
|
public var id: String {
|
|
self.rawValue
|
|
}
|
|
|
|
public var displayName: String {
|
|
switch self {
|
|
case .trace: "Trace"
|
|
case .verbose: "Verbose"
|
|
case .debug: "Debug"
|
|
case .info: "Info"
|
|
case .warning: "Warning"
|
|
case .error: "Error"
|
|
case .critical: "Critical"
|
|
}
|
|
}
|
|
|
|
public var rank: Int {
|
|
switch self {
|
|
case .trace: 0
|
|
case .verbose: 1
|
|
case .debug: 2
|
|
case .info: 3
|
|
case .warning: 4
|
|
case .error: 5
|
|
case .critical: 6
|
|
}
|
|
}
|
|
|
|
public var asSwiftLogLevel: Logger.Level {
|
|
switch self {
|
|
case .trace: .trace
|
|
case .verbose: .debug
|
|
case .debug: .debug
|
|
case .info: .info
|
|
case .warning: .warning
|
|
case .error: .error
|
|
case .critical: .critical
|
|
}
|
|
}
|
|
}
|
|
|
|
public struct Configuration: Sendable {
|
|
public let destination: Destination
|
|
public let level: Level
|
|
public let json: Bool
|
|
|
|
public init(destination: Destination, level: Level, json: Bool) {
|
|
self.destination = destination
|
|
self.level = level
|
|
self.json = json
|
|
}
|
|
}
|
|
|
|
private static let lock = NSLock()
|
|
private static let levelLock = NSLock()
|
|
private nonisolated(unsafe) static var isBootstrapped = false
|
|
private nonisolated(unsafe) static var currentLevel: Level = .info
|
|
|
|
public static func bootstrapIfNeeded(_ config: Configuration) {
|
|
self.lock.lock()
|
|
defer { lock.unlock() }
|
|
guard !self.isBootstrapped else { return }
|
|
self.currentLevel = config.level
|
|
|
|
let baseFactory: @Sendable (String) -> any LogHandler = { label in
|
|
switch config.destination {
|
|
case .stderr:
|
|
if config.json { return JSONStderrLogHandler(label: label) }
|
|
return StreamLogHandler.standardError(label: label)
|
|
case .discard:
|
|
return DiscardLogHandler()
|
|
case let .oslog(subsystem):
|
|
#if canImport(os)
|
|
return OSLogLogHandler(label: label, subsystem: subsystem)
|
|
#else
|
|
if config.json { return JSONStderrLogHandler(label: label) }
|
|
return StreamLogHandler.standardError(label: label)
|
|
#endif
|
|
}
|
|
}
|
|
|
|
LoggingSystem.bootstrap { label in
|
|
let primary = baseFactory(label)
|
|
let fileHandler = FileLogHandler(label: label)
|
|
var handler = CompositeLogHandler(primary: primary, secondary: fileHandler)
|
|
handler.logLevel = .trace
|
|
return handler
|
|
}
|
|
|
|
self.isBootstrapped = true
|
|
}
|
|
|
|
public static func logger(_ category: String) -> CodexBarLogger {
|
|
let logger = Logger(label: "com.steipete.codexbar.\(category)")
|
|
return CodexBarLogger(isEnabled: { level in
|
|
self.shouldLog(level)
|
|
}, log: { level, message, metadata in
|
|
let swiftLogLevel = level.asSwiftLogLevel
|
|
let safeMessage = LogRedactor.redact(message)
|
|
let meta = metadata?.reduce(into: Logger.Metadata()) { partial, entry in
|
|
partial[entry.key] = .string(LogRedactor.redact(entry.value))
|
|
}
|
|
logger.log(level: swiftLogLevel, "\(safeMessage)", metadata: meta)
|
|
})
|
|
}
|
|
|
|
public static func parseLevel(_ raw: String?) -> Level? {
|
|
guard let raw, !raw.isEmpty else { return nil }
|
|
return Level(rawValue: raw.trimmingCharacters(in: .whitespacesAndNewlines).lowercased())
|
|
}
|
|
|
|
public static func setLogLevel(_ level: Level) {
|
|
self.levelLock.lock()
|
|
self.currentLevel = level
|
|
self.levelLock.unlock()
|
|
let logger = self.logger(LogCategories.logging)
|
|
logger.info("Log level set to \(level.rawValue)")
|
|
}
|
|
|
|
public static func currentLogLevel() -> Level {
|
|
self.levelLock.lock()
|
|
defer { self.levelLock.unlock() }
|
|
return self.currentLevel
|
|
}
|
|
|
|
private static func shouldLog(_ level: Level) -> Bool {
|
|
level.rank >= self.currentLogLevel().rank
|
|
}
|
|
|
|
public static var fileLogURL: URL {
|
|
FileLogSink.defaultURL
|
|
}
|
|
|
|
public static func setFileLoggingEnabled(_ enabled: Bool) {
|
|
FileLogSink.shared.setEnabled(enabled, fileURL: self.fileLogURL)
|
|
let state = enabled ? "enabled" : "disabled"
|
|
let logger = self.logger(LogCategories.logging)
|
|
logger.info("File logging \(state)", metadata: ["path": self.fileLogURL.path])
|
|
}
|
|
}
|
|
|
|
private struct DiscardLogHandler: LogHandler {
|
|
var metadata: Logger.Metadata = [:]
|
|
var logLevel: Logger.Level = .critical
|
|
|
|
subscript(metadataKey metadataKey: String) -> Logger.Metadata.Value? {
|
|
get { self.metadata[metadataKey] }
|
|
set { self.metadata[metadataKey] = newValue }
|
|
}
|
|
|
|
func log(event _: LogEvent) {}
|
|
}
|
|
|
|
public struct CodexBarLogger: Sendable {
|
|
private let isEnabled: @Sendable (CodexBarLog.Level) -> Bool
|
|
private let logFn: @Sendable (CodexBarLog.Level, String, [String: String]?) -> Void
|
|
|
|
fileprivate init(
|
|
isEnabled: @escaping @Sendable (CodexBarLog.Level) -> Bool,
|
|
log: @escaping @Sendable (CodexBarLog.Level, String, [String: String]?) -> Void)
|
|
{
|
|
self.isEnabled = isEnabled
|
|
self.logFn = log
|
|
}
|
|
|
|
init(
|
|
minimumLevel: CodexBarLog.Level,
|
|
log: @escaping @Sendable (CodexBarLog.Level, String, [String: String]?) -> Void)
|
|
{
|
|
self.isEnabled = { level in level.rank >= minimumLevel.rank }
|
|
self.logFn = log
|
|
}
|
|
|
|
private func log(
|
|
_ level: CodexBarLog.Level,
|
|
_ message: @autoclosure () -> String,
|
|
metadata: [String: String]?)
|
|
{
|
|
guard self.isEnabled(level) else { return }
|
|
self.logFn(level, message(), metadata)
|
|
}
|
|
|
|
public func trace(_ message: @autoclosure () -> String, metadata: [String: String]? = nil) {
|
|
self.log(.trace, message(), metadata: metadata)
|
|
}
|
|
|
|
public func verbose(_ message: @autoclosure () -> String, metadata: [String: String]? = nil) {
|
|
self.log(.verbose, message(), metadata: metadata)
|
|
}
|
|
|
|
public func debug(_ message: @autoclosure () -> String, metadata: [String: String]? = nil) {
|
|
self.log(.debug, message(), metadata: metadata)
|
|
}
|
|
|
|
public func info(_ message: @autoclosure () -> String, metadata: [String: String]? = nil) {
|
|
self.log(.info, message(), metadata: metadata)
|
|
}
|
|
|
|
public func warning(_ message: @autoclosure () -> String, metadata: [String: String]? = nil) {
|
|
self.log(.warning, message(), metadata: metadata)
|
|
}
|
|
|
|
public func error(_ message: @autoclosure () -> String, metadata: [String: String]? = nil) {
|
|
self.log(.error, message(), metadata: metadata)
|
|
}
|
|
|
|
public func critical(_ message: @autoclosure () -> String, metadata: [String: String]? = nil) {
|
|
self.log(.critical, message(), metadata: metadata)
|
|
}
|
|
}
|