292 lines
16 KiB
Swift
292 lines
16 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
//
|
|
// This source file is part of the Swift Baggage Context open source project
|
|
//
|
|
// Copyright (c) 2020 Moritz Lang and the Swift Baggage Context project authors
|
|
// Licensed under Apache License v2.0
|
|
//
|
|
// See LICENSE.txt for license information
|
|
//
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
//
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
@_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
|
|
}
|
|
}
|