| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138 |
- import Foundation
- import OSLog
- public struct AirAcceptEvent: Codable, Equatable, Sendable {
- public var timestamp: Date
- public var hostProcess: String
- public var summary: String
- public var deviceName: String?
- public var action: String
- public var reason: String?
- public init(
- timestamp: Date = Date(),
- hostProcess: String,
- summary: String,
- deviceName: String?,
- action: String,
- reason: String?
- ) {
- self.timestamp = timestamp
- self.hostProcess = hostProcess
- self.summary = summary
- self.deviceName = deviceName
- self.action = action
- self.reason = reason
- }
- }
- public final class AirAcceptLogger {
- public let logURL: URL
- private let logger = Logger(subsystem: "com.airaccept.AirAccept", category: "AirAccept")
- private let fileManager: FileManager
- private let encoder: JSONEncoder
- private let maxLogSizeBytes = 1_000_000
- public init(
- logURL: URL = AirAcceptLogger.defaultLogURL(),
- fileManager: FileManager = .default
- ) {
- self.logURL = logURL
- self.fileManager = fileManager
- self.encoder = JSONEncoder()
- self.encoder.dateEncodingStrategy = .iso8601
- self.encoder.outputFormatting = [.sortedKeys]
- }
- public static func defaultLogURL() -> URL {
- FileManager.default.homeDirectoryForCurrentUser
- .appendingPathComponent("Library/Logs/AirAccept", isDirectory: true)
- .appendingPathComponent("airaccept.log", isDirectory: false)
- }
- public func info(_ message: String) {
- logger.info("\(message, privacy: .public)")
- appendLine("[info] \(isoTimestamp()) \(message)")
- }
- public func error(_ message: String) {
- logger.error("\(message, privacy: .public)")
- appendLine("[error] \(isoTimestamp()) \(message)")
- }
- public func event(_ event: AirAcceptEvent) {
- var event = event
- event.summary = sanitize(event.summary, limit: 600)
- event.reason = event.reason.map { sanitize($0, limit: 240) }
- logger.info(
- "host=\(event.hostProcess, privacy: .public) action=\(event.action, privacy: .public) device=\(event.deviceName ?? "unknown", privacy: .public) reason=\(event.reason ?? "", privacy: .public)"
- )
- do {
- let data = try encoder.encode(event)
- let line = String(decoding: data, as: UTF8.self)
- appendLine(line)
- } catch {
- appendLine("[error] \(isoTimestamp()) failed to encode event: \(error.localizedDescription)")
- }
- }
- private func appendLine(_ line: String) {
- do {
- try fileManager.createDirectory(
- at: logURL.deletingLastPathComponent(),
- withIntermediateDirectories: true
- )
- try rotateLogIfNeeded()
- let data = Data((line + "\n").utf8)
- if fileManager.fileExists(atPath: logURL.path) {
- let handle = try FileHandle(forWritingTo: logURL)
- try handle.seekToEnd()
- try handle.write(contentsOf: data)
- try handle.close()
- } else {
- try data.write(to: logURL, options: [.atomic])
- }
- } catch {
- logger.error("failed to append log file: \(error.localizedDescription, privacy: .public)")
- }
- }
- private func rotateLogIfNeeded() throws {
- guard
- fileManager.fileExists(atPath: logURL.path),
- let size = try fileManager.attributesOfItem(atPath: logURL.path)[.size] as? NSNumber,
- size.intValue >= maxLogSizeBytes
- else {
- return
- }
- let rotatedLogURL = logURL.deletingPathExtension().appendingPathExtension("log.1")
- if fileManager.fileExists(atPath: rotatedLogURL.path) {
- try fileManager.removeItem(at: rotatedLogURL)
- }
- try fileManager.moveItem(at: logURL, to: rotatedLogURL)
- }
- private func isoTimestamp() -> String {
- ISO8601DateFormatter().string(from: Date())
- }
- private func sanitize(_ value: String, limit: Int) -> String {
- var result = value
- .replacingOccurrences(of: "\n", with: " ")
- .replacingOccurrences(of: "\r", with: " ")
- .replacingOccurrences(of: "\t", with: " ")
- while result.contains(" ") {
- result = result.replacingOccurrences(of: " ", with: " ")
- }
- if result.count > limit {
- return String(result.prefix(limit - 3)) + "..."
- }
- return result
- }
- }
|