Skip to content

Commit 751f0b2

Browse files
authored
remove global observability scope (#3777)
motivation: better fit with long running process likes IDEs changes: * remove the global bootstrap and scope * add observabilityScope argument to functions that need to emit diagnostics * adjust call sites and tests
1 parent 263ad3a commit 751f0b2

File tree

53 files changed

+1471
-887
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+1471
-887
lines changed

Examples/package-info/Sources/package-info/main.swift

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import Basics
12
import TSCBasic
23
import Workspace
34

@@ -13,22 +14,16 @@ let packagePath = AbsolutePath(#file).parentDirectory.parentDirectory.parentDire
1314

1415
// There are several levels of information available.
1516
// Each takes longer to load than the level above it, but provides more detail.
16-
let diagnostics = DiagnosticsEngine(handlers: [{ print($0)}])
17+
18+
let observability = ObservabilitySystem({ print("\($0): \($1)") })
19+
1720
let workspace = try Workspace(forRootPackage: packagePath)
18-
let manifest = try tsc_await { workspace.loadRootManifest(at: packagePath, diagnostics: diagnostics, completion: $0) }
19-
guard !diagnostics.hasErrors else {
20-
fatalError("error loading manifest: \(diagnostics)")
21-
}
22-
23-
let package = try tsc_await { workspace.loadRootPackage(at: packagePath, diagnostics: diagnostics, completion: $0) }
24-
guard !diagnostics.hasErrors else {
25-
fatalError("error loading package: \(diagnostics)")
26-
}
27-
28-
let graph = try workspace.loadPackageGraph(rootPath: packagePath, diagnostics: diagnostics)
29-
guard !diagnostics.hasErrors else {
30-
fatalError("error loading graph: \(diagnostics)")
31-
}
21+
22+
let manifest = try tsc_await { workspace.loadRootManifest(at: packagePath, observabilityScope: observability.topScope, completion: $0) }
23+
24+
let package = try tsc_await { workspace.loadRootPackage(at: packagePath, observabilityScope: observability.topScope, completion: $0) }
25+
26+
let graph = try workspace.loadPackageGraph(rootPath: packagePath, observabilityScope: observability.topScope)
3227

3328
// EXAMPLES
3429
// ========

Sources/Basics/HTPClient+URLSession.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ public struct URLSessionHTTPClient: HTTPClientProtocol {
2727
}
2828

2929
public func execute(_ request: HTTPClient.Request, progress: ProgressHandler?, completion: @escaping CompletionHandler) {
30+
self.execute(request, observabilityScope: nil, progress: progress, completion: completion)
31+
}
32+
33+
public func execute(_ request: HTTPClient.Request, observabilityScope: ObservabilityScope? = nil, progress: ProgressHandler?, completion: @escaping CompletionHandler) {
3034
switch request.kind {
3135
case .generic:
3236
let task = self.dataTasksManager.makeTask(request: request, progress: progress, completion: completion)

Sources/Basics/HTTPClient.swift

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,11 @@ public protocol HTTPClientProtocol {
3333
///
3434
/// - Parameters:
3535
/// - request: The `HTTPClientRequest` to perform.
36+
/// - observabilityScope: the observability scope to emit diagnostics on
37+
/// - progress: A closure to handle progress for example for downloads
3638
/// - callback: A closure to be notified of the completion of the request.
3739
func execute(_ request: HTTPClientRequest,
40+
observabilityScope: ObservabilityScope?,
3841
progress: ProgressHandler?,
3942
completion: @escaping CompletionHandler)
4043
}
@@ -56,21 +59,19 @@ public struct HTTPClient: HTTPClientProtocol {
5659
public typealias Handler = (Request, ProgressHandler?, @escaping (Result<Response, Error>) -> Void) -> Void
5760

5861
public var configuration: HTTPClientConfiguration
59-
private let observabilityScope: ObservabilityScope
6062
private let underlying: Handler
6163

6264
// static to share across instances of the http client
6365
private static var hostsErrorsLock = Lock()
6466
private static var hostsErrors = [String: [Date]]()
6567

66-
public init(configuration: HTTPClientConfiguration = .init(), handler: Handler? = nil, observabilityScope: ObservabilityScope = ObservabilitySystem.topScope) {
68+
public init(configuration: HTTPClientConfiguration = .init(), handler: Handler? = nil) {
6769
self.configuration = configuration
68-
self.observabilityScope = observabilityScope
6970
// FIXME: inject platform specific implementation here
7071
self.underlying = handler ?? URLSessionHTTPClient().execute
7172
}
7273

73-
public func execute(_ request: Request, progress: ProgressHandler? = nil, completion: @escaping CompletionHandler) {
74+
public func execute(_ request: Request, observabilityScope: ObservabilityScope? = nil, progress: ProgressHandler? = nil, completion: @escaping CompletionHandler) {
7475
// merge configuration
7576
var request = request
7677
if request.options.callbackQueue == nil {
@@ -103,7 +104,9 @@ public struct HTTPClient: HTTPClientProtocol {
103104
// execute
104105
let callbackQueue = request.options.callbackQueue ?? self.configuration.callbackQueue
105106
self._execute(
106-
request: request, requestNumber: 0,
107+
request: request,
108+
requestNumber: 0,
109+
observabilityScope: observabilityScope,
107110
progress: progress.map { handler in
108111
{ received, expected in
109112
callbackQueue.async {
@@ -119,9 +122,9 @@ public struct HTTPClient: HTTPClientProtocol {
119122
)
120123
}
121124

122-
private func _execute(request: Request, requestNumber: Int, progress: ProgressHandler?, completion: @escaping CompletionHandler) {
125+
private func _execute(request: Request, requestNumber: Int, observabilityScope: ObservabilityScope?, progress: ProgressHandler?, completion: @escaping CompletionHandler) {
123126
if self.shouldCircuitBreak(request: request) {
124-
self.observabilityScope.emit(warning: "Circuit breaker triggered for \(request.url)")
127+
observabilityScope?.emit(warning: "Circuit breaker triggered for \(request.url)")
125128
return completion(.failure(HTTPClientError.circuitBreakerTriggered))
126129
}
127130

@@ -145,10 +148,10 @@ public struct HTTPClient: HTTPClientProtocol {
145148
self.recordErrorIfNecessary(response: response, request: request)
146149
// handle retry strategy
147150
if let retryDelay = self.shouldRetry(response: response, request: request, requestNumber: requestNumber) {
148-
self.observabilityScope.emit(warning: "\(request.url) failed, retrying in \(retryDelay)")
151+
observabilityScope?.emit(warning: "\(request.url) failed, retrying in \(retryDelay)")
149152
// TODO: dedicated retry queue?
150153
return self.configuration.callbackQueue.asyncAfter(deadline: .now() + retryDelay) {
151-
self._execute(request: request, requestNumber: requestNumber + 1, progress: progress, completion: completion)
154+
self._execute(request: request, requestNumber: requestNumber + 1, observabilityScope: observabilityScope, progress: progress, completion: completion)
152155
}
153156
}
154157
// check for valid response codes
@@ -220,24 +223,24 @@ public struct HTTPClient: HTTPClientProtocol {
220223
}
221224

222225
public extension HTTPClient {
223-
func head(_ url: URL, headers: HTTPClientHeaders = .init(), options: Request.Options = .init(), completion: @escaping (Result<Response, Error>) -> Void) {
224-
self.execute(Request(method: .head, url: url, headers: headers, body: nil, options: options), completion: completion)
226+
func head(_ url: URL, headers: HTTPClientHeaders = .init(), options: Request.Options = .init(), observabilityScope: ObservabilityScope? = .none, completion: @escaping (Result<Response, Error>) -> Void) {
227+
self.execute(Request(method: .head, url: url, headers: headers, body: nil, options: options), observabilityScope: observabilityScope, completion: completion)
225228
}
226229

227-
func get(_ url: URL, headers: HTTPClientHeaders = .init(), options: Request.Options = .init(), completion: @escaping (Result<Response, Error>) -> Void) {
228-
self.execute(Request(method: .get, url: url, headers: headers, body: nil, options: options), completion: completion)
230+
func get(_ url: URL, headers: HTTPClientHeaders = .init(), options: Request.Options = .init(), observabilityScope: ObservabilityScope? = .none, completion: @escaping (Result<Response, Error>) -> Void) {
231+
self.execute(Request(method: .get, url: url, headers: headers, body: nil, options: options), observabilityScope: observabilityScope, completion: completion)
229232
}
230233

231-
func put(_ url: URL, body: Data?, headers: HTTPClientHeaders = .init(), options: Request.Options = .init(), completion: @escaping (Result<Response, Error>) -> Void) {
232-
self.execute(Request(method: .put, url: url, headers: headers, body: body, options: options), completion: completion)
234+
func put(_ url: URL, body: Data?, headers: HTTPClientHeaders = .init(), options: Request.Options = .init(), observabilityScope: ObservabilityScope? = .none, completion: @escaping (Result<Response, Error>) -> Void) {
235+
self.execute(Request(method: .put, url: url, headers: headers, body: body, options: options), observabilityScope: observabilityScope, completion: completion)
233236
}
234237

235-
func post(_ url: URL, body: Data?, headers: HTTPClientHeaders = .init(), options: Request.Options = .init(), completion: @escaping (Result<Response, Error>) -> Void) {
236-
self.execute(Request(method: .post, url: url, headers: headers, body: body, options: options), completion: completion)
238+
func post(_ url: URL, body: Data?, headers: HTTPClientHeaders = .init(), options: Request.Options = .init(), observabilityScope: ObservabilityScope? = .none, completion: @escaping (Result<Response, Error>) -> Void) {
239+
self.execute(Request(method: .post, url: url, headers: headers, body: body, options: options), observabilityScope: observabilityScope, completion: completion)
237240
}
238241

239-
func delete(_ url: URL, headers: HTTPClientHeaders = .init(), options: Request.Options = .init(), completion: @escaping (Result<Response, Error>) -> Void) {
240-
self.execute(Request(method: .delete, url: url, headers: headers, body: nil, options: options), completion: completion)
242+
func delete(_ url: URL, headers: HTTPClientHeaders = .init(), options: Request.Options = .init(), observabilityScope: ObservabilityScope? = .none, completion: @escaping (Result<Response, Error>) -> Void) {
243+
self.execute(Request(method: .delete, url: url, headers: headers, body: nil, options: options), observabilityScope: observabilityScope, completion: completion)
241244
}
242245
}
243246

Sources/Basics/Observability.swift

Lines changed: 98 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -20,59 +20,46 @@ typealias TSCDiagnostic = TSCBasic.Diagnostic
2020
// designed after https://github.com/apple/swift-metrics
2121
// designed after https://github.com/apple/swift-distributed-tracing-baggage
2222
public class ObservabilitySystem {
23-
// global
24-
25-
private static var _global = ObservabilitySystem(factory: NOOPFactory())
26-
private static var bootstrapped = false
27-
private static let lock = Lock()
28-
29-
// as we transition to async/await we can take advantage of Task Local values instead of a global
30-
public static func bootstrapGlobal(factory: ObservabilityFactory) {
31-
Self.lock.withLock {
32-
// FIXME: disabled for testing
33-
precondition(!Self.bootstrapped, "ObservabilitySystem may only bootstrapped once")
34-
Self.bootstrapped = true
35-
}
36-
Self._global = .init(factory: factory)
37-
}
38-
39-
/// DO NOT USE, only for testing. friend visibility would be soooo nice here
40-
public static func _bootstrapGlobalForTestingOnlySeriously(factory: ObservabilityFactory) {
41-
Self._global = .init(factory: factory)
42-
}
43-
44-
public static var topScope: ObservabilityScope {
45-
self._global.topScope
46-
}
47-
48-
// instance
49-
5023
public let topScope: ObservabilityScope
5124

52-
public init(factory: ObservabilityFactory) {
25+
/// Create an ObservabilitySystem with a handler provider providing handler such as a collector.
26+
public init(_ handlerProvider: ObservabilityHandlerProvider) {
5327
self.topScope = .init(
5428
description: "top scope",
5529
parent: .none,
5630
metadata: .none,
57-
diagnosticsHandler: factory.diagnosticsHandler
31+
diagnosticsHandler: handlerProvider.diagnosticsHandler
5832
)
5933
}
6034

61-
private struct NOOPFactory: ObservabilityFactory, DiagnosticsHandler {
35+
/// Create an ObservabilitySystem with a single diagnostics handler.
36+
public convenience init(_ handler: @escaping (ObservabilityScope, Diagnostic) -> Void) {
37+
self.init(SingleDiagnosticsHandler(handler))
38+
}
39+
40+
private struct SingleDiagnosticsHandler: ObservabilityHandlerProvider, DiagnosticsHandler {
6241
var diagnosticsHandler: DiagnosticsHandler { self }
6342

64-
func handleDiagnostic(scope: ObservabilityScope, diagnostic: Diagnostic) {}
43+
let underlying: (ObservabilityScope, Diagnostic) -> Void
44+
45+
init(_ underlying: @escaping (ObservabilityScope, Diagnostic) -> Void) {
46+
self.underlying = underlying
47+
}
48+
49+
func handleDiagnostic(scope: ObservabilityScope, diagnostic: Diagnostic) {
50+
self.underlying(scope, diagnostic)
51+
}
6552
}
6653
}
6754

68-
public protocol ObservabilityFactory {
55+
public protocol ObservabilityHandlerProvider {
6956
var diagnosticsHandler: DiagnosticsHandler { get }
7057
}
7158

7259
// MARK: - ObservabilityScope
7360

74-
public final class ObservabilityScope: DiagnosticsEmitterProtocol {
75-
private let description: String
61+
public final class ObservabilityScope: DiagnosticsEmitterProtocol, CustomStringConvertible {
62+
public let description: String
7663
private let parent: ObservabilityScope?
7764
private let metadata: ObservabilityMetadata?
7865

@@ -115,18 +102,21 @@ public final class ObservabilityScope: DiagnosticsEmitterProtocol {
115102
self.makeDiagnosticsEmitter(metadata: metadataProvider())
116103
}
117104

118-
// FIXME: compatibility with DiagnosticsEngine, remove when transition is complete
119-
//@available(*, deprecated, message: "temporary for transition DiagnosticsEngine -> DiagnosticsEmitter")
120-
public func makeDiagnosticsEngine() -> DiagnosticsEngine {
121-
return .init(handlers: [{ Diagnostic($0).map{ self.diagnosticsHandler.handleDiagnostic(scope: self, diagnostic: $0) } }])
122-
}
123-
124105
// FIXME: we want to remove this functionality and move to more conventional error handling
125106
//@available(*, deprecated, message: "this pattern is deprecated, transition to error handling instead")
126107
public var errorsReported: Bool {
127108
self.diagnosticsHandler.errorsReported
128109
}
129110

111+
// FIXME: we want to remove this functionality and move to more conventional error handling
112+
//@available(*, deprecated, message: "this pattern is deprecated, transition to error handling instead")
113+
public var errorsReportedInAnyScope: Bool {
114+
if self.errorsReported {
115+
return true
116+
}
117+
return parent?.errorsReportedInAnyScope ?? false
118+
}
119+
130120
// DiagnosticsEmitterProtocol
131121
public func emit(_ diagnostic: Diagnostic) {
132122
var diagnostic = diagnostic
@@ -228,6 +218,9 @@ extension DiagnosticsEmitterProtocol {
228218
public func trap<T>(_ closure: () throws -> T) -> T? {
229219
do {
230220
return try closure()
221+
} catch Diagnostics.fatalError {
222+
// FIXME: (diagnostics) deprecate this with Diagnostics.fatalError
223+
return nil
231224
} catch {
232225
self.emit(error)
233226
return nil
@@ -457,6 +450,13 @@ extension ObservabilityMetadata.AnyKey: Hashable {
457450

458451
// MARK: - Compatibility with TSC Diagnostics APIs
459452

453+
//@available(*, deprecated, message: "temporary for transition DiagnosticsEngine -> DiagnosticsEmitter")
454+
extension ObservabilityScope {
455+
public func makeDiagnosticsEngine() -> DiagnosticsEngine {
456+
return .init(handlers: [{ Diagnostic($0).map{ self.diagnosticsHandler.handleDiagnostic(scope: self, diagnostic: $0) } }])
457+
}
458+
}
459+
460460
//@available(*, deprecated, message: "temporary for transition DiagnosticsEngine -> DiagnosticsEmitter")
461461
extension Diagnostic {
462462
init?(_ diagnostic: TSCDiagnostic) {
@@ -483,6 +483,46 @@ extension Diagnostic {
483483
}
484484
}
485485

486+
@available(*, deprecated, message: "temporary for transition DiagnosticsEngine -> DiagnosticsEmitter")
487+
extension ObservabilitySystem {
488+
public convenience init(diagnosticEngine: DiagnosticsEngine) {
489+
self.init(DiagnosticsEngineAdapter(diagnosticEngine: diagnosticEngine))
490+
}
491+
492+
private struct DiagnosticsEngineAdapter: ObservabilityHandlerProvider, DiagnosticsHandler {
493+
let diagnosticEngine: DiagnosticsEngine
494+
495+
var diagnosticsHandler: DiagnosticsHandler { self }
496+
497+
func handleDiagnostic(scope: ObservabilityScope, diagnostic: Diagnostic) {
498+
diagnosticEngine.emit(.init(diagnostic))
499+
}
500+
}
501+
}
502+
503+
@available(*, deprecated, message: "temporary for transition DiagnosticsEngine -> DiagnosticsEmitter")
504+
extension TSCDiagnostic {
505+
init(_ diagnostic: Diagnostic) {
506+
let location: DiagnosticLocation
507+
if let metadata = diagnostic.metadata {
508+
location = metadata.legacyDiagnosticLocation
509+
} else {
510+
location = UnknownLocation.location
511+
}
512+
513+
switch diagnostic.severity {
514+
case .error:
515+
self = .init(message: .error(diagnostic.message), location: location)
516+
case .warning:
517+
self = .init(message: .warning(diagnostic.message), location: location)
518+
case .info:
519+
self = .init(message: .note(diagnostic.message), location: location)
520+
case .debug:
521+
self = .init(message: .note(diagnostic.message), location: location)
522+
}
523+
}
524+
}
525+
486526
//@available(*, deprecated, message: "temporary for transition DiagnosticsEngine -> DiagnosticsEmitter")
487527
extension ObservabilityMetadata {
488528
public var legacyLocation: String? {
@@ -498,3 +538,21 @@ extension ObservabilityMetadata {
498538
typealias Value = String
499539
}
500540
}
541+
542+
@available(*, deprecated, message: "temporary for transition DiagnosticsEngine -> DiagnosticsEmitter")
543+
extension ObservabilityMetadata {
544+
fileprivate var legacyDiagnosticLocation: DiagnosticLocation {
545+
if let legacyLocation = self.legacyLocation {
546+
return DiagnosticLocationWrapper(legacyLocation)
547+
}
548+
return UnknownLocation.location
549+
}
550+
551+
private struct DiagnosticLocationWrapper: DiagnosticLocation {
552+
var description: String
553+
554+
init (_ location: String) {
555+
self.description = location
556+
}
557+
}
558+
}

0 commit comments

Comments
 (0)