Final API proposal - Baggage + Context (#34)

This commit is contained in:
Konrad `ktoso` Malawski 2020-09-30 19:23:49 +09:00 committed by GitHub
parent 11ce8e1b0e
commit dbcbed943e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 1527 additions and 772 deletions

16
Package.resolved Normal file
View File

@ -0,0 +1,16 @@
{
"object": {
"pins": [
{
"package": "swift-log",
"repositoryURL": "https://github.com/apple/swift-log.git",
"state": {
"branch": null,
"revision": "173f567a2dfec11d74588eea82cecea555bdc0bc",
"version": "1.4.0"
}
}
]
},
"version": 1
}

View File

@ -2,7 +2,7 @@
import PackageDescription
let package = Package(
name: "swift-baggage-context",
name: "swift-context",
products: [
.library(
name: "Baggage",
@ -11,14 +11,14 @@ let package = Package(
]
),
.library(
name: "BaggageLogging",
name: "BaggageContext",
targets: [
"BaggageLogging",
"BaggageContext",
]
),
],
dependencies: [
.package(url: "https://github.com/apple/swift-log.git", from: "1.3.0"),
.package(url: "https://github.com/apple/swift-log.git", from: "1.4.0"),
],
targets: [
.target(
@ -27,7 +27,7 @@ let package = Package(
),
.target(
name: "BaggageLogging",
name: "BaggageContext",
dependencies: [
"Baggage",
.product(name: "Logging", package: "swift-log"),
@ -45,10 +45,10 @@ let package = Package(
),
.testTarget(
name: "BaggageLoggingTests",
name: "BaggageContextTests",
dependencies: [
"Baggage",
"BaggageLogging",
"BaggageContext",
]
),
@ -56,15 +56,14 @@ let package = Package(
// MARK: Performance / Benchmarks
.target(
name: "BaggageBenchmarks",
name: "BaggageContextBenchmarks",
dependencies: [
"Baggage",
"BaggageLogging",
"BaggageBenchmarkTools",
"BaggageContext",
"BaggageContextBenchmarkTools",
]
),
.target(
name: "BaggageBenchmarkTools",
name: "BaggageContextBenchmarkTools",
dependencies: []
),
]

View File

@ -0,0 +1,218 @@
//===----------------------------------------------------------------------===//
//
// 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: Baggage
/// A `Baggage` is a heterogeneous storage type with value semantics for keyed values in a type-safe fashion.
///
/// Its values are uniquely identified via `Baggage.Key`s (by type identity). These keys also dictate the type of
/// value allowed for a specific key-value pair through their associated type `Value`.
///
/// ## Defining keys and accessing values
/// Baggage keys are defined as types, most commonly case-less enums (as no actual instances are actually required)
/// which conform to the `Baggage.Key` protocol:
///
/// private enum TestIDKey: Baggage.Key {
/// typealias Value = String
/// }
///
/// While defining a key, one should also immediately declare an extension on `Baggage`,
/// to allow convenient and discoverable ways to interact with the baggage item, the extension should take the form of:
///
/// extension Baggage {
/// var testID: String? {
/// get {
/// self[TestIDKey.self]
/// } set {
/// self[TestIDKey.self] = newValue
/// }
/// }
/// }
///
/// For consistency, it is recommended to name key types with the `...Key` suffix (e.g. `SomethingKey`) and the property
/// used to access a value identifier by such key the prefix of the key (e.g. `something`). Please also observe the usual
/// Swift naming conventions, e.g. prefer `ID` to `Id` etc.
///
/// ## Usage
/// Using a baggage container is fairly straight forward, as it boils down to using the prepared computed properties:
///
/// var baggage = Baggage.topLevel
/// // set a new value
/// baggage.testID = "abc"
/// // retrieve a stored value
/// let testID = baggage.testID ?? "default"
/// // remove a stored value
/// baggage.testIDKey = nil
///
/// Note that normally a baggage should not be "created" ad-hoc by user code, but rather it should be passed to it from
/// a runtime. For example, when working in an HTTP server framework, it is most likely that the baggage is already passed
/// directly or indirectly (e.g. in a `FrameworkContext`)
///
/// ### Accessing all values
///
/// The only way to access "all" values in a baggage context is by using the `forEach` function.
/// The baggage container on purpose does not expose more functions to prevent abuse and treating it as too much of an
/// arbitrary value smuggling container, but only make it convenient for tracing and instrumentation systems which need
/// to access either specific or all items carried inside a baggage.
public struct Baggage {
public typealias Key = BaggageKey
private var _storage = [AnyBaggageKey: Any]()
/// Internal on purpose, please use `Baggage.TODO` or `Baggage.topLevel` to create an "empty" context,
/// which carries more meaning to other developers why an empty context was used.
init() {}
}
extension Baggage {
/// Creates a new empty "top level" baggage, generally used as an "initial" baggage to immediately be populated with
/// some values by a framework or runtime. Another use case is for tasks starting in the "background" (e.g. on a timer),
/// which don't have a "request context" per se that they can pick up, and as such they have to create a "top level"
/// baggage for their work.
///
/// ## Usage in frameworks and libraries
/// This function is really only intended to be used frameworks and libraries, at the "top-level" where a request's,
/// message's or task's processing is initiated. For example, a framework handling requests, should create an empty
/// context when handling a request only to immediately populate it with useful trace information extracted from e.g.
/// request headers.
///
/// ## Usage in applications
/// Application code should never have to create an empty context during the processing lifetime of any request,
/// and only should create contexts if some processing is performed in the background - thus the naming of this property.
///
/// Usually, a framework such as an HTTP server or similar "request handler" would already provide users
/// with a context to be passed along through subsequent calls.
///
/// If unsure where to obtain a context from, prefer using `.TODO("Not sure where I should get a context from here?")`,
/// in order to inform other developers that the lack of context passing was not done on purpose, but rather because either
/// not being sure where to obtain a context from, or other framework limitations -- e.g. the outer framework not being
/// baggage context aware just yet.
public static var topLevel: Baggage {
return Baggage()
}
}
extension Baggage {
/// A baggage intended as a placeholder until a real value can be passed through a function call.
///
/// It should ONLY be used while prototyping or when the passing of the proper context is not yet possible,
/// e.g. because an external library did not pass it correctly and has to be fixed before the proper context
/// can be obtained where the TO-DO is currently used.
///
/// ## Crashing on TO-DO context creation
/// You may set the `BAGGAGE_CRASH_TODOS` variable while compiling a project in order to make calls to this function crash
/// with a fatal error, indicating where a to-do baggage context was used. This comes in handy when wanting to ensure that
/// a project never ends up using with code initially was written as "was lazy, did not pass context", yet the
/// project requires context passing to be done correctly throughout the application. Similar checks can be performed
/// at compile time easily using linters (not yet implemented), since it is always valid enough to detect a to-do context
/// being passed as illegal and warn or error when spotted.
///
/// ## Example
///
/// let baggage = Baggage.TODO("The framework XYZ should be modified to pass us a context here, and we'd pass it along"))
///
/// - Parameters:
/// - reason: Informational reason for developers, why a placeholder context was used instead of a proper one,
/// - Returns: Empty "to-do" baggage context which should be eventually replaced with a carried through one, or `background`.
public static func TODO(_ reason: StaticString? = "", function: String = #function, file: String = #file, line: UInt = #line) -> Baggage {
var context = Baggage.topLevel
#if BAGGAGE_CRASH_TODOS
fatalError("BAGGAGE_CRASH_TODOS: at \(file):\(line) (function \(function)), reason: \(reason)")
#else
context[TODOKey.self] = .init(file: file, line: line)
return context
#endif
}
private enum TODOKey: BaggageKey {
typealias Value = TODOLocation
static var nameOverride: String? {
return "todo"
}
}
}
extension Baggage {
/// Provides type-safe access to the baggage's values.
/// This API should ONLY be used inside of accessor implementations.
///
/// End users rather than using this subscript should use "accessors" the key's author MUST define, following this pattern:
///
/// internal enum TestID: Baggage.Key {
/// typealias Value = TestID
/// }
///
/// extension Baggage {
/// public internal(set) var testID: TestID? {
/// get {
/// self[TestIDKey.self]
/// }
/// set {
/// self[TestIDKey.self] = newValue
/// }
/// }
/// }
///
/// This is in order to enforce a consistent style across projects and also allow for fine grained control over
/// who may set and who may get such property. Just access control to the Key type itself lacks such fidelity.
///
/// Note that specific baggage and context types MAY (and usually do), offer also a way to set baggage values,
/// however in the most general case it is not required, as some frameworks may only be able to offer reading.
public subscript<Key: BaggageKey>(_ key: Key.Type) -> Key.Value? {
get {
guard let value = self._storage[AnyBaggageKey(key)] else { return nil }
// safe to force-cast as this subscript is the only way to set a value.
return (value as! Key.Value)
}
set {
self._storage[AnyBaggageKey(key)] = newValue
}
}
/// Number of contained baggage items.
public var count: Int {
return self._storage.count
}
/// Calls the given closure for each item contained in the underlying `Baggage`.
///
/// Order of those invocations is NOT guaranteed and should not be relied on.
///
/// - Parameter body: A closure invoked with the type erased key and value stored for the key in this baggage.
public func forEach(_ body: (AnyBaggageKey, Any) throws -> Void) rethrows {
try self._storage.forEach { key, value in
try body(key, value)
}
}
}
extension Baggage: CustomStringConvertible {
/// A context's description prints only keys of the contained values.
/// This is in order to prevent spilling a lot of detailed information of carried values accidentally.
///
/// `Baggage`s are not intended to be printed "raw" but rather inter-operate with tracing, logging and other systems,
/// which can use the `forEach` function providing access to its underlying values.
public var description: String {
return "\(type(of: self).self)(keys: \(self._storage.map { $0.key.name }))"
}
}
/// Carried automatically by a "to do" baggage.
/// It can be used to track where a context originated and which "to do" context must be fixed into a real one to avoid this.
public struct TODOLocation {
/// Source file location where the to-do `Baggage` was created
public let file: String
/// Source line location where the to-do `Baggage` was created
public let line: UInt
}

View File

@ -1,239 +0,0 @@
//===----------------------------------------------------------------------===//
//
// 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
//
//===----------------------------------------------------------------------===//
/// A `BaggageContext` is a heterogeneous storage type with value semantics for keyed values in a type-safe
/// fashion. Its values are uniquely identified via `BaggageContextKey`s. These keys also dictate the type of
/// value allowed for a specific key-value pair through their associated type `Value`.
///
/// ## Subscript access
/// You may access the stored values by subscripting with a key type conforming to `BaggageContextKey`.
///
/// enum TestIDKey: BaggageContextKey {
/// typealias Value = String
/// }
///
/// var context = BaggageContext.background
/// // set a new value
/// context[TestIDKey.self] = "abc"
/// // retrieve a stored value
/// context[TestIDKey.self] ?? "default"
/// // remove a stored value
/// context[TestIDKey.self] = nil
///
/// ## Convenience extensions
///
/// Libraries may also want to provide an extension, offering the values that users are expected to reach for
/// using the following pattern:
///
/// extension BaggageContextProtocol {
/// var testID: TestIDKey.Value? {
/// get {
/// self[TestIDKey.self]
/// } set {
/// self[TestIDKey.self] = newValue
/// }
/// }
/// }
public struct BaggageContext: BaggageContextProtocol {
private var _storage = [AnyBaggageContextKey: Any]()
/// Internal on purpose, please use `TODO` or `.background` to create an "empty" context,
/// which carries more meaning to other developers why an empty context was used.
init() {}
public subscript<Key: BaggageContextKey>(_ key: Key.Type) -> Key.Value? {
get {
guard let value = self._storage[AnyBaggageContextKey(key)] else { return nil }
// safe to force-cast as this subscript is the only way to set a value.
return (value as! Key.Value)
} set {
self._storage[AnyBaggageContextKey(key)] = newValue
}
}
public func forEach(_ body: (AnyBaggageContextKey, Any) throws -> Void) rethrows {
try self._storage.forEach { key, value in
try body(key, value)
}
}
}
extension BaggageContext: CustomStringConvertible {
/// A context's description prints only keys of the contained values.
/// This is in order to prevent spilling a lot of detailed information of carried values accidentally.
///
/// `BaggageContext`s are not intended to be printed "raw" but rather inter-operate with tracing, logging and other systems,
/// which can use the `forEach` function providing access to its underlying values.
public var description: String {
return "\(type(of: self).self)(keys: \(self._storage.map { $0.key.name }))"
}
}
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 }
/// Calls the given closure on each key/value pair in the `BaggageContext`.
///
/// - Parameter body: A closure invoked with the type erased key and value stored for the key in this baggage.
func forEach(_ body: (AnyBaggageContextKey, Any) throws -> Void) rethrows
}
// ==== ------------------------------------------------------------------------
// MARK: Baggage keys
/// `BaggageContextKey`s are used as keys in a `BaggageContext`. Their associated type `Value` guarantees type-safety.
/// To give your `BaggageContextKey` an explicit name you may override the `name` property.
///
/// In general, `BaggageContextKey`s should be `internal` to the part of a system using it. It is strongly recommended to do
/// convenience extensions on `BaggageContextProtocol`, using the keys directly is considered an anti-pattern.
///
/// extension BaggageContextProtocol {
/// var testID: TestIDKey.Value? {
/// get {
/// self[TestIDKey.self]
/// } set {
/// self[TestIDKey.self] = newValue
/// }
/// }
/// }
public protocol BaggageContextKey {
/// The type of `Value` uniquely identified by this key.
associatedtype Value
/// The human-readable name of this key. Defaults to `nil`.
static var name: String? { get }
}
extension BaggageContextKey {
public static var name: String? { return nil }
}
/// A type-erased `BaggageContextKey` used when iterating through the `BaggageContext` using its `forEach` method.
public struct AnyBaggageContextKey {
/// The key's type represented erased to an `Any.Type`.
public let keyType: Any.Type
private let _name: String?
/// A human-readable String representation of the underlying key.
/// If no explicit name has been set on the wrapped key the type name is used.
public var name: String {
return self._name ?? String(describing: self.keyType.self)
}
init<Key>(_ keyType: Key.Type) where Key: BaggageContextKey {
self.keyType = keyType
self._name = keyType.name
}
}
extension AnyBaggageContextKey: Hashable {
public static func == (lhs: AnyBaggageContextKey, rhs: AnyBaggageContextKey) -> Bool {
return ObjectIdentifier(lhs.keyType) == ObjectIdentifier(rhs.keyType)
}
public func hash(into hasher: inout Hasher) {
hasher.combine(ObjectIdentifier(self.keyType))
}
}
// ==== ----------------------------------------------------------------------------------------------------------------
// MARK: Background BaggageContext
extension BaggageContextProtocol {
/// An empty baggage context intended as the "root" or "initial" baggage context background processing tasks, or as the "root" baggage context.
///
/// It is never canceled, has no values, and has no deadline.
/// It is typically used by the main function, initialization, and tests, and as the top-level Context for incoming requests.
///
/// ### Usage in frameworks and libraries
/// This function is really only intended to be used frameworks and libraries, at the "top-level" where a request's,
/// message's or task's processing is initiated. For example, a framework handling requests, should create an empty
/// context when handling a request only to immediately populate it with useful trace information extracted from e.g.
/// request headers.
///
/// ### Usage in applications
/// Application code should never have to create an empty context during the processing lifetime of any request,
/// and only should create contexts if some processing is performed in the background - thus the naming of this property.
///
/// Usually, a framework such as an HTTP server or similar "request handler" would already provide users
/// with a context to be passed along through subsequent calls.
///
/// If unsure where to obtain a context from, prefer using `.TODO("Not sure where I should get a context from here?")`,
/// such that other developers are informed that the lack of context was not done on purpose, but rather because either
/// not being sure where to obtain a context from, or other framework limitations -- e.g. the outer framework not being
/// context aware just yet.
public static var background: BaggageContext {
return BaggageContext()
}
}
// ==== ----------------------------------------------------------------------------------------------------------------
// MARK: "TO DO" BaggageContext
extension BaggageContextProtocol {
/// A baggage context intended as a placeholder until a real value can be passed through a function call.
///
/// It should ONLY be used while prototyping or when the passing of the proper context is not yet possible,
/// e.g. because an external library did not pass it correctly and has to be fixed before the proper context
/// can be obtained where the TO-DO is currently used.
///
/// ### Crashing on TO-DO context creation
/// You may set the `BAGGAGE_CRASH_TODOS` variable while compiling a project in order to make calls to this function crash
/// with a fatal error, indicating where a to-do baggage context was used. This comes in handy when wanting to ensure that
/// a project never ends up using with code initially was written as "was lazy, did not pass context", yet the
/// project requires context passing to be done correctly throughout the application. Similar checks can be performed
/// at compile time easily using linters (not yet implemented), since it is always valid enough to detect a to-do context
/// being passed as illegal and warn or error when spotted.
///
/// - Parameters:
/// - reason: Informational reason for developers, why a placeholder context was used instead of a proper one,
/// - Returns: Empty "to-do" baggage context which should be eventually replaced with a carried through one, or `background`.
public static func TODO(_ reason: StaticString? = "", function: String = #function, file: String = #file, line: UInt = #line) -> BaggageContext {
var context = BaggageContext.background
#if BAGGAGE_CRASH_TODOS
fatalError("BAGGAGE_CRASH_TODOS: at \(file):\(line) (function \(function)), reason: \(reason)")
#else
context[TODOKey.self] = .init(file: file, line: line)
return context
#endif
}
}
internal enum TODOKey: BaggageContextKey {
typealias Value = TODOLocation
static var name: String? {
return "todo"
}
}
/// Carried automatically by a "to do" baggage context.
/// It can be used to track where a context originated and which "to do" context must be fixed into a real one to avoid this.
public struct TODOLocation {
let file: String
let line: UInt
}

View File

@ -1,54 +0,0 @@
//===----------------------------------------------------------------------===//
//
// 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 {
return self.baggage[baggageKey]
} set {
self.baggage[baggageKey] = newValue
}
}
public func forEach(_ body: (AnyBaggageContextKey, Any) throws -> Void) rethrows {
try self.baggage.forEach(body)
}
}
/// A baggage itself also is a carrier of _itself_.
extension BaggageContext: BaggageContextCarrier {
public var baggage: BaggageContext {
get {
return self
}
set {
self = newValue
}
}
}

View File

@ -0,0 +1,89 @@
//===----------------------------------------------------------------------===//
//
// 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
//
//===----------------------------------------------------------------------===//
/// `BaggageKey`s are used as keys in a `Baggage`. Their associated type `Value` guarantees type-safety.
/// To give your `BaggageKey` an explicit name you may override the `name` property.
///
/// In general, `BaggageKey`s should be `internal` or `private` to the part of a system using it.
///
/// All access to baggage items should be performed through an accessor computed property defined as shown below:
///
/// /// The Key type should be internal (or private).
/// enum TestIDKey: Baggage.Key {
/// typealias Value = String
/// static var nameOverride: String? { "test-id" }
/// }
///
/// extension Baggage {
/// /// This is some useful property documentation.
/// public internal(set) var testID: String? {
/// get {
/// self[TestIDKey.self]
/// }
/// set {
/// self[TestIDKey.self] = newValue
/// }
/// }
/// }
///
/// This pattern allows library authors fine-grained control over which values may be set, and whic only get by end-users.
public protocol BaggageKey {
/// The type of `Value` uniquely identified by this key.
associatedtype Value
/// The human-readable name of this key.
/// This name will be used instead of the type name when a value is printed.
///
/// It MAY also be picked up by an instrument (from Swift Tracing) which serializes baggage items and e.g. used as
/// header name for carried metadata. Though generally speaking header names are NOT required to use the nameOverride,
/// and MAY use their well known names for header names etc, as it depends on the specific transport and instrument used.
///
/// For example, a baggage key representing the W3C "trace-state" header may want to return "trace-state" here,
/// in order to achieve a consistent look and feel of this baggage item throughout logging and tracing systems.
///
/// Defaults to `nil`.
static var nameOverride: String? { get }
}
extension BaggageKey {
public static var nameOverride: String? { return nil }
}
/// A type-erased `BaggageKey` used when iterating through the `Baggage` using its `forEach` method.
public struct AnyBaggageKey {
/// The key's type represented erased to an `Any.Type`.
public let keyType: Any.Type
private let _nameOverride: String?
/// A human-readable String representation of the underlying key.
/// If no explicit name has been set on the wrapped key the type name is used.
public var name: String {
return self._nameOverride ?? String(describing: self.keyType.self)
}
init<Key>(_ keyType: Key.Type) where Key: BaggageKey {
self.keyType = keyType
self._nameOverride = keyType.nameOverride
}
}
extension AnyBaggageKey: Hashable {
public static func == (lhs: AnyBaggageKey, rhs: AnyBaggageKey) -> Bool {
return ObjectIdentifier(lhs.keyType) == ObjectIdentifier(rhs.keyType)
}
public func hash(into hasher: inout Hasher) {
hasher.combine(ObjectIdentifier(self.keyType))
}
}

View File

@ -0,0 +1,291 @@
//===----------------------------------------------------------------------===//
//
// 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
//
//===----------------------------------------------------------------------===//
@_exported import Baggage
import Logging
// ==== ----------------------------------------------------------------------------------------------------------------
// MARK: Context Protocol
/// The `ContextProtocol` MAY be adopted by specific "framework contexts" such as e.g. `CoolFramework.Context` in
/// order to allow users to pass such context directly to libraries accepting any context.
///
/// This allows frameworks and library authors to offer APIs which compose more easily.
/// Please refer to the "Reference Implementation" notes on each of the requirements to know how to implement this protocol correctly.
///
/// ### Implementation notes
/// It is STRONGLY encouraged that a context type should exhibit Value Semantics (i.e. be a pure `struct`, or implement
/// the Copy-on-Write pattern), in order to implement the `set` requirements of the baggage and logger effectively,
/// and also for their user's sanity, as a reference semantics context type can be very confusing to use when shared
/// between multiple threads, as often is the case in server side environments.
///
/// It is STRONGLY encouraged to use the `DefaultContext` as inspiration for a correct implementation of a `Context`,
/// as the relationship between `Logger` and `Baggage` can be tricky to wrap your head around at first.
public protocol Context {
/// Get the `Baggage` container.
///
/// ### Implementation notes
/// Libraries and/or frameworks which conform to this protocol with their "Framework Context" types MUST
/// ensure that a modification of the baggage is properly represented in the associated `logger`. Users expect values
/// from the baggage be visible in their log statements issued via `context.logger.info()`.
///
/// Please refer to `DefaultContext`'s implementation for a reference implementation,
/// here a short snippet of how the baggage itself should be implemented:
///
/// public var baggage: Baggage {
/// willSet {
/// self._logger.updateMetadata(previous: self.baggage, latest: newValue)
/// }
/// }
///
/// #### Thread Safety
/// Implementations / MUST take care of thread-safety of modifications of the baggage. They can achieve this by such
/// context type being a pure `struct` or by implementing Copy-on-Write semantics for their type, the latter gives
/// many benefits, allowing the context to avoid being copied unless needed to (e.g. if the context type contains
/// many other values, in addition to the baggage).
var baggage: Baggage { get set }
/// The `Logger` associated with this context carrier.
///
/// It automatically populates the loggers metadata based on the `Baggage` associated with this context object.
///
/// ### Implementation notes
/// 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 `_logger.with(self.baggage)` function,
/// which efficiently handles the bridging of baggage to logging metadata values.
///
/// If a new logger is set, it MUST populate itself with the latest (current) baggage of the context,
/// this is to ensure that even if users set a new logger (completely "fresh") here, the metadata from the baggage
/// still will properly be logged in other pieces of the application where the context might be passed to.
///
/// A correct implementation might look like the following:
///
/// public var _logger: Logger
/// public var logger: Logger {
/// get {
/// return self._logger
/// }
/// set {
/// self._logger = newValue
/// // Since someone could have completely replaced the logger (not just changed the log level),
/// // we have to update the baggage again, since perhaps the new logger has empty metadata.
/// self._logger.updateMetadata(previous: .topLevel, latest: self.baggage)
/// }
/// }
/// }
///
///
/// #### Thread Safety
/// Implementations MUST ensure the thread-safety of mutating the logger. This is usually handled best by the
/// framework context itself being a Copy-on-Write type, however the exact safety mechanism is left up to the libraries.
var logger: Logger { get set }
}
/// A default `Context` type.
///
/// It is a carrier of contextual `Baggage` and related `Logger`, allowing to log and trace throughout a system.
///
/// Any values set on the `baggage` will be made accessible to the logger as call-site metadata, allowing it to log those.
///
/// ### Logged Metadata and Baggage Items
///
/// Please refer to your configured log handler documentation about how to configure which metadata values should be logged
/// and which not, as each log handler may handle and configure those differently. The default implementations log *all*
/// metadata/baggage values present, which often is the right thing, however in larger systems one may want to choose a
/// log handler which allows for configuring these details.
///
/// ### Accepting context types in APIs
///
/// It is preferred to accept values of `ContextProtocol` in library APIs, as this yields a more flexible API shape,
/// to which other libraries/frameworks may pass their specific context objects.
///
/// - SeeAlso: `Baggage` from the Baggage module.
/// - SeeAlso: `Logger` from the SwiftLog package.
public struct DefaultContext: Context {
/// The `Baggage` carried with this context.
/// It's values will automatically be made available to the `logger` as metadata when logging.
///
/// Baggage values are different from plain logging metadata in that they are intended to be
/// carried across process and node boundaries (serialized and deserialized) and are made
/// available to instruments using `swift-distributed-tracing`.
public var baggage: Baggage {
willSet {
// every time the baggage changes, we need to update the logger;
// values removed from the baggage are also removed from the logger metadata.
//
// TODO: optimally, logger could some day accept baggage directly, without ever having to map it into `Metadata`,
// then we would not have to make those mappings at all and passing the logger.with(baggage) would be cheap.
//
// This implementation generally is a tradeoff, we bet on logging being performed far more often than baggage
// being changed; We do this logger update eagerly, so even if we never log anything, the logger has to be updated.
// Systems which never or rarely log will take the hit for it here. The alternative tradeoff to map lazily as `logger.with(baggage)`
// is available as well, but users would have to build their own context and specifically make use of that then -- that approach
// allows to not pay the mapping cost up front, but only if a log statement is made (but then again, the cost is paid every time we log something).
self._logger.updateMetadata(previous: self.baggage, latest: newValue)
}
}
// We need to store the logger as `_logger` in order to avoid cyclic updates triggering when baggage changes
public var _logger: Logger
public var logger: Logger {
get {
return self._logger
}
set {
self._logger = newValue
// Since someone could have completely replaced the logger (not just changed the log level),
// we have to update the baggage again, since perhaps the new logger has empty metadata.
self._logger.updateMetadata(previous: .topLevel, latest: self.baggage)
}
}
public init(baggage: Baggage, logger: Logger) {
self.baggage = baggage
self._logger = logger
self._logger.updateMetadata(previous: .topLevel, latest: baggage)
}
public init<C>(context: C) where C: Context {
self.baggage = context.baggage
self._logger = context.logger
self._logger.updateMetadata(previous: .topLevel, latest: self.baggage)
}
}
// ==== ----------------------------------------------------------------------------------------------------------------
// MARK: `with...` functions
extension DefaultContext {
/// Fluent API allowing for modification of underlying logger when passing the context to other functions.
///
/// - Parameter logger: Logger that should replace the underlying logger of this context.
/// - Returns: new context, with the passed in `logger`
public func withLogger(_ logger: Logger) -> DefaultContext {
var copy = self
copy.logger = logger
return copy
}
/// Fluent API allowing for modification of underlying logger when passing the context to other functions.
///
/// - Parameter logger: Logger that should replace the underlying logger of this context.
/// - Returns: new context, with the passed in `logger`
public func withLogger(_ function: (inout Logger) -> Void) -> DefaultContext {
var logger = self.logger
function(&logger)
return .init(baggage: self.baggage, logger: logger)
}
/// Fluent API allowing for modification of underlying log level when passing the context to other functions.
///
/// - Parameter logLevel: New log level which should be used to create the new context
/// - Returns: new context, with the passed in `logLevel` used for the underlying logger
public func withLogLevel(_ logLevel: Logger.Level) -> DefaultContext {
var copy = self
copy.logger.logLevel = logLevel
return copy
}
/// Fluent API allowing for modification a few baggage values when passing the context to other functions, e.g.
///
/// makeRequest(url, context: context.withBaggage {
/// $0.traceID = "fake-value"
/// $0.calledFrom = #function
/// })
///
/// - Parameter function:
public func withBaggage(_ function: (inout Baggage) -> Void) -> DefaultContext {
var baggage = self.baggage
function(&baggage)
return self.withBaggage(baggage)
}
/// Fluent API allowing for replacement of underlying baggage when passing the context to other functions.
///
/// - Warning: Use with caution, generally it is not recommended to modify an entire baggage, but rather only add a few values to it.
///
/// - Parameter baggage: baggage that should *replace* the context's current baggage.
/// - Returns: new context, with the passed in baggage
public func withBaggage(_ baggage: Baggage) -> DefaultContext {
var copy = self
copy.baggage = baggage
return copy
}
}
// ==== ----------------------------------------------------------------------------------------------------------------
// MARK: Context Initializers
extension DefaultContext {
/// Creates a new empty "top level" default baggage context, generally used as an "initial" context to immediately be populated with
/// some values by a framework or runtime. Another use case is for tasks starting in the "background" (e.g. on a timer),
/// which don't have a "request context" per se that they can pick up, and as such they have to create a "top level"
/// baggage for their work.
///
/// It is typically used by the main function, initialization, and tests, and as the top-level Context for incoming requests.
///
/// ### Usage in frameworks and libraries
/// This function is really only intended to be used frameworks and libraries, at the "top-level" where a request's,
/// message's or task's processing is initiated. For example, a framework handling requests, should create an empty
/// context when handling a request only to immediately populate it with useful trace information extracted from e.g.
/// request headers.
///
/// ### Usage in applications
/// Application code should never have to create an empty context during the processing lifetime of any request,
/// and only should create contexts if some processing is performed in the background - thus the naming of this property.
///
/// Usually, a framework such as an HTTP server or similar "request handler" would already provide users
/// with a context to be passed along through subsequent calls.
///
/// If unsure where to obtain a context from, prefer using `.TODO("Not sure where I should get a context from here?")`,
/// such that other developers are informed that the lack of context was not done on purpose, but rather because either
/// not being sure where to obtain a context from, or other framework limitations -- e.g. the outer framework not being
/// context aware just yet.
public static func topLevel(logger: Logger) -> DefaultContext {
return .init(baggage: .topLevel, logger: logger)
}
}
extension DefaultContext {
/// A baggage context intended as a placeholder until a real value can be passed through a function call.
///
/// It should ONLY be used while prototyping or when the passing of the proper context is not yet possible,
/// e.g. because an external library did not pass it correctly and has to be fixed before the proper context
/// can be obtained where the TO-DO is currently used.
///
/// ## Crashing on TO-DO context creation
/// You may set the `BAGGAGE_CRASH_TODOS` variable while compiling a project in order to make calls to this function crash
/// with a fatal error, indicating where a to-do baggage context was used. This comes in handy when wanting to ensure that
/// a project never ends up using with code initially was written as "was lazy, did not pass context", yet the
/// project requires context passing to be done correctly throughout the application. Similar checks can be performed
/// at compile time easily using linters (not yet implemented), since it is always valid enough to detect a to-do context
/// being passed as illegal and warn or error when spotted.
///
/// ## Example
///
/// frameworkHandler { what in
/// hello(who: "World", baggage: .TODO(logger: logger, "The framework XYZ should be modified to pass us a context here, and we'd pass it along"))
/// }
///
/// - Parameters:
/// - reason: Informational reason for developers, why a placeholder context was used instead of a proper one,
/// - Returns: Empty "to-do" baggage context which should be eventually replaced with a carried through one, or `background`.
public static func TODO(logger: Logger, _ reason: StaticString? = "", function: String = #function, file: String = #file, line: UInt = #line) -> DefaultContext {
let baggage = Baggage.TODO(reason, function: function, file: file, line: line)
#if BAGGAGE_CRASH_TODOS
fatalError("BAGGAGE_CRASH_TODOS: at \(file):\(line) (function \(function)), reason: \(reason)", file: file, line: line)
#else
return .init(baggage: baggage, logger: logger)
#endif
}
}

View File

@ -0,0 +1,164 @@
//===----------------------------------------------------------------------===//
//
// 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: Logger with Baggage
extension Logger {
/// Returns a logger that in addition to any explicit metadata passed to log statements,
/// also includes the `Baggage` 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`).
///
/// Note that when it is known that multiple log statements will be performed with the baggage it is preferable to
/// modify the logger's metadata by issuing `logger.updateMetadata(previous: baggage, latest: baggage)` instead.
///
/// - SeeAlso:
public func with(_ baggage: Baggage) -> Logger {
return Logger(
label: self.label,
factory: { _ in BaggageMetadataLogHandler(logger: self, baggage: baggage) }
)
}
/// Update the logger's metadata in accordance to the passed in baggage.
///
/// Items which were previously present in the baggage but are now removed will also be removed from the logger's
/// metadata.
///
/// - Parameters:
/// - previous: baggage which was previously used to set metadata on this logger (pass `.topLevel` if unknown or none)
/// - latest: the current, latest, state of the baggage container; all of it's loggable values will be set as the `Logger`'s metadata
public mutating func updateMetadata(previous: Baggage, latest: Baggage) {
var removedKeys: Set<AnyBaggageKey> = []
removedKeys.reserveCapacity(previous.count)
previous.forEach { key, _ in
removedKeys.insert(key)
}
latest.forEach { key, value in
removedKeys.remove(key)
if let convertible = value as? String {
self[metadataKey: key.name] = .string(convertible)
} else if let convertible = value as? CustomStringConvertible {
self[metadataKey: key.name] = .stringConvertible(convertible)
} else {
self[metadataKey: key.name] = .stringConvertible(BaggageValueCustomStringConvertible(value))
}
}
removedKeys.forEach { removedKey in
self[metadataKey: removedKey.name] = nil
}
}
}
// ==== ----------------------------------------------------------------------------------------------------------------
// MARK: Baggage (as additional Logger.Metadata) LogHandler
/// Proxying log handler which adds `Baggage` as metadata when log events are to be emitted.
///
/// The values stored in the `Baggage` are merged with the existing metadata on the logger. If both contain values for the same key,
/// the `Baggage` values are preferred.
public struct BaggageMetadataLogHandler: LogHandler {
private var underlying: Logger
private let baggage: Baggage
public init(logger underlying: Logger, baggage: Baggage) {
self.underlying = underlying
self.baggage = baggage
}
public var logLevel: Logger.Level {
get {
return 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 {
return [:]
}
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 `Baggage` offers in any case.
public subscript(metadataKey metadataKey: Logger.Metadata.Key) -> Logger.Metadata.Value? {
get {
return self.underlying[metadataKey: metadataKey]
}
set {
self.underlying[metadataKey: metadataKey] = newValue
}
}
private func baggageAsMetadata() -> Logger.Metadata {
var effectiveMetadata: Logger.Metadata = [:]
self.baggage.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 {
return "\(self.value)"
}
}

View File

@ -25,4 +25,7 @@ public enum BenchmarkCategory: String {
// Explicit skip marker
case skip
// --- custom ---
case logging
}

View File

@ -0,0 +1,379 @@
//===----------------------------------------------------------------------===//
//
// 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 BaggageContext
import BaggageContextBenchmarkTools
import Dispatch
import class Foundation.NSLock
import Logging
private let message: Logger.Message = "Hello world how are you"
func pad(_ label: String) -> String {
return "\(label)\(String(repeating: " ", count: max(0, 80 - label.count)))"
}
public let BaggageLoggingBenchmarks: [BenchmarkInfo] = [
// ==== ----------------------------------------------------------------------------------------------------------------
// MARK: Baseline
BenchmarkInfo(
name: pad("BaggageLoggingBenchmarks.0_log_noop_baseline_empty"),
runFunction: { iters in
let logger = Logger(label: "0_log_noop_baseline_empty", factory: { _ in SwiftLogNoOpLogHandler() })
log_baseline(logger: logger, iters: iters)
},
tags: [
.logging,
],
setUpFunction: { setUp() },
tearDownFunction: tearDown
),
BenchmarkInfo(
name: pad("BaggageLoggingBenchmarks.0_log_noop_baseline_smallMetadata"),
runFunction: { iters in
var logger = Logger(label: "0_log_noop_baseline_smallMetadata", factory: { _ in SwiftLogNoOpLogHandler() })
logger[metadataKey: "k1"] = "k1-value"
logger[metadataKey: "k2"] = "k2-value"
logger[metadataKey: "k3"] = "k3-value"
log_baseline(logger: logger, iters: iters)
},
tags: [
.logging,
],
setUpFunction: { setUp() },
tearDownFunction: tearDown
),
// ==== ----------------------------------------------------------------------------------------------------------------
// MARK: Context / Baggage (Really do log)
BenchmarkInfo(
name: pad("BaggageLoggingBenchmarks.0_log_noop_loggerWithBaggage_small"),
runFunction: { iters in
let logger = Logger(label: "0_log_noop_loggerWithBaggage_small", factory: { _ in SwiftLogNoOpLogHandler() })
var baggage = Baggage.topLevel
baggage[TestK1.self] = "k1-value"
baggage[TestK2.self] = "k2-value"
baggage[TestK3.self] = "k3-value"
log_loggerWithBaggage(logger: logger, baggage: baggage, iters: iters)
},
tags: [
.logging,
],
setUpFunction: { setUp() },
tearDownFunction: tearDown
),
BenchmarkInfo(
name: pad("BaggageLoggingBenchmarks.0_log_noop_context_with_baggage_small"),
runFunction: { iters in
var context = DefaultContext.topLevel(logger: Logger(label: "0_log_noop_context_with_baggage_small", factory: { _ in SwiftLogNoOpLogHandler() }))
context.baggage[TestK1.self] = "k1-value"
context.baggage[TestK2.self] = "k2-value"
context.baggage[TestK3.self] = "k3-value"
log_throughContext(context: context, iters: iters)
},
tags: [
.logging,
],
setUpFunction: { setUp() },
tearDownFunction: tearDown
),
// ==== ----------------------------------------------------------------------------------------------------------------
// MARK: Context / Baggage (do actually emit the logs)
BenchmarkInfo(
name: pad("BaggageLoggingBenchmarks.1_log_real_baseline_empty"),
runFunction: { iters in
let logger = Logger(label: "1_log_real_baseline_empty", factory: StreamLogHandler.standardError)
log_baseline(logger: logger, iters: iters)
},
tags: [
.logging,
],
setUpFunction: { setUp() },
tearDownFunction: tearDown
),
BenchmarkInfo(
name: pad("BaggageLoggingBenchmarks.1_log_real_baseline_smallMetadata"),
runFunction: { iters in
var logger = Logger(label: "1_log_real_baseline_smallMetadata", factory: StreamLogHandler.standardError)
logger[metadataKey: "k1"] = "k1-value"
logger[metadataKey: "k2"] = "k2-value"
logger[metadataKey: "k3"] = "k3-value"
log_baseline(logger: logger, iters: iters)
},
tags: [
.logging,
],
setUpFunction: { setUp() },
tearDownFunction: tearDown
),
BenchmarkInfo(
name: pad("BaggageLoggingBenchmarks.1_log_real_loggerWithBaggage_small"),
runFunction: { iters in
let logger = Logger(label: "1_log_real_loggerWithBaggage_small", factory: StreamLogHandler.standardError)
var baggage = Baggage.topLevel
baggage[TestK1.self] = "k1-value"
baggage[TestK2.self] = "k2-value"
baggage[TestK3.self] = "k3-value"
log_loggerWithBaggage(logger: logger, baggage: baggage, iters: iters)
},
tags: [
.logging,
],
setUpFunction: { setUp() },
tearDownFunction: tearDown
),
BenchmarkInfo(
name: pad("BaggageLoggingBenchmarks.1_log_real_context_with_baggage_small"),
runFunction: { iters in
var context = DefaultContext.topLevel(logger: Logger(label: "1_log_real_context_with_baggage_small", factory: StreamLogHandler.standardError))
context.baggage[TestK1.self] = "k1-value"
context.baggage[TestK2.self] = "k2-value"
context.baggage[TestK3.self] = "k3-value"
log_throughContext(context: context, iters: iters)
},
tags: [
.logging,
],
setUpFunction: { setUp() },
tearDownFunction: tearDown
),
// ==== ----------------------------------------------------------------------------------------------------------------
// MARK: Context / Baggage (log not emitted because logLevel)
BenchmarkInfo(
name: pad("BaggageLoggingBenchmarks.2_log_real-trace_baseline_empty"),
runFunction: { iters in
let logger = Logger(label: "trace_baseline_empty", factory: StreamLogHandler.standardError)
log_baseline_trace(logger: logger, iters: iters)
},
tags: [
.logging,
],
setUpFunction: { setUp() },
tearDownFunction: tearDown
),
BenchmarkInfo(
name: pad("BaggageLoggingBenchmarks.2_log_real-trace_baseline_smallMetadata"),
runFunction: { iters in
var logger = Logger(label: "2_log_real-trace_baseline_smallMetadata", factory: StreamLogHandler.standardError)
logger[metadataKey: "k1"] = "k1-value"
logger[metadataKey: "k2"] = "k2-value"
logger[metadataKey: "k3"] = "k3-value"
log_baseline_trace(logger: logger, iters: iters)
},
tags: [
.logging,
],
setUpFunction: { setUp() },
tearDownFunction: tearDown
),
BenchmarkInfo(
name: pad("BaggageLoggingBenchmarks.2_log_real-trace_loggerWithBaggage_small"),
runFunction: { iters in
let logger = Logger(label: "2_log_real-trace_loggerWithBaggage_small", factory: StreamLogHandler.standardError)
var baggage = Baggage.topLevel
baggage[TestK1.self] = "k1-value"
baggage[TestK2.self] = "k2-value"
baggage[TestK3.self] = "k3-value"
log_loggerWithBaggage_trace(logger: logger, baggage: baggage, iters: iters)
},
tags: [
.logging,
],
setUpFunction: { setUp() },
tearDownFunction: tearDown
),
BenchmarkInfo(
name: pad("BaggageLoggingBenchmarks.2_log_real-trace_context_with_baggage_small"),
runFunction: { iters in
var context = DefaultContext.topLevel(logger: Logger(label: "2_log_real-trace_context_with_baggage_small", factory: StreamLogHandler.standardError))
context.baggage[TestK1.self] = "k1-value"
context.baggage[TestK2.self] = "k2-value"
context.baggage[TestK3.self] = "k3-value"
log_throughContext_trace(context: context, iters: iters)
},
tags: [
.logging,
],
setUpFunction: { setUp() },
tearDownFunction: tearDown
),
// ==== ----------------------------------------------------------------------------------------------------------------
// MARK: materialize once once
BenchmarkInfo(
name: pad("BaggageLoggingBenchmarks.3_log_real_small_context_materializeOnce"),
runFunction: { iters in
var context = DefaultContext.topLevel(logger: Logger(label: "3_log_real_context_materializeOnce", factory: StreamLogHandler.standardError))
context.baggage[TestK1.self] = "k1-value"
context.baggage[TestK2.self] = "k2-value"
context.baggage[TestK3.self] = "k3-value"
log_materializeOnce(context: context, iters: iters)
},
tags: [
.logging,
],
setUpFunction: { setUp() },
tearDownFunction: tearDown
),
BenchmarkInfo(
name: pad("BaggageLoggingBenchmarks.3_log_real-trace_small_context_materializeOnce"),
runFunction: { iters in
var context = DefaultContext.topLevel(logger: Logger(label: "3_log_real_context_materializeOnce", factory: StreamLogHandler.standardError))
context.baggage[TestK1.self] = "k1-value"
context.baggage[TestK2.self] = "k2-value"
context.baggage[TestK3.self] = "k3-value"
log_materializeOnce_trace(context: context, iters: iters)
},
tags: [
.logging,
],
setUpFunction: { setUp() },
tearDownFunction: tearDown
),
]
private func setUp() {
// ...
}
private func tearDown() {
// ...
}
// ==== ----------------------------------------------------------------------------------------------------------------
// MARK: Benchmarks
@inline(never)
func log_baseline(logger: Logger, iters remaining: Int) {
for _ in 0 ..< remaining {
logger.warning(message)
}
}
@inline(never)
func log_baseline_trace(logger: Logger, iters remaining: Int) {
for _ in 0 ..< remaining {
logger.trace(message)
}
}
@inline(never)
func log_loggerWithBaggage(logger: Logger, baggage: Baggage, iters remaining: Int) {
for _ in 0 ..< remaining {
logger.with(baggage).warning(message)
}
}
@inline(never)
func log_throughContext(context: Context, iters remaining: Int) {
for _ in 0 ..< remaining {
context.logger.warning(message)
}
}
@inline(never)
func log_loggerWithBaggage_trace(logger: Logger, baggage: Baggage, iters remaining: Int) {
for _ in 0 ..< remaining {
logger.with(baggage).trace(message)
}
}
@inline(never)
func log_throughContext_trace(context: Context, iters remaining: Int) {
for _ in 0 ..< remaining {
context.logger.trace(message)
}
}
@inline(never)
func log_materializeOnce_trace(context: Context, iters remaining: Int) {
var logger = context.logger
context.baggage.forEach { key, value in
logger[metadataKey: key.name] = "\(value)"
}
for _ in 0 ..< remaining {
logger.trace(message)
}
}
@inline(never)
func log_materializeOnce(context: Context, iters remaining: Int) {
var logger = context.logger
context.baggage.forEach { key, value in
logger[metadataKey: key.name] = "\(value)"
}
for _ in 0 ..< remaining {
logger.warning(message)
}
}
// ==== ----------------------------------------------------------------------------------------------------------------
// MARK: Baggage Keys
private enum TestK1: BaggageKey {
typealias Value = String
}
private enum TestK2: BaggageKey {
typealias Value = String
}
private enum TestK3: BaggageKey {
typealias Value = String
}
private enum TestK4: BaggageKey {
typealias Value = String
}
private enum TestKD1: BaggageKey {
typealias Value = [String: String]
}
extension Baggage {
fileprivate var k1: TestK1.Value? {
get { return self[TestK1.self] }
set { self[TestK1.self] = newValue }
}
fileprivate var k2: TestK2.Value? {
get { return self[TestK2.self] }
set { self[TestK2.self] = newValue }
}
fileprivate var k3: TestK3.Value? {
get { return self[TestK3.self] }
set { self[TestK3.self] = newValue }
}
fileprivate var k4: TestK4.Value? {
get { return self[TestK4.self] }
set { self[TestK4.self] = newValue }
}
fileprivate var kd1: TestKD1.Value? {
get { return self[TestKD1.self] }
set { self[TestKD1.self] = newValue }
}
}

View File

@ -12,16 +12,17 @@
//===----------------------------------------------------------------------===//
import Baggage
import BaggageBenchmarkTools
import BaggageContextBenchmarkTools
import Dispatch
import class Foundation.NSLock
public let BaggagePassingBenchmarks: [BenchmarkInfo] = [
// ==== ----------------------------------------------------------------------------------------------------------------
// MARK: "Read only" context passing around
BenchmarkInfo(
name: "BaggagePassingBenchmarks.pass_async_empty_100_000 ",
runFunction: { _ in
let context = BaggageContext.background
let context = Baggage.topLevel
pass_async(context: context, times: 100_000)
},
tags: [],
@ -31,7 +32,7 @@ public let BaggagePassingBenchmarks: [BenchmarkInfo] = [
BenchmarkInfo(
name: "BaggagePassingBenchmarks.pass_async_smol_100_000 ",
runFunction: { _ in
var context = BaggageContext.background
var context = Baggage.topLevel
context.k1 = "one"
context.k2 = "two"
context.k3 = "three"
@ -45,7 +46,7 @@ public let BaggagePassingBenchmarks: [BenchmarkInfo] = [
BenchmarkInfo(
name: "BaggagePassingBenchmarks.pass_async_small_nonconst_100_000",
runFunction: { _ in
var context = BaggageContext.background
var context = Baggage.topLevel
context.k1 = "\(Int.random(in: 1 ... Int.max))"
context.k2 = "\(Int.random(in: 1 ... Int.max))"
context.k3 = "\(Int.random(in: 1 ... Int.max))"
@ -65,7 +66,7 @@ public let BaggagePassingBenchmarks: [BenchmarkInfo] = [
BenchmarkInfo(
name: "BaggagePassingBenchmarks.pass_mut_async_small_100_000 ",
runFunction: { _ in
var context = BaggageContext.background
var context = Baggage.topLevel
context.k1 = "\(Int.random(in: 1 ... Int.max))"
context.k2 = "\(Int.random(in: 1 ... Int.max))"
context.k3 = "\(Int.random(in: 1 ... Int.max))"
@ -87,10 +88,10 @@ private func tearDown() {
}
@inline(never)
func pass_async(context: BaggageContext, times remaining: Int) {
func pass_async(context: Baggage, times remaining: Int) {
let latch = CountDownLatch(from: 1)
func pass_async0(context: BaggageContext, times remaining: Int) {
func pass_async0(context: Baggage, times remaining: Int) {
if remaining == 0 {
latch.countDown()
}
@ -105,11 +106,11 @@ func pass_async(context: BaggageContext, times remaining: Int) {
}
@inline(never)
func pass_mut_async(context: BaggageContext, times remaining: Int) {
func pass_mut_async(context: Baggage, times remaining: Int) {
var context = context
let latch = CountDownLatch(from: 1)
func pass_async0(context: BaggageContext, times remaining: Int) {
func pass_async0(context: Baggage, times remaining: Int) {
if remaining == 0 {
latch.countDown()
}
@ -132,31 +133,31 @@ func pass_mut_async(context: BaggageContext, times remaining: Int) {
// ==== ----------------------------------------------------------------------------------------------------------------
// MARK: Baggage Keys
private enum TestPassCounterKey: BaggageContextKey {
private enum TestPassCounterKey: BaggageKey {
typealias Value = Int
}
private enum TestK1: BaggageContextKey {
private enum TestK1: BaggageKey {
typealias Value = String
}
private enum TestK2: BaggageContextKey {
private enum TestK2: BaggageKey {
typealias Value = String
}
private enum TestK3: BaggageContextKey {
private enum TestK3: BaggageKey {
typealias Value = String
}
private enum TestK4: BaggageContextKey {
private enum TestK4: BaggageKey {
typealias Value = String
}
private enum TestKD1: BaggageContextKey {
private enum TestKD1: BaggageKey {
typealias Value = [String: String]
}
extension BaggageContext {
extension Baggage {
fileprivate var passCounter: TestPassCounterKey.Value {
get { return self[TestPassCounterKey.self] ?? 0 }
set { self[TestPassCounterKey.self] = newValue }

View File

@ -11,7 +11,7 @@
//
//===----------------------------------------------------------------------===//
import BaggageBenchmarkTools
import BaggageContextBenchmarkTools
assert({
print("===========================================================================")
@ -37,5 +37,6 @@ private func registerBenchmark(_ name: String, _ function: @escaping (Int) -> Vo
}
registerBenchmark(BaggagePassingBenchmarks)
registerBenchmark(BaggageLoggingBenchmarks)
main()

View File

@ -1,113 +0,0 @@
//===----------------------------------------------------------------------===//
//
// 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.
///
/// The values stored in the `BaggageContext` are merged with the existing metadata on the logger. If both contain values for the same key,
/// the `BaggageContext` values are preferred.
public struct BaggageMetadataLogHandler: LogHandler {
private var underlying: Logger
private let context: BaggageContext
public init(logger underlying: Logger, context: BaggageContext) {
self.underlying = underlying
self.context = context
}
public var logLevel: Logger.Level {
get {
return 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 {
return [:]
}
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 {
return 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 {
return "\(self.value)"
}
}
}

View File

@ -1,29 +0,0 @@
//===----------------------------------------------------------------------===//
//
// 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 {
return Logger(
label: self.label,
factory: { _ in BaggageMetadataLogHandler(logger: self, context: context) }
)
}
}

View File

@ -1,44 +0,0 @@
//===----------------------------------------------------------------------===//
//
// 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 `LoggingBaggageContextCarrier` 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 context carrier.
///
/// 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.
///
/// ### Example implementation
///
/// Writes to the `logger` metadata SHOULD NOT be reflected in the `baggage`,
/// however writes to the underlying `baggage` SHOULD be reflected in the `logger`.
///
/// struct MyFrameworkContext: LoggingBaggageContextCarrier {
/// var baggage = BaggageContext()
/// private let _logger: Logger
///
/// var logger: Logger {
/// return self._logger.with(context: self.baggage)
/// }
/// }
var logger: Logger { get }
}

View File

@ -11,7 +11,7 @@
//
//===----------------------------------------------------------------------===//
//
// LoggingBaggageContextCarrierTests+XCTest.swift
// BaggageContextTests+XCTest.swift
//
import XCTest
///
@ -20,14 +20,15 @@ import XCTest
/// Do NOT edit this file directly as it will be regenerated automatically when needed.
///
extension LoggingBaggageContextCarrierTests {
extension BaggageContextTests {
@available(*, deprecated, message: "not actually deprecated. Just deprecated to allow deprecated tests (which test deprecated functionality) without warnings")
static var allTests : [(String, (LoggingBaggageContextCarrierTests) -> () throws -> Void)] {
static var allTests : [(String, (BaggageContextTests) -> () throws -> Void)] {
return [
("test_ContextWithLogger_dumpBaggage", test_ContextWithLogger_dumpBaggage),
("test_ContextWithLogger_log_withBaggage", test_ContextWithLogger_log_withBaggage),
("test_ContextWithLogger_log_prefersBaggageContextOverExistingLoggerMetadata", test_ContextWithLogger_log_prefersBaggageContextOverExistingLoggerMetadata),
("test_ExampleFrameworkContext_dumpBaggage", test_ExampleFrameworkContext_dumpBaggage),
("test_ExampleFrameworkContext_log_withBaggage", test_ExampleFrameworkContext_log_withBaggage),
("test_DefaultContext_log_withBaggage", test_DefaultContext_log_withBaggage),
("test_ExampleFrameworkContext_log_prefersBaggageContextOverExistingLoggerMetadata", test_ExampleFrameworkContext_log_prefersBaggageContextOverExistingLoggerMetadata),
]
}
}

View File

@ -0,0 +1,213 @@
//===----------------------------------------------------------------------===//
//
// 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 BaggageContext
import Logging
import XCTest
final class BaggageContextTests: XCTestCase {
func test_ExampleFrameworkContext_dumpBaggage() throws {
var baggage = Baggage.topLevel
let logger = Logger(label: "TheLogger")
baggage.testID = 42
let context = ExampleFrameworkContext(context: baggage, logger: logger)
func frameworkFunctionDumpsBaggage(param: String, context: Context) -> 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_ExampleFrameworkContext_log_withBaggage() throws {
let baggage = Baggage.topLevel
let logging = TestLogging()
let logger = Logger(label: "TheLogger", factory: { label in logging.make(label: label) })
var context = ExampleFrameworkContext(context: baggage, logger: logger)
context.baggage.secondTestID = "value"
context.baggage.testID = 42
context.logger.info("Hello")
context.baggage.testID = nil
context.logger.warning("World")
context.baggage.secondTestID = nil
context.logger[metadataKey: "metadata"] = "on-logger"
context.logger.warning("!")
// These implicitly exercise logger.updateMetadata
logging.history.assertExist(level: .info, message: "Hello", metadata: [
"TestIDKey": .stringConvertible(42),
"secondIDExplicitlyNamed": "value",
])
logging.history.assertExist(level: .warning, message: "World", metadata: [
"secondIDExplicitlyNamed": "value",
])
logging.history.assertExist(level: .warning, message: "!", metadata: [
"metadata": "on-logger",
])
}
func test_DefaultContext_log_withBaggage() throws {
let logging = TestLogging()
let logger = Logger(label: "TheLogger", factory: { label in logging.make(label: label) })
var context = DefaultContext.topLevel(logger: logger)
context.baggage.secondTestID = "value"
context.baggage.testID = 42
context.logger.info("Hello")
context.baggage.testID = nil
context.logger.warning("World")
context.baggage.secondTestID = nil
context.logger[metadataKey: "metadata"] = "on-logger"
context.logger.warning("!")
// These implicitly exercise logger.updateMetadata
logging.history.assertExist(level: .info, message: "Hello", metadata: [
"TestIDKey": .stringConvertible(42),
"secondIDExplicitlyNamed": "value",
])
logging.history.assertExist(level: .warning, message: "World", metadata: [
"secondIDExplicitlyNamed": "value",
])
logging.history.assertExist(level: .warning, message: "!", metadata: [
"metadata": "on-logger",
])
}
func test_ExampleFrameworkContext_log_prefersBaggageContextOverExistingLoggerMetadata() {
let baggage = Baggage.topLevel
let logging = TestLogging()
var logger = Logger(label: "TheLogger", factory: { label in logging.make(label: label) })
logger[metadataKey: "secondIDExplicitlyNamed"] = "set on logger"
var context = ExampleFrameworkContext(context: baggage, logger: logger)
context.baggage.secondTestID = "set on baggage"
context.logger.info("Hello")
logging.history.assertExist(level: .info, message: "Hello", metadata: [
"secondIDExplicitlyNamed": "set on baggage",
])
}
}
struct ExampleFrameworkContext: BaggageContext.Context {
var baggage: Baggage {
willSet {
self._logger.updateMetadata(previous: self.baggage, latest: newValue)
}
}
var _logger: Logger
var logger: Logger {
get {
return self._logger
}
set {
self._logger = newValue
self._logger.updateMetadata(previous: self.baggage, latest: self.baggage)
}
}
init(context baggage: Baggage, logger: Logger) {
self.baggage = baggage
self._logger = logger
self._logger.updateMetadata(previous: .topLevel, latest: baggage)
}
}
struct CoolFrameworkContext: BaggageContext.Context {
var baggage: Baggage {
willSet {
self.logger.updateMetadata(previous: self.baggage, latest: newValue)
}
}
var logger: Logger {
didSet {
self.logger.updateMetadata(previous: self.baggage, latest: self.baggage)
}
}
// framework context defines other values as well
let frameworkField: String
// including the popular eventLoop
let eventLoop: FakeEventLoop
init() {
self.baggage = .topLevel
self.logger = Logger(label: "some-framework-logger")
self.eventLoop = FakeEventLoop()
self.frameworkField = ""
self.logger.updateMetadata(previous: .topLevel, latest: self.baggage)
}
func forEachBaggageItem(_ body: (AnyBaggageKey, Any) throws -> Void) rethrows {
return try self.baggage.forEach(body)
}
}
struct FakeEventLoop {}
private extension Baggage {
var testID: Int? {
get {
return self[TestIDKey.self]
}
set {
self[TestIDKey.self] = newValue
}
}
var secondTestID: String? {
get {
return self[SecondTestIDKey.self]
}
set {
self[SecondTestIDKey.self] = newValue
}
}
}
private enum TestIDKey: Baggage.Key {
typealias Value = Int
}
private enum SecondTestIDKey: Baggage.Key {
typealias Value = String
static let nameOverride: String? = "secondIDExplicitlyNamed"
}

View File

@ -11,7 +11,7 @@
//
//===----------------------------------------------------------------------===//
//
// BaggageContextCarrierTests+XCTest.swift
// FrameworkContextTests+XCTest.swift
//
import XCTest
///
@ -20,14 +20,13 @@ import XCTest
/// Do NOT edit this file directly as it will be regenerated automatically when needed.
///
extension BaggageContextCarrierTests {
extension FrameworkBaggageContextTests {
@available(*, deprecated, message: "not actually deprecated. Just deprecated to allow deprecated tests (which test deprecated functionality) without warnings")
static var allTests : [(String, (BaggageContextCarrierTests) -> () throws -> Void)] {
static var allTests : [(String, (FrameworkBaggageContextTests) -> () throws -> Void)] {
return [
("testBaggageContextSubscript", testBaggageContextSubscript),
("testBaggageContextForEach", testBaggageContextForEach),
("testBaggageContextCarriesItself", testBaggageContextCarriesItself),
]
}
}

View File

@ -0,0 +1,88 @@
//===----------------------------------------------------------------------===//
//
// 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
//
//===----------------------------------------------------------------------===//
@testable import Baggage
import BaggageContext
import Logging
import XCTest
final class FrameworkBaggageContextTests: XCTestCase {
func testBaggageContextSubscript() {
var context = TestFrameworkContext()
// mutate baggage context directly
context.baggage[OtherKey.self] = "test"
XCTAssertEqual(context.baggage.otherKey, "test")
}
func testBaggageContextForEach() {
var contents = [AnyBaggageKey: Any]()
var context = TestFrameworkContext()
context.baggage.testKey = 42
context.baggage.otherKey = "test"
context.baggage.forEach { key, value in
contents[key] = value
}
XCTAssertNotNil(contents[AnyBaggageKey(TestKey.self)])
XCTAssertEqual(contents[AnyBaggageKey(TestKey.self)] as? Int, 42)
XCTAssertNotNil(contents[AnyBaggageKey(OtherKey.self)])
XCTAssertEqual(contents[AnyBaggageKey(OtherKey.self)] as? String, "test")
}
}
private struct TestFrameworkContext: Context {
var baggage = Baggage.topLevel
private var _logger = Logger(label: "test")
var logger: Logger {
get {
return self._logger.with(self.baggage)
}
set {
self._logger = newValue
}
}
}
private enum TestKey: Baggage.Key {
typealias Value = Int
}
extension Baggage {
var testKey: Int? {
get {
return self[TestKey.self]
}
set {
self[TestKey.self] = newValue
}
}
}
private enum OtherKey: Baggage.Key {
typealias Value = String
}
extension Baggage {
var otherKey: String? {
get {
return self[OtherKey.self]
}
set {
self[OtherKey.self] = newValue
}
}
}

View File

@ -84,11 +84,9 @@ internal struct TestLogHandler: LogHandler {
public var metadata: Logger.Metadata {
get {
// return self.logger.metadata
return self.metadataLock.withLock { self._metadata }
}
set {
// self.logger.metadata = newValue
self.metadataLock.withLock { self._metadata = newValue }
}
}
@ -96,11 +94,9 @@ internal struct TestLogHandler: LogHandler {
// 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]
return self.metadataLock.withLock { self._metadata[metadataKey] }
}
set {
// return logger[metadataKey: metadataKey] = newValue
self.metadataLock.withLock {
self._metadata[metadataKey] = newValue
}

View File

@ -1,145 +0,0 @@
//===----------------------------------------------------------------------===//
//
// 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.background
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.background
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",
])
}
func test_ContextWithLogger_log_prefersBaggageContextOverExistingLoggerMetadata() {
let baggage = BaggageContext.background
let logging = TestLogging()
var logger = Logger(label: "TheLogger", factory: { label in logging.make(label: label) })
logger[metadataKey: "secondIDExplicitlyNamed"] = "set on logger"
var context: LoggingBaggageContextCarrier = ExampleFrameworkContext(context: baggage, logger: logger)
context.secondTestID = "set on baggage"
context.logger.info("Hello")
logging.history.assertExist(level: .info, message: "Hello", metadata: [
"secondIDExplicitlyNamed": "set on baggage",
])
}
}
struct ExampleFrameworkContext: LoggingBaggageContextCarrier {
var baggage: BaggageContext
private var _logger: Logger
var logger: Logger {
return self._logger.with(context: self.baggage)
}
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 {
return self._logger.with(context: self.baggage)
}
var baggage: BaggageContext = .background
// 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 {
return self[TestIDKey.self]
}
set {
self[TestIDKey.self] = newValue
}
}
var secondTestID: String? {
get {
return 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"
}

View File

@ -1,67 +0,0 @@
//===----------------------------------------------------------------------===//
//
// 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
//
//===----------------------------------------------------------------------===//
@testable import Baggage
import XCTest
final class BaggageContextCarrierTests: XCTestCase {
func testBaggageContextSubscript() {
var carrier = TestFrameworkContext()
// mutate baggage context through carrier
carrier[TestKey.self] = 42
XCTAssertEqual(carrier[TestKey.self], 42)
XCTAssertEqual(carrier.baggage[TestKey.self], 42)
// mutate baggage context directly
carrier.baggage[OtherKey.self] = "test"
XCTAssertEqual(carrier.baggage[OtherKey.self], "test")
XCTAssertEqual(carrier[OtherKey.self], "test")
}
func testBaggageContextForEach() {
var contents = [AnyBaggageContextKey: Any]()
var carrier = TestFrameworkContext()
carrier[TestKey.self] = 42
carrier[OtherKey.self] = "test"
carrier.forEach { key, value in
contents[key] = value
}
XCTAssertNotNil(contents[AnyBaggageContextKey(TestKey.self)])
XCTAssertEqual(contents[AnyBaggageContextKey(TestKey.self)] as? Int, 42)
XCTAssertNotNil(contents[AnyBaggageContextKey(OtherKey.self)])
XCTAssertEqual(contents[AnyBaggageContextKey(OtherKey.self)] as? String, "test")
}
func testBaggageContextCarriesItself() {
var context: BaggageContextCarrier = BaggageContext()
context.baggage[TestKey.self] = 42
XCTAssertEqual(context.baggage[TestKey.self], 42)
}
}
private struct TestFrameworkContext: BaggageContextCarrier {
var baggage = BaggageContext()
}
private enum TestKey: BaggageContextKey {
typealias Value = Int
}
private enum OtherKey: BaggageContextKey {
typealias Value = String
}

View File

@ -11,7 +11,7 @@
//
//===----------------------------------------------------------------------===//
//
// BaggageContextTests+XCTest.swift
// BaggageTests+XCTest.swift
//
import XCTest
///
@ -20,10 +20,10 @@ import XCTest
/// Do NOT edit this file directly as it will be regenerated automatically when needed.
///
extension BaggageContextTests {
extension BaggageTests {
@available(*, deprecated, message: "not actually deprecated. Just deprecated to allow deprecated tests (which test deprecated functionality) without warnings")
static var allTests : [(String, (BaggageContextTests) -> () throws -> Void)] {
static var allTests : [(String, (BaggageTests) -> () throws -> Void)] {
return [
("testSubscriptAccess", testSubscriptAccess),
("testRecommendedConvenienceExtension", testRecommendedConvenienceExtension),
@ -31,7 +31,7 @@ extension BaggageContextTests {
("testSingleKeyBaggageDescription", testSingleKeyBaggageDescription),
("testMultiKeysBaggageDescription", testMultiKeysBaggageDescription),
("test_todo_context", test_todo_context),
("test_todo_empty", test_todo_empty),
("test_topLevel", test_topLevel),
]
}
}

View File

@ -14,11 +14,11 @@
import Baggage
import XCTest
final class BaggageContextTests: XCTestCase {
final class BaggageTests: XCTestCase {
func testSubscriptAccess() {
let testID = 42
var baggage = BaggageContext.background
var baggage = Baggage.topLevel
XCTAssertNil(baggage[TestIDKey.self])
baggage[TestIDKey.self] = testID
@ -31,7 +31,7 @@ final class BaggageContextTests: XCTestCase {
func testRecommendedConvenienceExtension() {
let testID = 42
var baggage = BaggageContext.background
var baggage = Baggage.topLevel
XCTAssertNil(baggage.testID)
baggage.testID = testID
@ -42,26 +42,26 @@ final class BaggageContextTests: XCTestCase {
}
func testEmptyBaggageDescription() {
XCTAssertEqual(String(describing: BaggageContext.background), "BaggageContext(keys: [])")
XCTAssertEqual(String(describing: Baggage.topLevel), "Baggage(keys: [])")
}
func testSingleKeyBaggageDescription() {
var baggage = BaggageContext.background
var baggage = Baggage.topLevel
baggage.testID = 42
XCTAssertEqual(String(describing: baggage), #"BaggageContext(keys: ["TestIDKey"])"#)
XCTAssertEqual(String(describing: baggage), #"Baggage(keys: ["TestIDKey"])"#)
}
func testMultiKeysBaggageDescription() {
var baggage = BaggageContext.background
var baggage = Baggage.topLevel
baggage.testID = 42
baggage[SecondTestIDKey.self] = "test"
let description = String(describing: baggage)
XCTAssert(description.starts(with: "BaggageContext(keys: ["))
XCTAssert(description.starts(with: "Baggage(keys: ["), "Was: \(description)")
// use contains instead of `XCTAssertEqual` because the order is non-predictable (Dictionary)
XCTAssert(description.contains("TestIDKey"))
XCTAssert(description.contains("ExplicitKeyName"))
XCTAssert(description.contains("TestIDKey"), "Was: \(description)")
XCTAssert(description.contains("ExplicitKeyName"), "Was: \(description)")
}
// ==== ------------------------------------------------------------------------------------------------------------
@ -69,45 +69,33 @@ final class BaggageContextTests: XCTestCase {
func test_todo_context() {
// the to-do context can be used to record intentions for why a context could not be passed through
let context = BaggageContext.TODO("#1245 Some other library should be adjusted to pass us context")
let context = Baggage.TODO("#1245 Some other library should be adjusted to pass us context")
_ = context // avoid "not used" warning
// TODO: Can't work with protocols; re-consider the entire carrier approach... Context being a Baggage + Logger, and a specific type.
// func take(context: BaggageContextProtocol) {
// _ = context // ignore
// }
// take(context: .TODO("pass from request instead"))
}
func test_todo_empty() {
let context = BaggageContext.background
func test_topLevel() {
let context = Baggage.topLevel
_ = context // avoid "not used" warning
// TODO: Can't work with protocols; re-consider the entire carrier approach... Context being a Baggage + Logger, and a specific type.
// static member 'empty' cannot be used on protocol metatype 'BaggageContextProtocol.Protocol'
// func take(context: BaggageContextProtocol) {
// _ = context // ignore
// }
// take(context: .background)
}
}
private enum TestIDKey: BaggageContextKey {
private enum TestIDKey: Baggage.Key {
typealias Value = Int
}
private extension BaggageContext {
private extension Baggage {
var testID: Int? {
get {
return self[TestIDKey.self]
} set {
}
set {
self[TestIDKey.self] = newValue
}
}
}
private enum SecondTestIDKey: BaggageContextKey {
private enum SecondTestIDKey: Baggage.Key {
typealias Value = String
static let name: String? = "ExplicitKeyName"
static let nameOverride: String? = "ExplicitKeyName"
}

View File

@ -21,7 +21,7 @@ import XCTest
///
#if os(Linux) || os(FreeBSD)
@testable import BaggageLoggingTests
@testable import BaggageContextTests
@testable import BaggageTests
// This protocol is necessary to we can call the 'run' method (on an existential of this protocol)
@ -33,9 +33,9 @@ class LinuxMainRunnerImpl: LinuxMainRunner {
@available(*, deprecated, message: "not actually deprecated. Just deprecated to allow deprecated tests (which test deprecated functionality) without warnings")
func run() {
XCTMain([
testCase(BaggageContextCarrierTests.allTests),
testCase(BaggageContextTests.allTests),
testCase(LoggingBaggageContextCarrierTests.allTests),
testCase(BaggageTests.allTests),
testCase(FrameworkBaggageContextTests.allTests),
])
}
}