-
Notifications
You must be signed in to change notification settings - Fork 28
/
FileRotationLogger.swift
182 lines (157 loc) · 7.53 KB
/
FileRotationLogger.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
@preconcurrency import Dispatch
import Foundation
public struct FileRotationLogger: FileLoggerable {
public let label: String
public let queue: DispatchQueue
public let logLevel: LogLevel
public let logFormat: LogFormattable?
public let fileURL: URL
public let filePermission: String
let rotationConfig: RotationConfig
private weak var delegate: FileRotationLoggerDelegate?
private var dateFormat: DateFormatter
public init(_ label: String, logLevel: LogLevel = .trace, logFormat: LogFormattable? = nil, fileURL: URL, filePermission: String = "640", rotationConfig: RotationConfig, delegate: FileRotationLoggerDelegate? = nil) throws {
self.label = label
self.queue = DispatchQueue(label: label)
self.logLevel = logLevel
self.logFormat = logFormat
self.dateFormat = DateFormatter()
self.dateFormat.dateFormat = "yyyyMMdd'T'HHmmssZZZZZ"
self.dateFormat.timeZone = TimeZone(identifier: "UTC")
self.dateFormat.locale = Locale(identifier: "en_US_POSIX")
self.fileURL = fileURL
puppyDebug("initialized, fileURL: \(fileURL)")
self.filePermission = filePermission
self.rotationConfig = rotationConfig
self.delegate = delegate
try validateFileURL(fileURL)
try validateFilePermission(fileURL, filePermission: filePermission)
try openFile()
}
public func log(_ level: LogLevel, string: String) {
rotateFiles()
append(level, string: string)
rotateFiles()
}
private func fileSize(_ fileURL: URL) throws -> UInt64 {
#if os(Windows)
return try FileManager.default.windowsFileSize(atPath: fileURL.path)
#else
let attributes = try FileManager.default.attributesOfItem(atPath: fileURL.path)
// swiftlint:disable force_cast
return attributes[.size] as! UInt64
// swiftlint:enable force_cast
#endif
}
private func rotateFiles() {
guard let size = try? fileSize(fileURL), size > rotationConfig.maxFileSize else { return }
// Rotates old archived files.
rotateOldArchivedFiles()
// Archives the target file.
archiveTargetFiles()
// Removes extra archived files.
removeArchivedFiles(fileURL, maxArchivedFilesCount: rotationConfig.maxArchivedFilesCount)
// Opens a new target file.
do {
puppyDebug("will openFile in rotateFiles")
try openFile()
} catch {
print("error in openFile while rotating, error: \(error.localizedDescription)")
}
}
private func archiveTargetFiles() {
do {
var archivedFileURL: URL
switch rotationConfig.suffixExtension {
case .numbering:
archivedFileURL = fileURL.appendingPathExtension("1")
case .date_uuid:
archivedFileURL = fileURL.appendingPathExtension(dateFormatter(Date(), withFormatter: self.dateFormat) + "_" + UUID().uuidString.lowercased())
}
try FileManager.default.moveItem(at: fileURL, to: archivedFileURL)
delegate?.fileRotationLogger(self, didArchiveFileURL: fileURL, toFileURL: archivedFileURL)
} catch {
print("error in archiving the target file, error: \(error.localizedDescription)")
}
}
private func rotateOldArchivedFiles() {
switch rotationConfig.suffixExtension {
case .numbering:
do {
let oldArchivedFileURLs = ascArchivedFileURLs(fileURL)
for (index, oldArchivedFileURL) in oldArchivedFileURLs.enumerated() {
let generationNumber = oldArchivedFileURLs.count + 1 - index
let rotatedFileURL = oldArchivedFileURL.deletingPathExtension().appendingPathExtension("\(generationNumber)")
puppyDebug("generationNumber: \(generationNumber), rotatedFileURL: \(rotatedFileURL)")
if !FileManager.default.fileExists(atPath: rotatedFileURL.path) {
try FileManager.default.moveItem(at: oldArchivedFileURL, to: rotatedFileURL)
}
}
} catch {
print("error in rotating old archive files, error: \(error.localizedDescription)")
}
case .date_uuid:
break
}
}
private func ascArchivedFileURLs(_ fileURL: URL) -> [URL] {
var ascArchivedFileURLs: [URL] = []
do {
let archivedDirectoryURL: URL = fileURL.deletingLastPathComponent()
let archivedFileURLs = try FileManager.default.contentsOfDirectory(atPath: archivedDirectoryURL.path)
.map { archivedDirectoryURL.appendingPathComponent($0) }
.filter { $0 != fileURL && $0.deletingPathExtension() == fileURL }
ascArchivedFileURLs = try archivedFileURLs.sorted {
#if os(Windows)
let modificationTime0 = try FileManager.default.windowsModificationTime(atPath: $0.path)
let modificationTime1 = try FileManager.default.windowsModificationTime(atPath: $1.path)
return modificationTime0 < modificationTime1
#else
// swiftlint:disable force_cast
let modificationDate0 = try FileManager.default.attributesOfItem(atPath: $0.path)[.modificationDate] as! Date
let modificationDate1 = try FileManager.default.attributesOfItem(atPath: $1.path)[.modificationDate] as! Date
// swiftlint:enable force_cast
return modificationDate0.timeIntervalSince1970 < modificationDate1.timeIntervalSince1970
#endif
}
} catch {
print("error in ascArchivedFileURLs, error: \(error.localizedDescription)")
}
puppyDebug("ascArchivedFileURLs: \(ascArchivedFileURLs)")
return ascArchivedFileURLs
}
private func removeArchivedFiles(_ fileURL: URL, maxArchivedFilesCount: UInt8) {
do {
let archivedFileURLs = ascArchivedFileURLs(fileURL)
if archivedFileURLs.count > maxArchivedFilesCount {
for index in 0 ..< archivedFileURLs.count - Int(maxArchivedFilesCount) {
puppyDebug("\(archivedFileURLs[index]) will be removed...")
try FileManager.default.removeItem(at: archivedFileURLs[index])
puppyDebug("\(archivedFileURLs[index]) has been removed")
delegate?.fileRotationLogger(self, didRemoveArchivedFileURL: archivedFileURLs[index])
}
}
} catch {
print("error in removing extra archived files, error: \(error.localizedDescription)")
}
}
}
public struct RotationConfig: Sendable {
public enum SuffixExtension: Sendable {
case numbering
case date_uuid
}
public var suffixExtension: SuffixExtension
public typealias ByteCount = UInt64
public var maxFileSize: ByteCount
public var maxArchivedFilesCount: UInt8
public init(suffixExtension: SuffixExtension = .numbering, maxFileSize: ByteCount = 10 * 1024 * 1024, maxArchivedFilesCount: UInt8 = 5) {
self.suffixExtension = suffixExtension
self.maxFileSize = maxFileSize
self.maxArchivedFilesCount = maxArchivedFilesCount
}
}
public protocol FileRotationLoggerDelegate: AnyObject, Sendable {
func fileRotationLogger(_ fileRotationLogger: FileRotationLogger, didArchiveFileURL: URL, toFileURL: URL)
func fileRotationLogger(_ fileRotationLogger: FileRotationLogger, didRemoveArchivedFileURL: URL)
}