@@ -27,6 +27,8 @@ If file does not exist, it will be created on the first log, or re-created if de
27
27
public class FileAppender : Appender {
28
28
public enum DictionaryKey : String {
29
29
case FilePath = " FilePath "
30
+ case MaxFileAge = " MaxFileAge "
31
+ case MaxFileSize = " MaxFileSize "
30
32
}
31
33
32
34
@objc
@@ -40,16 +42,47 @@ public class FileAppender : Appender {
40
42
didLogFailure = false
41
43
}
42
44
}
45
+
46
+ /// The maximum size of the file in octets before rotation is triggered.
47
+ /// Nil or zero disables the file size trigger for rotation
48
+ public var maxFileSize : UInt64 ?
49
+
50
+ /// The maximum age of the file in seconds before rotation is triggered.
51
+ /// Nil or zero disables the file age trigger for rotation.
52
+ public var maxFileAge : TimeInterval ?
53
+
54
+ /// The maximum number of rotated log files kept.
55
+ /// Files exceeding this limit will be deleted during rotation.
56
+ public var maxRotatedFiles : UInt ?
57
+
43
58
private var fileHandler : FileHandle ?
59
+ private var currentFileSize : UInt64 ?
60
+ private var currentFileCreationCreationDate : Date ?
44
61
private var didLogFailure = false
62
+ private var loggingMutex = PThreadMutex ( )
45
63
46
64
@objc
47
65
public init ( identifier: String , filePath: String ) {
48
66
self . fileHandler = nil
67
+ self . currentFileSize = nil
68
+ self . currentFileCreationCreationDate = nil
49
69
self . filePath = ( filePath as NSString ) . expandingTildeInPath
50
70
51
71
super. init ( identifier)
52
72
}
73
+
74
+ /// - Parameter identifier: the identifier of the appender.
75
+ /// - Parameter filePath: the path to the logfile. If possible and needed, the directory
76
+ /// structure will be created when creating the log file.
77
+ /// - Parameter maxFileSize: the maximum size of the file in octets before rotation is triggered.
78
+ /// Nil or zero disables the file size trigger for rotation. Default value is nil.
79
+ /// - Parameter maxFileAge: the maximum age of the file in seconds before rotation is triggered.
80
+ /// Nil or zero disables the file age trigger for rotation. Default value is nil.
81
+ public convenience init ( identifier: String , filePath: String , maxFileSize: UInt64 ? = nil , maxFileAge: TimeInterval ? = nil ) {
82
+ self . init ( identifier: identifier, filePath: filePath)
83
+ self . maxFileAge = maxFileAge
84
+ self . maxFileSize = maxFileSize
85
+ }
53
86
54
87
public required convenience init ( _ identifier: String ) {
55
88
self . init ( identifier: identifier, filePath: " /dev/null " )
@@ -66,17 +99,25 @@ public class FileAppender : Appender {
66
99
}
67
100
}
68
101
102
+ /// This is the only entry point to log.
103
+ /// It is thread safe, calling that method from multiple threads will not
104
+ // cause logs to interleave, or mess with the rotation mechanism.
69
105
public override func performLog( _ log: String , level: LogLevel , info: LogInfoDictionary ) {
70
- guard createFileHandlerIfNeeded ( ) else {
71
- return
72
- }
73
-
106
+
74
107
var normalizedLog = log
75
108
if ( !normalizedLog. hasSuffix ( " \n " ) ) {
76
109
normalizedLog = normalizedLog + " \n "
77
110
}
78
- if let dataToLog = normalizedLog. data ( using: String . Encoding. utf8, allowLossyConversion: true ) {
79
- self . fileHandler? . write ( dataToLog)
111
+
112
+ loggingMutex. sync {
113
+ try ? rotateFileIfNeeded ( )
114
+ guard createFileHandlerIfNeeded ( ) else {
115
+ return
116
+ }
117
+ if let dataToLog = normalizedLog. data ( using: String . Encoding. utf8, allowLossyConversion: true ) {
118
+ self . fileHandler? . write ( dataToLog)
119
+ self . currentFileSize? += UInt64 ( dataToLog. count)
120
+ }
80
121
}
81
122
}
82
123
@@ -92,10 +133,15 @@ public class FileAppender : Appender {
92
133
try fileManager. createDirectory ( atPath: directoryPath, withIntermediateDirectories: true , attributes: nil )
93
134
94
135
fileManager. createFile ( atPath: filePath, contents: nil , attributes: nil )
136
+ self . currentFileCreationCreationDate = Date ( )
137
+ self . currentFileSize = 0
95
138
}
96
- if fileHandler == nil {
139
+ if self . fileHandler == nil {
97
140
self . fileHandler = FileHandle ( forWritingAtPath: self . filePath)
98
141
self . fileHandler? . seekToEndOfFile ( )
142
+ let fileAttributes = try fileManager. attributesOfItem ( atPath: self . filePath)
143
+ self . currentFileSize = fileAttributes [ FileAttributeKey . size] as? UInt64 ?? 0
144
+ self . currentFileCreationCreationDate = fileAttributes [ FileAttributeKey . creationDate] as? Date ?? Date ( )
99
145
}
100
146
didLogFailure = false
101
147
@@ -109,5 +155,48 @@ public class FileAppender : Appender {
109
155
return self . fileHandler != nil
110
156
}
111
157
158
+ private func rotateFileIfNeeded( ) throws {
159
+ guard shouldFileRotateForAge ( ) || shouldFileRotateForSize ( ) else { return }
160
+
161
+ self . fileHandler? . closeFile ( )
162
+ self . fileHandler = nil
163
+
164
+ let fileManager = FileManager . default
165
+ let fileUrl = URL ( fileURLWithPath: self . filePath)
166
+ let logFileName = fileUrl. lastPathComponent
167
+ let logFileDirectory = fileUrl. deletingLastPathComponent ( )
168
+
169
+ let files = try fileManager. contentsOfDirectory ( atPath: logFileDirectory. path)
170
+ . filter { $0. hasPrefix ( logFileName) }
171
+ . sorted { $0. localizedStandardCompare ( $1) == . orderedAscending }
172
+ . reversed ( )
173
+
174
+ var currentFileIndex = UInt ( files. count)
175
+ try files. forEach { currentFileName in
176
+ let newFileName = logFileName. appending ( " . \( currentFileIndex) " )
177
+ let currentFilePath = logFileDirectory. appendingPathComponent ( currentFileName)
178
+ let rotatedFilePath = logFileDirectory. appendingPathComponent ( newFileName)
179
+
180
+ if let maxRotatedFiles = self . maxRotatedFiles, currentFileIndex > maxRotatedFiles {
181
+ try fileManager. removeItem ( at: currentFilePath)
182
+ } else {
183
+ try fileManager. moveItem ( at: currentFilePath,
184
+ to: rotatedFilePath)
185
+ }
186
+ currentFileIndex -= 1
187
+ }
188
+ }
189
+
190
+ private func shouldFileRotateForAge( ) -> Bool {
191
+ guard let maxFileAge = self . maxFileAge, let fileDate = self . currentFileCreationCreationDate else { return false }
192
+
193
+ return Date ( ) . timeIntervalSince ( fileDate) >= maxFileAge
194
+ }
195
+
196
+ private func shouldFileRotateForSize( ) -> Bool {
197
+ guard let maxFileSize = self . maxFileSize, let currentFileSize = self . currentFileSize else { return false }
198
+
199
+ return currentFileSize >= maxFileSize
200
+ }
112
201
}
113
202
0 commit comments