Skip to content

Commit 2f54516

Browse files
committed
Added size and date based rotation to FileAppender
1 parent 5d881ce commit 2f54516

File tree

7 files changed

+367
-34
lines changed

7 files changed

+367
-34
lines changed

Log4swift.xcodeproj/project.pbxproj

+12
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@
77
objects = {
88

99
/* Begin PBXBuildFile section */
10+
0201CDC421199725006E3608 /* MultithreadingUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0201CDC321199725006E3608 /* MultithreadingUtilities.swift */; };
11+
0201CDC521199725006E3608 /* MultithreadingUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0201CDC321199725006E3608 /* MultithreadingUtilities.swift */; };
12+
0201CDC621199725006E3608 /* MultithreadingUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0201CDC321199725006E3608 /* MultithreadingUtilities.swift */; };
13+
0201CDC821199C8F006E3608 /* FileAppenderPerformanceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0201CDC721199C8F006E3608 /* FileAppenderPerformanceTests.swift */; };
1014
021136B31B55A85A000954DF /* ValidCompleteConfiguration.plist in Resources */ = {isa = PBXBuildFile; fileRef = 021136B21B55A85A000954DF /* ValidCompleteConfiguration.plist */; };
1115
021DCE491BE17C4F0026633E /* Logger-AsynchronicityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 021DCE481BE17C4F0026633E /* Logger-AsynchronicityTests.swift */; };
1216
0241A2D81C32ACD400A624CF /* FileObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0241A2D71C32ACD400A624CF /* FileObserver.swift */; };
@@ -145,6 +149,8 @@
145149
/* End PBXContainerItemProxy section */
146150

147151
/* Begin PBXFileReference section */
152+
0201CDC321199725006E3608 /* MultithreadingUtilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultithreadingUtilities.swift; sourceTree = "<group>"; };
153+
0201CDC721199C8F006E3608 /* FileAppenderPerformanceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileAppenderPerformanceTests.swift; sourceTree = "<group>"; };
148154
021136B21B55A85A000954DF /* ValidCompleteConfiguration.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = ValidCompleteConfiguration.plist; sourceTree = "<group>"; };
149155
021DCE481BE17C4F0026633E /* Logger-AsynchronicityTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = "Logger-AsynchronicityTests.swift"; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
150156
0237CF351B2E220F004D5B9F /* SimpleUsage-OSX.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; path = "SimpleUsage-OSX.playground"; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
@@ -290,6 +296,7 @@
290296
D9322BF81B440E7D00C9F6D9 /* Array+utilities.swift */,
291297
0241A2D71C32ACD400A624CF /* FileObserver.swift */,
292298
78389E461ECC26B0008898E7 /* Class+utilities.swift */,
299+
0201CDC321199725006E3608 /* MultithreadingUtilities.swift */,
293300
);
294301
path = Utilities;
295302
sourceTree = "<group>";
@@ -388,6 +395,7 @@
388395
02B61A4A1B56D16900C683D8 /* PerformanceTests.swift */,
389396
5E06D56D1B98810E00273AC7 /* PatternFormatterPerformanceTests.swift */,
390397
02B61A641B56D1F200C683D8 /* log4swiftPerformanceTests-Info.plist */,
398+
0201CDC721199C8F006E3608 /* FileAppenderPerformanceTests.swift */,
391399
);
392400
path = Log4swiftPerformanceTests;
393401
sourceTree = "<group>";
@@ -683,6 +691,7 @@
683691
02664C311B2DA6A200B695DE /* Appender.swift in Sources */,
684692
5E66F76A1B42C959009395CE /* Bool+utilities.swift in Sources */,
685693
02664C271B2D92CB00B695DE /* LoggerFactory.swift in Sources */,
694+
0201CDC421199725006E3608 /* MultithreadingUtilities.swift in Sources */,
686695
5E484E511B46843D00CA5C01 /* Errors.swift in Sources */,
687696
5ED244E51B34408600453B31 /* Formatter.swift in Sources */,
688697
02664C251B2CEB2900B695DE /* Logger.swift in Sources */,
@@ -739,6 +748,7 @@
739748
buildActionMask = 2147483647;
740749
files = (
741750
0285F3BA1CC01FE200CA850D /* XCTestCase+fileAdditions.swift in Sources */,
751+
0201CDC821199C8F006E3608 /* FileAppenderPerformanceTests.swift in Sources */,
742752
5E06D56E1B98810E00273AC7 /* PatternFormatterPerformanceTests.swift in Sources */,
743753
02C9E1581B56D40F0038AB0E /* PerformanceTests.swift in Sources */,
744754
);
@@ -753,6 +763,7 @@
753763
5E3F86E51DC25EDF008F296C /* Logger.swift in Sources */,
754764
5E3F86E21DC25EDF008F296C /* LoggerFactory.swift in Sources */,
755765
5E3F86E91DC25EE3008F296C /* Appender.swift in Sources */,
766+
0201CDC621199725006E3608 /* MultithreadingUtilities.swift in Sources */,
756767
5E3F86F01DC25EE8008F296C /* PatternFormatter.swift in Sources */,
757768
5E3F86F41DC25EED008F296C /* FileObserver.swift in Sources */,
758769
5E3F86E31DC25EDF008F296C /* LoggerFactory+loadFromFile.swift in Sources */,
@@ -785,6 +796,7 @@
785796
5E3E71FD1B7A185400EEE46B /* Logger.swift in Sources */,
786797
5E3E71FE1B7A185400EEE46B /* Logger+convenience.swift in Sources */,
787798
5E3E72141B7A263100EEE46B /* LoggerClient.m in Sources */,
799+
0201CDC521199725006E3608 /* MultithreadingUtilities.swift in Sources */,
788800
5E3E71FF1B7A185400EEE46B /* Logger+objectiveC.swift in Sources */,
789801
5E3E72001B7A185400EEE46B /* LoggerFactory.swift in Sources */,
790802
5E3E72011B7A185400EEE46B /* LoggerFactory+loadFromFile.swift in Sources */,

Log4swift.xcodeproj/xcshareddata/xcschemes/log4swift-OSX.xcscheme

+4-2
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,9 @@
2626
buildConfiguration = "Debug"
2727
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
2828
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
29-
codeCoverageEnabled = "YES"
30-
shouldUseLaunchSchemeArgsEnv = "YES">
29+
language = ""
30+
shouldUseLaunchSchemeArgsEnv = "YES"
31+
codeCoverageEnabled = "YES">
3132
<Testables>
3233
<TestableReference
3334
skipped = "NO">
@@ -56,6 +57,7 @@
5657
buildConfiguration = "Debug"
5758
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
5859
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
60+
language = ""
5961
launchStyle = "0"
6062
useCustomWorkingDirectory = "NO"
6163
ignoresPersistentStateOnLaunch = "NO"

Log4swift/Appenders/FileAppender.swift

+96-7
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ If file does not exist, it will be created on the first log, or re-created if de
2727
public class FileAppender : Appender {
2828
public enum DictionaryKey: String {
2929
case FilePath = "FilePath"
30+
case MaxFileAge = "MaxFileAge"
31+
case MaxFileSize = "MaxFileSize"
3032
}
3133

3234
@objc
@@ -40,16 +42,47 @@ public class FileAppender : Appender {
4042
didLogFailure = false
4143
}
4244
}
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+
4358
private var fileHandler: FileHandle?
59+
private var currentFileSize: UInt64?
60+
private var currentFileCreationCreationDate: Date?
4461
private var didLogFailure = false
62+
private var loggingMutex = PThreadMutex()
4563

4664
@objc
4765
public init(identifier: String, filePath: String) {
4866
self.fileHandler = nil
67+
self.currentFileSize = nil
68+
self.currentFileCreationCreationDate = nil
4969
self.filePath = (filePath as NSString).expandingTildeInPath
5070

5171
super.init(identifier)
5272
}
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+
}
5386

5487
public required convenience init(_ identifier: String) {
5588
self.init(identifier: identifier, filePath: "/dev/null")
@@ -66,17 +99,25 @@ public class FileAppender : Appender {
6699
}
67100
}
68101

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.
69105
public override func performLog(_ log: String, level: LogLevel, info: LogInfoDictionary) {
70-
guard createFileHandlerIfNeeded() else {
71-
return
72-
}
73-
106+
74107
var normalizedLog = log
75108
if(!normalizedLog.hasSuffix("\n")) {
76109
normalizedLog = normalizedLog + "\n"
77110
}
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+
}
80121
}
81122
}
82123

@@ -92,10 +133,15 @@ public class FileAppender : Appender {
92133
try fileManager.createDirectory(atPath: directoryPath, withIntermediateDirectories: true, attributes: nil)
93134

94135
fileManager.createFile(atPath: filePath, contents: nil, attributes: nil)
136+
self.currentFileCreationCreationDate = Date()
137+
self.currentFileSize = 0
95138
}
96-
if fileHandler == nil {
139+
if self.fileHandler == nil {
97140
self.fileHandler = FileHandle(forWritingAtPath: self.filePath)
98141
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()
99145
}
100146
didLogFailure = false
101147

@@ -109,5 +155,48 @@ public class FileAppender : Appender {
109155
return self.fileHandler != nil
110156
}
111157

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+
}
112201
}
113202

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
//
2+
// MultithreadingUtilities.swift
3+
// Log4swift
4+
//
5+
// Created by Jérôme Duquennoy on 07/08/2018.
6+
// Copyright © 2018 jerome. All rights reserved.
7+
//
8+
9+
import Foundation
10+
11+
internal final class PThreadMutex {
12+
public typealias MutexPrimitive = pthread_mutex_t
13+
14+
public enum PThreadMutexType {
15+
case normal // PTHREAD_MUTEX_NORMAL
16+
case recursive // PTHREAD_MUTEX_RECURSIVE
17+
}
18+
19+
public var unsafeMutex = pthread_mutex_t()
20+
21+
/// - parameter type: wether the mutex is a normal or recursive one. Default value is normal.
22+
public init(type: PThreadMutexType = .normal) {
23+
var attr = pthread_mutexattr_t()
24+
guard pthread_mutexattr_init(&attr) == 0 else {
25+
preconditionFailure()
26+
}
27+
switch type {
28+
case .normal:
29+
pthread_mutexattr_settype(&attr, Int32(PTHREAD_MUTEX_NORMAL))
30+
case .recursive:
31+
pthread_mutexattr_settype(&attr, Int32(PTHREAD_MUTEX_RECURSIVE))
32+
}
33+
guard pthread_mutex_init(&unsafeMutex, &attr) == 0 else {
34+
preconditionFailure()
35+
}
36+
pthread_mutexattr_destroy(&attr)
37+
}
38+
39+
deinit {
40+
pthread_mutex_destroy(&unsafeMutex)
41+
}
42+
43+
public func unbalancedLock() {
44+
pthread_mutex_lock(&unsafeMutex)
45+
}
46+
47+
public func unbalancedTryLock() -> Bool {
48+
return pthread_mutex_trylock(&unsafeMutex) == 0
49+
}
50+
51+
public func unbalancedUnlock() {
52+
pthread_mutex_unlock(&unsafeMutex)
53+
}
54+
}
55+
56+
internal extension PThreadMutex {
57+
/// Executes a closure as a critical section (not executable concurrently by different threads).
58+
internal func sync<R>(execute: () throws -> R) rethrows -> R {
59+
self.unbalancedLock()
60+
defer { self.unbalancedUnlock() }
61+
return try execute()
62+
}
63+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
//
2+
// FileAppenderPerformanceTests.swift
3+
// log4swiftPerformanceTests
4+
//
5+
// Created by Jérôme Duquennoy on 07/08/2018.
6+
// Copyright © 2018 jerome. All rights reserved.
7+
//
8+
9+
import XCTest
10+
import Log4swift
11+
12+
class FileAppenderPerformanceTests: XCTestCase {
13+
var logFilePath: String = ""
14+
15+
override func setUp() {
16+
XCTAssertNoThrow(
17+
self.logFilePath = try self.createTemporaryFilePath(fileExtension: "log")
18+
)
19+
}
20+
21+
override func tearDown() {
22+
try! FileManager().removeItem(atPath: self.logFilePath)
23+
}
24+
25+
26+
func testFileAppenderPerformanceWhenFileIsNotDeleted() {
27+
do {
28+
let tempFilePath = try self.createTemporaryFilePath(fileExtension: "log")
29+
let fileAppender = FileAppender(identifier: "test.appender", filePath: tempFilePath)
30+
defer {
31+
unlink((tempFilePath as NSString).fileSystemRepresentation)
32+
}
33+
34+
measure { () -> Void in
35+
for _ in 1...1000 {
36+
fileAppender.log("This is a test log", level: LogLevel.Debug, info: LogInfoDictionary())
37+
}
38+
}
39+
} catch let error {
40+
XCTAssert(false, "Error in test : \(error)")
41+
}
42+
}
43+
44+
func testPerformanceWithoutRotation() throws {
45+
let appender = FileAppender(identifier: "testAppender", filePath: self.logFilePath)
46+
47+
self.measure {
48+
for _ in 1...10_000 {
49+
appender.performLog("This is a test log string", level: .Info, info: LogInfoDictionary())
50+
}
51+
}
52+
}
53+
54+
func testPerformanceWithDateRotationTrigger() throws {
55+
let appender = FileAppender(identifier: "testAppender", filePath: self.logFilePath, maxFileAge: 60*60)
56+
57+
self.measure {
58+
for _ in 1...10_000 {
59+
appender.performLog("This is a test log string", level: .Info, info: LogInfoDictionary())
60+
}
61+
}
62+
}
63+
64+
func testPerformanceWithSizeRotationTrigger() throws {
65+
let appender = FileAppender(identifier: "testAppender", filePath: self.logFilePath, maxFileSize: 1024 * 1024)
66+
67+
self.measure {
68+
for _ in 1...10_000 {
69+
appender.performLog("This is a test log string", level: .Info, info: LogInfoDictionary())
70+
}
71+
}
72+
}
73+
74+
}

0 commit comments

Comments
 (0)