chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,229 @@
|
||||
import Foundation
|
||||
import Logging
|
||||
|
||||
public enum CodexBarLog {
|
||||
public enum Destination: Sendable {
|
||||
case stderr
|
||||
case discard
|
||||
case oslog(subsystem: String)
|
||||
}
|
||||
|
||||
public enum Level: String, CaseIterable, Identifiable, Sendable {
|
||||
case trace
|
||||
case verbose
|
||||
case debug
|
||||
case info
|
||||
case warning
|
||||
case error
|
||||
case critical
|
||||
|
||||
public var id: String {
|
||||
self.rawValue
|
||||
}
|
||||
|
||||
public var displayName: String {
|
||||
switch self {
|
||||
case .trace: "Trace"
|
||||
case .verbose: "Verbose"
|
||||
case .debug: "Debug"
|
||||
case .info: "Info"
|
||||
case .warning: "Warning"
|
||||
case .error: "Error"
|
||||
case .critical: "Critical"
|
||||
}
|
||||
}
|
||||
|
||||
public var rank: Int {
|
||||
switch self {
|
||||
case .trace: 0
|
||||
case .verbose: 1
|
||||
case .debug: 2
|
||||
case .info: 3
|
||||
case .warning: 4
|
||||
case .error: 5
|
||||
case .critical: 6
|
||||
}
|
||||
}
|
||||
|
||||
public var asSwiftLogLevel: Logger.Level {
|
||||
switch self {
|
||||
case .trace: .trace
|
||||
case .verbose: .debug
|
||||
case .debug: .debug
|
||||
case .info: .info
|
||||
case .warning: .warning
|
||||
case .error: .error
|
||||
case .critical: .critical
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public struct Configuration: Sendable {
|
||||
public let destination: Destination
|
||||
public let level: Level
|
||||
public let json: Bool
|
||||
|
||||
public init(destination: Destination, level: Level, json: Bool) {
|
||||
self.destination = destination
|
||||
self.level = level
|
||||
self.json = json
|
||||
}
|
||||
}
|
||||
|
||||
private static let lock = NSLock()
|
||||
private static let levelLock = NSLock()
|
||||
private nonisolated(unsafe) static var isBootstrapped = false
|
||||
private nonisolated(unsafe) static var currentLevel: Level = .info
|
||||
|
||||
public static func bootstrapIfNeeded(_ config: Configuration) {
|
||||
self.lock.lock()
|
||||
defer { lock.unlock() }
|
||||
guard !self.isBootstrapped else { return }
|
||||
self.currentLevel = config.level
|
||||
|
||||
let baseFactory: @Sendable (String) -> any LogHandler = { label in
|
||||
switch config.destination {
|
||||
case .stderr:
|
||||
if config.json { return JSONStderrLogHandler(label: label) }
|
||||
return StreamLogHandler.standardError(label: label)
|
||||
case .discard:
|
||||
return DiscardLogHandler()
|
||||
case let .oslog(subsystem):
|
||||
#if canImport(os)
|
||||
return OSLogLogHandler(label: label, subsystem: subsystem)
|
||||
#else
|
||||
if config.json { return JSONStderrLogHandler(label: label) }
|
||||
return StreamLogHandler.standardError(label: label)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
LoggingSystem.bootstrap { label in
|
||||
let primary = baseFactory(label)
|
||||
let fileHandler = FileLogHandler(label: label)
|
||||
var handler = CompositeLogHandler(primary: primary, secondary: fileHandler)
|
||||
handler.logLevel = .trace
|
||||
return handler
|
||||
}
|
||||
|
||||
self.isBootstrapped = true
|
||||
}
|
||||
|
||||
public static func logger(_ category: String) -> CodexBarLogger {
|
||||
let logger = Logger(label: "com.steipete.codexbar.\(category)")
|
||||
return CodexBarLogger(isEnabled: { level in
|
||||
self.shouldLog(level)
|
||||
}, log: { level, message, metadata in
|
||||
let swiftLogLevel = level.asSwiftLogLevel
|
||||
let safeMessage = LogRedactor.redact(message)
|
||||
let meta = metadata?.reduce(into: Logger.Metadata()) { partial, entry in
|
||||
partial[entry.key] = .string(LogRedactor.redact(entry.value))
|
||||
}
|
||||
logger.log(level: swiftLogLevel, "\(safeMessage)", metadata: meta)
|
||||
})
|
||||
}
|
||||
|
||||
public static func parseLevel(_ raw: String?) -> Level? {
|
||||
guard let raw, !raw.isEmpty else { return nil }
|
||||
return Level(rawValue: raw.trimmingCharacters(in: .whitespacesAndNewlines).lowercased())
|
||||
}
|
||||
|
||||
public static func setLogLevel(_ level: Level) {
|
||||
self.levelLock.lock()
|
||||
self.currentLevel = level
|
||||
self.levelLock.unlock()
|
||||
let logger = self.logger(LogCategories.logging)
|
||||
logger.info("Log level set to \(level.rawValue)")
|
||||
}
|
||||
|
||||
public static func currentLogLevel() -> Level {
|
||||
self.levelLock.lock()
|
||||
defer { self.levelLock.unlock() }
|
||||
return self.currentLevel
|
||||
}
|
||||
|
||||
private static func shouldLog(_ level: Level) -> Bool {
|
||||
level.rank >= self.currentLogLevel().rank
|
||||
}
|
||||
|
||||
public static var fileLogURL: URL {
|
||||
FileLogSink.defaultURL
|
||||
}
|
||||
|
||||
public static func setFileLoggingEnabled(_ enabled: Bool) {
|
||||
FileLogSink.shared.setEnabled(enabled, fileURL: self.fileLogURL)
|
||||
let state = enabled ? "enabled" : "disabled"
|
||||
let logger = self.logger(LogCategories.logging)
|
||||
logger.info("File logging \(state)", metadata: ["path": self.fileLogURL.path])
|
||||
}
|
||||
}
|
||||
|
||||
private struct DiscardLogHandler: LogHandler {
|
||||
var metadata: Logger.Metadata = [:]
|
||||
var logLevel: Logger.Level = .critical
|
||||
|
||||
subscript(metadataKey metadataKey: String) -> Logger.Metadata.Value? {
|
||||
get { self.metadata[metadataKey] }
|
||||
set { self.metadata[metadataKey] = newValue }
|
||||
}
|
||||
|
||||
func log(event _: LogEvent) {}
|
||||
}
|
||||
|
||||
public struct CodexBarLogger: Sendable {
|
||||
private let isEnabled: @Sendable (CodexBarLog.Level) -> Bool
|
||||
private let logFn: @Sendable (CodexBarLog.Level, String, [String: String]?) -> Void
|
||||
|
||||
fileprivate init(
|
||||
isEnabled: @escaping @Sendable (CodexBarLog.Level) -> Bool,
|
||||
log: @escaping @Sendable (CodexBarLog.Level, String, [String: String]?) -> Void)
|
||||
{
|
||||
self.isEnabled = isEnabled
|
||||
self.logFn = log
|
||||
}
|
||||
|
||||
init(
|
||||
minimumLevel: CodexBarLog.Level,
|
||||
log: @escaping @Sendable (CodexBarLog.Level, String, [String: String]?) -> Void)
|
||||
{
|
||||
self.isEnabled = { level in level.rank >= minimumLevel.rank }
|
||||
self.logFn = log
|
||||
}
|
||||
|
||||
private func log(
|
||||
_ level: CodexBarLog.Level,
|
||||
_ message: @autoclosure () -> String,
|
||||
metadata: [String: String]?)
|
||||
{
|
||||
guard self.isEnabled(level) else { return }
|
||||
self.logFn(level, message(), metadata)
|
||||
}
|
||||
|
||||
public func trace(_ message: @autoclosure () -> String, metadata: [String: String]? = nil) {
|
||||
self.log(.trace, message(), metadata: metadata)
|
||||
}
|
||||
|
||||
public func verbose(_ message: @autoclosure () -> String, metadata: [String: String]? = nil) {
|
||||
self.log(.verbose, message(), metadata: metadata)
|
||||
}
|
||||
|
||||
public func debug(_ message: @autoclosure () -> String, metadata: [String: String]? = nil) {
|
||||
self.log(.debug, message(), metadata: metadata)
|
||||
}
|
||||
|
||||
public func info(_ message: @autoclosure () -> String, metadata: [String: String]? = nil) {
|
||||
self.log(.info, message(), metadata: metadata)
|
||||
}
|
||||
|
||||
public func warning(_ message: @autoclosure () -> String, metadata: [String: String]? = nil) {
|
||||
self.log(.warning, message(), metadata: metadata)
|
||||
}
|
||||
|
||||
public func error(_ message: @autoclosure () -> String, metadata: [String: String]? = nil) {
|
||||
self.log(.error, message(), metadata: metadata)
|
||||
}
|
||||
|
||||
public func critical(_ message: @autoclosure () -> String, metadata: [String: String]? = nil) {
|
||||
self.log(.critical, message(), metadata: metadata)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import Logging
|
||||
|
||||
struct CompositeLogHandler: LogHandler {
|
||||
private var primary: any LogHandler
|
||||
private var secondary: any LogHandler
|
||||
|
||||
init(primary: any LogHandler, secondary: any LogHandler) {
|
||||
self.primary = primary
|
||||
self.secondary = secondary
|
||||
}
|
||||
|
||||
var metadata: Logger.Metadata {
|
||||
get { self.primary.metadata }
|
||||
set {
|
||||
self.primary.metadata = newValue
|
||||
self.secondary.metadata = newValue
|
||||
}
|
||||
}
|
||||
|
||||
var logLevel: Logger.Level {
|
||||
get { self.primary.logLevel }
|
||||
set {
|
||||
self.primary.logLevel = newValue
|
||||
self.secondary.logLevel = newValue
|
||||
}
|
||||
}
|
||||
|
||||
subscript(metadataKey metadataKey: String) -> Logger.Metadata.Value? {
|
||||
get { self.primary[metadataKey: metadataKey] }
|
||||
set {
|
||||
self.primary[metadataKey: metadataKey] = newValue
|
||||
self.secondary[metadataKey: metadataKey] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
func log(event: LogEvent) {
|
||||
self.primary.log(event: event)
|
||||
self.secondary.log(event: event)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import Foundation
|
||||
import Logging
|
||||
#if canImport(Darwin)
|
||||
import Darwin
|
||||
#elseif canImport(Glibc)
|
||||
import Glibc
|
||||
#elseif canImport(Musl)
|
||||
import Musl
|
||||
#endif
|
||||
|
||||
struct JSONStderrLogHandler: LogHandler {
|
||||
var metadata: Logger.Metadata = [:]
|
||||
var logLevel: Logger.Level = .info
|
||||
|
||||
private let label: String
|
||||
private let encoder: JSONEncoder
|
||||
|
||||
init(label: String) {
|
||||
self.label = label
|
||||
self.encoder = JSONEncoder()
|
||||
self.encoder.dateEncodingStrategy = .iso8601
|
||||
self.encoder.outputFormatting = [.sortedKeys]
|
||||
}
|
||||
|
||||
subscript(metadataKey metadataKey: String) -> Logger.Metadata.Value? {
|
||||
get { self.metadata[metadataKey] }
|
||||
set { self.metadata[metadataKey] = newValue }
|
||||
}
|
||||
|
||||
func log(event: LogEvent) {
|
||||
let ts = Date()
|
||||
var combined = self.metadata
|
||||
if let metadata = event.metadata { combined.merge(metadata, uniquingKeysWith: { _, new in new }) }
|
||||
|
||||
let payload = JSONLogLine(
|
||||
timestamp: ts,
|
||||
level: event.level.rawValue,
|
||||
label: self.label,
|
||||
message: event.message.description,
|
||||
source: event.source,
|
||||
file: event.file,
|
||||
function: event.function,
|
||||
line: event.line,
|
||||
metadata: combined.isEmpty ? nil : combined.mapValues(\.description))
|
||||
|
||||
guard let data = try? self.encoder.encode(payload),
|
||||
let text = String(data: data, encoding: .utf8) else { return }
|
||||
Self.writeStderr(text + "\n")
|
||||
}
|
||||
}
|
||||
|
||||
private struct JSONLogLine: Encodable {
|
||||
let timestamp: Date
|
||||
let level: String
|
||||
let label: String
|
||||
let message: String
|
||||
let source: String
|
||||
let file: String
|
||||
let function: String
|
||||
let line: UInt
|
||||
let metadata: [String: String]?
|
||||
}
|
||||
|
||||
extension JSONStderrLogHandler {
|
||||
private static func writeStderr(_ text: String) {
|
||||
let bytes = Array(text.utf8)
|
||||
bytes.withUnsafeBytes { buffer in
|
||||
guard let baseAddress = buffer.baseAddress else { return }
|
||||
_ = write(STDERR_FILENO, baseAddress, buffer.count)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
public enum LogCategories {
|
||||
public static let abacusCookie = "abacus-cookie"
|
||||
public static let abacusUsage = "abacus-usage"
|
||||
public static let adaptiveRefresh = "adaptive-refresh"
|
||||
public static let amp = "amp"
|
||||
public static let antigravity = "antigravity"
|
||||
public static let app = "app"
|
||||
public static let auggieCLI = "auggie-cli"
|
||||
public static let augment = "augment"
|
||||
public static let augmentKeepalive = "augment-keepalive"
|
||||
public static let bedrockUsage = "bedrock-usage"
|
||||
public static let browserCookieGate = "browser-cookie-gate"
|
||||
public static let claudeCLI = "claude-cli"
|
||||
public static let claudeProbe = "claude-probe"
|
||||
public static let claudeUsage = "claude-usage"
|
||||
public static let codexRPC = "codex-rpc"
|
||||
public static let commandcodeCookie = "commandcode-cookie"
|
||||
public static let commandcodeUsage = "commandcode-usage"
|
||||
public static let configMigration = "config-migration"
|
||||
public static let configStore = "config-store"
|
||||
public static let confetti = "confetti"
|
||||
public static let cookieCache = "cookie-cache"
|
||||
public static let cookieHeaderStore = "cookie-header-store"
|
||||
public static let copilotTokenStore = "copilot-token-store"
|
||||
public static let creditsPurchase = "creditsPurchase"
|
||||
public static let crossModelUsage = "crossmodel-usage"
|
||||
public static let cursorLogin = "cursor-login"
|
||||
public static let deepSeekSettings = "deepseek-settings"
|
||||
public static let deepSeekUsage = "deepseek-usage"
|
||||
public static let deepgramUsage = "deepgram-usage"
|
||||
public static let devin = "devin"
|
||||
public static let doubaoUsage = "doubao-usage"
|
||||
public static let elevenLabsUsage = "elevenlabs-usage"
|
||||
public static let geminiProbe = "gemini-probe"
|
||||
public static let grok = "grok"
|
||||
public static let keychainCache = "keychain-cache"
|
||||
public static let keychainMigration = "keychain-migration"
|
||||
public static let keychainPreflight = "keychain-preflight"
|
||||
public static let keychainPrompt = "keychain-prompt"
|
||||
public static let kimiAPI = "kimi-api"
|
||||
public static let kimiCookie = "kimi-cookie"
|
||||
public static let kimiK2TokenStore = "kimi-k2-token-store"
|
||||
public static let kimiK2Usage = "kimi-k2-usage"
|
||||
public static let kimiTokenStore = "kimi-token-store"
|
||||
public static let kimiWeb = "kimi-web"
|
||||
public static let kiro = "kiro"
|
||||
public static let launchAtLogin = "launch-at-login"
|
||||
public static let login = "login"
|
||||
public static let logging = "logging"
|
||||
public static let manusAPI = "manus-api"
|
||||
public static let manusCookie = "manus-cookie"
|
||||
public static let manusWeb = "manus-web"
|
||||
public static let memoryPressure = "memory-pressure"
|
||||
public static let minimaxAPITokenStore = "minimax-api-token-store"
|
||||
public static let minimaxCookie = "minimax-cookie"
|
||||
public static let minimaxCookieStore = "minimax-cookie-store"
|
||||
public static let minimaxUsage = "minimax-usage"
|
||||
public static let minimaxWeb = "minimax-web"
|
||||
public static let moonshotUsage = "moonshot-usage"
|
||||
public static let notifications = "notifications"
|
||||
public static let openAIWeb = "openai-web"
|
||||
public static let openAIWebview = "openai-webview"
|
||||
public static let ollama = "ollama"
|
||||
public static let opencodeUsage = "opencode-usage"
|
||||
public static let opencodeGoUsage = "opencode-go-usage"
|
||||
public static let openRouterUsage = "openrouter-usage"
|
||||
public static let perplexityAPI = "perplexity-api"
|
||||
public static let perplexityCookie = "perplexity-cookie"
|
||||
public static let perplexityWeb = "perplexity-web"
|
||||
public static let poeUsage = "poe-usage"
|
||||
public static let providerDetection = "provider-detection"
|
||||
public static let providers = "providers"
|
||||
public static let qoderCookie = "qoder-cookie"
|
||||
public static let qoderUsage = "qoder-usage"
|
||||
public static let quotaWarningNotifications = "quotaWarningNotifications"
|
||||
public static let sessionQuota = "sessionQuota"
|
||||
public static let sessionQuotaNotifications = "sessionQuotaNotifications"
|
||||
public static let settings = "settings"
|
||||
public static let subprocess = "subprocess"
|
||||
public static let syntheticTokenStore = "synthetic-token-store"
|
||||
public static let syntheticUsage = "synthetic-usage"
|
||||
public static let t3chat = "t3chat"
|
||||
public static let terminal = "terminal"
|
||||
public static let tokenAccounts = "token-accounts"
|
||||
public static let tokenCost = "token-cost"
|
||||
public static let ttyRunner = "tty-runner"
|
||||
public static let veniceUsage = "venice-usage"
|
||||
public static let vertexAIFetcher = "vertexai-fetcher"
|
||||
public static let warpUsage = "warp-usage"
|
||||
public static let zed = "zed"
|
||||
public static let webkitTeardown = "webkit-teardown"
|
||||
public static let zaiSettings = "zai-settings"
|
||||
public static let zaiTokenStore = "zai-token-store"
|
||||
public static let zaiUsage = "zai-usage"
|
||||
public static let stepfunUsage = "stepfun-usage"
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import Foundation
|
||||
|
||||
public enum LogMetadata {
|
||||
public static func secretSummary(_ value: String?) -> [String: String] {
|
||||
let trimmed = value?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
let state = trimmed.isEmpty ? "cleared" : "set"
|
||||
let length = trimmed.count
|
||||
return ["state": state, "length": "\(length)"]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import Foundation
|
||||
|
||||
public enum LogRedactor {
|
||||
private static let fallbackRegex: NSRegularExpression = {
|
||||
do {
|
||||
return try NSRegularExpression(pattern: "$^", options: [])
|
||||
} catch {
|
||||
fatalError("Failed to build fallback regex: \(error)")
|
||||
}
|
||||
}()
|
||||
|
||||
private static let emailRegex = Self.makeRegex(
|
||||
pattern: #"[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}"#,
|
||||
options: [.caseInsensitive])
|
||||
private static let cookieHeaderRegex = Self.makeRegex(
|
||||
pattern: #"(?i)(cookie\s*:\s*)([^\r\n]+)"#)
|
||||
private static let authorizationRegex = Self.makeRegex(
|
||||
pattern: #"(?i)(authorization\s*:\s*)([^\r\n]+)"#)
|
||||
private static let bearerRegex = Self.makeRegex(
|
||||
pattern: #"(?i)\bbearer\s+[a-z0-9._\-]+=*\b"#)
|
||||
private static let minimaxCodingPlanTokenRegex = Self.makeRegex(
|
||||
pattern: #"sk-cp-[^\s"'`;,)>\]]+"#)
|
||||
private static let minimaxApiTokenRegex = Self.makeRegex(
|
||||
pattern: #"sk-api-[^\s"'`;,)>\]]+"#)
|
||||
|
||||
public static func redact(_ text: String) -> String {
|
||||
guard self.mayContainSensitiveValue(text) else { return text }
|
||||
|
||||
var output = text
|
||||
// Email is broad and safe first
|
||||
output = self.replace(self.emailRegex, in: output, with: "<redacted-email>")
|
||||
// MiniMax tokens before broader rules catch them
|
||||
output = self.replace(self.minimaxCodingPlanTokenRegex, in: output, with: "<redacted-minimax-token>")
|
||||
output = self.replace(self.minimaxApiTokenRegex, in: output, with: "<redacted-minimax-token>")
|
||||
// Bearer catches "bearer <token>" before authorization wraps it
|
||||
output = self.replace(self.bearerRegex, in: output, with: "Bearer <redacted>")
|
||||
// Authorization catches the rest (already-redacted content)
|
||||
output = self.replace(self.cookieHeaderRegex, in: output, with: "$1<redacted>")
|
||||
output = self.replace(self.authorizationRegex, in: output, with: "$1<redacted>")
|
||||
return output
|
||||
}
|
||||
|
||||
private static func mayContainSensitiveValue(_ text: String) -> Bool {
|
||||
if text.range(of: "@") != nil { return true }
|
||||
if text.range(of: "sk-cp-", options: [.caseInsensitive]) != nil { return true }
|
||||
if text.range(of: "sk-api-", options: [.caseInsensitive]) != nil { return true }
|
||||
if text.range(of: "bearer", options: [.caseInsensitive]) != nil { return true }
|
||||
if text.range(of: "cookie", options: [.caseInsensitive]) != nil { return true }
|
||||
if text.range(of: "authorization", options: [.caseInsensitive]) != nil { return true }
|
||||
return false
|
||||
}
|
||||
|
||||
private static func makeRegex(pattern: String, options: NSRegularExpression.Options = []) -> NSRegularExpression {
|
||||
(try? NSRegularExpression(pattern: pattern, options: options)) ?? self.fallbackRegex
|
||||
}
|
||||
|
||||
private static func replace(_ regex: NSRegularExpression, in text: String, with template: String) -> String {
|
||||
let range = NSRange(text.startIndex..<text.endIndex, in: text)
|
||||
return regex.stringByReplacingMatches(in: text, options: [], range: range, withTemplate: template)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
#if canImport(os)
|
||||
import Foundation
|
||||
import Logging
|
||||
import os
|
||||
|
||||
struct OSLogLogHandler: LogHandler {
|
||||
var metadata: Logging.Logger.Metadata = [:]
|
||||
var logLevel: Logging.Logger.Level = .info
|
||||
|
||||
private let label: String
|
||||
private let subsystem: String
|
||||
private let logger: os.Logger
|
||||
|
||||
init(label: String, subsystem: String) {
|
||||
self.label = label
|
||||
self.subsystem = subsystem
|
||||
let category = Self.category(from: label, subsystem: subsystem)
|
||||
self.logger = os.Logger(subsystem: subsystem, category: category)
|
||||
}
|
||||
|
||||
subscript(metadataKey metadataKey: String) -> Logging.Logger.Metadata.Value? {
|
||||
get { self.metadata[metadataKey] }
|
||||
set { self.metadata[metadataKey] = newValue }
|
||||
}
|
||||
|
||||
func log(event: LogEvent) {
|
||||
let msg = Self.decorate(
|
||||
message: event.message.description,
|
||||
label: self.label,
|
||||
subsystem: self.subsystem,
|
||||
metadata: self.metadata,
|
||||
extraMetadata: event.metadata)
|
||||
|
||||
switch event.level {
|
||||
case .trace:
|
||||
self.logger.debug("\(msg, privacy: .public)")
|
||||
case .debug:
|
||||
self.logger.debug("\(msg, privacy: .public)")
|
||||
case .info, .notice:
|
||||
self.logger.info("\(msg, privacy: .public)")
|
||||
case .warning:
|
||||
self.logger.warning("\(msg, privacy: .public)")
|
||||
case .error:
|
||||
self.logger.error("\(msg, privacy: .public)")
|
||||
case .critical:
|
||||
self.logger.fault("\(msg, privacy: .public)")
|
||||
}
|
||||
}
|
||||
|
||||
private static func category(from label: String, subsystem: String) -> String {
|
||||
let prefix = subsystem + "."
|
||||
guard label.hasPrefix(prefix) else { return label }
|
||||
return String(label.dropFirst(prefix.count))
|
||||
}
|
||||
|
||||
private static func decorate(
|
||||
message: String,
|
||||
label: String,
|
||||
subsystem: String,
|
||||
metadata: Logging.Logger.Metadata,
|
||||
extraMetadata: Logging.Logger.Metadata?)
|
||||
-> String
|
||||
{
|
||||
var merged = metadata
|
||||
if let extraMetadata { merged.merge(extraMetadata, uniquingKeysWith: { _, new in new }) }
|
||||
guard !merged.isEmpty else { return message }
|
||||
|
||||
let suffix = merged
|
||||
.sorted(by: { $0.key < $1.key })
|
||||
.map { "\($0.key)=\($0.value)" }
|
||||
.joined(separator: " ")
|
||||
_ = label
|
||||
_ = subsystem
|
||||
return "\(message) (\(suffix))"
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,27 @@
|
||||
public enum ProviderLogging {
|
||||
public static func logStartupState(
|
||||
logger: CodexBarLogger,
|
||||
providers: [UsageProvider],
|
||||
isEnabled: (UsageProvider) -> Bool,
|
||||
modeSnapshot: [String: String])
|
||||
{
|
||||
let ordered = providers.sorted { $0.rawValue < $1.rawValue }
|
||||
let states = ordered
|
||||
.map { provider -> String in
|
||||
let enabled = isEnabled(provider)
|
||||
return "\(provider.rawValue)=\(enabled ? "1" : "0")"
|
||||
}
|
||||
.joined(separator: ",")
|
||||
let enabledProviders = ordered
|
||||
.filter { isEnabled($0) }
|
||||
.map(\.rawValue)
|
||||
.joined(separator: ",")
|
||||
logger.info(
|
||||
"Provider enablement at startup",
|
||||
metadata: [
|
||||
"states": states,
|
||||
"enabled": enabledProviders.isEmpty ? "none" : enabledProviders,
|
||||
])
|
||||
logger.info("Provider mode snapshot", metadata: modeSnapshot)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user