BaseDestination.swift 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510
  1. //
  2. // BaseDestination.swift
  3. // SwiftyBeaver
  4. //
  5. // Created by Sebastian Kreutzberger (Twitter @skreutzb) on 05.12.15.
  6. // Copyright © 2015 Sebastian Kreutzberger
  7. // Some rights reserved: http://opensource.org/licenses/MIT
  8. //
  9. import Foundation
  10. import Dispatch
  11. // store operating system / platform
  12. #if os(iOS)
  13. let OS = "iOS"
  14. #elseif os(OSX)
  15. let OS = "OSX"
  16. #elseif os(watchOS)
  17. let OS = "watchOS"
  18. #elseif os(tvOS)
  19. let OS = "tvOS"
  20. #elseif os(Linux)
  21. let OS = "Linux"
  22. #elseif os(FreeBSD)
  23. let OS = "FreeBSD"
  24. #elseif os(Windows)
  25. let OS = "Windows"
  26. #elseif os(Android)
  27. let OS = "Android"
  28. #else
  29. let OS = "Unknown"
  30. #endif
  31. /// destination which all others inherit from. do not directly use
  32. open class BaseDestination: Hashable, Equatable {
  33. /// output format pattern, see documentation for syntax
  34. open var format = "$DHH:mm:ss.SSS$d $C$L$c $N.$F:$l - $M"
  35. /// runs in own serial background thread for better performance
  36. open var asynchronously = true
  37. /// do not log any message which has a lower level than this one
  38. open var minLevel = SwiftyBeaver.Level.verbose
  39. /// set custom log level words for each level
  40. open var levelString = LevelString()
  41. /// set custom log level colors for each level
  42. open var levelColor = LevelColor()
  43. public struct LevelString {
  44. public var verbose = "VERBOSE"
  45. public var debug = "DEBUG"
  46. public var info = "INFO"
  47. public var warning = "WARNING"
  48. public var error = "ERROR"
  49. }
  50. // For a colored log level word in a logged line
  51. // empty on default
  52. public struct LevelColor {
  53. public var verbose = "" // silver
  54. public var debug = "" // green
  55. public var info = "" // blue
  56. public var warning = "" // yellow
  57. public var error = "" // red
  58. }
  59. var reset = ""
  60. var escape = ""
  61. var filters = [FilterType]()
  62. let formatter = DateFormatter()
  63. let startDate = Date()
  64. // each destination class must have an own hashValue Int
  65. #if swift(>=4.2)
  66. public func hash(into hasher: inout Hasher) {
  67. hasher.combine(defaultHashValue)
  68. }
  69. #else
  70. lazy public var hashValue: Int = self.defaultHashValue
  71. #endif
  72. open var defaultHashValue: Int {return 0}
  73. // each destination instance must have an own serial queue to ensure serial output
  74. // GCD gives it a prioritization between User Initiated and Utility
  75. var queue: DispatchQueue? //dispatch_queue_t?
  76. var debugPrint = false // set to true to debug the internal filter logic of the class
  77. public init() {
  78. let uuid = NSUUID().uuidString
  79. let queueLabel = "swiftybeaver-queue-" + uuid
  80. queue = DispatchQueue(label: queueLabel, target: queue)
  81. }
  82. /// send / store the formatted log message to the destination
  83. /// returns the formatted log message for processing by inheriting method
  84. /// and for unit tests (nil if error)
  85. open func send(_ level: SwiftyBeaver.Level, msg: String, thread: String, file: String,
  86. function: String, line: Int, context: Any? = nil) -> String? {
  87. if format.hasPrefix("$J") {
  88. return messageToJSON(level, msg: msg, thread: thread,
  89. file: file, function: function, line: line, context: context)
  90. } else {
  91. return formatMessage(format, level: level, msg: msg, thread: thread,
  92. file: file, function: function, line: line, context: context)
  93. }
  94. }
  95. public func execute(synchronously: Bool, block: @escaping () -> Void) {
  96. guard let queue = queue else {
  97. fatalError("Queue not set")
  98. }
  99. if synchronously {
  100. queue.sync(execute: block)
  101. } else {
  102. queue.async(execute: block)
  103. }
  104. }
  105. public func executeSynchronously<T>(block: @escaping () throws -> T) rethrows -> T {
  106. guard let queue = queue else {
  107. fatalError("Queue not set")
  108. }
  109. return try queue.sync(execute: block)
  110. }
  111. ////////////////////////////////
  112. // MARK: Format
  113. ////////////////////////////////
  114. /// returns (padding length value, offset in string after padding info)
  115. private func parsePadding(_ text: String) -> (Int, Int) {
  116. // look for digits followed by a alpha character
  117. var s: String!
  118. var sign: Int = 1
  119. if text.firstChar == "-" {
  120. sign = -1
  121. s = String(text.suffix(from: text.index(text.startIndex, offsetBy: 1)))
  122. } else {
  123. s = text
  124. }
  125. let numStr = String(s.prefix { $0 >= "0" && $0 <= "9" })
  126. if let num = Int(numStr) {
  127. return (sign * num, (sign == -1 ? 1 : 0) + numStr.count)
  128. } else {
  129. return (0, 0)
  130. }
  131. }
  132. private func paddedString(_ text: String, _ toLength: Int, truncating: Bool = false) -> String {
  133. if toLength > 0 {
  134. // Pad to the left of the string
  135. if text.count > toLength {
  136. // Hm... better to use suffix or prefix?
  137. return truncating ? String(text.suffix(toLength)) : text
  138. } else {
  139. return "".padding(toLength: toLength - text.count, withPad: " ", startingAt: 0) + text
  140. }
  141. } else if toLength < 0 {
  142. // Pad to the right of the string
  143. let maxLength = truncating ? -toLength : max(-toLength, text.count)
  144. return text.padding(toLength: maxLength, withPad: " ", startingAt: 0)
  145. } else {
  146. return text
  147. }
  148. }
  149. /// returns the log message based on the format pattern
  150. func formatMessage(_ format: String, level: SwiftyBeaver.Level, msg: String, thread: String,
  151. file: String, function: String, line: Int, context: Any? = nil) -> String {
  152. var text = ""
  153. // Prepend a $I for 'ignore' or else the first character is interpreted as a format character
  154. // even if the format string did not start with a $.
  155. let phrases: [String] = ("$I" + format).components(separatedBy: "$")
  156. for phrase in phrases where !phrase.isEmpty {
  157. let (padding, offset) = parsePadding(phrase)
  158. let formatCharIndex = phrase.index(phrase.startIndex, offsetBy: offset)
  159. let formatChar = phrase[formatCharIndex]
  160. let rangeAfterFormatChar = phrase.index(formatCharIndex, offsetBy: 1)..<phrase.endIndex
  161. let remainingPhrase = phrase[rangeAfterFormatChar]
  162. switch formatChar {
  163. case "I": // ignore
  164. text += remainingPhrase
  165. case "L":
  166. text += paddedString(levelWord(level), padding) + remainingPhrase
  167. case "M":
  168. text += paddedString(msg, padding) + remainingPhrase
  169. case "T":
  170. text += paddedString(thread, padding) + remainingPhrase
  171. case "N":
  172. // name of file without suffix
  173. text += paddedString(fileNameWithoutSuffix(file), padding) + remainingPhrase
  174. case "n":
  175. // name of file with suffix
  176. text += paddedString(fileNameOfFile(file), padding) + remainingPhrase
  177. case "F":
  178. text += paddedString(function, padding) + remainingPhrase
  179. case "l":
  180. text += paddedString(String(line), padding) + remainingPhrase
  181. case "D":
  182. // start of datetime format
  183. #if swift(>=3.2)
  184. text += paddedString(formatDate(String(remainingPhrase)), padding)
  185. #else
  186. text += paddedString(formatDate(remainingPhrase), padding)
  187. #endif
  188. case "d":
  189. text += remainingPhrase
  190. case "U":
  191. text += paddedString(uptime(), padding) + remainingPhrase
  192. case "Z":
  193. // start of datetime format in UTC timezone
  194. #if swift(>=3.2)
  195. text += paddedString(formatDate(String(remainingPhrase), timeZone: "UTC"), padding)
  196. #else
  197. text += paddedString(formatDate(remainingPhrase, timeZone: "UTC"), padding)
  198. #endif
  199. case "z":
  200. text += remainingPhrase
  201. case "C":
  202. // color code ("" on default)
  203. text += escape + colorForLevel(level) + remainingPhrase
  204. case "c":
  205. text += reset + remainingPhrase
  206. case "X":
  207. // add the context
  208. if let cx = context {
  209. text += paddedString(String(describing: cx).trimmingCharacters(in: .whitespacesAndNewlines), padding) + remainingPhrase
  210. } else {
  211. text += paddedString("", padding) + remainingPhrase
  212. }
  213. default:
  214. text += phrase
  215. }
  216. }
  217. // right trim only
  218. return text.replacingOccurrences(of: "\\s+$", with: "", options: .regularExpression)
  219. }
  220. /// returns the log payload as optional JSON string
  221. func messageToJSON(_ level: SwiftyBeaver.Level, msg: String,
  222. thread: String, file: String, function: String, line: Int, context: Any? = nil) -> String? {
  223. var dict: [String: Any] = [
  224. "timestamp": Date().timeIntervalSince1970,
  225. "level": level.rawValue,
  226. "message": msg,
  227. "thread": thread,
  228. "file": file,
  229. "function": function,
  230. "line": line
  231. ]
  232. if let cx = context {
  233. dict["context"] = cx
  234. }
  235. return jsonStringFromDict(dict)
  236. }
  237. /// returns the string of a level
  238. func levelWord(_ level: SwiftyBeaver.Level) -> String {
  239. var str = ""
  240. switch level {
  241. case .debug:
  242. str = levelString.debug
  243. case .info:
  244. str = levelString.info
  245. case .warning:
  246. str = levelString.warning
  247. case .error:
  248. str = levelString.error
  249. default:
  250. // Verbose is default
  251. str = levelString.verbose
  252. }
  253. return str
  254. }
  255. /// returns color string for level
  256. func colorForLevel(_ level: SwiftyBeaver.Level) -> String {
  257. var color = ""
  258. switch level {
  259. case .debug:
  260. color = levelColor.debug
  261. case .info:
  262. color = levelColor.info
  263. case .warning:
  264. color = levelColor.warning
  265. case .error:
  266. color = levelColor.error
  267. default:
  268. color = levelColor.verbose
  269. }
  270. return color
  271. }
  272. /// returns the filename of a path
  273. func fileNameOfFile(_ file: String) -> String {
  274. let fileParts = file.components(separatedBy: "/")
  275. if let lastPart = fileParts.last {
  276. return lastPart
  277. }
  278. return ""
  279. }
  280. /// returns the filename without suffix (= file ending) of a path
  281. func fileNameWithoutSuffix(_ file: String) -> String {
  282. let fileName = fileNameOfFile(file)
  283. if !fileName.isEmpty {
  284. let fileNameParts = fileName.components(separatedBy: ".")
  285. if let firstPart = fileNameParts.first {
  286. return firstPart
  287. }
  288. }
  289. return ""
  290. }
  291. /// returns a formatted date string
  292. /// optionally in a given abbreviated timezone like "UTC"
  293. func formatDate(_ dateFormat: String, timeZone: String = "") -> String {
  294. if !timeZone.isEmpty {
  295. formatter.timeZone = TimeZone(abbreviation: timeZone)
  296. }
  297. formatter.dateFormat = dateFormat
  298. //let dateStr = formatter.string(from: NSDate() as Date)
  299. let dateStr = formatter.string(from: Date())
  300. return dateStr
  301. }
  302. /// returns a uptime string
  303. func uptime() -> String {
  304. let interval = Date().timeIntervalSince(startDate)
  305. let hours = Int(interval) / 3600
  306. let minutes = Int(interval / 60) - Int(hours * 60)
  307. let seconds = Int(interval) - (Int(interval / 60) * 60)
  308. let milliseconds = Int(interval.truncatingRemainder(dividingBy: 1) * 1000)
  309. return String(format: "%0.2d:%0.2d:%0.2d.%03d", arguments: [hours, minutes, seconds, milliseconds])
  310. }
  311. /// returns the json-encoded string value
  312. /// after it was encoded by jsonStringFromDict
  313. func jsonStringValue(_ jsonString: String?, key: String) -> String {
  314. guard let str = jsonString else {
  315. return ""
  316. }
  317. // remove the leading {"key":" from the json string and the final }
  318. let offset = key.length + 5
  319. let endIndex = str.index(str.startIndex,
  320. offsetBy: str.length - 2)
  321. let range = str.index(str.startIndex, offsetBy: offset)..<endIndex
  322. #if swift(>=3.2)
  323. return String(str[range])
  324. #else
  325. return str[range]
  326. #endif
  327. }
  328. /// turns dict into JSON-encoded string
  329. func jsonStringFromDict(_ dict: [String: Any]) -> String? {
  330. var jsonString: String?
  331. // try to create JSON string
  332. do {
  333. let jsonData = try JSONSerialization.data(withJSONObject: dict, options: [])
  334. jsonString = String(data: jsonData, encoding: .utf8)
  335. } catch {
  336. print("SwiftyBeaver could not create JSON from dict.")
  337. }
  338. return jsonString
  339. }
  340. ////////////////////////////////
  341. // MARK: Filters
  342. ////////////////////////////////
  343. /// Add a filter that determines whether or not a particular message will be logged to this destination
  344. public func addFilter(_ filter: FilterType) {
  345. filters.append(filter)
  346. }
  347. /// Remove a filter from the list of filters
  348. public func removeFilter(_ filter: FilterType) {
  349. #if swift(>=5)
  350. let index = filters.firstIndex {
  351. return ObjectIdentifier($0) == ObjectIdentifier(filter)
  352. }
  353. #else
  354. let index = filters.index {
  355. return ObjectIdentifier($0) == ObjectIdentifier(filter)
  356. }
  357. #endif
  358. guard let filterIndex = index else {
  359. return
  360. }
  361. filters.remove(at: filterIndex)
  362. }
  363. /// Answer whether the destination has any message filters
  364. /// returns boolean and is used to decide whether to resolve
  365. /// the message before invoking shouldLevelBeLogged
  366. func hasMessageFilters() -> Bool {
  367. return !getFiltersTargeting(Filter.TargetType.Message(.Equals([], true)),
  368. fromFilters: self.filters).isEmpty
  369. }
  370. /// checks if level is at least minLevel or if a minLevel filter for that path does exist
  371. /// returns boolean and can be used to decide if a message should be logged or not
  372. func shouldLevelBeLogged(_ level: SwiftyBeaver.Level, path: String,
  373. function: String, message: String? = nil) -> Bool {
  374. if filters.isEmpty {
  375. if level.rawValue >= minLevel.rawValue {
  376. if debugPrint {
  377. print("filters are empty and level >= minLevel")
  378. }
  379. return true
  380. } else {
  381. if debugPrint {
  382. print("filters are empty and level < minLevel")
  383. }
  384. return false
  385. }
  386. }
  387. let filterCheckResult = FilterValidator.validate(input: .init(filters: self.filters, level: level, path: path, function: function, message: message))
  388. // Exclusion filters match if they do NOT meet the filter condition (see Filter.apply(_:) method)
  389. switch filterCheckResult[.excluded] {
  390. case .some(.someFiltersMatch):
  391. // Exclusion filters are present and at least one of them matches the log entry
  392. if debugPrint {
  393. print("filters are not empty and message was excluded")
  394. }
  395. return false
  396. case .some(.allFiltersMatch), .some(.noFiltersMatchingType), .none: break
  397. }
  398. // If required filters exist, we should validate or invalidate the log if all of them pass or not
  399. switch filterCheckResult[.required] {
  400. case .some(.allFiltersMatch): return true
  401. case .some(.someFiltersMatch): return false
  402. case .some(.noFiltersMatchingType), .none: break
  403. }
  404. let checkLogLevel: () -> Bool = {
  405. // Check if the log message's level matches or exceeds the minLevel of the destination
  406. return level.rawValue >= self.minLevel.rawValue
  407. }
  408. // Non-required filters should only be applied if the log entry matches the filter condition (e.g. path)
  409. switch filterCheckResult[.nonRequired] {
  410. case .some(.allFiltersMatch): return true
  411. case .some(.noFiltersMatchingType), .none: return checkLogLevel()
  412. case .some(.someFiltersMatch(let partialMatchData)):
  413. if partialMatchData.fullMatchCount > 0 {
  414. // The log entry matches at least one filter condition and the destination's log level
  415. return true
  416. } else if partialMatchData.conditionMatchCount > 0 {
  417. // The log entry matches at least one filter condition, but does not match or exceed the destination's log level
  418. return false
  419. } else {
  420. // There is no filter with a matching filter condition. Check the destination's log level
  421. return checkLogLevel()
  422. }
  423. }
  424. }
  425. func getFiltersTargeting(_ target: Filter.TargetType, fromFilters: [FilterType]) -> [FilterType] {
  426. return fromFilters.filter { filter in
  427. return filter.getTarget() == target
  428. }
  429. }
  430. /**
  431. Triggered by main flush() method on each destination. Runs in background thread.
  432. Use for destinations that buffer log items, implement this function to flush those
  433. buffers to their final destination (web server...)
  434. */
  435. func flush() {
  436. // no implementation in base destination needed
  437. }
  438. }
  439. public func == (lhs: BaseDestination, rhs: BaseDestination) -> Bool {
  440. return ObjectIdentifier(lhs) == ObjectIdentifier(rhs)
  441. }