chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
import Foundation
|
||||
import Logging
|
||||
|
||||
final class FileLogSink: @unchecked Sendable {
|
||||
static let shared = FileLogSink()
|
||||
static let defaultURL: URL = {
|
||||
let base = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask).first
|
||||
?? FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent("Library", isDirectory: true)
|
||||
return base
|
||||
.appendingPathComponent("Logs", isDirectory: true)
|
||||
.appendingPathComponent("CodexBar", isDirectory: true)
|
||||
.appendingPathComponent("CodexBar.log")
|
||||
}()
|
||||
|
||||
private let queue = DispatchQueue(label: "com.steipete.codexbar.filelog", qos: .utility)
|
||||
private let fileManager: FileManager
|
||||
private var isEnabled = false
|
||||
private var fileHandle: FileHandle?
|
||||
private var fileURL: URL = FileLogSink.defaultURL
|
||||
private let maxBytes: Int64 = 10 * 1024 * 1024
|
||||
|
||||
init(fileManager: FileManager = .default) {
|
||||
self.fileManager = fileManager
|
||||
}
|
||||
|
||||
func setEnabled(_ enabled: Bool, fileURL: URL = FileLogSink.defaultURL) {
|
||||
self.queue.async {
|
||||
self.isEnabled = enabled
|
||||
self.fileURL = fileURL
|
||||
if !enabled {
|
||||
self.closeHandle()
|
||||
return
|
||||
}
|
||||
_ = self.openHandleIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
func currentURL() -> URL {
|
||||
self.queue.sync { self.fileURL }
|
||||
}
|
||||
|
||||
func isWriteEnabled() -> Bool {
|
||||
self.queue.sync { self.isEnabled }
|
||||
}
|
||||
|
||||
func write(_ text: String) {
|
||||
self.queue.async {
|
||||
guard self.isEnabled else { return }
|
||||
guard let data = text.data(using: .utf8) else { return }
|
||||
guard let handle = self.openHandleIfNeeded() else { return }
|
||||
handle.write(data)
|
||||
}
|
||||
}
|
||||
|
||||
private func openHandleIfNeeded() -> FileHandle? {
|
||||
if let handle = self.fileHandle { return handle }
|
||||
do {
|
||||
try self.prepareFile(at: self.fileURL)
|
||||
let handle = try FileHandle(forWritingTo: self.fileURL)
|
||||
handle.seekToEndOfFile()
|
||||
self.fileHandle = handle
|
||||
return handle
|
||||
} catch {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private func prepareFile(at url: URL) throws {
|
||||
let directory = url.deletingLastPathComponent()
|
||||
if !self.fileManager.fileExists(atPath: directory.path) {
|
||||
try self.fileManager.createDirectory(at: directory, withIntermediateDirectories: true)
|
||||
}
|
||||
if self.fileManager.fileExists(atPath: url.path) {
|
||||
let attributes = try self.fileManager.attributesOfItem(atPath: url.path)
|
||||
let size = (attributes[.size] as? NSNumber)?.int64Value ?? 0
|
||||
if size > self.maxBytes {
|
||||
try self.fileManager.removeItem(at: url)
|
||||
}
|
||||
}
|
||||
if !self.fileManager.fileExists(atPath: url.path) {
|
||||
_ = self.fileManager.createFile(atPath: url.path, contents: nil)
|
||||
}
|
||||
}
|
||||
|
||||
private func closeHandle() {
|
||||
if let handle = self.fileHandle {
|
||||
try? handle.close()
|
||||
}
|
||||
self.fileHandle = nil
|
||||
}
|
||||
}
|
||||
|
||||
struct FileLogHandler: LogHandler {
|
||||
var metadata: Logger.Metadata = [:]
|
||||
var logLevel: Logger.Level = .info
|
||||
|
||||
private let label: String
|
||||
private let sink: FileLogSink
|
||||
|
||||
init(label: String, sink: FileLogSink = .shared) {
|
||||
self.label = label
|
||||
self.sink = sink
|
||||
}
|
||||
|
||||
subscript(metadataKey metadataKey: String) -> Logger.Metadata.Value? {
|
||||
get { self.metadata[metadataKey] }
|
||||
set { self.metadata[metadataKey] = newValue }
|
||||
}
|
||||
|
||||
func log(event: LogEvent) {
|
||||
guard self.sink.isWriteEnabled() else { return }
|
||||
let ts = Self.timestamp()
|
||||
var combined = self.metadata
|
||||
if let metadata = event.metadata { combined.merge(metadata, uniquingKeysWith: { _, new in new }) }
|
||||
var metaText = ""
|
||||
if !combined.isEmpty {
|
||||
let pairs = combined
|
||||
.sorted(by: { $0.key < $1.key })
|
||||
.map { key, value in
|
||||
let rendered = Self.renderMetadataValue(value)
|
||||
let safeValue = LogRedactor.redact(rendered)
|
||||
return "\(key)=\(safeValue)"
|
||||
}
|
||||
.joined(separator: " ")
|
||||
metaText = " \(pairs)"
|
||||
}
|
||||
let safeMessage = LogRedactor.redact("\(event.message)")
|
||||
let lineText = "[\(ts)] [\(event.level.rawValue.uppercased())] \(self.label): \(safeMessage)\(metaText)\n"
|
||||
_ = event.source
|
||||
_ = event.file
|
||||
_ = event.function
|
||||
_ = event.line
|
||||
self.sink.write(lineText)
|
||||
}
|
||||
|
||||
private static func timestamp() -> String {
|
||||
let formatter = ISO8601DateFormatter()
|
||||
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
||||
return formatter.string(from: Date())
|
||||
}
|
||||
|
||||
private static func renderMetadataValue(_ value: Logger.Metadata.Value) -> String {
|
||||
switch value {
|
||||
case let .string(text):
|
||||
text
|
||||
default:
|
||||
String(describing: value)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user