FusionAds-iOS/FusionAds/Classes/fusion/utils/log/FileLogHandler.swift

305 lines
11 KiB
Swift
Raw Normal View History

//
// FileLogHandler.swift
// Pods
//
// Created by 250102 on 2025/5/21.
//
import Foundation
import os.log
/// rotatablerobustefficient
public class FileLogHandler {
// MARK: -
public enum LogLevel: Int, Comparable {
case verbose = 0
case debug = 1
case info = 2
case warning = 3
case error = 4
case fatal = 5
public static func < (lhs: LogLevel, rhs: LogLevel) -> Bool {
return lhs.rawValue < rhs.rawValue
}
public var prefix: String {
switch self {
case .verbose: return "V"
case .debug: return "D"
case .info: return "I"
case .warning: return "W"
case .error: return "E"
case .fatal: return "WTF"
}
}
}
// MARK: -
public struct RotationConfig {
/// 10MB
public var maxFileSize: UInt64
/// 5
public var maxBackupCount: Int
public init(maxFileSize: UInt64 = 10_485_760, // 10MB
maxBackupCount: Int = 5) {
self.maxFileSize = maxFileSize
self.maxBackupCount = maxBackupCount
}
}
// MARK: -
private let fileURL: URL
private let rotationConfig: RotationConfig
private let dateFormatter: DateFormatter
private let minimumLogLevel: LogLevel
private let queue: DispatchQueue
private let fileManager: FileManager
private var fileHandle: FileHandle?
// MARK: -
/// FileLogHandler
/// - :
/// - directory:
/// - filename:
/// - rotationConfig: rotation
/// - minimumLogLevel:
public init(directory: String? = nil,
filename: String = "app.log",
rotationConfig: RotationConfig = RotationConfig(),
minimumLogLevel: LogLevel = .debug) {
self.rotationConfig = rotationConfig
self.minimumLogLevel = minimumLogLevel
//
self.dateFormatter = DateFormatter()
self.dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS"
// 线
self.queue = DispatchQueue(label: "com.fileloghandler.queue", qos: .utility)
// 使
self.fileManager = FileManager.default
//
let baseDirectory: URL
if let directory = directory {
baseDirectory = fileManager.urls(for: .cachesDirectory, in: .userDomainMask).first!.appendingPathComponent("Logs", isDirectory: true).appendingPathComponent(directory, isDirectory: true)
} else {
//
#if os(macOS)
let cacheDir = fileManager.urls(for: .cachesDirectory, in: .userDomainMask).first!
#else
let cacheDir = fileManager.urls(for: .cachesDirectory, in: .userDomainMask).first!
#endif
baseDirectory = cacheDir.appendingPathComponent("Logs", isDirectory: true)
}
//
let logsDirectory = baseDirectory
try? fileManager.createDirectory(at: logsDirectory, withIntermediateDirectories: true, attributes: nil)
// URL
self.fileURL = logsDirectory.appendingPathComponent(filename)
//
self.openLogFile()
}
deinit {
try? fileHandle?.close()
}
// MARK: -
/// 使
/// - :
/// - message:
/// - level:
/// - file:
/// - function:
/// - line:
public func log(_ tag: String, _ message: String,
level: LogLevel = .info) {
guard level >= minimumLogLevel else { return }
queue.async { [weak self] in
guard let self = self else { return }
//
let timestamp = self.dateFormatter.string(from: Date())
let pid = ProcessInfo.processInfo.processIdentifier
let logMessage = "\(timestamp) \(pid) \(level.prefix)/\(tag): \(message)\n"
//
self.writeToFile(logMessage)
// rotate
self.checkSizeRotation()
}
}
/// rotate
public func rotateLogFile() {
queue.async { [weak self] in
self?.performRotation()
}
}
// MARK: -
private func openLogFile() {
do {
//
if !fileManager.fileExists(atPath: fileURL.path) {
try "".write(to: fileURL, atomically: true, encoding: .utf8)
}
//
fileHandle = try FileHandle(forWritingTo: fileURL)
//
fileHandle?.seekToEndOfFile()
} catch {
print("打开日志文件时出错: \(error)")
}
}
private func writeToFile(_ message: String) {
guard let data = message.data(using: .utf8) else { return }
do {
if fileHandle == nil {
// fileHandlenil
openLogFile()
}
if #available(iOS 13.4, macOS 10.15.4, watchOS 6.4, tvOS 13.4, *) {
try fileHandle?.write(contentsOf: data)
} else {
fileHandle?.write(data)
}
} catch {
print("写入日志文件时出错: \(error)")
//
try? fileHandle?.close()
fileHandle = nil
}
}
private func checkSizeRotation() {
do {
let attributes = try fileManager.attributesOfItem(atPath: fileURL.path)
let fileSize = attributes[.size] as? UInt64 ?? 0
if fileSize >= rotationConfig.maxFileSize {
performRotation()
}
} catch {
print("检查文件大小时出错: \(error)")
}
}
private func performRotation() {
do {
//
try fileHandle?.close()
fileHandle = nil
//
let directory = fileURL.deletingLastPathComponent()
let filename = fileURL.lastPathComponent
//
let baseFilename = filename
let pattern = "^\(baseFilename)\\.(\\d+)$"
let regex = try NSRegularExpression(pattern: pattern, options: [])
let logFiles = try fileManager.contentsOfDirectory(atPath: directory.path)
.filter { filename in
let range = NSRange(location: 0, length: filename.utf16.count)
return regex.firstMatch(in: filename, options: [], range: range) != nil
}
.sorted { file1, file2 in
//
let range1 = NSRange(location: 0, length: file1.utf16.count)
let range2 = NSRange(location: 0, length: file2.utf16.count)
guard let match1 = regex.firstMatch(in: file1, options: [], range: range1),
let match2 = regex.firstMatch(in: file2, options: [], range: range2) else {
return false
}
let numberRange1 = match1.range(at: 1)
let numberRange2 = match2.range(at: 1)
let number1 = strToInt(file1.utf16.dropFirst(baseFilename.utf16.count + 1).prefix(numberRange1.length)) ?? 0
let number2 = strToInt(file2.utf16.dropFirst(baseFilename.utf16.count + 1).prefix(numberRange2.length)) ?? 0
return number1 < number2
}
//
if logFiles.count >= rotationConfig.maxBackupCount {
for i in (rotationConfig.maxBackupCount - 1)..<logFiles.count {
try fileManager.removeItem(at: directory.appendingPathComponent(logFiles[i]))
}
}
//
for i in (0..<logFiles.count).reversed() {
let file = logFiles[i]
let range = NSRange(location: 0, length: file.utf16.count)
guard let match = regex.firstMatch(in: file, options: [], range: range) else {
continue
}
let numberRange = match.range(at: 1)
guard let number = strToInt(file.utf16.dropFirst(baseFilename.utf16.count + 1).prefix(numberRange.length)) else {
continue
}
if number < rotationConfig.maxBackupCount - 1 {
let oldPath = directory.appendingPathComponent(file)
let newPath = directory.appendingPathComponent("\(baseFilename).\(number + 1)")
try fileManager.moveItem(at: oldPath, to: newPath)
} else {
try fileManager.removeItem(at: directory.appendingPathComponent(file))
}
}
//
let newBackupPath = directory.appendingPathComponent("\(baseFilename).1")
try fileManager.moveItem(at: fileURL, to: newBackupPath)
//
openLogFile()
// rotation
let timestamp = dateFormatter.string(from: Date())
let rotationMessage = "[\(timestamp)] [I] [FileLogHandler] Rotated\n"
writeToFile(rotationMessage)
} catch {
print("rotate日志文件时出错: \(error)")
// 使rotate
openLogFile()
}
}
private func strToInt(_ number: Substring.UTF16View) -> Int? {
guard let numStr = String(number) else {
return nil
}
return Int(numStr)
}
}