231 lines
6.8 KiB
Swift
231 lines
6.8 KiB
Swift
import Foundation
|
|
@_exported import Logging
|
|
import os
|
|
import OSLog
|
|
|
|
typealias Logger = Logging.Logger
|
|
|
|
enum AppLogSettings {
|
|
static let logLevelKey = appLogLevelKey
|
|
|
|
static func logLevel() -> Logger.Level {
|
|
if let raw = UserDefaults.standard.string(forKey: self.logLevelKey),
|
|
let level = Logger.Level(rawValue: raw)
|
|
{
|
|
return level
|
|
}
|
|
return .info
|
|
}
|
|
|
|
static func setLogLevel(_ level: Logger.Level) {
|
|
UserDefaults.standard.set(level.rawValue, forKey: self.logLevelKey)
|
|
}
|
|
|
|
static func fileLoggingEnabled() -> Bool {
|
|
UserDefaults.standard.bool(forKey: debugFileLogEnabledKey)
|
|
}
|
|
}
|
|
|
|
enum AppLogLevel: String, CaseIterable, Identifiable {
|
|
case trace
|
|
case debug
|
|
case info
|
|
case notice
|
|
case warning
|
|
case error
|
|
case critical
|
|
|
|
static let `default`: AppLogLevel = .info
|
|
|
|
var id: String { self.rawValue }
|
|
|
|
var title: String {
|
|
switch self {
|
|
case .trace: "Trace"
|
|
case .debug: "Debug"
|
|
case .info: "Info"
|
|
case .notice: "Notice"
|
|
case .warning: "Warning"
|
|
case .error: "Error"
|
|
case .critical: "Critical"
|
|
}
|
|
}
|
|
}
|
|
|
|
enum ClawdbotLogging {
|
|
private static let labelSeparator = "::"
|
|
|
|
private static let didBootstrap: Void = {
|
|
LoggingSystem.bootstrap { label in
|
|
let (subsystem, category) = Self.parseLabel(label)
|
|
let osHandler = ClawdbotOSLogHandler(subsystem: subsystem, category: category)
|
|
let fileHandler = ClawdbotFileLogHandler(label: label)
|
|
return MultiplexLogHandler([osHandler, fileHandler])
|
|
}
|
|
}()
|
|
|
|
static func bootstrapIfNeeded() {
|
|
_ = self.didBootstrap
|
|
}
|
|
|
|
static func makeLabel(subsystem: String, category: String) -> String {
|
|
"\(subsystem)\(self.labelSeparator)\(category)"
|
|
}
|
|
|
|
static func parseLabel(_ label: String) -> (String, String) {
|
|
guard let range = label.range(of: labelSeparator) else {
|
|
return ("com.clawdbot", label)
|
|
}
|
|
let subsystem = String(label[..<range.lowerBound])
|
|
let category = String(label[range.upperBound...])
|
|
return (subsystem, category)
|
|
}
|
|
}
|
|
|
|
extension Logging.Logger {
|
|
init(subsystem: String, category: String) {
|
|
ClawdbotLogging.bootstrapIfNeeded()
|
|
let label = ClawdbotLogging.makeLabel(subsystem: subsystem, category: category)
|
|
self.init(label: label)
|
|
}
|
|
}
|
|
|
|
extension Logger.Message.StringInterpolation {
|
|
mutating func appendInterpolation(_ value: some Any, privacy: OSLogPrivacy) {
|
|
self.appendInterpolation(String(describing: value))
|
|
}
|
|
}
|
|
|
|
struct ClawdbotOSLogHandler: LogHandler {
|
|
private let osLogger: os.Logger
|
|
var metadata: Logger.Metadata = [:]
|
|
|
|
var logLevel: Logger.Level {
|
|
get { AppLogSettings.logLevel() }
|
|
set { AppLogSettings.setLogLevel(newValue) }
|
|
}
|
|
|
|
init(subsystem: String, category: String) {
|
|
self.osLogger = os.Logger(subsystem: subsystem, category: category)
|
|
}
|
|
|
|
subscript(metadataKey key: String) -> Logger.Metadata.Value? {
|
|
get { self.metadata[key] }
|
|
set { self.metadata[key] = newValue }
|
|
}
|
|
|
|
func log(
|
|
level: Logger.Level,
|
|
message: Logger.Message,
|
|
metadata: Logger.Metadata?,
|
|
source: String,
|
|
file: String,
|
|
function: String,
|
|
line: UInt)
|
|
{
|
|
let merged = Self.mergeMetadata(self.metadata, metadata)
|
|
let rendered = Self.renderMessage(message, metadata: merged)
|
|
self.osLogger.log(level: Self.osLogType(for: level), "\(rendered, privacy: .public)")
|
|
}
|
|
|
|
private static func osLogType(for level: Logger.Level) -> OSLogType {
|
|
switch level {
|
|
case .trace, .debug:
|
|
.debug
|
|
case .info, .notice:
|
|
.info
|
|
case .warning:
|
|
.default
|
|
case .error:
|
|
.error
|
|
case .critical:
|
|
.fault
|
|
}
|
|
}
|
|
|
|
private static func mergeMetadata(
|
|
_ base: Logger.Metadata,
|
|
_ extra: Logger.Metadata?) -> Logger.Metadata
|
|
{
|
|
guard let extra else { return base }
|
|
return base.merging(extra, uniquingKeysWith: { _, new in new })
|
|
}
|
|
|
|
private static func renderMessage(_ message: Logger.Message, metadata: Logger.Metadata) -> String {
|
|
guard !metadata.isEmpty else { return message.description }
|
|
let meta = metadata
|
|
.sorted(by: { $0.key < $1.key })
|
|
.map { "\($0.key)=\(self.stringify($0.value))" }
|
|
.joined(separator: " ")
|
|
return "\(message.description) [\(meta)]"
|
|
}
|
|
|
|
private static func stringify(_ value: Logger.Metadata.Value) -> String {
|
|
switch value {
|
|
case let .string(text):
|
|
text
|
|
case let .stringConvertible(value):
|
|
String(describing: value)
|
|
case let .array(values):
|
|
"[" + values.map { self.stringify($0) }.joined(separator: ",") + "]"
|
|
case let .dictionary(entries):
|
|
"{" + entries.map { "\($0.key)=\(self.stringify($0.value))" }.joined(separator: ",") + "}"
|
|
}
|
|
}
|
|
}
|
|
|
|
struct ClawdbotFileLogHandler: LogHandler {
|
|
let label: String
|
|
var metadata: Logger.Metadata = [:]
|
|
|
|
var logLevel: Logger.Level {
|
|
get { AppLogSettings.logLevel() }
|
|
set { AppLogSettings.setLogLevel(newValue) }
|
|
}
|
|
|
|
subscript(metadataKey key: String) -> Logger.Metadata.Value? {
|
|
get { self.metadata[key] }
|
|
set { self.metadata[key] = newValue }
|
|
}
|
|
|
|
func log(
|
|
level: Logger.Level,
|
|
message: Logger.Message,
|
|
metadata: Logger.Metadata?,
|
|
source: String,
|
|
file: String,
|
|
function: String,
|
|
line: UInt)
|
|
{
|
|
guard AppLogSettings.fileLoggingEnabled() else { return }
|
|
let (subsystem, category) = ClawdbotLogging.parseLabel(self.label)
|
|
var fields: [String: String] = [
|
|
"subsystem": subsystem,
|
|
"category": category,
|
|
"level": level.rawValue,
|
|
"source": source,
|
|
"file": file,
|
|
"function": function,
|
|
"line": "\(line)",
|
|
]
|
|
let merged = self.metadata.merging(metadata ?? [:], uniquingKeysWith: { _, new in new })
|
|
for (key, value) in merged {
|
|
fields["meta.\(key)"] = Self.stringify(value)
|
|
}
|
|
DiagnosticsFileLog.shared.log(category: category, event: message.description, fields: fields)
|
|
}
|
|
|
|
private static func stringify(_ value: Logger.Metadata.Value) -> String {
|
|
switch value {
|
|
case let .string(text):
|
|
text
|
|
case let .stringConvertible(value):
|
|
String(describing: value)
|
|
case let .array(values):
|
|
"[" + values.map { self.stringify($0) }.joined(separator: ",") + "]"
|
|
case let .dictionary(entries):
|
|
"{" + entries.map { "\($0.key)=\(self.stringify($0.value))" }.joined(separator: ",") + "}"
|
|
}
|
|
}
|
|
}
|