Initial commit

This commit is contained in:
oscarz
2024-08-12 10:49:20 +08:00
parent 3002510aaf
commit 00fd0adf89
331 changed files with 53210 additions and 130 deletions

165
Pods/SwiftyBeaver/Sources/Base64.swift generated Normal file
View File

@ -0,0 +1,165 @@
//
// Base64.swift
// SwiftyBeaver (macOS)
//
// Copyright © 2017 Sebastian Kreutzberger. All rights reserved.
//
#if os(Linux)
import Foundation
struct InvalidBase64: Error {}
struct Base64 {
static func decode(_ string: String) throws -> [UInt8] {
return try decode([UInt8](string.utf8))
}
/// Decodes a Base64 encoded String into Data
///
/// - throws: If the string isn't base64 encoded
static func decode(_ string: [UInt8]) throws -> [UInt8] {
let lookupTable: [UInt8] = [
64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,
64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,
64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 62, 64, 62, 64, 63,
52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 64, 64, 64, 64, 64, 64,
64, 00, 01, 02, 03, 04, 05, 06, 07, 08, 09, 10, 11, 12, 13, 14,
15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 64, 64, 64, 64, 63,
64, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 64, 64, 64, 64, 64,
64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,
64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,
64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,
64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,
64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,
64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,
64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,
64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,
64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64
]
let remainder = string.count % 4
let length = (string.count - remainder) / 4
var decoded = [UInt8]()
decoded.reserveCapacity(length)
var index = 0
var i0: UInt8 = 0
var i1: UInt8 = 0
var i2: UInt8 = 0
var i3: UInt8 = 0
while index &+ 4 < string.count {
i0 = lookupTable[numericCast(string[index])]
i1 = lookupTable[numericCast(string[index &+ 1])]
i2 = lookupTable[numericCast(string[index &+ 2])]
i3 = lookupTable[numericCast(string[index &+ 3])]
if i0 > 63 || i1 > 63 || i2 > 63 || i3 > 63 {
throw InvalidBase64()
}
decoded.append(i0 << 2 | i1 >> 4)
decoded.append(i1 << 4 | i2 >> 2)
decoded.append(i2 << 6 | i3)
index += 4
}
if string.count &- index > 1 {
i0 = lookupTable[numericCast(string[index])]
i1 = lookupTable[numericCast(string[index &+ 1])]
if i1 > 63 {
guard string[index] == 61 else {
throw InvalidBase64()
}
return decoded
}
if i2 > 63 {
guard string[index &+ 2] == 61 else {
throw InvalidBase64()
}
return decoded
}
decoded.append(i0 << 2 | i1 >> 4)
if string.count &- index > 2 {
i2 = lookupTable[numericCast(string[index &+ 2])]
if i2 > 63 {
guard string[index &+ 2] == 61 else {
throw InvalidBase64()
}
return decoded
}
decoded.append(i1 << 4 | i2 >> 2)
if string.count &- index > 3 {
i3 = lookupTable[numericCast(string[index &+ 3])]
if i3 > 63 {
guard string[index &+ 3] == 61 else {
throw InvalidBase64()
}
return decoded
}
decoded.append(i2 << 6 | i3)
}
}
}
return decoded
}
static func encode(_ data: [UInt8]) -> String {
let base64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
var encoded: String = ""
func appendCharacterFromBase(_ character: Int) {
encoded.append(base64[base64.index(base64.startIndex, offsetBy: character)])
}
func byte(_ index: Int) -> Int {
return Int(data[index])
}
let decodedBytes = data.map { Int($0) }
var i = 0
while i < decodedBytes.count - 2 {
appendCharacterFromBase((byte(i) >> 2) & 0x3F)
appendCharacterFromBase(((byte(i) & 0x3) << 4) | ((byte(i + 1) & 0xF0) >> 4))
appendCharacterFromBase(((byte(i + 1) & 0xF) << 2) | ((byte(i + 2) & 0xC0) >> 6))
appendCharacterFromBase(byte(i + 2) & 0x3F)
i += 3
}
if i < decodedBytes.count {
appendCharacterFromBase((byte(i) >> 2) & 0x3F)
if i == decodedBytes.count - 1 {
appendCharacterFromBase(((byte(i) & 0x3) << 4))
encoded.append("=")
} else {
appendCharacterFromBase(((byte(i) & 0x3) << 4) | ((byte(i + 1) & 0xF0) >> 4))
appendCharacterFromBase(((byte(i + 1) & 0xF) << 2))
}
encoded.append("=")
}
return encoded
}
}
#endif

View 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)
}

View File

@ -0,0 +1,130 @@
//
// ConsoleDestination.swift
// SwiftyBeaver
//
// Created by Sebastian Kreutzberger on 05.12.15.
// Copyright © 2015 Sebastian Kreutzberger
// Some rights reserved: http://opensource.org/licenses/MIT
//
import Foundation
#if canImport(OSLog)
import OSLog
#endif
open class ConsoleDestination: BaseDestination {
public enum LogPrintWay {
case logger(subsystem: String, category: String)
case nslog
case print
}
/// Use this to change the logging method to the console. By default, it is set to .print. You can switch to .logger(subsystem:category:) to utilize the OSLog API.
public var logPrintWay: LogPrintWay = .print
/// use NSLog instead of print, default is false
public var useNSLog = false {
didSet {
if useNSLog {
logPrintWay = .nslog
}
}
}
/// uses colors compatible to Terminal instead of Xcode, default is false
public var useTerminalColors: Bool = false {
didSet {
if useTerminalColors {
// use Terminal colors
reset = "\u{001b}[0m"
escape = "\u{001b}[38;5;"
levelColor.verbose = "251m" // silver
levelColor.debug = "35m" // green
levelColor.info = "38m" // blue
levelColor.warning = "178m" // yellow
levelColor.error = "197m" // red
levelColor.critical = "197m" // red
levelColor.fault = "197m" // red
} else {
// use colored Emojis for better visual distinction
// of log level for Xcode 8
levelColor.verbose = "💜 " // purple
levelColor.debug = "💚 " // green
levelColor.info = "💙 " // blue
levelColor.warning = "💛 " // yellow
levelColor.error = "❤️ " // red
levelColor.critical = "❤️ " // red
levelColor.fault = "❤️ " // red
}
}
}
override public var defaultHashValue: Int { return 1 }
public override init() {
super.init()
levelColor.verbose = "💜 " // purple
levelColor.debug = "💚 " // green
levelColor.info = "💙 " // blue
levelColor.warning = "💛 " // yellow
levelColor.error = "❤️ " // red
levelColor.critical = "❤️ " // red
levelColor.fault = "❤️ " // red
}
// print to Xcode Console. uses full base class functionality
override open func send(_ level: SwiftyBeaver.Level, msg: String, thread: String,
file: String, function: String, line: Int, context: Any? = nil) -> String? {
let formattedString = super.send(level, msg: msg, thread: thread, file: file, function: function, line: line, context: context)
if let message = formattedString {
#if os(Linux)
print(message)
#else
switch logPrintWay {
case let .logger(subsystem, category):
_logger(message: message, level: level, subsystem: subsystem, category: category)
case .nslog:
_nslog(message: message)
case .print:
_print(message: message)
}
#endif
}
return formattedString
}
private func _logger(message: String, level: SwiftyBeaver.Level, subsystem: String, category: String) {
#if canImport(OSLog)
if #available(macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0, *) {
let logger = Logger(subsystem: subsystem, category: category)
switch level {
case .verbose:
logger.trace("\(message)")
case .debug:
logger.debug("\(message)")
case .info:
logger.info("\(message)")
case .warning:
logger.warning("\(message)")
case .error:
logger.error("\(message)")
case .critical:
logger.critical("\(message)")
case .fault:
logger.fault("\(message)")
}
} else {
_print(message: message)
}
#else
_print(message: message)
#endif
}
private func _nslog(message: String) {
NSLog("%@", message)
}
private func _print(message: String) {
print(message)
}
}

View File

@ -0,0 +1,35 @@
//
// Extensions.swift
// SwiftyBeaver
//
// Created by Sebastian Kreutzberger on 13.12.17.
// Copyright © 2017 Sebastian Kreutzberger. All rights reserved.
//
import Foundation
extension String {
/// cross-Swift compatible characters count
var length: Int {
return self.count
}
/// cross-Swift-compatible first character
var firstChar: Character? {
return self.first
}
/// cross-Swift-compatible last character
var lastChar: Character? {
return self.last
}
/// cross-Swift-compatible index
func find(_ char: Character) -> Index? {
#if swift(>=5)
return self.firstIndex(of: char)
#else
return self.index(of: char)
#endif
}
}

View File

@ -0,0 +1,240 @@
//
// FileDestination.swift
// SwiftyBeaver
//
// Created by Sebastian Kreutzberger on 05.12.15.
// Copyright © 2015 Sebastian Kreutzberger
// Some rights reserved: http://opensource.org/licenses/MIT
//
import Foundation
open class FileDestination: BaseDestination {
public var logFileURL: URL?
public var syncAfterEachWrite: Bool = false
public var colored: Bool = false {
didSet {
if colored {
// bash font color, first value is intensity, second is color
// see http://bit.ly/1Otu3Zr & for syntax http://bit.ly/1Tp6Fw9
// uses the 256-color table from http://bit.ly/1W1qJuH
reset = "\u{001b}[0m"
escape = "\u{001b}[38;5;"
levelColor.verbose = "251m" // silver
levelColor.debug = "35m" // green
levelColor.info = "38m" // blue
levelColor.warning = "178m" // yellow
levelColor.error = "197m" // red
} else {
reset = ""
escape = ""
levelColor.verbose = ""
levelColor.debug = ""
levelColor.info = ""
levelColor.warning = ""
levelColor.error = ""
}
}
}
// LOGFILE ROTATION
// ho many bytes should a logfile have until it is rotated?
// default is 5 MB. Just is used if logFileAmount > 1
public var logFileMaxSize = (5 * 1024 * 1024)
// Number of log files used in rotation, default is 1 which deactivates file rotation
public var logFileAmount = 1
override public var defaultHashValue: Int {return 2}
let fileManager = FileManager.default
public init(logFileURL: URL? = nil) {
if let logFileURL = logFileURL {
self.logFileURL = logFileURL
super.init()
return
}
// platform-dependent logfile directory default
var baseURL: URL?
#if os(OSX)
if let url = fileManager.urls(for: .cachesDirectory, in: .userDomainMask).first {
baseURL = url
// try to use ~/Library/Caches/APP NAME instead of ~/Library/Caches
if let appName = Bundle.main.object(forInfoDictionaryKey: "CFBundleExecutable") as? String {
do {
if let appURL = baseURL?.appendingPathComponent(appName, isDirectory: true) {
try fileManager.createDirectory(at: appURL,
withIntermediateDirectories: true, attributes: nil)
baseURL = appURL
}
} catch {
print("Warning! Could not create folder /Library/Caches/\(appName)")
}
}
}
#else
#if os(Linux)
baseURL = URL(fileURLWithPath: "/var/cache")
#else
// iOS, watchOS, etc. are using the caches directory
if let url = fileManager.urls(for: .cachesDirectory, in: .userDomainMask).first {
baseURL = url
}
#endif
#endif
if let baseURL = baseURL {
self.logFileURL = baseURL.appendingPathComponent("swiftybeaver.log", isDirectory: false)
}
super.init()
}
// append to file. uses full base class functionality
override open func send(_ level: SwiftyBeaver.Level, msg: String, thread: String,
file: String, function: String, line: Int, context: Any? = nil) -> String? {
let formattedString = super.send(level, msg: msg, thread: thread, file: file, function: function, line: line, context: context)
if let str = formattedString {
_ = validateSaveFile(str: str)
}
return formattedString
}
// check if filesize is bigger than wanted and if yes then rotate them
func validateSaveFile(str: String) -> Bool {
if self.logFileAmount > 1 {
guard let url = logFileURL else { return false }
let filePath = url.path
if FileManager.default.fileExists(atPath: filePath) == true {
do {
// Get file size
let attr = try FileManager.default.attributesOfItem(atPath: filePath)
let fileSize = attr[FileAttributeKey.size] as! UInt64
// Do file rotation
if fileSize > logFileMaxSize {
rotateFile(url)
}
} catch {
print("validateSaveFile error: \(error)")
}
}
}
return saveToFile(str: str)
}
private func rotateFile(_ fileUrl: URL) {
let filePath = fileUrl.path
let lastIndex = (logFileAmount-1)
let firstIndex = 1
do {
for index in stride(from: lastIndex, through: firstIndex, by: -1) {
let oldFile = makeRotatedFileUrl(fileUrl, index: index).path
if FileManager.default.fileExists(atPath: oldFile) {
if index == lastIndex {
// Delete the last file
try FileManager.default.removeItem(atPath: oldFile)
} else {
// Move the current file to next index
let newFile = makeRotatedFileUrl(fileUrl, index: index + 1).path
try FileManager.default.moveItem(atPath: oldFile, toPath: newFile)
}
}
}
// Finally, move the current file
let newFile = makeRotatedFileUrl(fileUrl, index: firstIndex).path
try FileManager.default.moveItem(atPath: filePath, toPath: newFile)
} catch {
print("rotateFile error: \(error)")
}
}
private func makeRotatedFileUrl(_ fileUrl: URL, index: Int) -> URL {
// The index is appended to the file name, to preserve the original extension.
fileUrl.deletingPathExtension()
.appendingPathExtension("\(index).\(fileUrl.pathExtension)")
}
/// appends a string as line to a file.
/// returns boolean about success
func saveToFile(str: String) -> Bool {
guard let url = logFileURL else { return false }
let line = str + "\n"
guard let data = line.data(using: String.Encoding.utf8) else { return false }
return write(data: data, to: url)
}
private func write(data: Data, to url: URL) -> Bool {
#if os(Linux)
return true
#else
var success = false
let coordinator = NSFileCoordinator(filePresenter: nil)
var error: NSError?
coordinator.coordinate(writingItemAt: url, error: &error) { url in
do {
if fileManager.fileExists(atPath: url.path) == false {
let directoryURL = url.deletingLastPathComponent()
if fileManager.fileExists(atPath: directoryURL.path) == false {
try fileManager.createDirectory(
at: directoryURL,
withIntermediateDirectories: true
)
}
fileManager.createFile(atPath: url.path, contents: nil)
#if os(iOS) || os(watchOS)
if #available(iOS 10.0, watchOS 3.0, *) {
var attributes = try fileManager.attributesOfItem(atPath: url.path)
attributes[FileAttributeKey.protectionKey] = FileProtectionType.none
try fileManager.setAttributes(attributes, ofItemAtPath: url.path)
}
#endif
}
let fileHandle = try FileHandle(forWritingTo: url)
fileHandle.seekToEndOfFile()
if #available(iOS 13.4, watchOS 6.2, tvOS 13.4, macOS 10.15.4, *) {
try fileHandle.write(contentsOf: data)
} else {
fileHandle.write(data)
}
if syncAfterEachWrite {
fileHandle.synchronizeFile()
}
fileHandle.closeFile()
success = true
} catch {
print("SwiftyBeaver File Destination could not write to file \(url).")
}
}
if let error = error {
print("Failed writing file with error: \(String(describing: error))")
return false
}
return success
#endif
}
/// deletes log file.
/// returns true if file was removed or does not exist, false otherwise
public func deleteLogFile() -> Bool {
guard let url = logFileURL, fileManager.fileExists(atPath: url.path) == true else { return true }
do {
try fileManager.removeItem(at: url)
return true
} catch {
print("SwiftyBeaver File Destination could not remove file \(url).")
return false
}
}
}

283
Pods/SwiftyBeaver/Sources/Filter.swift generated Normal file
View File

@ -0,0 +1,283 @@
//
// Filter.swift
// SwiftyBeaver
//
// Created by Jeff Roberts on 5/31/16.
// Copyright © 2015 Sebastian Kreutzberger
// Some rights reserved: http://opensource.org/licenses/MIT
//
import Foundation
/// FilterType is a protocol that describes something that determines
/// whether or not a message gets logged. A filter answers a Bool when it
/// is applied to a value. If the filter passes, it shall return true,
/// false otherwise.
///
/// A filter must contain a target, which identifies what it filters against
/// A filter can be required meaning that all required filters against a specific
/// target must pass in order for the message to be logged.
public protocol FilterType : AnyObject {
func apply(_ value: String?) -> Bool
func getTarget() -> Filter.TargetType
func isRequired() -> Bool
func isExcluded() -> Bool
func reachedMinLevel(_ level: SwiftyBeaver.Level) -> Bool
}
/// Filters is syntactic sugar used to easily construct filters
public class Filters {
public static let Path = PathFilterFactory.self
public static let Function = FunctionFilterFactory.self
public static let Message = MessageFilterFactory.self
}
/// Filter is an abstract base class for other filters
public class Filter {
public enum TargetType {
case Path(Filter.ComparisonType)
case Function(Filter.ComparisonType)
case Message(Filter.ComparisonType)
}
public enum ComparisonType {
case StartsWith([String], Bool)
case Contains([String], Bool)
case Excludes([String], Bool)
case EndsWith([String], Bool)
case Equals([String], Bool)
case Custom((String) -> Bool)
}
let targetType: Filter.TargetType
let required: Bool
let minLevel: SwiftyBeaver.Level
public init(_ target: Filter.TargetType, required: Bool, minLevel: SwiftyBeaver.Level) {
self.targetType = target
self.required = required
self.minLevel = minLevel
}
public func getTarget() -> Filter.TargetType {
return self.targetType
}
public func isRequired() -> Bool {
return self.required
}
public func isExcluded() -> Bool {
return false
}
/// returns true of set minLevel is >= as given level
public func reachedMinLevel(_ level: SwiftyBeaver.Level) -> Bool {
//print("checking if given level \(level) >= \(minLevel)")
return level.rawValue >= minLevel.rawValue
}
}
/// CompareFilter is a FilterType that can filter based upon whether a target
/// starts with, contains or ends with a specific string. CompareFilters can be
/// case sensitive.
public class CompareFilter: Filter, FilterType {
private var filterComparisonType: Filter.ComparisonType?
override public init(_ target: Filter.TargetType, required: Bool, minLevel: SwiftyBeaver.Level) {
super.init(target, required: required, minLevel: minLevel)
let comparisonType: Filter.ComparisonType?
switch self.getTarget() {
case let .Function(comparison):
comparisonType = comparison
case let .Path(comparison):
comparisonType = comparison
case let .Message(comparison):
comparisonType = comparison
/*default:
comparisonType = nil*/
}
self.filterComparisonType = comparisonType
}
public func apply(_ value: String?) -> Bool {
guard let value = value else {
return false
}
guard let filterComparisonType = self.filterComparisonType else {
return false
}
let matches: Bool
switch filterComparisonType {
case let .Contains(strings, caseSensitive):
matches = !strings.filter { string in
return caseSensitive ? value.contains(string) :
value.lowercased().contains(string.lowercased())
}.isEmpty
case let .Excludes(strings, caseSensitive):
matches = !strings.filter { string in
return caseSensitive ? !value.contains(string) :
!value.lowercased().contains(string.lowercased())
}.isEmpty
case let .StartsWith(strings, caseSensitive):
matches = !strings.filter { string in
return caseSensitive ? value.hasPrefix(string) :
value.lowercased().hasPrefix(string.lowercased())
}.isEmpty
case let .EndsWith(strings, caseSensitive):
matches = !strings.filter { string in
return caseSensitive ? value.hasSuffix(string) :
value.lowercased().hasSuffix(string.lowercased())
}.isEmpty
case let .Equals(strings, caseSensitive):
matches = !strings.filter { string in
return caseSensitive ? value == string :
value.lowercased() == string.lowercased()
}.isEmpty
case let .Custom(predicate):
matches = predicate(value)
}
return matches
}
override public func isExcluded() -> Bool {
guard let filterComparisonType = self.filterComparisonType else { return false }
switch filterComparisonType {
case .Excludes:
return true
default:
return false
}
}
}
// Syntactic sugar for creating a function comparison filter
public class FunctionFilterFactory {
public static func startsWith(_ prefixes: String..., caseSensitive: Bool = false,
required: Bool = false, minLevel: SwiftyBeaver.Level = .verbose) -> FilterType {
return CompareFilter(.Function(.StartsWith(prefixes, caseSensitive)), required: required, minLevel: minLevel)
}
public static func contains(_ strings: String..., caseSensitive: Bool = false,
required: Bool = false, minLevel: SwiftyBeaver.Level = .verbose) -> FilterType {
return CompareFilter(.Function(.Contains(strings, caseSensitive)), required: required, minLevel: minLevel)
}
public static func excludes(_ strings: String..., caseSensitive: Bool = false,
required: Bool = false, minLevel: SwiftyBeaver.Level = .verbose) -> FilterType {
return CompareFilter(.Function(.Excludes(strings, caseSensitive)), required: required, minLevel: minLevel)
}
public static func endsWith(_ suffixes: String..., caseSensitive: Bool = false,
required: Bool = false, minLevel: SwiftyBeaver.Level = .verbose) -> FilterType {
return CompareFilter(.Function(.EndsWith(suffixes, caseSensitive)), required: required, minLevel: minLevel)
}
public static func equals(_ strings: String..., caseSensitive: Bool = false,
required: Bool = false, minLevel: SwiftyBeaver.Level = .verbose) -> FilterType {
return CompareFilter(.Function(.Equals(strings, caseSensitive)), required: required, minLevel: minLevel)
}
public static func custom(required: Bool = false, minLevel: SwiftyBeaver.Level = .verbose, filterPredicate: @escaping (String) -> Bool) -> FilterType {
return CompareFilter(.Function(.Custom(filterPredicate)), required: required, minLevel: minLevel)
}
}
// Syntactic sugar for creating a message comparison filter
public class MessageFilterFactory {
public static func startsWith(_ prefixes: String..., caseSensitive: Bool = false,
required: Bool = false, minLevel: SwiftyBeaver.Level = .verbose) -> FilterType {
return CompareFilter(.Message(.StartsWith(prefixes, caseSensitive)), required: required, minLevel: minLevel)
}
public static func contains(_ strings: String..., caseSensitive: Bool = false,
required: Bool = false, minLevel: SwiftyBeaver.Level = .verbose) -> FilterType {
return CompareFilter(.Message(.Contains(strings, caseSensitive)), required: required, minLevel: minLevel)
}
public static func excludes(_ strings: String..., caseSensitive: Bool = false,
required: Bool = false, minLevel: SwiftyBeaver.Level = .verbose) -> FilterType {
return CompareFilter(.Message(.Excludes(strings, caseSensitive)), required: required, minLevel: minLevel)
}
public static func endsWith(_ suffixes: String..., caseSensitive: Bool = false,
required: Bool = false, minLevel: SwiftyBeaver.Level = .verbose) -> FilterType {
return CompareFilter(.Message(.EndsWith(suffixes, caseSensitive)), required: required, minLevel: minLevel)
}
public static func equals(_ strings: String..., caseSensitive: Bool = false,
required: Bool = false, minLevel: SwiftyBeaver.Level = .verbose) -> FilterType {
return CompareFilter(.Message(.Equals(strings, caseSensitive)), required: required, minLevel: minLevel)
}
public static func custom(required: Bool = false, minLevel: SwiftyBeaver.Level = .verbose, filterPredicate: @escaping (String) -> Bool) -> FilterType {
return CompareFilter(.Message(.Custom(filterPredicate)), required: required, minLevel: minLevel)
}
}
// Syntactic sugar for creating a path comparison filter
public class PathFilterFactory {
public static func startsWith(_ prefixes: String..., caseSensitive: Bool = false,
required: Bool = false, minLevel: SwiftyBeaver.Level = .verbose) -> FilterType {
return CompareFilter(.Path(.StartsWith(prefixes, caseSensitive)), required: required, minLevel: minLevel)
}
public static func contains(_ strings: String..., caseSensitive: Bool = false,
required: Bool = false, minLevel: SwiftyBeaver.Level = .verbose) -> FilterType {
return CompareFilter(.Path(.Contains(strings, caseSensitive)), required: required, minLevel: minLevel)
}
public static func excludes(_ strings: String..., caseSensitive: Bool = false,
required: Bool = false, minLevel: SwiftyBeaver.Level = .verbose) -> FilterType {
return CompareFilter(.Path(.Excludes(strings, caseSensitive)), required: required, minLevel: minLevel)
}
public static func endsWith(_ suffixes: String..., caseSensitive: Bool = false,
required: Bool = false, minLevel: SwiftyBeaver.Level = .verbose) -> FilterType {
return CompareFilter(.Path(.EndsWith(suffixes, caseSensitive)), required: required, minLevel: minLevel)
}
public static func equals(_ strings: String..., caseSensitive: Bool = false,
required: Bool = false, minLevel: SwiftyBeaver.Level = .verbose) -> FilterType {
return CompareFilter(.Path(.Equals(strings, caseSensitive)), required: required, minLevel: minLevel)
}
public static func custom(required: Bool = false, minLevel: SwiftyBeaver.Level = .verbose, filterPredicate: @escaping (String) -> Bool) -> FilterType {
return CompareFilter(.Path(.Custom(filterPredicate)), required: required, minLevel: minLevel)
}
}
extension Filter.TargetType: Equatable {
}
// The == does not compare associated values for each enum. Instead == evaluates to true
// if both enums are the same "types", ignoring the associated values of each enum
public func == (lhs: Filter.TargetType, rhs: Filter.TargetType) -> Bool {
switch (lhs, rhs) {
case (.Path, .Path):
return true
case (.Function, .Function):
return true
case (.Message, .Message):
return true
default:
return false
}
}

View File

@ -0,0 +1,129 @@
//
// FilterValidator.swift
// SwiftyBeaver (iOS)
//
// Created by Felix Lisczyk on 07.07.19.
// Copyright © 2019 Sebastian Kreutzberger. All rights reserved.
//
import Foundation
/// FilterValidator is a utility class used by BaseDestination.
/// It encapsulates the filtering logic for excluded, required
/// and non-required filters.
///
/// FilterValidator evaluates a set of filters for a single log
/// entry. It determines if these filters apply to the log entry
/// based on their condition (path, function, message) and their
/// minimum log level.
struct FilterValidator {
// These are the different filter types that the user can set
enum ValidationType: CaseIterable {
case excluded
case required
case nonRequired
func apply(to filters: [FilterType]) -> [FilterType] {
switch self {
case .excluded:
return filters.filter { $0.isExcluded() }
case .required:
return filters.filter { $0.isRequired() && !$0.isExcluded() }
case .nonRequired:
return filters.filter { !$0.isRequired() && !$0.isExcluded() }
}
}
}
// Wrapper object for input parameters
struct Input {
let filters: [FilterType]
let level: SwiftyBeaver.Level
let path: String
let function: String
let message: String?
}
// Result wrapper object
enum Result {
case allFiltersMatch // All filters fully match the log entry (condition + minimum log level)
case someFiltersMatch(PartialMatchData) // Only some filters fully match the log entry (condition + minimum log level)
case noFiltersMatchingType // There are no filters set for a particular type (excluded, required, nonRequired)
struct PartialMatchData {
let fullMatchCount: Int // Number of filters that match both the condition and the minimum log level of the log entry
let conditionMatchCount: Int // Number of filters that match ONLY the condition of the log entry (path, function, message)
let logLevelMatchCount: Int // Number of filters that match ONLY the minimum log level of the log entry
}
}
static func validate(input: Input, for types: [ValidationType] = ValidationType.allCases) -> [ValidationType: Result] {
var results = [ValidationType: Result]()
for type in types {
let filtersToValidate = type.apply(to: input.filters)
if filtersToValidate.isEmpty {
// There are no filters set for this particular type
results[type] = .noFiltersMatchingType
} else {
var fullMatchCount: Int = 0
var conditionMatchCount: Int = 0
var logLevelMatchCount: Int = 0
for filter in filtersToValidate {
let filterMatchesCondition = self.filterMatchesCondition(filter, level: input.level, path: input.path, function: input.function, message: input.message)
let filterMatchesMinLogLevel = self.filterMatchesMinLogLevel(filter, level: input.level)
switch (filterMatchesCondition, filterMatchesMinLogLevel) {
// Filter matches both the condition and the minimum log level
case (true, true): fullMatchCount += 1
// Filter matches only the condition (path, function, message)
case (true, false): conditionMatchCount += 1
// Filter matches only the minimum log level
case (false, true): logLevelMatchCount += 1
// Filter does not match the condition nor the minimum log level
case (false, false): break
}
}
if filtersToValidate.count == fullMatchCount {
// All filters fully match the log entry
results[type] = .allFiltersMatch
} else {
// Only some filters match the log entry
results[type] = .someFiltersMatch(.init(fullMatchCount: fullMatchCount, conditionMatchCount: conditionMatchCount, logLevelMatchCount: logLevelMatchCount))
}
}
}
return results
}
private static func filterMatchesCondition(_ filter: FilterType, level: SwiftyBeaver.Level,
path: String, function: String, message: String?) -> Bool {
let passes: Bool
switch filter.getTarget() {
case .Path(_):
passes = filter.apply(path)
case .Function(_):
passes = filter.apply(function)
case .Message(_):
guard let message = message else {
return false
}
passes = filter.apply(message)
}
return passes
}
private static func filterMatchesMinLogLevel(_ filter: FilterType, level: SwiftyBeaver.Level) -> Bool {
return filter.reachedMinLevel(level)
}
}

View File

@ -0,0 +1,99 @@
//
// GoogleCloudDestination.swift
// SwiftyBeaver
//
// Copyright © 2017 Sebastian Kreutzberger. All rights reserved.
//
import Foundation
public final class GoogleCloudDestination: BaseDestination {
private let serviceName: String
public init(serviceName: String) {
self.serviceName = serviceName
super.init()
}
override public var asynchronously: Bool {
get {
return false
}
set {
return
}
}
override public func send(_ level: SwiftyBeaver.Level, msg: String, thread: String,
file: String, function: String, line: Int, context: Any? = nil) -> String? {
let reportLocation: [String: Any] = ["filePath": file, "lineNumber": line, "functionName": function]
var gcpContext: [String: Any] = ["reportLocation": reportLocation]
if let context = context as? [String: Any] {
if let httpRequestContext = context["httpRequest"] as? [String: Any] {
gcpContext["httpRequest"] = httpRequestContext
}
if let user = context["user"] as? String {
gcpContext["user"] = user
}
}
let gcpJSON: [String: Any] = [
"serviceContext": [
"service": serviceName
],
"message": msg,
"severity": level.severity,
"context": gcpContext
]
let finalLogString: String
do {
finalLogString = try jsonString(obj: gcpJSON)
} catch {
let uncrashableLogString = "{\"context\":{\"reportLocation\":{\"filePath\": \"\(file)\"" +
",\"functionName\":\"\(function)\"" +
",\"lineNumber\":\(line)},\"severity\"" +
":\"CRITICAL\",\"message\":\"Error encoding " +
"JSON log entry. You may be losing log messages!\"}"
finalLogString = uncrashableLogString.description
}
print(finalLogString)
return finalLogString
}
private func jsonString(obj: Dictionary<String, Any>) throws -> String {
let json = try JSONSerialization.data(withJSONObject: obj, options: [])
guard let string = String(data: json, encoding: .utf8) else {
throw GCPError.serialization
}
return string
}
}
///
/// https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#LogSeverity
extension SwiftyBeaver.Level {
/// Verbose is reported as Debug to GCP.
/// Recommend you don't bother using it.
var severity: String {
switch self {
// There is only one level below "Debug": "Default", which becomes "Any" and is considered as a potential error as well
case .verbose: return "DEBUG"
case .debug: return "DEBUG"
case .info: return "INFO"
case .warning: return "WARNING"
case .error: return "ERROR"
case .critical: return "CRITICAL"
case .fault: return "FAULT"
}
}
}
private enum GCPError: Error {
case serialization
}

View File

@ -0,0 +1,246 @@
//
// SwiftyBeaver.swift
// SwiftyBeaver
//
// Created by Sebastian Kreutzberger (Twitter @skreutzb) on 28.11.15.
// Copyright © 2015 Sebastian Kreutzberger
// Some rights reserved: http://opensource.org/licenses/MIT
//
import Foundation
open class SwiftyBeaver {
/// version string of framework
public static let version = "2.1.1" // UPDATE ON RELEASE!
/// build number of framework
public static let build = 2110 // version 1.6.2 -> 1620, UPDATE ON RELEASE!
public enum Level: Int {
case verbose = 0
case debug = 1
case info = 2
case warning = 3
case error = 4
case critical = 5
case fault = 6
}
// a set of active destinations
public private(set) static var destinations = Set<BaseDestination>()
/// A private queue for synchronizing access to `destinations`.
/// Read accesses are done concurrently.
/// Write accesses are done with a barrier, ensuring only 1 operation is ran at that time.
private static let queue = DispatchQueue(label: "destination queue", attributes: .concurrent)
// MARK: Destination Handling
/// returns boolean about success
@discardableResult
open class func addDestination(_ destination: BaseDestination) -> Bool {
return queue.sync(flags: DispatchWorkItemFlags.barrier) {
if destinations.contains(destination) {
return false
}
destinations.insert(destination)
return true
}
}
/// returns boolean about success
@discardableResult
open class func removeDestination(_ destination: BaseDestination) -> Bool {
return queue.sync(flags: DispatchWorkItemFlags.barrier) {
if destinations.contains(destination) == false {
return false
}
destinations.remove(destination)
return true
}
}
/// if you need to start fresh
open class func removeAllDestinations() {
queue.sync(flags: DispatchWorkItemFlags.barrier) {
destinations.removeAll()
}
}
/// returns the amount of destinations
open class func countDestinations() -> Int {
return queue.sync { destinations.count }
}
/// returns the current thread name
open class func threadName() -> String {
#if os(Linux)
// on 9/30/2016 not yet implemented in server-side Swift:
// > import Foundation
// > Thread.isMainThread
return ""
#else
if Thread.isMainThread {
return ""
} else {
let name = __dispatch_queue_get_label(nil)
return String(cString: name, encoding: .utf8) ?? Thread.current.description
}
#endif
}
// MARK: Levels
/// log something generally unimportant (lowest priority)
open class func verbose(_ message: @autoclosure () -> Any,
file: String = #file, function: String = #function, line: Int = #line, context: Any? = nil) {
#if swift(>=5)
custom(level: .verbose, message: message(), file: file, function: function, line: line, context: context)
#else
custom(level: .verbose, message: message, file: file, function: function, line: line, context: context)
#endif
}
/// log something which help during debugging (low priority)
open class func debug(_ message: @autoclosure () -> Any,
file: String = #file, function: String = #function, line: Int = #line, context: Any? = nil) {
#if swift(>=5)
custom(level: .debug, message: message(), file: file, function: function, line: line, context: context)
#else
custom(level: .debug, message: message, file: file, function: function, line: line, context: context)
#endif
}
/// log something which you are really interested but which is not an issue or error (normal priority)
open class func info(_ message: @autoclosure () -> Any,
file: String = #file, function: String = #function, line: Int = #line, context: Any? = nil) {
#if swift(>=5)
custom(level: .info, message: message(), file: file, function: function, line: line, context: context)
#else
custom(level: .info, message: message, file: file, function: function, line: line, context: context)
#endif
}
/// log something which may cause big trouble soon (high priority)
open class func warning(_ message: @autoclosure () -> Any,
file: String = #file, function: String = #function, line: Int = #line, context: Any? = nil) {
#if swift(>=5)
custom(level: .warning, message: message(), file: file, function: function, line: line, context: context)
#else
custom(level: .warning, message: message, file: file, function: function, line: line, context: context)
#endif
}
/// log something which will keep you awake at night (highest priority)
open class func error(_ message: @autoclosure () -> Any,
file: String = #file, function: String = #function, line: Int = #line, context: Any? = nil) {
#if swift(>=5)
custom(level: .error, message: message(), file: file, function: function, line: line, context: context)
#else
custom(level: .error, message: message, file: file, function: function, line: line, context: context)
#endif
}
/// log something which will keep you awake at night (highest priority)
open class func critical(_ message: @autoclosure () -> Any,
file: String = #file, function: String = #function, line: Int = #line, context: Any? = nil) {
#if swift(>=5)
custom(level: .critical, message: message(), file: file, function: function, line: line, context: context)
#else
custom(level: .critical, message: message, file: file, function: function, line: line, context: context)
#endif
}
/// log something which will keep you awake at night (highest priority)
open class func fault(_ message: @autoclosure () -> Any,
file: String = #file, function: String = #function, line: Int = #line, context: Any? = nil) {
#if swift(>=5)
custom(level: .fault, message: message(), file: file, function: function, line: line, context: context)
#else
custom(level: .fault, message: message, file: file, function: function, line: line, context: context)
#endif
}
/// custom logging to manually adjust values, should just be used by other frameworks
open class func custom(level: SwiftyBeaver.Level, message: @autoclosure () -> Any,
file: String = #file, function: String = #function, line: Int = #line, context: Any? = nil) {
#if swift(>=5)
dispatch_send(level: level, message: message(), thread: threadName(),
file: file, function: function, line: line, context: context)
#else
dispatch_send(level: level, message: message, thread: threadName(),
file: file, function: function, line: line, context: context)
#endif
}
/// internal helper which dispatches send to dedicated queue if minLevel is ok
class func dispatch_send(level: SwiftyBeaver.Level, message: @autoclosure () -> Any,
thread: String, file: String, function: String, line: Int, context: Any?) {
var resolvedMessage: String?
let destinations = queue.sync { self.destinations }
for dest in destinations {
guard let queue = dest.queue else {
continue
}
resolvedMessage = resolvedMessage == nil && dest.hasMessageFilters() ? "\(message())" : resolvedMessage
if dest.shouldLevelBeLogged(level, path: file, function: function, message: resolvedMessage) {
// try to convert msg object to String and put it on queue
let msgStr = resolvedMessage == nil ? "\(message())" : resolvedMessage!
let f = stripParams(function: function)
if dest.asynchronously {
queue.async {
_ = dest.send(level, msg: msgStr, thread: thread, file: file, function: f, line: line, context: context)
}
} else {
queue.sync {
_ = dest.send(level, msg: msgStr, thread: thread, file: file, function: f, line: line, context: context)
}
}
}
}
}
/// flush all destinations to make sure all logging messages have been written out
/// returns after all messages flushed or timeout seconds
/// returns: true if all messages flushed, false if timeout or error occurred
public class func flush(secondTimeout: Int64) -> Bool {
let grp = DispatchGroup()
let destinations = queue.sync { self.destinations }
for dest in destinations {
guard let queue = dest.queue else {
continue
}
grp.enter()
if dest.asynchronously {
queue.async {
dest.flush()
grp.leave()
}
} else {
queue.sync {
dest.flush()
grp.leave()
}
}
}
return grp.wait(timeout: .now() + .seconds(Int(secondTimeout))) == .success
}
/// removes the parameters from a function because it looks weird with a single param
class func stripParams(function: String) -> String {
var f = function
if let indexOfBrace = f.find("(") {
#if swift(>=4.0)
f = String(f[..<indexOfBrace])
#else
f = f.substring(to: indexOfBrace)
#endif
}
f += "()"
return f
}
}