Sources/SourceParsingFramework/Utilities/Logger.swift (91 lines of code) (raw):
//
// Copyright (c) 2018. Uber Technologies
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Concurrency
import Foundation
/// The various levels of logging.
public enum LoggingLevel: Int {
/// The most verbose level including all logs.
case debug
/// The level that includes everything except debug logs.
case info
/// The level that only includes warning logs.
case warning
/// The level that indicates a fatal error has occurred.
case error
/// An cute emoticon describing the log level.
public var emoticon: String {
switch self {
case .debug: return "🐞"
case .info: return "📋"
case .warning: return "❗️"
case .error: return "💩"
}
}
/// A text description the log level.
public var text: String {
switch self {
case .debug: return "debug"
case .info: return "info"
case .warning: return "warning"
case .error: return "error"
}
}
/// Retrieve the logging level based on the given String value.
///
/// - parameter value: The `String` value to parse from.
/// - returns: The corresponding `LoggingLevel` if there is one.
/// `nil` otherwise.
public static func level(from value: String) -> LoggingLevel? {
switch value {
case "debug": return .debug
case "info": return .info
case "warning": return .warning
case "error": return .error
default: return nil
}
}
}
// Use `AtomicInt` since logging may be invoked from multiple threads.
private let minLoggingOutputLevel = AtomicInt(initialValue: LoggingLevel.warning.rawValue)
/// Set the minimum logging level required to output a message.
///
/// - parameter minLoggingOutputLevel: The minimum logging level.
public func set(minLoggingOutputLevel level: LoggingLevel) {
minLoggingOutputLevel.value = level.rawValue
}
/// Log the given message at the `debug` level.
///
/// - parameter message: The message to log.
/// - parameter path: The file path this error applies to
/// (if specified, the log output changes to one that matches compiler error and can be parsed easily by build systems)
/// - note: The mesasge is only logged if the current `minLoggingOutputLevel`
/// is set at or below the `debug` level.
public func debug(_ message: String, path: String? = nil) {
log(message, atLevel: .debug, path: path)
}
/// Log the given message at the `info` level.
///
/// - parameter message: The message to log.
/// - parameter path: The file path this error applies to
/// (if specified, the log output changes to one that matches compiler error and can be parsed easily by build systems)
/// - note: The mesasge is only logged if the current `minLoggingOutputLevel`
/// is set at or below the `info` level.
public func info(_ message: String, path: String? = nil) {
log(message, atLevel: .info, path: path)
}
/// Log the given message at the `warning` level.
///
/// - parameter message: The message to log.
/// - parameter path: The file path this error applies to
/// (if specified, the log output changes to one that matches compiler error and can be parsed easily by build systems)
/// - note: The mesasge is only logged if the current `minLoggingOutputLevel`
/// is set at or below the `warning` level.
public func warning(_ message: String, path: String? = nil) {
log(message, atLevel: .warning, path: path)
}
/// Log the given message at the `error` level and terminate.
///
/// - parameter message: The message to log.
/// - parameter path: The file path this error applies to
/// (if specified, the log output changes to one that matches compiler error and can be parsed easily by build systems)
/// - returns: `Never`.
/// - note: The mesasge is only logged if the current `minLoggingOutputLevel`
/// is set at or below the `error` level.
/// - note: This method terminates the program. It returns `Never`.
public func error(_ message: String, path: String? = nil) -> Never {
log(message, atLevel: .error, path: path)
exit(1)
}
private func log(_ message: String, atLevel level: LoggingLevel, path: String?) {
#if DEBUG
UnitTestLogger.instance.log(message, at: level)
#endif
if level.rawValue >= minLoggingOutputLevel.value {
if let path = path {
print("\(path):1:1: \(level.text): \(message)")
} else {
print("\(level.text): \(level.emoticon) \(message)")
}
}
}
// MARK: - Unit Test
/// A logger that accumulates log messages to support unit testing.
public class UnitTestLogger {
/// The singleton instance.
public static let instance = UnitTestLogger()
/// The current set of logged messages.
public var messages: [String] {
return lockedMessages.values
}
// NARK: - Private
private let lockedMessages = LockedArray<String>()
private init() {}
fileprivate func log(_ message: String, at level: LoggingLevel) {
lockedMessages.append(message)
}
}
private class LockedArray<ValueType> {
private let lock = NSLock()
private var unsafeValues = [ValueType]()
fileprivate var values: [ValueType] {
lock.lock()
defer {
lock.unlock()
}
return Array(unsafeValues)
}
fileprivate func append(_ value: ValueType) {
lock.lock()
defer {
lock.unlock()
}
unsafeValues.append(value)
}
}