Skip to content
This repository was archived by the owner on Apr 23, 2021. It is now read-only.

Holding baggage in framework protocols #9

Merged
merged 1 commit into from
Aug 5, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions Package.resolved
Original file line number Diff line number Diff line change
@@ -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
}
33 changes: 31 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -22,15 +42,24 @@ let package = Package(
]
),

.testTarget(
name: "BaggageLoggingTests",
dependencies: [
"Baggage",
"BaggageLogging"
]
),

// ==== --------------------------------------------------------------------------------------------------------
// MARK: Performance / Benchmarks

.target(
name: "Benchmarks",
dependencies: [
"Baggage",
"BaggageLogging",
"SwiftBenchmarkTools",
]
]
),
.target(
name: "SwiftBenchmarkTools",
Expand Down
37 changes: 31 additions & 6 deletions Sources/Baggage/BaggageContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -44,7 +44,7 @@
/// }
/// }
/// }
public struct BaggageContext {
public struct BaggageContext: BaggageContextProtocol {
private var _storage = [AnyBaggageContextKey: ValueContainer]()

/// Create an empty `BaggageContext`.
Expand All @@ -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) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In line with the forEachs in swift-tracing 👍

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, and enforced to match via the BaggageContextProtocol

self._storage.forEach { key, container in
callback(key, container.value)
}
}

Expand All @@ -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 {
Expand Down
54 changes: 54 additions & 0 deletions Sources/Baggage/BaggageContextCarrier.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
}
110 changes: 110 additions & 0 deletions Sources/BaggageLogging/BaggageMetadataLogHandler.swift
Original file line number Diff line number Diff line change
@@ -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)"
}
}
}
29 changes: 29 additions & 0 deletions Sources/BaggageLogging/Logger+BaggageContext.swift
Original file line number Diff line number Diff line change
@@ -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) }
)
}
}
33 changes: 33 additions & 0 deletions Sources/BaggageLogging/LoggingBaggageContextCarrier.swift
Original file line number Diff line number Diff line change
@@ -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 }
}
Loading