@@ -21,8 +21,9 @@ import ServiceContextModule
2121/// Set up the instrumentation using ``bootstrap(_:)``, and access the globally available instrument using ``instrument``.
2222/// If you need to use more that one cross-cutting tool you can do so by using ``MultiplexInstrument``.
2323///
24- /// To enable scoped overrides (per-test, per-subsystem), use ``withInstrument(_:_:)``, which installs a
25- /// dedicated task-local instrument as the bootstrap on first use and sets the active instrument on it.
24+ /// To override the active instrument for a scope — a test, a subsystem — without touching the process-wide
25+ /// bootstrap, use ``withInstrument(_:_:)``. It binds a task-local instrument that ``instrument`` and discovery
26+ /// resolve ahead of the bootstrapped one, for the duration of a closure.
2627@available ( macOS 10 . 15 , iOS 13 , tvOS 13 , watchOS 6 , * ) // for TaskLocal ServiceContext
2728public enum InstrumentationSystem {
2829 /// Marked as @unchecked Sendable due to the synchronization being
@@ -52,64 +53,57 @@ public enum InstrumentationSystem {
5253 }
5354 }
5455
55- /// Ensure a ``TaskLocalInstrument`` is installed so a scope can be entered, installing one lazily
56- /// over the default ``NoOpInstrument`` when nothing has been bootstrapped.
57- ///
58- /// Uses double-checked locking: the common case — a ``TaskLocalInstrument`` is already installed —
59- /// is satisfied under a reader lock, matching the cost of reading ``instrument``. Only the one-time
60- /// install (or the crash) takes the writer lock.
61- ///
62- /// - NoOp bootstrap (nothing installed): install an empty `TaskLocalInstrument()` so the wrapper is
63- /// reachable from discovery and propagation for the duration of any scope.
64- /// - ``TaskLocalInstrument`` already installed: nothing to do.
65- /// - Any other bootstrapped instrument: crash. The task-local instrument can only be installed when
66- /// the system is otherwise un-bootstrapped. It cannot replace an instrument the application chose to
67- /// bootstrap. `withInstrument` is an application-level facility and must not be called against a
68- /// non-scoping bootstrap (typically from library code).
69- @available ( macOS 10 . 15 , iOS 13 . 0 , tvOS 13 . 0 , watchOS 6 . 0 , * )
70- func installTaskLocalInstrumentIfNeeded( ) {
71- // Fast path: already scoping-capable, no write needed.
72- if self . lock. withReaderLock ( { self . _instrument is TaskLocalInstrument } ) {
73- return
74- }
75- // Slow path: install over NoOp, or crash. Re-check under the writer lock.
76- self . lock. withWriterLock {
77- if self . _instrument is TaskLocalInstrument {
78- return
79- }
80- guard self . _instrument is NoOpInstrument else {
81- fatalError (
82- """
83- withInstrument(_:_:) requires either an un-bootstrapped InstrumentationSystem or one \
84- already using task-local scoping, but a \( type ( of: self . _instrument) ) was bootstrapped. \
85- It cannot replace a plain bootstrapped instrument. withInstrument(_:_:) is an \
86- application-level facility and must not be called from library code.
87- """
88- )
89- }
90- self . _instrument = TaskLocalInstrument ( )
91- }
92- }
93-
9456 var instrument : Instrument {
9557 self . lock. withReaderLock { self . _instrument }
9658 }
9759
9860 func _findInstrument( where predicate: ( Instrument ) -> Bool ) -> Instrument ? {
9961 self . lock. withReaderLock {
100- if let container = self . _instrument as? _InstrumentContainer {
101- return container. firstInstrument ( where: predicate)
102- } else if predicate ( self . _instrument) {
103- return self . _instrument
104- } else {
105- return nil
106- }
62+ InstrumentationSystem . firstInstrument ( in: self . _instrument, where: predicate)
10763 }
10864 }
10965 }
11066
11167 private static let shared = Storage ( )
11268
69+ /// Task-local instrument override set by ``withInstrument(_:_:)``.
70+ ///
71+ /// Resolved ahead of the bootstrapped instrument by ``instrument`` and ``_findInstrument(where:)`` for the
72+ /// duration of a scope. Internal storage — callers set it by calling ``withInstrument(_:_:)``.
73+ @TaskLocal
74+ @usableFromInline
75+ internal static var _taskLocalInstrument : ( any Instrument ) ?
76+
77+ /// Runs `operation` with `instrument` bound to the task-local override. Backs ``withInstrument(_:_:)``.
78+ @usableFromInline
79+ static func withTaskLocalInstrument< Result> (
80+ _ instrument: any Instrument ,
81+ operation: ( ) throws -> Result
82+ ) rethrows -> Result {
83+ try Self . $_taskLocalInstrument. withValue ( instrument, operation: operation)
84+ }
85+
86+ #if compiler(>=6.2)
87+ /// Async variant of ``withTaskLocalInstrument(_:operation:)``.
88+ @usableFromInline
89+ nonisolated ( nonsending) static func withTaskLocalInstrument< Result> (
90+ _ instrument: any Instrument ,
91+ operation: nonisolated( nonsending ) ( ) async throws -> Result
92+ ) async rethrows -> Result {
93+ try await Self . $_taskLocalInstrument. withValue ( instrument, operation: operation)
94+ }
95+ #else
96+ /// Async variant of ``withTaskLocalInstrument(_:operation:)``.
97+ @usableFromInline
98+ static func withTaskLocalInstrument< Result> (
99+ _ instrument: any Instrument ,
100+ isolation: isolated ( any Actor ) ? = #isolation,
101+ operation: ( ) async throws -> Result
102+ ) async rethrows -> Result {
103+ try await Self . $_taskLocalInstrument. withValue ( instrument, operation: operation)
104+ }
105+ #endif
106+
113107 /// Globally select the desired ``Instrument`` implementation.
114108 ///
115109 /// - Parameter instrument: The ``Instrument`` you want to share globally within your system.
@@ -126,38 +120,42 @@ public enum InstrumentationSystem {
126120 self . shared. bootstrapInternal ( instrument)
127121 }
128122
129- /// Ensures the system can enter a task-local scope, installing a ``TaskLocalInstrument`` over the
130- /// default ``NoOpInstrument`` if nothing has been bootstrapped. Backs ``withInstrument(_:_:)``.
131- ///
132- /// Crashes if a plain (non-scoping) instrument was bootstrapped. See
133- /// ``Storage/installTaskLocalInstrumentIfNeeded()`` for the full semantics.
134- @available ( macOS 10 . 15 , iOS 13 . 0 , tvOS 13 . 0 , watchOS 6 . 0 , * )
135- internal static func installTaskLocalInstrumentIfNeeded( ) {
136- self . shared. installTaskLocalInstrumentIfNeeded ( )
137- }
138-
139- /// Returns the currently bootstrapped ``Instrument``, or a ``NoOpInstrument`` if none was set.
123+ /// The currently active ``Instrument``.
140124 ///
141- /// When an override has been set via ``withInstrument(_:_:)``, it participates when the returned
142- /// instrument's methods are invoked — `inject` / `extract` and discovery (``tracer``,
143- /// ``_findInstrument(where:)``) resolve through it.
144- ///
145- /// > Warning: Do not pass this value to ``withInstrument(_:_:)`` (directly or inside a
146- /// > ``MultiplexInstrument``). It is task-local-backed, so re-installing it would resolve a scope that
147- /// > contains itself; `withInstrument` rejects it with a crash.
125+ /// This is the instrument bound by the innermost enclosing ``withInstrument(_:_:)`` scope, if any,
126+ /// otherwise the one set with ``bootstrap(_:)`` — and a ``NoOpInstrument`` if neither was set.
148127 public static var instrument : Instrument {
149- shared. instrument
128+ Self . _taskLocalInstrument ?? self . shared. instrument
150129 }
151130}
152131
153132@available ( macOS 10 . 15 , iOS 13 , tvOS 13 , watchOS 6 , * ) // for TaskLocal ServiceContext
154133extension InstrumentationSystem {
155134 /// INTERNAL API: Do Not Use
156135 ///
157- /// Walks the bootstrapped instrument for the first member matching `predicate`. Recurses into
158- /// `_InstrumentContainer` members, including ``MultiplexInstrument`` members and the task-local override
159- /// installed by ``withInstrument(_:_:)`` .
136+ /// Finds the first instrument matching `predicate` in the currently active instrument — the
137+ /// ``withInstrument(_:_:)`` override if one is in scope, otherwise the bootstrapped instrument. If the
138+ /// active instrument is a ``MultiplexInstrument``, its direct members are checked in order .
160139 public static func _findInstrument( where predicate: ( Instrument ) -> Bool ) -> Instrument ? {
161- self . shared. _findInstrument ( where: predicate)
140+ if let scoped = Self . _taskLocalInstrument {
141+ return Self . firstInstrument ( in: scoped, where: predicate)
142+ }
143+ return self . shared. _findInstrument ( where: predicate)
144+ }
145+
146+ /// Returns the first match for `predicate`: `instrument` itself, or — when it is a ``MultiplexInstrument``
147+ /// — its first direct member satisfying `predicate`. This is not recursive: a ``MultiplexInstrument``
148+ /// nested inside another is tested as a whole, not descended into.
149+ fileprivate static func firstInstrument(
150+ in instrument: Instrument ,
151+ where predicate: ( Instrument ) -> Bool
152+ ) -> Instrument ? {
153+ if let multiplex = instrument as? MultiplexInstrument {
154+ return multiplex. firstInstrument ( where: predicate)
155+ } else if predicate ( instrument) {
156+ return instrument
157+ } else {
158+ return nil
159+ }
162160 }
163161}
0 commit comments