Files
oai-swift/oAI/Utilities/Logging.swift
T
rune 76b58e6fdc Rename app from oAI to Confab
"oAI" reads as easily confused with OpenAI, both visually and in
casual conversation. Renamed to "Confab" throughout: Xcode
target/scheme/bundle ID (com.oai.Confab), Info.plist and Help Book
identity, all user-facing UI text, internal Log subsystem and color
identifiers, localization catalogs (6 languages, including a proper
reworded/retranslated Intel-deprecation notice), Help Book HTML
content, and docs (README/DEVELOPMENT/PRIVACY/SECURITY).

Deliberately cosmetic-only: the on-disk data folder
(~/Library/Application Support/oAI/), database/backup filenames,
Keychain service identifiers, and EncryptionService's key-derivation
inputs are all left untouched so existing conversations, settings,
and stored API keys survive the update with zero migration and no
re-entering credentials. Verified live: a real signed build
successfully decrypted a stored API key and loaded an existing
conversation database after the bundle ID change.

Also includes a small already-completed, previously uncommitted
model-release-date feature (ModelInfo/OpenRouterModels/
OpenRouterProvider/ModelInfoView) that happened to share several
files with this rename.

Gitignored on this branch and updated on disk but not part of this
commit: CLAUDE.md, RELEASE_NOTES.md, and the build*.sh scripts.
2026-08-02 14:58:14 +02:00

156 lines
5.4 KiB
Swift

//
// Logging.swift
// Confab
//
// 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 Confab.
//
// Confab 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 Confab 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://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("Confab.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 {
nonisolated static let subsystem = "com.oai.Confab"
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")
}