155 lines
5.0 KiB
Swift
155 lines
5.0 KiB
Swift
//
|
|
// Logging.swift
|
|
// oAI
|
|
//
|
|
// Dual logging: os.Logger (unified log) + file (~Library/Logs/oAI.log)
|
|
//
|
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
// Copyright (C) 2026 Rune Olsen
|
|
//
|
|
// This file is part of oAI.
|
|
//
|
|
// oAI is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU Affero General Public License as
|
|
// published by the Free Software Foundation, either version 3 of the
|
|
// License, or (at your option) any later version.
|
|
//
|
|
// oAI is distributed in the hope that it will be useful, but WITHOUT
|
|
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
|
// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General
|
|
// Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Affero General Public
|
|
// License along with oAI. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
|
|
import Foundation
|
|
import os
|
|
|
|
// MARK: - Log Level
|
|
|
|
enum LogLevel: Int, Comparable, CaseIterable, Sendable {
|
|
case debug = 0
|
|
case info = 1
|
|
case warning = 2
|
|
case error = 3
|
|
|
|
var label: String {
|
|
switch self {
|
|
case .debug: return "DEBUG"
|
|
case .info: return "INFO"
|
|
case .warning: return "WARN"
|
|
case .error: return "ERROR"
|
|
}
|
|
}
|
|
|
|
var displayName: String {
|
|
switch self {
|
|
case .debug: return "Debug"
|
|
case .info: return "Info"
|
|
case .warning: return "Warning"
|
|
case .error: return "Error"
|
|
}
|
|
}
|
|
|
|
static func < (lhs: LogLevel, rhs: LogLevel) -> Bool {
|
|
lhs.rawValue < rhs.rawValue
|
|
}
|
|
}
|
|
|
|
// MARK: - File Logger
|
|
|
|
final class FileLogger: @unchecked Sendable {
|
|
static let shared = FileLogger()
|
|
|
|
private let fileHandle: FileHandle?
|
|
private let queue = DispatchQueue(label: "com.oai.filelogger")
|
|
private let dateFormatter: DateFormatter = {
|
|
let f = DateFormatter()
|
|
f.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS"
|
|
return f
|
|
}()
|
|
|
|
/// Current minimum log level (read from UserDefaults for thread safety)
|
|
var minimumLevel: LogLevel {
|
|
get {
|
|
let raw = UserDefaults.standard.integer(forKey: "logLevel")
|
|
return LogLevel(rawValue: raw) ?? .info
|
|
}
|
|
set {
|
|
UserDefaults.standard.set(newValue.rawValue, forKey: "logLevel")
|
|
}
|
|
}
|
|
|
|
private init() {
|
|
let logsDir = FileManager.default.homeDirectoryForCurrentUser
|
|
.appendingPathComponent("Library/Logs")
|
|
let logFile = logsDir.appendingPathComponent("oAI.log")
|
|
|
|
// Ensure file exists
|
|
if !FileManager.default.fileExists(atPath: logFile.path) {
|
|
FileManager.default.createFile(atPath: logFile.path, contents: nil)
|
|
}
|
|
|
|
fileHandle = try? FileHandle(forWritingTo: logFile)
|
|
fileHandle?.seekToEndOfFile()
|
|
}
|
|
|
|
func write(_ level: LogLevel, category: String, message: String) {
|
|
guard level >= minimumLevel else { return }
|
|
queue.async { [weak self] in
|
|
guard let self, let fh = self.fileHandle else { return }
|
|
let timestamp = self.dateFormatter.string(from: Date())
|
|
let line = "[\(timestamp)] [\(level.label)] [\(category)] \(message)\n"
|
|
if let data = line.data(using: .utf8) {
|
|
fh.write(data)
|
|
}
|
|
}
|
|
}
|
|
|
|
deinit {
|
|
fileHandle?.closeFile()
|
|
}
|
|
}
|
|
|
|
// MARK: - App Logger (wraps os.Logger + file)
|
|
|
|
struct AppLogger {
|
|
let osLogger: Logger
|
|
let category: String
|
|
|
|
func debug(_ message: String) {
|
|
FileLogger.shared.write(.debug, category: category, message: message)
|
|
osLogger.debug("\(message, privacy: .public)")
|
|
}
|
|
|
|
func info(_ message: String) {
|
|
FileLogger.shared.write(.info, category: category, message: message)
|
|
osLogger.info("\(message, privacy: .public)")
|
|
}
|
|
|
|
func warning(_ message: String) {
|
|
FileLogger.shared.write(.warning, category: category, message: message)
|
|
osLogger.warning("\(message, privacy: .public)")
|
|
}
|
|
|
|
func error(_ message: String) {
|
|
FileLogger.shared.write(.error, category: category, message: message)
|
|
osLogger.error("\(message, privacy: .public)")
|
|
}
|
|
}
|
|
|
|
// MARK: - Log Namespace
|
|
|
|
enum Log {
|
|
private static let subsystem = "com.oai.oAI"
|
|
|
|
static let api = AppLogger(osLogger: Logger(subsystem: subsystem, category: "api"), category: "api")
|
|
static let db = AppLogger(osLogger: Logger(subsystem: subsystem, category: "database"), category: "database")
|
|
static let mcp = AppLogger(osLogger: Logger(subsystem: subsystem, category: "mcp"), category: "mcp")
|
|
static let settings = AppLogger(osLogger: Logger(subsystem: subsystem, category: "settings"), category: "settings")
|
|
static let search = AppLogger(osLogger: Logger(subsystem: subsystem, category: "search"), category: "search")
|
|
static let ui = AppLogger(osLogger: Logger(subsystem: subsystem, category: "ui"), category: "ui")
|
|
static let general = AppLogger(osLogger: Logger(subsystem: subsystem, category: "general"), category: "general")
|
|
}
|