334 lines
9.9 KiB
Swift
334 lines
9.9 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
//
|
|
// This source file is part of the Swift Baggage Context open source project
|
|
//
|
|
// Copyright (c) 2020 Moritz Lang and the Swift Baggage Context project authors
|
|
// Licensed under Apache License v2.0
|
|
//
|
|
// See LICENSE.txt for license information
|
|
//
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
//
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
//===----------------------------------------------------------------------===//
|
|
//
|
|
// This source file is part of the Swift Logging API open source project
|
|
//
|
|
// Copyright (c) 2018-2019 Apple Inc. and the Swift Logging API project authors
|
|
// Licensed under Apache License v2.0
|
|
//
|
|
// See LICENSE.txt for license information
|
|
// See CONTRIBUTORS.txt for the list of Swift Logging API project authors
|
|
//
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
//
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
import Foundation
|
|
@testable import Logging
|
|
import XCTest
|
|
|
|
/// Copy of swift-log's TestLogging
|
|
internal struct TestLogging {
|
|
private let _config = Config() // shared among loggers
|
|
private let recorder = Recorder() // shared among loggers
|
|
|
|
func make(label: String) -> LogHandler {
|
|
return TestLogHandler(label: label, config: self.config, recorder: self.recorder)
|
|
}
|
|
|
|
var config: Config { return self._config }
|
|
var history: History { return self.recorder }
|
|
}
|
|
|
|
internal struct TestLogHandler: LogHandler {
|
|
private let logLevelLock = NSLock()
|
|
private let metadataLock = NSLock()
|
|
private let recorder: Recorder
|
|
private let config: Config
|
|
private var logger: Logger // the actual logger
|
|
|
|
let label: String
|
|
init(label: String, config: Config, recorder: Recorder) {
|
|
self.label = label
|
|
self.config = config
|
|
self.recorder = recorder
|
|
self.logger = Logger(label: "test", StreamLogHandler.standardOutput(label: label))
|
|
self.logger.logLevel = .debug
|
|
}
|
|
|
|
func log(level: Logger.Level, message: Logger.Message, metadata: Logger.Metadata?, source: String, file: String, function: String, line: UInt) {
|
|
let metadata = (self._metadataSet ? self.metadata : MDC.global.metadata).merging(metadata ?? [:], uniquingKeysWith: { _, new in new })
|
|
self.logger.log(level: level, message, metadata: metadata, source: source, file: file, function: function, line: line)
|
|
self.recorder.record(level: level, metadata: metadata, message: message, source: source)
|
|
}
|
|
|
|
private var _logLevel: Logger.Level?
|
|
var logLevel: Logger.Level {
|
|
get {
|
|
// get from config unless set
|
|
return self.logLevelLock.withLock { self._logLevel } ?? self.config.get(key: self.label)
|
|
}
|
|
set {
|
|
self.logLevelLock.withLock { self._logLevel = newValue }
|
|
}
|
|
}
|
|
|
|
private var _metadataSet = false
|
|
private var _metadata = Logger.Metadata() {
|
|
didSet {
|
|
self._metadataSet = true
|
|
}
|
|
}
|
|
|
|
public var metadata: Logger.Metadata {
|
|
get {
|
|
return self.metadataLock.withLock { self._metadata }
|
|
}
|
|
set {
|
|
self.metadataLock.withLock { self._metadata = newValue }
|
|
}
|
|
}
|
|
|
|
// TODO: would be nice to delegate to local copy of logger but StdoutLogger is a reference type. why?
|
|
subscript(metadataKey metadataKey: Logger.Metadata.Key) -> Logger.Metadata.Value? {
|
|
get {
|
|
return self.metadataLock.withLock { self._metadata[metadataKey] }
|
|
}
|
|
set {
|
|
self.metadataLock.withLock {
|
|
self._metadata[metadataKey] = newValue
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
internal class Config {
|
|
private static let ALL = "*"
|
|
|
|
private let lock = NSLock()
|
|
private var storage = [String: Logger.Level]()
|
|
|
|
func get(key: String) -> Logger.Level {
|
|
return self.get(key) ?? self.get(Config.ALL) ?? Logger.Level.debug
|
|
}
|
|
|
|
func get(_ key: String) -> Logger.Level? {
|
|
guard let value = (self.lock.withLock { self.storage[key] }) else {
|
|
return nil
|
|
}
|
|
return value
|
|
}
|
|
|
|
func set(key: String = Config.ALL, value: Logger.Level) {
|
|
self.lock.withLock { self.storage[key] = value }
|
|
}
|
|
|
|
func clear() {
|
|
self.lock.withLock { self.storage.removeAll() }
|
|
}
|
|
}
|
|
|
|
internal class Recorder: History {
|
|
private let lock = NSLock()
|
|
private var _entries = [LogEntry]()
|
|
|
|
func record(level: Logger.Level, metadata: Logger.Metadata?, message: Logger.Message, source: String) {
|
|
self.lock.withLock {
|
|
self._entries.append(LogEntry(level: level, metadata: metadata, message: message.description, source: source))
|
|
}
|
|
}
|
|
|
|
var entries: [LogEntry] {
|
|
return self.lock.withLock { return self._entries }
|
|
}
|
|
}
|
|
|
|
internal protocol History {
|
|
var entries: [LogEntry] { get }
|
|
}
|
|
|
|
internal extension History {
|
|
func atLevel(level: Logger.Level) -> [LogEntry] {
|
|
return self.entries.filter { entry in
|
|
level == entry.level
|
|
}
|
|
}
|
|
|
|
var trace: [LogEntry] {
|
|
return self.atLevel(level: .debug)
|
|
}
|
|
|
|
var debug: [LogEntry] {
|
|
return self.atLevel(level: .debug)
|
|
}
|
|
|
|
var info: [LogEntry] {
|
|
return self.atLevel(level: .info)
|
|
}
|
|
|
|
var warning: [LogEntry] {
|
|
return self.atLevel(level: .warning)
|
|
}
|
|
|
|
var error: [LogEntry] {
|
|
return self.atLevel(level: .error)
|
|
}
|
|
}
|
|
|
|
internal struct LogEntry {
|
|
let level: Logger.Level
|
|
let metadata: Logger.Metadata?
|
|
let message: String
|
|
let source: String
|
|
}
|
|
|
|
extension History {
|
|
func assertExist(level: Logger.Level,
|
|
message: String,
|
|
metadata: Logger.Metadata? = nil,
|
|
source: String? = nil,
|
|
file: StaticString = #file,
|
|
line: UInt = #line) {
|
|
let source = source ?? Logger.currentModule(filePath: "\(file)")
|
|
let entry = self.find(level: level, message: message, metadata: metadata, source: source)
|
|
XCTAssertNotNil(
|
|
entry,
|
|
"""
|
|
entry not found: \(level), \(source), \(String(describing: metadata)), \(message)
|
|
All entries:
|
|
\(self.entries.map { "\($0)" }.joined(separator: "\n"))
|
|
""",
|
|
file: file,
|
|
line: line
|
|
)
|
|
}
|
|
|
|
func assertNotExist(level: Logger.Level,
|
|
message: String,
|
|
metadata: Logger.Metadata? = nil,
|
|
source: String? = nil,
|
|
file: StaticString = #file,
|
|
line: UInt = #line) {
|
|
let source = source ?? Logger.currentModule(filePath: "\(file)")
|
|
let entry = self.find(level: level, message: message, metadata: metadata, source: source)
|
|
XCTAssertNil(
|
|
entry,
|
|
"entry was found: \(level), \(source), \(String(describing: metadata)), \(message)",
|
|
file: file,
|
|
line: line
|
|
)
|
|
}
|
|
|
|
func find(level: Logger.Level, message: String, metadata: Logger.Metadata? = nil, source: String) -> LogEntry? {
|
|
return self.entries.first { entry in
|
|
entry.level == level &&
|
|
entry.message == message &&
|
|
entry.metadata ?? [:] == metadata ?? [:] &&
|
|
entry.source == source
|
|
}
|
|
}
|
|
}
|
|
|
|
public class MDC {
|
|
private let lock = NSLock()
|
|
private var storage = [Int: Logger.Metadata]()
|
|
|
|
public static var global = MDC()
|
|
|
|
private init() {}
|
|
|
|
public subscript(metadataKey: String) -> Logger.Metadata.Value? {
|
|
get {
|
|
return self.lock.withLock {
|
|
return self.storage[self.threadId]?[metadataKey]
|
|
}
|
|
}
|
|
set {
|
|
self.lock.withLock {
|
|
if self.storage[self.threadId] == nil {
|
|
self.storage[self.threadId] = Logger.Metadata()
|
|
}
|
|
self.storage[self.threadId]![metadataKey] = newValue
|
|
}
|
|
}
|
|
}
|
|
|
|
public var metadata: Logger.Metadata {
|
|
return self.lock.withLock {
|
|
return self.storage[self.threadId] ?? [:]
|
|
}
|
|
}
|
|
|
|
public func clear() {
|
|
self.lock.withLock {
|
|
_ = self.storage.removeValue(forKey: self.threadId)
|
|
}
|
|
}
|
|
|
|
public func with(metadata: Logger.Metadata, _ body: () throws -> Void) rethrows {
|
|
metadata.forEach { self[$0] = $1 }
|
|
defer {
|
|
metadata.keys.forEach { self[$0] = nil }
|
|
}
|
|
try body()
|
|
}
|
|
|
|
public func with<T>(metadata: Logger.Metadata, _ body: () throws -> T) rethrows -> T {
|
|
metadata.forEach { self[$0] = $1 }
|
|
defer {
|
|
metadata.keys.forEach { self[$0] = nil }
|
|
}
|
|
return try body()
|
|
}
|
|
|
|
// for testing
|
|
internal func flush() {
|
|
self.lock.withLock {
|
|
self.storage.removeAll()
|
|
}
|
|
}
|
|
|
|
private var threadId: Int {
|
|
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
|
|
return Int(pthread_mach_thread_np(pthread_self()))
|
|
#else
|
|
return Int(pthread_self())
|
|
#endif
|
|
}
|
|
}
|
|
|
|
internal extension NSLock {
|
|
func withLock<T>(_ body: () -> T) -> T {
|
|
self.lock()
|
|
defer {
|
|
self.unlock()
|
|
}
|
|
return body()
|
|
}
|
|
}
|
|
|
|
internal struct TestLibrary {
|
|
private let logger = Logger(label: "TestLibrary")
|
|
private let queue = DispatchQueue(label: "TestLibrary")
|
|
|
|
public init() {}
|
|
|
|
public func doSomething() {
|
|
self.logger.info("TestLibrary::doSomething")
|
|
}
|
|
|
|
public func doSomethingAsync(completion: @escaping () -> Void) {
|
|
// libraries that use global loggers and async, need to make sure they propagate the
|
|
// logging metadata when creating a new thread
|
|
let metadata = MDC.global.metadata
|
|
self.queue.asyncAfter(deadline: .now() + 0.1) {
|
|
MDC.global.with(metadata: metadata) {
|
|
self.logger.info("TestLibrary::doSomethingAsync")
|
|
completion()
|
|
}
|
|
}
|
|
}
|
|
}
|