diff --git a/CleanroomLogger.podspec b/CleanroomLogger.podspec new file mode 100644 index 00000000..d21c69aa --- /dev/null +++ b/CleanroomLogger.podspec @@ -0,0 +1,16 @@ +Pod::Spec.new do |s| + s.name = 'CleanroomLogger' + s.version = '7.0.1' + s.summary = 'Extensible Swift-based logging API that is simple, lightweight and performant' + s.homepage = 'https://github.com/emaloney/CleanroomLogger' + s.author = 'emaloney' + s.source = { :git => 'https://github.com/emaloney/CleanroomLogger.git', :tag => s.version } + s.ios.deployment_target = "9.0" + s.watchos.deployment_target = "4.0" + s.tvos.deployment_target = "12.0" + s.osx.deployment_target = "10.10" + s.source_files = 'Sources/*.swift' + s.license = 'MIT' + + s.swift_version = '5.0' +end \ No newline at end of file diff --git a/CleanroomLogger.xcodeproj/project.pbxproj b/CleanroomLogger.xcodeproj/project.pbxproj index fc765979..6047dcac 100644 --- a/CleanroomLogger.xcodeproj/project.pbxproj +++ b/CleanroomLogger.xcodeproj/project.pbxproj @@ -311,7 +311,6 @@ TargetAttributes = { 3B9059021DAECB5200B4EEC0 = { CreatedOnToolsVersion = 8.0; - DevelopmentTeam = MTP6A36P8K; LastSwiftMigration = 0900; ProvisioningStyle = Automatic; }; @@ -328,6 +327,7 @@ developmentRegion = English; hasScannedForEncodings = 0; knownRegions = ( + English, en, ); mainGroup = 3B9058F91DAECB5200B4EEC0; @@ -548,6 +548,7 @@ baseConfigurationReference = 3B9059251DAECB9E00B4EEC0 /* Debug.xcconfig */; buildSettings = { DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 35; DYLIB_INSTALL_NAME_BASE = "@rpath"; @@ -562,6 +563,7 @@ baseConfigurationReference = 3B9059281DAECB9E00B4EEC0 /* Release.xcconfig */; buildSettings = { DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 35; DYLIB_INSTALL_NAME_BASE = "@rpath"; diff --git a/Sources/Log.swift b/Sources/Log.swift index 5986196e..ad213d9f 100644 --- a/Sources/Log.swift +++ b/Sources/Log.swift @@ -233,6 +233,28 @@ public struct Log } logLock.unlock() } + + /** + Disables the logger, setting all existing channels to nil. + + Disabling the logger allows you to restart/reconfigure the logger in real-time without the need to restart the whole app. + */ + public static func disable() + { + logLock.lock() + + if didEnable + { + self.error = nil + self.warning = nil + self.info = nil + self.debug = nil + self.verbose = nil + didEnable = false + } + + logLock.unlock() + } private static let logLock = NSLock() private static var didEnable = false diff --git a/Sources/RotatingLogFileConfiguration.swift b/Sources/RotatingLogFileConfiguration.swift index bc3b2262..6816abb3 100644 --- a/Sources/RotatingLogFileConfiguration.swift +++ b/Sources/RotatingLogFileConfiguration.swift @@ -56,10 +56,13 @@ open class RotatingLogFileConfiguration: BasicLogConfiguration sequence, and the formatted string returned by the first formatter to yield a non-`nil` value will be recorded. If every formatter returns `nil`, the log entry is silently ignored and not recorded. + + - parameter maximumFileSize: The approximate maximum size (in bytes) to allow log files to grow. + If a log file is larger than this value after a log statement is appended, then the log file is rolled. */ - public init(minimumSeverity: LogSeverity, daysToKeep: Int, directoryPath: String, synchronousMode: Bool = false, filters: [LogFilter] = [], formatters: [LogFormatter] = [ReadableLogFormatter()]) + public init(minimumSeverity: LogSeverity, daysToKeep: Int, directoryPath: String, synchronousMode: Bool = false, filters: [LogFilter] = [], formatters: [LogFormatter] = [ReadableLogFormatter()], maximumFileSize: Int64? = nil) { - logFileRecorder = RotatingLogFileRecorder(daysToKeep: daysToKeep, directoryPath: directoryPath, formatters: formatters) + logFileRecorder = RotatingLogFileRecorder(daysToKeep: daysToKeep, directoryPath: directoryPath, formatters: formatters, maximumFileSize: maximumFileSize) super.init(minimumSeverity: minimumSeverity, filters: filters, recorders: [logFileRecorder], synchronousMode: synchronousMode) } diff --git a/Sources/RotatingLogFileRecorder.swift b/Sources/RotatingLogFileRecorder.swift index d5c534de..571d2c1b 100644 --- a/Sources/RotatingLogFileRecorder.swift +++ b/Sources/RotatingLogFileRecorder.swift @@ -25,21 +25,27 @@ open class RotatingLogFileRecorder: LogRecorderBase /** The filesystem path to a directory where the log files will be stored. */ public let directoryPath: String + + /** The approximate maximum size (in bytes) to allow log files to grow. + If a log file is larger than this value after a log statement is appended, + then the log file is rolled. */ + public let maximumFileSize: Int64? private static let filenameFormatter: DateFormatter = { let fmt = DateFormatter() - fmt.dateFormat = "yyyy-MM-dd'.log'" + fmt.dateFormat = "yyyy-MM-dd" return fmt }() private var mostRecentLogTime: Date? private var currentFileRecorder: FileLogRecorder? + private static var currentNumberOfRolledFiles: Int? /** Initializes a new `RotatingLogFileRecorder` instance. - warning: The `RotatingLogFileRecorder` expects to have full control over - the contents of its `directoryPath`. Any file not recognized as an active + the contents of its `directoryPath`. Any file not recognized as an active log file will be deleted during the automatic pruning process, which may occur at any time. Therefore, be __extremely careful__ when constructing the value passed in as the `directoryPath`. @@ -56,11 +62,15 @@ open class RotatingLogFileRecorder: LogRecorderBase sequence, and the formatted string returned by the first formatter to yield a non-`nil` value will be recorded. If every formatter returns `nil`, the log entry is silently ignored and not recorded. + + - parameter maximumFileSize: The approximate maximum size (in bytes) to allow log files to grow. + If a log file is larger than this value after a log statement is appended, then the log file is rolled. */ - public init(daysToKeep: Int, directoryPath: String, formatters: [LogFormatter] = [ReadableLogFormatter()]) + public init(daysToKeep: Int, directoryPath: String, formatters: [LogFormatter] = [ReadableLogFormatter()], maximumFileSize: Int64? = nil) { self.daysToKeep = daysToKeep self.directoryPath = directoryPath + self.maximumFileSize = maximumFileSize super.init(formatters: formatters) } @@ -73,24 +83,109 @@ open class RotatingLogFileRecorder: LogRecorderBase - returns: The filename. */ - open class func logFilename(forDate date: Date) + open class func logFilename(forDate date: Date, rolledLogFileNumber: Int? = nil, withExtension: Bool = true) -> String { - return filenameFormatter.string(from: date) + guard let rolledLogFileNumber = rolledLogFileNumber, + rolledLogFileNumber > 0 else + { + return "\(filenameFormatter.string(from: date))\(withExtension ? ".log" : "")" + } + return "\(filenameFormatter.string(from: date))(\(rolledLogFileNumber))\(withExtension ? ".log" : "")" } + + /** + Returns a bool defining whether the size of the file at the provided path is greater than the provided file size + + - parameter fileSize: The file size `(in bytes)` that is used as the max size for file + - parameter path: The path to the file to be checked - private class func fileLogRecorder(_ date: Date, directoryPath: String, formatters: [LogFormatter]) + - returns: A bool indicating if the file exceeds the given file size. + */ + private class func hasExceeded(fileSize: Int64, at path: String) -> Bool + { + guard let fileAttributes = try? FileManager.default.attributesOfItem(atPath: path), + let bytes = fileAttributes[.size] as? Int64, + bytes >= fileSize else + { + return false + } + + return true + } + + private class func fileLogRecorder(_ date: Date, directoryPath: String, formatters: [LogFormatter], maximumFileSize: Int64? = nil) -> FileLogRecorder? { - let fileName = logFilename(forDate: date) - let filePath = (directoryPath as NSString).appendingPathComponent(fileName) + let fileNameWithoutExtension = logFilename(forDate: date, withExtension: false) + var fileName = logFilename(forDate: date) + var filePath = (directoryPath as NSString).appendingPathComponent(fileName) + + guard let maximumFileSize = maximumFileSize else + { + return FileLogRecorder(filePath: filePath, formatters: formatters) + } + + if FileManager.default.fileExists(atPath: filePath) + { + // Check if the first file is larger than the maxLogSize + guard hasExceeded(fileSize: maximumFileSize, at: filePath) else + { + return FileLogRecorder(filePath: filePath, formatters: formatters) + } + + if let currentNumberOfRolledFiles = self.currentNumberOfRolledFiles + { + let nextRolledNumber = currentNumberOfRolledFiles + 1 + + fileName = logFilename(forDate: date, rolledLogFileNumber: nextRolledNumber) + filePath = (directoryPath as NSString).appendingPathComponent(fileName) + self.currentNumberOfRolledFiles = nextRolledNumber + return FileLogRecorder(filePath: filePath, formatters: formatters) + } + else + { + // Identify the current number of rolled log files for this date + let directoryContents = try? FileManager.default.contentsOfDirectory(atPath: directoryPath) + .filter { return $0 != fileName } + .filter { return $0.contains(fileNameWithoutExtension) } + .sorted() + + guard directoryContents?.isEmpty == false else + { + // If no rolled files, start with 1 + fileName = logFilename(forDate: date, rolledLogFileNumber: 1) + filePath = (directoryPath as NSString).appendingPathComponent(fileName) + self.currentNumberOfRolledFiles = 1 + return FileLogRecorder(filePath: filePath, formatters: formatters) + } + + // Check if the newest rolled file exceeds the limit or not + let newestFilePath = (directoryPath as NSString).appendingPathComponent(directoryContents!.last!) + guard hasExceeded(fileSize: maximumFileSize, at: newestFilePath) else + { + self.currentNumberOfRolledFiles = directoryContents!.count + fileName = logFilename(forDate: date, rolledLogFileNumber: directoryContents!.count) + filePath = (directoryPath as NSString).appendingPathComponent(fileName) + return FileLogRecorder(filePath: filePath, formatters: formatters) + } + + // If it does, create a new file + // +1 because the initial (non-rolled) file has been filtered from the directoryContents + fileName = logFilename(forDate: date, rolledLogFileNumber: directoryContents!.count + 1) + filePath = (directoryPath as NSString).appendingPathComponent(fileName) + self.currentNumberOfRolledFiles = directoryContents!.count + 1 + + } + } + return FileLogRecorder(filePath: filePath, formatters: formatters) } private func fileLogRecorder(_ date: Date) -> FileLogRecorder? { - return type(of: self).fileLogRecorder(date, directoryPath: directoryPath, formatters: formatters) + return type(of: self).fileLogRecorder(date, directoryPath: directoryPath, formatters: formatters, maximumFileSize: self.maximumFileSize) } private func isDate(_ firstDate: Date, onSameDayAs secondDate: Date) @@ -100,6 +195,20 @@ open class RotatingLogFileRecorder: LogRecorderBase let secondDateStr = type(of: self).logFilename(forDate: secondDate) return firstDateStr == secondDateStr } + + /** + Checks if the current log file exceeds the maximum file size allowed, if it does, the log files are rolled. + + - parameter entry: the log entry to base the new log file off if required + */ + private func rollLogFileIfNeeded(_ entry: LogEntry) + { + guard let maximumFileSize = self.maximumFileSize, + let filePath = currentFileRecorder?.filePath, + RotatingLogFileRecorder.hasExceeded(fileSize: maximumFileSize, at: filePath) else { return } + + currentFileRecorder = fileLogRecorder(entry.timestamp) + } /** Attempts to create—if it does not already exist—the directory indicated @@ -140,6 +249,8 @@ open class RotatingLogFileRecorder: LogRecorderBase mostRecentLogTime = entry.timestamp as Date currentFileRecorder?.record(message: message, for: entry, currentQueue: queue, synchronousMode: synchronousMode) + + rollLogFileIfNeeded(entry) } /** @@ -156,7 +267,7 @@ open class RotatingLogFileRecorder: LogRecorderBase var date = Date() var filesToKeep = Set() for _ in 0.. Bool { + strings.contains { contains($0) } + } +}