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

305 lines
11 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

//
// 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)
}
}