Initial commit
This commit is contained in:
529
Pods/SwiftyBeaver/Sources/BaseDestination.swift
generated
Normal file
529
Pods/SwiftyBeaver/Sources/BaseDestination.swift
generated
Normal file
@ -0,0 +1,529 @@
|
||||
//
|
||||
// BaseDestination.swift
|
||||
// SwiftyBeaver
|
||||
//
|
||||
// Created by Sebastian Kreutzberger (Twitter @skreutzb) on 05.12.15.
|
||||
// Copyright © 2015 Sebastian Kreutzberger
|
||||
// Some rights reserved: http://opensource.org/licenses/MIT
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Dispatch
|
||||
|
||||
// store operating system / platform
|
||||
#if os(iOS)
|
||||
let OS = "iOS"
|
||||
#elseif os(OSX)
|
||||
let OS = "OSX"
|
||||
#elseif os(watchOS)
|
||||
let OS = "watchOS"
|
||||
#elseif os(tvOS)
|
||||
let OS = "tvOS"
|
||||
#elseif os(Linux)
|
||||
let OS = "Linux"
|
||||
#elseif os(FreeBSD)
|
||||
let OS = "FreeBSD"
|
||||
#elseif os(Windows)
|
||||
let OS = "Windows"
|
||||
#elseif os(Android)
|
||||
let OS = "Android"
|
||||
#else
|
||||
let OS = "Unknown"
|
||||
#endif
|
||||
|
||||
/// destination which all others inherit from. do not directly use
|
||||
open class BaseDestination: Hashable, Equatable {
|
||||
|
||||
/// output format pattern, see documentation for syntax
|
||||
open var format = "$DHH:mm:ss.SSS$d $C$L$c $N.$F:$l - $M"
|
||||
|
||||
/// runs in own serial background thread for better performance
|
||||
open var asynchronously = true
|
||||
|
||||
/// do not log any message which has a lower level than this one
|
||||
open var minLevel = SwiftyBeaver.Level.verbose
|
||||
|
||||
/// set custom log level words for each level
|
||||
open var levelString = LevelString()
|
||||
|
||||
/// set custom log level colors for each level
|
||||
open var levelColor = LevelColor()
|
||||
|
||||
/// set custom calendar for dateFormatter
|
||||
open var calendar = Calendar.current
|
||||
|
||||
public struct LevelString {
|
||||
public var verbose = "VERBOSE"
|
||||
public var debug = "DEBUG"
|
||||
public var info = "INFO"
|
||||
public var warning = "WARNING"
|
||||
public var error = "ERROR"
|
||||
public var critical = "CRITICAL"
|
||||
public var fault = "FAULT"
|
||||
}
|
||||
|
||||
// For a colored log level word in a logged line
|
||||
// empty on default
|
||||
public struct LevelColor {
|
||||
public var verbose = "" // silver
|
||||
public var debug = "" // green
|
||||
public var info = "" // blue
|
||||
public var warning = "" // yellow
|
||||
public var error = "" // red
|
||||
public var critical = "" // red
|
||||
public var fault = "" // red
|
||||
}
|
||||
|
||||
var reset = ""
|
||||
var escape = ""
|
||||
|
||||
var filters = [FilterType]()
|
||||
let formatter = DateFormatter()
|
||||
let startDate = Date()
|
||||
|
||||
// each destination class must have an own hashValue Int
|
||||
#if swift(>=4.2)
|
||||
public func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(defaultHashValue)
|
||||
}
|
||||
#else
|
||||
lazy public var hashValue: Int = self.defaultHashValue
|
||||
#endif
|
||||
|
||||
open var defaultHashValue: Int {return 0}
|
||||
|
||||
// each destination instance must have an own serial queue to ensure serial output
|
||||
// GCD gives it a prioritization between User Initiated and Utility
|
||||
var queue: DispatchQueue? //dispatch_queue_t?
|
||||
var debugPrint = false // set to true to debug the internal filter logic of the class
|
||||
|
||||
public init() {
|
||||
let uuid = NSUUID().uuidString
|
||||
let queueLabel = "swiftybeaver-queue-" + uuid
|
||||
queue = DispatchQueue(label: queueLabel, target: queue)
|
||||
}
|
||||
|
||||
/// send / store the formatted log message to the destination
|
||||
/// returns the formatted log message for processing by inheriting method
|
||||
/// and for unit tests (nil if error)
|
||||
open func send(_ level: SwiftyBeaver.Level, msg: String, thread: String, file: String,
|
||||
function: String, line: Int, context: Any? = nil) -> String? {
|
||||
|
||||
if format.hasPrefix("$J") {
|
||||
return messageToJSON(level, msg: msg, thread: thread,
|
||||
file: file, function: function, line: line, context: context)
|
||||
|
||||
} else {
|
||||
return formatMessage(format, level: level, msg: msg, thread: thread,
|
||||
file: file, function: function, line: line, context: context)
|
||||
}
|
||||
}
|
||||
|
||||
public func execute(synchronously: Bool, block: @escaping () -> Void) {
|
||||
guard let queue = queue else {
|
||||
fatalError("Queue not set")
|
||||
}
|
||||
if synchronously {
|
||||
queue.sync(execute: block)
|
||||
} else {
|
||||
queue.async(execute: block)
|
||||
}
|
||||
}
|
||||
|
||||
public func executeSynchronously<T>(block: @escaping () throws -> T) rethrows -> T {
|
||||
guard let queue = queue else {
|
||||
fatalError("Queue not set")
|
||||
}
|
||||
return try queue.sync(execute: block)
|
||||
}
|
||||
|
||||
////////////////////////////////
|
||||
// MARK: Format
|
||||
////////////////////////////////
|
||||
|
||||
/// returns (padding length value, offset in string after padding info)
|
||||
private func parsePadding(_ text: String) -> (Int, Int) {
|
||||
// look for digits followed by a alpha character
|
||||
var s: String!
|
||||
var sign: Int = 1
|
||||
if text.firstChar == "-" {
|
||||
sign = -1
|
||||
s = String(text.suffix(from: text.index(text.startIndex, offsetBy: 1)))
|
||||
} else {
|
||||
s = text
|
||||
}
|
||||
let numStr = String(s.prefix { $0 >= "0" && $0 <= "9" })
|
||||
if let num = Int(numStr) {
|
||||
return (sign * num, (sign == -1 ? 1 : 0) + numStr.count)
|
||||
} else {
|
||||
return (0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
private func paddedString(_ text: String, _ toLength: Int, truncating: Bool = false) -> String {
|
||||
if toLength > 0 {
|
||||
// Pad to the left of the string
|
||||
if text.count > toLength {
|
||||
// Hm... better to use suffix or prefix?
|
||||
return truncating ? String(text.suffix(toLength)) : text
|
||||
} else {
|
||||
return "".padding(toLength: toLength - text.count, withPad: " ", startingAt: 0) + text
|
||||
}
|
||||
} else if toLength < 0 {
|
||||
// Pad to the right of the string
|
||||
let maxLength = truncating ? -toLength : max(-toLength, text.count)
|
||||
return text.padding(toLength: maxLength, withPad: " ", startingAt: 0)
|
||||
} else {
|
||||
return text
|
||||
}
|
||||
}
|
||||
|
||||
/// returns the log message based on the format pattern
|
||||
func formatMessage(_ format: String, level: SwiftyBeaver.Level, msg: String, thread: String,
|
||||
file: String, function: String, line: Int, context: Any? = nil) -> String {
|
||||
|
||||
var text = ""
|
||||
// Prepend a $I for 'ignore' or else the first character is interpreted as a format character
|
||||
// even if the format string did not start with a $.
|
||||
let phrases: [String] = ("$I" + format).components(separatedBy: "$")
|
||||
|
||||
for phrase in phrases where !phrase.isEmpty {
|
||||
let (padding, offset) = parsePadding(phrase)
|
||||
let formatCharIndex = phrase.index(phrase.startIndex, offsetBy: offset)
|
||||
let formatChar = phrase[formatCharIndex]
|
||||
let rangeAfterFormatChar = phrase.index(formatCharIndex, offsetBy: 1)..<phrase.endIndex
|
||||
let remainingPhrase = phrase[rangeAfterFormatChar]
|
||||
|
||||
switch formatChar {
|
||||
case "I": // ignore
|
||||
text += remainingPhrase
|
||||
case "L":
|
||||
text += paddedString(levelWord(level), padding) + remainingPhrase
|
||||
case "M":
|
||||
text += paddedString(msg, padding) + remainingPhrase
|
||||
case "T":
|
||||
text += paddedString(thread, padding) + remainingPhrase
|
||||
case "N":
|
||||
// name of file without suffix
|
||||
text += paddedString(fileNameWithoutSuffix(file), padding) + remainingPhrase
|
||||
case "n":
|
||||
// name of file with suffix
|
||||
text += paddedString(fileNameOfFile(file), padding) + remainingPhrase
|
||||
case "F":
|
||||
text += paddedString(function, padding) + remainingPhrase
|
||||
case "l":
|
||||
text += paddedString(String(line), padding) + remainingPhrase
|
||||
case "D":
|
||||
// start of datetime format
|
||||
#if swift(>=3.2)
|
||||
text += paddedString(formatDate(String(remainingPhrase)), padding)
|
||||
#else
|
||||
text += paddedString(formatDate(remainingPhrase), padding)
|
||||
#endif
|
||||
case "d":
|
||||
text += remainingPhrase
|
||||
case "U":
|
||||
text += paddedString(uptime(), padding) + remainingPhrase
|
||||
case "Z":
|
||||
// start of datetime format in UTC timezone
|
||||
#if swift(>=3.2)
|
||||
text += paddedString(formatDate(String(remainingPhrase), timeZone: "UTC"), padding)
|
||||
#else
|
||||
text += paddedString(formatDate(remainingPhrase, timeZone: "UTC"), padding)
|
||||
#endif
|
||||
case "z":
|
||||
text += remainingPhrase
|
||||
case "C":
|
||||
// color code ("" on default)
|
||||
text += escape + colorForLevel(level) + remainingPhrase
|
||||
case "c":
|
||||
text += reset + remainingPhrase
|
||||
case "X":
|
||||
// add the context
|
||||
if let cx = context {
|
||||
text += paddedString(String(describing: cx).trimmingCharacters(in: .whitespacesAndNewlines), padding) + remainingPhrase
|
||||
} else {
|
||||
text += paddedString("", padding) + remainingPhrase
|
||||
}
|
||||
default:
|
||||
text += phrase
|
||||
}
|
||||
}
|
||||
// right trim only
|
||||
return text.replacingOccurrences(of: "\\s+$", with: "", options: .regularExpression)
|
||||
}
|
||||
|
||||
/// returns the log payload as optional JSON string
|
||||
func messageToJSON(_ level: SwiftyBeaver.Level, msg: String,
|
||||
thread: String, file: String, function: String, line: Int, context: Any? = nil) -> String? {
|
||||
var dict: [String: Any] = [
|
||||
"timestamp": Date().timeIntervalSince1970,
|
||||
"level": level.rawValue,
|
||||
"message": msg,
|
||||
"thread": thread,
|
||||
"file": file,
|
||||
"function": function,
|
||||
"line": line
|
||||
]
|
||||
if let cx = context {
|
||||
dict["context"] = cx
|
||||
}
|
||||
return jsonStringFromDict(dict)
|
||||
}
|
||||
|
||||
/// returns the string of a level
|
||||
func levelWord(_ level: SwiftyBeaver.Level) -> String {
|
||||
|
||||
var str = ""
|
||||
|
||||
switch level {
|
||||
case .verbose:
|
||||
str = levelString.verbose
|
||||
|
||||
case .debug:
|
||||
str = levelString.debug
|
||||
|
||||
case .info:
|
||||
str = levelString.info
|
||||
|
||||
case .warning:
|
||||
str = levelString.warning
|
||||
|
||||
case .error:
|
||||
str = levelString.error
|
||||
|
||||
case .critical:
|
||||
str = levelString.critical
|
||||
|
||||
case .fault:
|
||||
str = levelString.fault
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
/// returns color string for level
|
||||
func colorForLevel(_ level: SwiftyBeaver.Level) -> String {
|
||||
var color = ""
|
||||
|
||||
switch level {
|
||||
case .verbose:
|
||||
color = levelColor.verbose
|
||||
|
||||
case .debug:
|
||||
color = levelColor.debug
|
||||
|
||||
case .info:
|
||||
color = levelColor.info
|
||||
|
||||
case .warning:
|
||||
color = levelColor.warning
|
||||
|
||||
case .error:
|
||||
color = levelColor.error
|
||||
|
||||
case .critical:
|
||||
color = levelColor.critical
|
||||
|
||||
case .fault:
|
||||
color = levelColor.fault
|
||||
}
|
||||
return color
|
||||
}
|
||||
|
||||
/// returns the filename of a path
|
||||
func fileNameOfFile(_ file: String) -> String {
|
||||
let fileParts = file.components(separatedBy: "/")
|
||||
if let lastPart = fileParts.last {
|
||||
return lastPart
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
/// returns the filename without suffix (= file ending) of a path
|
||||
func fileNameWithoutSuffix(_ file: String) -> String {
|
||||
let fileName = fileNameOfFile(file)
|
||||
|
||||
if !fileName.isEmpty {
|
||||
let fileNameParts = fileName.components(separatedBy: ".")
|
||||
if let firstPart = fileNameParts.first {
|
||||
return firstPart
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
/// returns a formatted date string
|
||||
/// optionally in a given abbreviated timezone like "UTC"
|
||||
func formatDate(_ dateFormat: String, timeZone: String = "") -> String {
|
||||
if !timeZone.isEmpty {
|
||||
formatter.timeZone = TimeZone(abbreviation: timeZone)
|
||||
}
|
||||
formatter.calendar = calendar
|
||||
formatter.dateFormat = dateFormat
|
||||
//let dateStr = formatter.string(from: NSDate() as Date)
|
||||
let dateStr = formatter.string(from: Date())
|
||||
return dateStr
|
||||
}
|
||||
|
||||
/// returns a uptime string
|
||||
func uptime() -> String {
|
||||
let interval = Date().timeIntervalSince(startDate)
|
||||
|
||||
let hours = Int(interval) / 3600
|
||||
let minutes = Int(interval / 60) - Int(hours * 60)
|
||||
let seconds = Int(interval) - (Int(interval / 60) * 60)
|
||||
let milliseconds = Int(interval.truncatingRemainder(dividingBy: 1) * 1000)
|
||||
|
||||
return String(format: "%0.2d:%0.2d:%0.2d.%03d", arguments: [hours, minutes, seconds, milliseconds])
|
||||
}
|
||||
|
||||
/// returns the json-encoded string value
|
||||
/// after it was encoded by jsonStringFromDict
|
||||
func jsonStringValue(_ jsonString: String?, key: String) -> String {
|
||||
guard let str = jsonString else {
|
||||
return ""
|
||||
}
|
||||
|
||||
// remove the leading {"key":" from the json string and the final }
|
||||
let offset = key.length + 5
|
||||
let endIndex = str.index(str.startIndex,
|
||||
offsetBy: str.length - 2)
|
||||
let range = str.index(str.startIndex, offsetBy: offset)..<endIndex
|
||||
#if swift(>=3.2)
|
||||
return String(str[range])
|
||||
#else
|
||||
return str[range]
|
||||
#endif
|
||||
}
|
||||
|
||||
/// turns dict into JSON-encoded string
|
||||
func jsonStringFromDict(_ dict: [String: Any]) -> String? {
|
||||
var jsonString: String?
|
||||
|
||||
// try to create JSON string
|
||||
do {
|
||||
let jsonData = try JSONSerialization.data(withJSONObject: dict, options: [])
|
||||
jsonString = String(data: jsonData, encoding: .utf8)
|
||||
} catch {
|
||||
print("SwiftyBeaver could not create JSON from dict.")
|
||||
}
|
||||
return jsonString
|
||||
}
|
||||
|
||||
////////////////////////////////
|
||||
// MARK: Filters
|
||||
////////////////////////////////
|
||||
|
||||
/// Add a filter that determines whether or not a particular message will be logged to this destination
|
||||
public func addFilter(_ filter: FilterType) {
|
||||
filters.append(filter)
|
||||
}
|
||||
|
||||
/// Remove a filter from the list of filters
|
||||
public func removeFilter(_ filter: FilterType) {
|
||||
#if swift(>=5)
|
||||
let index = filters.firstIndex {
|
||||
return ObjectIdentifier($0) == ObjectIdentifier(filter)
|
||||
}
|
||||
#else
|
||||
let index = filters.index {
|
||||
return ObjectIdentifier($0) == ObjectIdentifier(filter)
|
||||
}
|
||||
#endif
|
||||
|
||||
guard let filterIndex = index else {
|
||||
return
|
||||
}
|
||||
|
||||
filters.remove(at: filterIndex)
|
||||
}
|
||||
|
||||
/// Answer whether the destination has any message filters
|
||||
/// returns boolean and is used to decide whether to resolve
|
||||
/// the message before invoking shouldLevelBeLogged
|
||||
func hasMessageFilters() -> Bool {
|
||||
return !getFiltersTargeting(Filter.TargetType.Message(.Equals([], true)),
|
||||
fromFilters: self.filters).isEmpty
|
||||
}
|
||||
|
||||
/// checks if level is at least minLevel or if a minLevel filter for that path does exist
|
||||
/// returns boolean and can be used to decide if a message should be logged or not
|
||||
func shouldLevelBeLogged(_ level: SwiftyBeaver.Level, path: String,
|
||||
function: String, message: String? = nil) -> Bool {
|
||||
|
||||
if filters.isEmpty {
|
||||
if level.rawValue >= minLevel.rawValue {
|
||||
if debugPrint {
|
||||
print("filters are empty and level >= minLevel")
|
||||
}
|
||||
return true
|
||||
} else {
|
||||
if debugPrint {
|
||||
print("filters are empty and level < minLevel")
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
let filterCheckResult = FilterValidator.validate(input: .init(filters: self.filters, level: level, path: path, function: function, message: message))
|
||||
|
||||
// Exclusion filters match if they do NOT meet the filter condition (see Filter.apply(_:) method)
|
||||
switch filterCheckResult[.excluded] {
|
||||
case .some(.someFiltersMatch):
|
||||
// Exclusion filters are present and at least one of them matches the log entry
|
||||
if debugPrint {
|
||||
print("filters are not empty and message was excluded")
|
||||
}
|
||||
return false
|
||||
case .some(.allFiltersMatch), .some(.noFiltersMatchingType), .none: break
|
||||
}
|
||||
|
||||
// If required filters exist, we should validate or invalidate the log if all of them pass or not
|
||||
switch filterCheckResult[.required] {
|
||||
case .some(.allFiltersMatch): return true
|
||||
case .some(.someFiltersMatch): return false
|
||||
case .some(.noFiltersMatchingType), .none: break
|
||||
}
|
||||
|
||||
let checkLogLevel: () -> Bool = {
|
||||
// Check if the log message's level matches or exceeds the minLevel of the destination
|
||||
return level.rawValue >= self.minLevel.rawValue
|
||||
}
|
||||
|
||||
// Non-required filters should only be applied if the log entry matches the filter condition (e.g. path)
|
||||
switch filterCheckResult[.nonRequired] {
|
||||
case .some(.allFiltersMatch): return true
|
||||
case .some(.noFiltersMatchingType), .none: return checkLogLevel()
|
||||
case .some(.someFiltersMatch(let partialMatchData)):
|
||||
if partialMatchData.fullMatchCount > 0 {
|
||||
// The log entry matches at least one filter condition and the destination's log level
|
||||
return true
|
||||
} else if partialMatchData.conditionMatchCount > 0 {
|
||||
// The log entry matches at least one filter condition, but does not match or exceed the destination's log level
|
||||
return false
|
||||
} else {
|
||||
// There is no filter with a matching filter condition. Check the destination's log level
|
||||
return checkLogLevel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getFiltersTargeting(_ target: Filter.TargetType, fromFilters: [FilterType]) -> [FilterType] {
|
||||
return fromFilters.filter { filter in
|
||||
return filter.getTarget() == target
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Triggered by main flush() method on each destination. Runs in background thread.
|
||||
Use for destinations that buffer log items, implement this function to flush those
|
||||
buffers to their final destination (web server...)
|
||||
*/
|
||||
func flush() {
|
||||
// no implementation in base destination needed
|
||||
}
|
||||
}
|
||||
|
||||
public func == (lhs: BaseDestination, rhs: BaseDestination) -> Bool {
|
||||
return ObjectIdentifier(lhs) == ObjectIdentifier(rhs)
|
||||
}
|
||||
Reference in New Issue
Block a user