BaseDestination.swift 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580
  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. {
  117. // look for digits followed by a alpha character
  118. var s: String!
  119. var sign: Int = 1
  120. if text.firstChar == "-" {
  121. sign = -1
  122. s = String(text.suffix(from: text.index(text.startIndex, offsetBy: 1)))
  123. } else {
  124. s = text
  125. }
  126. let numStr = s.prefix { $0 >= "0" && $0 <= "9" }
  127. if let num = Int(String(numStr)) {
  128. return (sign * num, (sign == -1 ? 1 : 0) + numStr.count)
  129. } else {
  130. return (0, 0)
  131. }
  132. }
  133. private func paddedString(_ text: String, _ toLength: Int, truncating: Bool = false) -> String {
  134. if toLength > 0 {
  135. // Pad to the left of the string
  136. if text.count > toLength {
  137. // Hm... better to use suffix or prefix?
  138. return truncating ? String(text.suffix(toLength)) : text
  139. } else {
  140. return "".padding(toLength: toLength - text.count, withPad: " ", startingAt: 0) + text
  141. }
  142. } else if toLength < 0 {
  143. // Pad to the right of the string
  144. let maxLength = truncating ? -toLength : max(-toLength, text.count)
  145. return text.padding(toLength: maxLength, withPad: " ", startingAt: 0)
  146. } else {
  147. return text
  148. }
  149. }
  150. /// returns the log message based on the format pattern
  151. func formatMessage(_ format: String, level: SwiftyBeaver.Level, msg: String, thread: String,
  152. file: String, function: String, line: Int, context: Any? = nil) -> String {
  153. var text = ""
  154. // Prepend a $I for 'ignore' or else the first character is interpreted as a format character
  155. // even if the format string did not start with a $.
  156. let phrases: [String] = ("$I" + format).components(separatedBy: "$")
  157. for phrase in phrases where !phrase.isEmpty {
  158. let (padding, offset) = parsePadding(phrase)
  159. let formatCharIndex = phrase.index(phrase.startIndex, offsetBy: offset)
  160. let formatChar = phrase[formatCharIndex]
  161. let rangeAfterFormatChar = phrase.index(formatCharIndex, offsetBy: 1)..<phrase.endIndex
  162. let remainingPhrase = phrase[rangeAfterFormatChar]
  163. switch formatChar {
  164. case "I": // ignore
  165. text += remainingPhrase
  166. case "L":
  167. text += paddedString(levelWord(level), padding) + remainingPhrase
  168. case "M":
  169. text += paddedString(msg, padding) + remainingPhrase
  170. case "T":
  171. text += paddedString(thread, padding) + remainingPhrase
  172. case "N":
  173. // name of file without suffix
  174. text += paddedString(fileNameWithoutSuffix(file), padding) + remainingPhrase
  175. case "n":
  176. // name of file with suffix
  177. text += paddedString(fileNameOfFile(file), padding) + remainingPhrase
  178. case "F":
  179. text += paddedString(function, padding) + remainingPhrase
  180. case "l":
  181. text += paddedString(String(line), padding) + remainingPhrase
  182. case "D":
  183. // start of datetime format
  184. #if swift(>=3.2)
  185. text += paddedString(formatDate(String(remainingPhrase)), padding)
  186. #else
  187. text += paddedString(formatDate(remainingPhrase), padding)
  188. #endif
  189. case "d":
  190. text += remainingPhrase
  191. case "U":
  192. text += paddedString(uptime(), padding) + remainingPhrase
  193. case "Z":
  194. // start of datetime format in UTC timezone
  195. #if swift(>=3.2)
  196. text += paddedString(formatDate(String(remainingPhrase), timeZone: "UTC"), padding)
  197. #else
  198. text += paddedString(formatDate(remainingPhrase, timeZone: "UTC"), padding)
  199. #endif
  200. case "z":
  201. text += remainingPhrase
  202. case "C":
  203. // color code ("" on default)
  204. text += escape + colorForLevel(level) + remainingPhrase
  205. case "c":
  206. text += reset + remainingPhrase
  207. case "X":
  208. // add the context
  209. if let cx = context {
  210. text += paddedString(String(describing: cx).trimmingCharacters(in: .whitespacesAndNewlines), padding) + remainingPhrase
  211. } else {
  212. text += paddedString("", padding) + remainingPhrase
  213. }
  214. default:
  215. text += phrase
  216. }
  217. }
  218. // right trim only
  219. return text.replacingOccurrences(of: "\\s+$", with: "", options: .regularExpression)
  220. }
  221. /// returns the log payload as optional JSON string
  222. func messageToJSON(_ level: SwiftyBeaver.Level, msg: String,
  223. thread: String, file: String, function: String, line: Int, context: Any? = nil) -> String? {
  224. var dict: [String: Any] = [
  225. "timestamp": Date().timeIntervalSince1970,
  226. "level": level.rawValue,
  227. "message": msg,
  228. "thread": thread,
  229. "file": file,
  230. "function": function,
  231. "line": line
  232. ]
  233. if let cx = context {
  234. dict["context"] = cx
  235. }
  236. return jsonStringFromDict(dict)
  237. }
  238. /// returns the string of a level
  239. func levelWord(_ level: SwiftyBeaver.Level) -> String {
  240. var str = ""
  241. switch level {
  242. case .debug:
  243. str = levelString.debug
  244. case .info:
  245. str = levelString.info
  246. case .warning:
  247. str = levelString.warning
  248. case .error:
  249. str = levelString.error
  250. default:
  251. // Verbose is default
  252. str = levelString.verbose
  253. }
  254. return str
  255. }
  256. /// returns color string for level
  257. func colorForLevel(_ level: SwiftyBeaver.Level) -> String {
  258. var color = ""
  259. switch level {
  260. case .debug:
  261. color = levelColor.debug
  262. case .info:
  263. color = levelColor.info
  264. case .warning:
  265. color = levelColor.warning
  266. case .error:
  267. color = levelColor.error
  268. default:
  269. color = levelColor.verbose
  270. }
  271. return color
  272. }
  273. /// returns the filename of a path
  274. func fileNameOfFile(_ file: String) -> String {
  275. let fileParts = file.components(separatedBy: "/")
  276. if let lastPart = fileParts.last {
  277. return lastPart
  278. }
  279. return ""
  280. }
  281. /// returns the filename without suffix (= file ending) of a path
  282. func fileNameWithoutSuffix(_ file: String) -> String {
  283. let fileName = fileNameOfFile(file)
  284. if !fileName.isEmpty {
  285. let fileNameParts = fileName.components(separatedBy: ".")
  286. if let firstPart = fileNameParts.first {
  287. return firstPart
  288. }
  289. }
  290. return ""
  291. }
  292. /// returns a formatted date string
  293. /// optionally in a given abbreviated timezone like "UTC"
  294. func formatDate(_ dateFormat: String, timeZone: String = "") -> String {
  295. if !timeZone.isEmpty {
  296. formatter.timeZone = TimeZone(abbreviation: timeZone)
  297. }
  298. formatter.dateFormat = dateFormat
  299. //let dateStr = formatter.string(from: NSDate() as Date)
  300. let dateStr = formatter.string(from: Date())
  301. return dateStr
  302. }
  303. /// returns a uptime string
  304. func uptime() -> String {
  305. let interval = Date().timeIntervalSince(startDate)
  306. let hours = Int(interval) / 3600
  307. let minutes = Int(interval / 60) - Int(hours * 60)
  308. let seconds = Int(interval) - (Int(interval / 60) * 60)
  309. let milliseconds = Int(interval.truncatingRemainder(dividingBy: 1) * 1000)
  310. return String(format: "%0.2d:%0.2d:%0.2d.%03d", arguments: [hours, minutes, seconds, milliseconds])
  311. }
  312. /// returns the json-encoded string value
  313. /// after it was encoded by jsonStringFromDict
  314. func jsonStringValue(_ jsonString: String?, key: String) -> String {
  315. guard let str = jsonString else {
  316. return ""
  317. }
  318. // remove the leading {"key":" from the json string and the final }
  319. let offset = key.length + 5
  320. let endIndex = str.index(str.startIndex,
  321. offsetBy: str.length - 2)
  322. let range = str.index(str.startIndex, offsetBy: offset)..<endIndex
  323. #if swift(>=3.2)
  324. return String(str[range])
  325. #else
  326. return str[range]
  327. #endif
  328. }
  329. /// turns dict into JSON-encoded string
  330. func jsonStringFromDict(_ dict: [String: Any]) -> String? {
  331. var jsonString: String?
  332. // try to create JSON string
  333. do {
  334. let jsonData = try JSONSerialization.data(withJSONObject: dict, options: [])
  335. jsonString = String(data: jsonData, encoding: .utf8)
  336. } catch {
  337. print("SwiftyBeaver could not create JSON from dict.")
  338. }
  339. return jsonString
  340. }
  341. ////////////////////////////////
  342. // MARK: Filters
  343. ////////////////////////////////
  344. /// Add a filter that determines whether or not a particular message will be logged to this destination
  345. public func addFilter(_ filter: FilterType) {
  346. filters.append(filter)
  347. }
  348. /// Remove a filter from the list of filters
  349. public func removeFilter(_ filter: FilterType) {
  350. #if swift(>=5)
  351. let index = filters.firstIndex {
  352. return ObjectIdentifier($0) == ObjectIdentifier(filter)
  353. }
  354. #else
  355. let index = filters.index {
  356. return ObjectIdentifier($0) == ObjectIdentifier(filter)
  357. }
  358. #endif
  359. guard let filterIndex = index else {
  360. return
  361. }
  362. filters.remove(at: filterIndex)
  363. }
  364. /// Answer whether the destination has any message filters
  365. /// returns boolean and is used to decide whether to resolve
  366. /// the message before invoking shouldLevelBeLogged
  367. func hasMessageFilters() -> Bool {
  368. return !getFiltersTargeting(Filter.TargetType.Message(.Equals([], true)),
  369. fromFilters: self.filters).isEmpty
  370. }
  371. /// checks if level is at least minLevel or if a minLevel filter for that path does exist
  372. /// returns boolean and can be used to decide if a message should be logged or not
  373. func shouldLevelBeLogged(_ level: SwiftyBeaver.Level, path: String,
  374. function: String, message: String? = nil) -> Bool {
  375. if filters.isEmpty {
  376. if level.rawValue >= minLevel.rawValue {
  377. if debugPrint {
  378. print("filters is empty and level >= minLevel")
  379. }
  380. return true
  381. } else {
  382. if debugPrint {
  383. print("filters is empty and level < minLevel")
  384. }
  385. return false
  386. }
  387. }
  388. let (matchedExclude, allExclude) = passedExcludedFilters(level, path: path,
  389. function: function, message: message)
  390. if allExclude > 0 && matchedExclude != allExclude {
  391. if debugPrint {
  392. print("filters is not empty and message was excluded")
  393. }
  394. return false
  395. }
  396. let (matchedRequired, allRequired) = passedRequiredFilters(level, path: path,
  397. function: function, message: message)
  398. let (matchedNonRequired, allNonRequired) = passedNonRequiredFilters(level, path: path,
  399. function: function, message: message)
  400. // If required filters exist, we should validate or invalidate the log if all of them pass or not
  401. if allRequired > 0 {
  402. return matchedRequired == allRequired
  403. }
  404. // If a non-required filter matches, the log is validated
  405. if allNonRequired > 0 { // Non-required filters exist
  406. if matchedNonRequired > 0 { return true } // At least one non-required filter matched
  407. else { return false } // No non-required filters matched
  408. }
  409. if level.rawValue < minLevel.rawValue {
  410. if debugPrint {
  411. print("filters is not empty and level < minLevel")
  412. }
  413. return false
  414. }
  415. return true
  416. }
  417. func getFiltersTargeting(_ target: Filter.TargetType, fromFilters: [FilterType]) -> [FilterType] {
  418. return fromFilters.filter { filter in
  419. return filter.getTarget() == target
  420. }
  421. }
  422. /// returns a tuple of matched and all filters
  423. func passedRequiredFilters(_ level: SwiftyBeaver.Level, path: String,
  424. function: String, message: String?) -> (Int, Int) {
  425. let requiredFilters = self.filters.filter { filter in
  426. return filter.isRequired() && !filter.isExcluded()
  427. }
  428. let matchingFilters = applyFilters(requiredFilters, level: level, path: path,
  429. function: function, message: message)
  430. if debugPrint {
  431. print("matched \(matchingFilters) of \(requiredFilters.count) required filters")
  432. }
  433. return (matchingFilters, requiredFilters.count)
  434. }
  435. /// returns a tuple of matched and all filters
  436. func passedNonRequiredFilters(_ level: SwiftyBeaver.Level,
  437. path: String, function: String, message: String?) -> (Int, Int) {
  438. let nonRequiredFilters = self.filters.filter { filter in
  439. return !filter.isRequired() && !filter.isExcluded()
  440. }
  441. let matchingFilters = applyFilters(nonRequiredFilters, level: level,
  442. path: path, function: function, message: message)
  443. if debugPrint {
  444. print("matched \(matchingFilters) of \(nonRequiredFilters.count) non-required filters")
  445. }
  446. return (matchingFilters, nonRequiredFilters.count)
  447. }
  448. /// returns a tuple of matched and all exclude filters
  449. func passedExcludedFilters(_ level: SwiftyBeaver.Level,
  450. path: String, function: String, message: String?) -> (Int, Int) {
  451. let excludeFilters = self.filters.filter { filter in
  452. return filter.isExcluded()
  453. }
  454. let matchingFilters = applyFilters(excludeFilters, level: level,
  455. path: path, function: function, message: message)
  456. if debugPrint {
  457. print("matched \(matchingFilters) of \(excludeFilters.count) exclude filters")
  458. }
  459. return (matchingFilters, excludeFilters.count)
  460. }
  461. func applyFilters(_ targetFilters: [FilterType], level: SwiftyBeaver.Level,
  462. path: String, function: String, message: String?) -> Int {
  463. return targetFilters.filter { filter in
  464. let passes: Bool
  465. if !filter.reachedMinLevel(level) {
  466. return false
  467. }
  468. switch filter.getTarget() {
  469. case .Path(_):
  470. passes = filter.apply(path)
  471. case .Function(_):
  472. passes = filter.apply(function)
  473. case .Message(_):
  474. guard let message = message else {
  475. return false
  476. }
  477. passes = filter.apply(message)
  478. }
  479. return passes
  480. }.count
  481. }
  482. /**
  483. Triggered by main flush() method on each destination. Runs in background thread.
  484. Use for destinations that buffer log items, implement this function to flush those
  485. buffers to their final destination (web server...)
  486. */
  487. func flush() {
  488. // no implementation in base destination needed
  489. }
  490. }
  491. public func == (lhs: BaseDestination, rhs: BaseDestination) -> Bool {
  492. return ObjectIdentifier(lhs) == ObjectIdentifier(rhs)
  493. }