2017-05-27 12:09:31 +03:00
|
|
|
/**
|
|
|
|
* Tae Won Ha - http://taewon.de - @hataewon
|
|
|
|
* See LICENSE
|
|
|
|
*/
|
|
|
|
|
|
|
|
import Foundation
|
|
|
|
|
2017-05-28 08:50:57 +03:00
|
|
|
class FileAppender {
|
|
|
|
|
|
|
|
init(with fileUrl: URL) {
|
|
|
|
guard fileUrl.isFileURL else {
|
|
|
|
preconditionFailure("\(fileUrl) must be a file URL!")
|
|
|
|
}
|
|
|
|
|
|
|
|
self.fileUrl = fileUrl
|
|
|
|
self.fileDateFormatter.dateFormat = "yyyy-MM-dd_HH-mm-SSS"
|
|
|
|
self.setupFileHandle(at: fileUrl)
|
|
|
|
}
|
|
|
|
|
|
|
|
func write(_ data: Data) {
|
|
|
|
self.fileHandle.write(data)
|
|
|
|
|
|
|
|
if self.fileHandle.offsetInFile >= maxFileSize {
|
|
|
|
self.archiveLogFile()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
deinit {
|
|
|
|
self.fileHandle.closeFile()
|
|
|
|
}
|
|
|
|
|
|
|
|
fileprivate let fileUrl: URL
|
|
|
|
fileprivate var fileHandle = FileHandle.standardOutput
|
|
|
|
fileprivate let fileDateFormatter = DateFormatter()
|
|
|
|
|
|
|
|
fileprivate func setupFileHandle(at fileUrl: URL) {
|
|
|
|
if !fileManager.fileExists(atPath: fileUrl.path) {
|
|
|
|
fileManager.createFile(atPath: fileUrl.path, contents: nil)
|
|
|
|
}
|
|
|
|
|
|
|
|
if let fileHandle = try? FileHandle(forWritingTo: fileUrl) {
|
|
|
|
self.fileHandle = fileHandle
|
|
|
|
self.fileHandle.seekToEndOfFile()
|
|
|
|
} else {
|
|
|
|
NSLog("[ERROR] Could not get handle for \(fileUrl), defaulting to STDOUT")
|
|
|
|
self.fileHandle = FileHandle.standardOutput
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fileprivate func archiveLogFile() {
|
|
|
|
self.fileHandle.closeFile()
|
|
|
|
|
|
|
|
do {
|
|
|
|
let fileTimestamp = self.fileDateFormatter.string(from: Date())
|
|
|
|
let fileName = self.fileUrl.deletingPathExtension().lastPathComponent
|
|
|
|
let archiveFileName = "\(fileName)-\(fileTimestamp).\(self.fileUrl.pathExtension)"
|
|
|
|
let archiveFileUrl = self.fileUrl
|
|
|
|
.deletingLastPathComponent().appendingPathComponent(archiveFileName)
|
|
|
|
|
|
|
|
try fileManager.moveItem(at: self.fileUrl, to: archiveFileUrl)
|
|
|
|
} catch let error as NSError {
|
|
|
|
NSLog("[ERROR] Could not archive log file: \(error)")
|
|
|
|
}
|
|
|
|
|
|
|
|
self.setupFileHandle(at: self.fileUrl)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-05-27 12:09:31 +03:00
|
|
|
class FileLogger {
|
|
|
|
|
2017-05-27 16:58:20 +03:00
|
|
|
enum Level: String {
|
2017-05-27 12:09:31 +03:00
|
|
|
|
2017-05-27 16:58:20 +03:00
|
|
|
case `default` = "DEFAULT"
|
|
|
|
case info = "INFO"
|
|
|
|
case debug = "DEBUG"
|
|
|
|
case error = "ERROR"
|
|
|
|
case fault = "FAULT"
|
2017-05-27 12:09:31 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
let uuid = UUID().uuidString
|
|
|
|
let name: String
|
|
|
|
|
2017-05-31 00:41:29 +03:00
|
|
|
var shouldLogDebug: Bool
|
2017-05-27 16:58:20 +03:00
|
|
|
|
2017-05-31 00:41:29 +03:00
|
|
|
init<T>(as name: T, with fileUrl: URL, shouldLogDebug: Bool? = nil) {
|
|
|
|
if let debug = shouldLogDebug {
|
|
|
|
self.shouldLogDebug = debug
|
|
|
|
} else {
|
|
|
|
#if DEBUG
|
|
|
|
self.shouldLogDebug = true
|
|
|
|
#else
|
|
|
|
self.shouldLogDebug = false
|
|
|
|
#endif
|
|
|
|
}
|
2017-05-27 16:58:20 +03:00
|
|
|
|
2017-05-27 12:09:31 +03:00
|
|
|
switch name {
|
|
|
|
case let str as String: self.name = str
|
|
|
|
default: self.name = String(describing: name)
|
|
|
|
}
|
|
|
|
|
|
|
|
guard fileUrl.isFileURL else {
|
|
|
|
preconditionFailure("\(fileUrl) must be a file URL!")
|
|
|
|
}
|
|
|
|
|
|
|
|
self.fileUrl = fileUrl
|
|
|
|
self.logDateFormatter.dateFormat = "dd HH:mm:SSS"
|
|
|
|
|
2017-05-28 08:50:57 +03:00
|
|
|
fileAppendersAccess.lock()
|
|
|
|
defer { fileAppendersAccess.unlock() }
|
|
|
|
if let fileAppender = fileAppenders[fileUrl], let ref = fileAppenderRefs[fileUrl] {
|
|
|
|
self.fileAppender = fileAppender
|
|
|
|
fileAppenderRefs[fileUrl] = ref + 1
|
2017-05-27 12:09:31 +03:00
|
|
|
} else {
|
2017-05-28 08:50:57 +03:00
|
|
|
self.fileAppender = FileAppender(with: fileUrl)
|
|
|
|
fileAppenders[fileUrl] = self.fileAppender
|
|
|
|
fileAppenderRefs[fileUrl] = 1
|
2017-05-27 12:09:31 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
deinit {
|
2017-05-28 08:50:57 +03:00
|
|
|
fileAppendersAccess.lock()
|
|
|
|
defer { fileAppendersAccess.unlock() }
|
|
|
|
|
|
|
|
guard let ref = fileAppenderRefs[self.fileUrl] else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
guard ref > 1 else {
|
|
|
|
fileAppenderRefs.removeValue(forKey: self.fileUrl)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
fileAppenderRefs[self.fileUrl] = ref - 1
|
2017-05-27 12:09:31 +03:00
|
|
|
}
|
|
|
|
|
2017-05-31 00:41:29 +03:00
|
|
|
func hr(file: String = #file, line: Int = #line, function: String = #function) {
|
|
|
|
self.log("----------", level: .debug, file: file, line: line, function: function)
|
|
|
|
}
|
2017-05-27 16:58:20 +03:00
|
|
|
|
2017-05-31 00:41:29 +03:00
|
|
|
func mark(file: String = #file, line: Int = #line, function: String = #function) {
|
2017-05-27 12:09:31 +03:00
|
|
|
self.log("", level: .debug, file: file, line: line, function: function)
|
|
|
|
}
|
|
|
|
|
2017-05-31 00:41:29 +03:00
|
|
|
func `default`<T>(_ message: T,
|
2017-05-27 12:09:31 +03:00
|
|
|
file: String = #file, line: Int = #line, function: String = #function) {
|
|
|
|
|
|
|
|
self.log(message, level: .default, file: file, line: line, function: function)
|
|
|
|
}
|
|
|
|
|
2017-05-31 00:41:29 +03:00
|
|
|
func info<T>(_ message: T,
|
2017-05-27 12:09:31 +03:00
|
|
|
file: String = #file, line: Int = #line, function: String = #function) {
|
|
|
|
|
|
|
|
self.log(message, level: .info, file: file, line: line, function: function)
|
|
|
|
}
|
|
|
|
|
2017-05-31 00:41:29 +03:00
|
|
|
func debug<T>(_ message: T,
|
2017-05-27 12:09:31 +03:00
|
|
|
file: String = #file, line: Int = #line, function: String = #function) {
|
|
|
|
|
|
|
|
self.log(message, level: .debug, file: file, line: line, function: function)
|
|
|
|
}
|
|
|
|
|
2017-05-31 00:41:29 +03:00
|
|
|
func error<T>(_ message: T,
|
2017-05-27 12:09:31 +03:00
|
|
|
file: String = #file, line: Int = #line, function: String = #function) {
|
|
|
|
|
|
|
|
self.log(message, level: .error, file: file, line: line, function: function)
|
|
|
|
}
|
|
|
|
|
2017-05-31 00:41:29 +03:00
|
|
|
func fault<T>(_ message: T,
|
2017-05-27 12:09:31 +03:00
|
|
|
file: String = #file, line: Int = #line, function: String = #function) {
|
|
|
|
|
|
|
|
self.log(message, level: .fault, file: file, line: line, function: function)
|
|
|
|
}
|
|
|
|
|
2017-05-31 00:41:29 +03:00
|
|
|
func log<T>(_ message: T, level: Level = .default,
|
2017-05-27 12:09:31 +03:00
|
|
|
file: String = #file, line: Int = #line, function: String = #function) {
|
|
|
|
|
2017-05-31 00:41:29 +03:00
|
|
|
guard self.shouldLogDebug else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2017-05-28 08:50:57 +03:00
|
|
|
queue.async {
|
2017-05-27 12:09:31 +03:00
|
|
|
let timestamp = self.logDateFormatter.string(from: Date())
|
2017-05-31 00:41:29 +03:00
|
|
|
let strMsg = self.string(from: message)
|
2017-05-27 16:58:20 +03:00
|
|
|
|
2017-05-27 17:41:28 +03:00
|
|
|
let logMsg = "\(timestamp) \(self.name) \(function) \(strMsg)"
|
2017-05-27 16:58:20 +03:00
|
|
|
let data = "[\(level.rawValue)] \(logMsg)\n".data(using: .utf8) ?? conversionError
|
2017-05-28 08:50:57 +03:00
|
|
|
self.fileAppender.write(data)
|
2017-05-27 12:09:31 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fileprivate func string<T>(from obj: T) -> String {
|
|
|
|
switch obj {
|
|
|
|
case let str as String: return str
|
|
|
|
case let convertible as CustomStringConvertible: return convertible.description
|
|
|
|
case let convertible as CustomDebugStringConvertible: return convertible.debugDescription
|
|
|
|
default: return String(describing: obj)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fileprivate let fileUrl: URL
|
2017-05-28 08:50:57 +03:00
|
|
|
fileprivate let fileAppender: FileAppender
|
2017-05-27 12:09:31 +03:00
|
|
|
fileprivate let logDateFormatter = DateFormatter()
|
|
|
|
}
|
|
|
|
|
2017-05-27 16:58:20 +03:00
|
|
|
fileprivate let conversionError = "[ERROR] Could not convert log msg to Data!".data(using: .utf8)!
|
2017-05-27 12:09:31 +03:00
|
|
|
fileprivate let fileManager = FileManager.default
|
2017-05-28 08:50:57 +03:00
|
|
|
fileprivate let maxFileSize: UInt64 = 4 * 1024 * 1024
|
|
|
|
fileprivate let queue = DispatchQueue(label: "logger", qos: .background)
|
|
|
|
fileprivate let fileAppendersAccess = NSRecursiveLock()
|
|
|
|
fileprivate var fileAppenders: [URL: FileAppender] = [:]
|
|
|
|
fileprivate var fileAppenderRefs: [URL: Int] = [:]
|