Files
oai-swift/oAI/Utilities/Logging.swift
T
runeandClaude Sonnet 5 cf3f4ebfe4 Relicense oAI from AGPL-3.0-or-later to PolyForm Noncommercial 1.0.0
Switches the project from AGPL to a source-available license that
restricts commercial use — selling oAI or any part of it, standalone
or bundled into another product/service, now requires a separate
commercial license from the copyright holder. Noncommercial use,
study, modification, and sharing remain fully permitted.

Updates: LICENSE (canonical PolyForm Noncommercial 1.0.0 text +
commercial licensing contact note), SPDX headers and file-header
boilerplate across all Swift source files, the in-app About dialog's
license link (+ its localization catalog entry), README.md and
DEVELOPMENT.md license sections.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-15 11:16:09 +02:00

156 lines
5.4 KiB
Swift

//
// Logging.swift
// oAI
//
// Dual logging: os.Logger (unified log) + file (~Library/Logs/oAI.log)
//
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
// Copyright (C) 2026 Rune Olsen
//
// This file is part of oAI.
//
// oAI is licensed under the PolyForm Noncommercial License 1.0.0.
// You may use, study, modify, and share it for any noncommercial
// purpose. Commercial use — including selling oAI or any part of
// it, standalone or bundled into another product or service —
// requires a separate commercial license from the copyright holder.
//
// See the LICENSE file or
// <https://polyformproject.org/licenses/noncommercial/1.0.0> for
// the full license text. For commercial licensing, contact Rune
// Olsen via <https://mac.oai.pm>.
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"
}
}
nonisolated static func < (lhs: LogLevel, rhs: LogLevel) -> Bool {
lhs.rawValue < rhs.rawValue
}
}
// MARK: - File Logger
final class FileLogger: @unchecked Sendable {
nonisolated 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 (backed by UserDefaults — thread-safe).
nonisolated 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()
}
nonisolated 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)
// os.Logger methods are @MainActor in macOS 27. AppLogger is Sendable and all methods are
// nonisolated — FileLogger runs on its own serial queue, os.Logger dispatches to main actor.
struct AppLogger: Sendable {
let subsystem: String
let category: String
nonisolated func debug(_ message: String) {
FileLogger.shared.write(.debug, category: category, message: message)
Task { @MainActor [self] in Logger(subsystem: subsystem, category: category).debug("\(message, privacy: .public)") }
}
nonisolated func info(_ message: String) {
FileLogger.shared.write(.info, category: category, message: message)
Task { @MainActor [self] in Logger(subsystem: subsystem, category: category).info("\(message, privacy: .public)") }
}
nonisolated func warning(_ message: String) {
FileLogger.shared.write(.warning, category: category, message: message)
Task { @MainActor [self] in Logger(subsystem: subsystem, category: category).warning("\(message, privacy: .public)") }
}
nonisolated func error(_ message: String) {
FileLogger.shared.write(.error, category: category, message: message)
Task { @MainActor [self] in Logger(subsystem: subsystem, category: category).error("\(message, privacy: .public)") }
}
}
// MARK: - Log Namespace
enum Log {
private nonisolated static let subsystem = "com.oai.oAI"
nonisolated static let api = AppLogger(subsystem: subsystem, category: "api")
nonisolated static let db = AppLogger(subsystem: subsystem, category: "database")
nonisolated static let mcp = AppLogger(subsystem: subsystem, category: "mcp")
nonisolated static let settings = AppLogger(subsystem: subsystem, category: "settings")
nonisolated static let search = AppLogger(subsystem: subsystem, category: "search")
nonisolated static let ui = AppLogger(subsystem: subsystem, category: "ui")
nonisolated static let general = AppLogger(subsystem: subsystem, category: "general")
nonisolated static let extMcp = AppLogger(subsystem: subsystem, category: "ext-mcp")
}