+ initial BaggageContextCarrier types for "framework contexts"
This commit is contained in:
parent
c78c490045
commit
fa304c69a5
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"object": {
|
||||
"pins": [
|
||||
{
|
||||
"package": "swift-log",
|
||||
"repositoryURL": "https://github.com/apple/swift-log.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "57c6bd04256ba47590ee2285e208f731210c5c10",
|
||||
"version": "1.3.0"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"version": 1
|
||||
}
|
|
@ -4,14 +4,34 @@ import PackageDescription
|
|||
let package = Package(
|
||||
name: "swift-baggage-context",
|
||||
products: [
|
||||
.library(name: "Baggage", targets: ["Baggage"])
|
||||
.library(name: "Baggage",
|
||||
targets: [
|
||||
"Baggage"
|
||||
]
|
||||
),
|
||||
.library(name: "BaggageLogging",
|
||||
targets: [
|
||||
"BaggageLogging"
|
||||
]
|
||||
),
|
||||
],
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/apple/swift-log.git", from: "1.3.0")
|
||||
],
|
||||
targets: [
|
||||
|
||||
.target(
|
||||
name: "Baggage",
|
||||
dependencies: []
|
||||
),
|
||||
|
||||
.target(
|
||||
name: "BaggageLogging",
|
||||
dependencies: [
|
||||
.product(name: "Logging", package: "swift-log")
|
||||
]
|
||||
),
|
||||
|
||||
// ==== --------------------------------------------------------------------------------------------------------
|
||||
// MARK: Tests
|
||||
|
||||
|
@ -22,6 +42,14 @@ let package = Package(
|
|||
]
|
||||
),
|
||||
|
||||
.testTarget(
|
||||
name: "BaggageLoggingTests",
|
||||
dependencies: [
|
||||
"Baggage",
|
||||
"BaggageLogging"
|
||||
]
|
||||
),
|
||||
|
||||
// ==== --------------------------------------------------------------------------------------------------------
|
||||
// MARK: Performance / Benchmarks
|
||||
|
||||
|
@ -29,8 +57,9 @@ let package = Package(
|
|||
name: "Benchmarks",
|
||||
dependencies: [
|
||||
"Baggage",
|
||||
"BaggageLogging",
|
||||
"SwiftBenchmarkTools",
|
||||
]
|
||||
]
|
||||
),
|
||||
.target(
|
||||
name: "SwiftBenchmarkTools",
|
||||
|
|
|
@ -35,7 +35,7 @@
|
|||
/// Libraries may also want to provide an extension, offering the values that users are expected to reach for
|
||||
/// using the following pattern:
|
||||
///
|
||||
/// extension BaggageContext {
|
||||
/// extension BaggageContextProtocol {
|
||||
/// var testID: TestIDKey.Value {
|
||||
/// get {
|
||||
/// self[TestIDKey.self]
|
||||
|
@ -44,7 +44,7 @@
|
|||
/// }
|
||||
/// }
|
||||
/// }
|
||||
public struct BaggageContext {
|
||||
public struct BaggageContext: BaggageContextProtocol {
|
||||
private var _storage = [AnyBaggageContextKey: ValueContainer]()
|
||||
|
||||
/// Create an empty `BaggageContext`.
|
||||
|
@ -60,10 +60,9 @@ public struct BaggageContext {
|
|||
}
|
||||
}
|
||||
|
||||
public var baggageItems: [AnyBaggageContextKey: Any] {
|
||||
// TODO: key may not be unique
|
||||
self._storage.reduce(into: [:]) {
|
||||
$0[$1.key] = $1.value.value
|
||||
public func forEach(_ callback: (AnyBaggageContextKey, Any) -> Void) {
|
||||
self._storage.forEach { key, container in
|
||||
callback(key, container.value)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -82,6 +81,32 @@ extension BaggageContext: CustomStringConvertible {
|
|||
}
|
||||
}
|
||||
|
||||
public protocol BaggageContextProtocol {
|
||||
/// Provides type-safe access to the baggage's values.
|
||||
///
|
||||
/// Rather than using this subscript directly, users are encouraged to offer a convenience accessor to their values,
|
||||
/// using the following pattern:
|
||||
///
|
||||
/// extension BaggageContextProtocol {
|
||||
/// var testID: TestIDKey.Value {
|
||||
/// get {
|
||||
/// self[TestIDKey.self]
|
||||
/// } set {
|
||||
/// self[TestIDKey.self] = newValue
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
subscript<Key: BaggageContextKey>(_ key: Key.Type) -> Key.Value? { get set }
|
||||
|
||||
/// Iterates over the baggage context's contents invoking the callback one-by one.
|
||||
///
|
||||
/// - Parameter callback: invoked with the type erased key and value stored for the key in this baggage.
|
||||
func forEach(_ callback: (AnyBaggageContextKey, Any) -> Void)
|
||||
}
|
||||
|
||||
// ==== ------------------------------------------------------------------------
|
||||
// MARK: Baggage keys
|
||||
|
||||
/// `BaggageContextKey`s are used as keys in a `BaggageContext`. Their associated type `Value` gurantees type-safety.
|
||||
/// To give your `BaggageContextKey` an explicit name you may override the `name` property.
|
||||
public protocol BaggageContextKey {
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
//===----------------------------------------------------------------------===//
|
||||
//
|
||||
// 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
|
||||
//
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
// ==== ----------------------------------------------------------------------------------------------------------------
|
||||
// MARK: Framework Context Protocols
|
||||
|
||||
/// Framework context protocols may conform to this protocol if they are used to carry a baggage object.
|
||||
///
|
||||
/// Notice that the baggage context property is spelled as `baggage`, this is purposefully designed in order to read well
|
||||
/// with framework context's which often will be passed as `context: FrameworkContext` and used as `context.baggage`.
|
||||
///
|
||||
/// Such carrier protocol also conforms to `BaggageContextProtocol` meaning that it has the same convenient accessors
|
||||
/// as the actual baggage type. Users should be able to use the `context.myValue` the same way if a raw baggage context,
|
||||
/// or a framework context was passed around as `context` parameter, allowing for easier migrations between those two when needed.
|
||||
public protocol BaggageContextCarrier: BaggageContextProtocol {
|
||||
/// The underlying `BaggageContext`.
|
||||
var baggage: BaggageContext { get set }
|
||||
}
|
||||
|
||||
extension BaggageContextCarrier {
|
||||
public subscript<Key: BaggageContextKey>(baggageKey: Key.Type) -> Key.Value? {
|
||||
get {
|
||||
self.baggage[baggageKey]
|
||||
} set {
|
||||
self.baggage[baggageKey] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
public func forEach(_ callback: (AnyBaggageContextKey, Any) -> Void) {
|
||||
self.baggage.forEach(callback)
|
||||
}
|
||||
}
|
||||
|
||||
/// A baggage itself also is a carrier of _itself_.
|
||||
extension BaggageContext: BaggageContextCarrier {
|
||||
public var baggage: BaggageContext {
|
||||
get {
|
||||
self
|
||||
}
|
||||
set {
|
||||
self = newValue
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,110 @@
|
|||
//===----------------------------------------------------------------------===//
|
||||
//
|
||||
// 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
|
||||
//
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
import Baggage
|
||||
import Logging
|
||||
|
||||
// ==== ----------------------------------------------------------------------------------------------------------------
|
||||
// MARK: BaggageContext (as additional Logger.Metadata) LogHandler
|
||||
|
||||
/// Proxying log handler which adds `BaggageContext` as metadata when log events are to be emitted.
|
||||
public struct BaggageMetadataLogHandler: LogHandler {
|
||||
var underlying: Logger
|
||||
let context: BaggageContext
|
||||
|
||||
public init(logger underlying: Logger, context: BaggageContext) {
|
||||
self.underlying = underlying
|
||||
self.context = context
|
||||
}
|
||||
|
||||
public var logLevel: Logger.Level {
|
||||
get {
|
||||
self.underlying.logLevel
|
||||
}
|
||||
set {
|
||||
self.underlying.logLevel = newValue
|
||||
}
|
||||
}
|
||||
|
||||
public func log(
|
||||
level: Logger.Level,
|
||||
message: Logger.Message,
|
||||
metadata: Logger.Metadata?,
|
||||
source: String,
|
||||
file: String,
|
||||
function: String,
|
||||
line: UInt
|
||||
) {
|
||||
guard self.underlying.logLevel <= level else {
|
||||
return
|
||||
}
|
||||
|
||||
var effectiveMetadata = self.baggageAsMetadata()
|
||||
if let metadata = metadata {
|
||||
effectiveMetadata.merge(metadata, uniquingKeysWith: { _, r in r })
|
||||
}
|
||||
self.underlying.log(level: level, message, metadata: effectiveMetadata, source: source, file: file, function: function, line: line)
|
||||
}
|
||||
|
||||
public var metadata: Logger.Metadata {
|
||||
get {
|
||||
[:]
|
||||
}
|
||||
set {
|
||||
newValue.forEach { k, v in
|
||||
self.underlying[metadataKey: k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Note that this does NOT look up inside the baggage.
|
||||
///
|
||||
/// This is because a context lookup either has to use the specific type key, or iterate over all keys to locate one by name,
|
||||
/// which may be incorrect still, thus rather than making an potentially slightly incorrect lookup, we do not implement peeking
|
||||
/// into a baggage with String keys through this handler (as that is not a capability `BaggageContext` offers in any case.
|
||||
public subscript(metadataKey metadataKey: Logger.Metadata.Key) -> Logger.Metadata.Value? {
|
||||
get {
|
||||
self.underlying[metadataKey: metadataKey]
|
||||
}
|
||||
set {
|
||||
self.underlying[metadataKey: metadataKey] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
private func baggageAsMetadata() -> Logger.Metadata {
|
||||
var effectiveMetadata: Logger.Metadata = [:]
|
||||
self.context.forEach { key, value in
|
||||
if let convertible = value as? String {
|
||||
effectiveMetadata[key.name] = .string(convertible)
|
||||
} else if let convertible = value as? CustomStringConvertible {
|
||||
effectiveMetadata[key.name] = .stringConvertible(convertible)
|
||||
} else {
|
||||
effectiveMetadata[key.name] = .stringConvertible(BaggageValueCustomStringConvertible(value))
|
||||
}
|
||||
}
|
||||
|
||||
return effectiveMetadata
|
||||
}
|
||||
|
||||
struct BaggageValueCustomStringConvertible: CustomStringConvertible {
|
||||
let value: Any
|
||||
|
||||
init(_ value: Any) {
|
||||
self.value = value
|
||||
}
|
||||
|
||||
var description: String {
|
||||
"\(self.value)"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
//===----------------------------------------------------------------------===//
|
||||
//
|
||||
// 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
|
||||
//
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
import Baggage
|
||||
import Logging
|
||||
|
||||
extension Logger {
|
||||
/// Returns a logger that in addition to any explicit metadata passed to log statements,
|
||||
/// also includes the `BaggageContext` adapted into metadata values.
|
||||
///
|
||||
/// The rendering of baggage values into metadata values is performed on demand,
|
||||
/// whenever a log statement is effective (i.e. will be logged, according to active `logLevel`).
|
||||
public func with(context: BaggageContext) -> Logger {
|
||||
Logger(
|
||||
label: self.label,
|
||||
factory: { _ in BaggageMetadataLogHandler(logger: self, context: context) }
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
//===----------------------------------------------------------------------===//
|
||||
//
|
||||
// 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
|
||||
//
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
import Baggage
|
||||
import Logging
|
||||
|
||||
/// A `BaggageContextLogging` purpose is to be adopted by frameworks which already provide a "FrameworkContext",
|
||||
/// and to such frameworks to pass their context as `BaggageContextCarrier`.
|
||||
public protocol LoggingBaggageContextCarrier: BaggageContextCarrier {
|
||||
/// The logger associated with this carrier context.
|
||||
///
|
||||
/// It should automatically populate the loggers metadata based on the `BaggageContext` associated with this context object.
|
||||
///
|
||||
/// ### Implementation note
|
||||
///
|
||||
/// Libraries and/or frameworks which conform to this protocol with their "Framework Context" types,
|
||||
/// SHOULD implement this logger by wrapping the "raw" logger associated with this context with the `logger.with(BaggageContext:)` function,
|
||||
/// which efficiently handles the bridging of baggage to logging metadata values.
|
||||
///
|
||||
/// Writes to the `logger` metadata SHOULD NOT be reflected in the `baggage`,
|
||||
/// however writes to the underlying `baggage` SHOULD be reflected in the `logger`.
|
||||
var logger: Logger { get set }
|
||||
}
|
|
@ -0,0 +1,138 @@
|
|||
//===----------------------------------------------------------------------===//
|
||||
//
|
||||
// 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
|
||||
//
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
import Baggage
|
||||
import BaggageLogging
|
||||
import Logging
|
||||
import XCTest
|
||||
|
||||
final class LoggingBaggageContextCarrierTests: XCTestCase {
|
||||
func test_ContextWithLogger_dumpBaggage() throws {
|
||||
let baggage = BaggageContext()
|
||||
let logger = Logger(label: "TheLogger")
|
||||
|
||||
var context: LoggingBaggageContextCarrier = ExampleFrameworkContext(context: baggage, logger: logger)
|
||||
context.testID = 42
|
||||
|
||||
func frameworkFunctionDumpsBaggage(param: String, context: LoggingBaggageContextCarrier) -> String {
|
||||
var s = ""
|
||||
context.baggage.forEach { key, item in
|
||||
s += "\(key.name): \(item)\n"
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
let result = frameworkFunctionDumpsBaggage(param: "x", context: context)
|
||||
XCTAssertEqual(
|
||||
result,
|
||||
"""
|
||||
TestIDKey: 42
|
||||
|
||||
"""
|
||||
)
|
||||
}
|
||||
|
||||
func test_ContextWithLogger_log_withBaggage() throws {
|
||||
let baggage = BaggageContext()
|
||||
let logging = TestLogging()
|
||||
let logger = Logger(label: "TheLogger", factory: { label in logging.make(label: label) })
|
||||
|
||||
var context: LoggingBaggageContextCarrier = ExampleFrameworkContext(context: baggage, logger: logger)
|
||||
|
||||
context.secondTestID = "value"
|
||||
context.testID = 42
|
||||
context.logger.info("Hello")
|
||||
|
||||
context.testID = nil
|
||||
context.logger.warning("World")
|
||||
|
||||
logging.history.assertExist(level: .info, message: "Hello", metadata: [
|
||||
"TestIDKey": .stringConvertible(42),
|
||||
"secondIDExplicitlyNamed": "value",
|
||||
])
|
||||
logging.history.assertExist(level: .warning, message: "World", metadata: [
|
||||
"secondIDExplicitlyNamed": "value",
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
public struct ExampleFrameworkContext: LoggingBaggageContextCarrier {
|
||||
public var baggage: BaggageContext
|
||||
|
||||
private var _logger: Logger
|
||||
public var logger: Logger {
|
||||
get {
|
||||
self._logger.with(context: self.baggage)
|
||||
}
|
||||
set {
|
||||
self._logger = newValue
|
||||
}
|
||||
}
|
||||
|
||||
public init(context baggage: BaggageContext, logger: Logger) {
|
||||
self.baggage = baggage
|
||||
self._logger = logger
|
||||
}
|
||||
}
|
||||
|
||||
struct CoolFrameworkContext: LoggingBaggageContextCarrier {
|
||||
private var _logger: Logger = Logger(label: "some frameworks logger")
|
||||
var logger: Logger {
|
||||
get {
|
||||
self._logger.with(context: self.baggage)
|
||||
}
|
||||
set {
|
||||
self._logger = newValue
|
||||
}
|
||||
}
|
||||
|
||||
var baggage: BaggageContext = .init()
|
||||
|
||||
// framework context defines other values as well
|
||||
let frameworkField: String = ""
|
||||
|
||||
// including the popular eventLoop
|
||||
let eventLoop: FakeEventLoop
|
||||
}
|
||||
|
||||
struct FakeEventLoop {}
|
||||
|
||||
private extension BaggageContextProtocol {
|
||||
var testID: Int? {
|
||||
get {
|
||||
self[TestIDKey.self]
|
||||
}
|
||||
set {
|
||||
self[TestIDKey.self] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
var secondTestID: String? {
|
||||
get {
|
||||
self[SecondTestIDKey.self]
|
||||
}
|
||||
set {
|
||||
self[SecondTestIDKey.self] = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private enum TestIDKey: BaggageContextKey {
|
||||
typealias Value = Int
|
||||
}
|
||||
|
||||
private enum SecondTestIDKey: BaggageContextKey {
|
||||
typealias Value = String
|
||||
|
||||
static let name: String? = "secondIDExplicitlyNamed"
|
||||
}
|
|
@ -0,0 +1,337 @@
|
|||
//===----------------------------------------------------------------------===//
|
||||
//
|
||||
// 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 {
|
||||
TestLogHandler(label: label, config: self.config, recorder: self.recorder)
|
||||
}
|
||||
|
||||
var config: Config { self._config }
|
||||
var history: History { 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
|
||||
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.logger.metadata
|
||||
self.metadataLock.withLock { self._metadata }
|
||||
}
|
||||
set {
|
||||
// self.logger.metadata = newValue
|
||||
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.logger[metadataKey: metadataKey]
|
||||
self.metadataLock.withLock { self._metadata[metadataKey] }
|
||||
}
|
||||
set {
|
||||
// return logger[metadataKey: metadataKey] = newValue
|
||||
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 {
|
||||
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] {
|
||||
self.lock.withLock { self._entries }
|
||||
}
|
||||
}
|
||||
|
||||
internal protocol History {
|
||||
var entries: [LogEntry] { get }
|
||||
}
|
||||
|
||||
internal extension History {
|
||||
func atLevel(level: Logger.Level) -> [LogEntry] {
|
||||
self.entries.filter { entry in
|
||||
level == entry.level
|
||||
}
|
||||
}
|
||||
|
||||
var trace: [LogEntry] {
|
||||
self.atLevel(level: .debug)
|
||||
}
|
||||
|
||||
var debug: [LogEntry] {
|
||||
self.atLevel(level: .debug)
|
||||
}
|
||||
|
||||
var info: [LogEntry] {
|
||||
self.atLevel(level: .info)
|
||||
}
|
||||
|
||||
var warning: [LogEntry] {
|
||||
self.atLevel(level: .warning)
|
||||
}
|
||||
|
||||
var error: [LogEntry] {
|
||||
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? {
|
||||
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 {
|
||||
self.lock.withLock {
|
||||
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 {
|
||||
self.lock.withLock {
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue