Files
steipete--codexbar/Sources/CodexBarCore/Logging/CodexBarLog.swift
T
2026-07-13 12:22:33 +08:00

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)
}
}