[SDT-0001]: task-local instrument proposal#231
Conversation
slashmo
left a comment
There was a problem hiding this comment.
This sound like a great addition to me 👍 Perhaps another future direction could be a test trait that encapsulates the task-local tracer so we don't have to pay the indentation cost in every test function. Is this something you already considered, either for Tracing or Metrics/Logging?
|
@slashmo yes, this seems to be a reasonable future direction. Similar in spirit to https://forums.swift.org/t/pitch-add-a-tasklocal-trait-to-swift-testing/87603 |
|
How does one get the task-local tracer, then? Do they have to dynamically cast the instrument? What should they do if the Instrument fails to cast to Tracer? |
|
|
I see, the language in the proposal seemed to imply that this is entirely opt-in, not that the existing API starts checking for the task local. Don't get me wrong, I think this is the right solution, just explaining where my confusion came from. Looks good! |
|
@czechboy0 opt-in in a sense |
|
Thanks for making me better understand the proposal now - unfortunately, I now don't think it's the right approach.
I think it's great you've explored this approach, but I believe we need to stay better aligned with Log/Metrics and not provide a worse experience here. If much of the motivation was driven out of avoiding a task local lookup when acquiring a Tracer, I think it's the wrong thing to optimize and instead we should optimize the long term shape of this API and make it easy to adopt and reason about. |
|
@czechboy0 Libraries can call Context stays with |
Right, but why? In Log/Metrics, you can use the with* functions in libraries just fine, and they don't interact with the process bootstrap'd backend. Why did we diverge here?
In Log/Metrics, there is, and I think it's a crucial feature for gradual migration. What might help - could you add more examples to the proposal about how to cross manual propagation and task local propagation, in both directions, in a library? Maybe I'm missing something 🙂 |
|
@czechboy0 let's verify if I understand what kind of example you have in mind before adding to the proposal. Does this work? // Application
// sets INSTRUMENT task-local
try await withInstrument(OTelTracer(configuration: config)) {
let client = TracedClient(transport: transport)
let server = TracedServer { request in
try await client.send(HTTPRequest(method: .get, path: "/users"))
}
try await transport.serve(server.serve)
}
// Inbound library
public struct TracedServer {
let handler: (HTTPRequest) async throws -> HTTPResponse
public func serve(_ request: HTTPRequest) async throws -> HTTPResponse {
var context = ServiceContext.topLevel
// reads INSTRUMENT task-local
InstrumentationSystem.instrument.extract(request.headers, into: &context, using: HTTPHeadersExtractor())
// sets CONTEXT task-local (manual → task-local)
return try await ServiceContext.withValue(context) {
try await withSpan("\(request.method) \(request.path)", ofKind: .server) { _ in
try await self.handler(request)
}
}
}
}
// Outbound library
public struct TracedClient {
let transport: any HTTPTransport
public func send(_ request: HTTPRequest) async throws -> HTTPResponse {
try await withSpan("\(request.method) \(request.path)", ofKind: .client) { span in
var request = request
// reads INSTRUMENT task-local
InstrumentationSystem.instrument.inject(
// reads CONTEXT task-local (task-local → manual)
span.context,
into: &request.headers,
using: HTTPHeadersInjector()
)
return try await self.transport.send(request)
}
}
} |
|
Not quite, I'm thinking a library that gets a Tracer passed explicitly, and calls out to another library in an async func, so needs to put that Tracer into a task local. And another example for the inverse, where the library has an async public method that reads out the task-local tracer, and passes it explicitly to code that isn't async. And importantly - this behavior must work reliably for the library, it can't depend on the exact backend that the application bootstrapped. The idea of hiding the task local in the backend was considered for Log/Metrics and ultimately rejected, so I'm not sure why we brought it back for tracing. The flaws are still there - the libraries suddenly can't reliably pass around telemetry handles unless the application bootstraps a specific backend, which causes undesirable coupling. The library should be able to write a unit test for its own passing around of telemetry handles and that correctness shouldn't depend on the application. |
Right, yes. Application (A) -> lib (B) with explicit
|
|
I understand the implications of the current design. My point is that I don't think it's the right design for the various reasons I outlined. Mainly because it's completely diverging from Swift Log/Metrics for reasons I don't understand. My question: why don't we do the same for Instrument/Tracer that we did for Logger and MetricsFactory? |
|
@czechboy0 I think this design is more aligned with the existing metrics and log design. Let me explain. Metrics read But we do not prevent ourselves from changing it later either. The But happy to get @ktoso in the discussion to hear his opinion here as well. I think we've been keeping this discussion in the PR rather than in the forums, I wonder if others missed it. |
|
Thanks for more details, I still think this optimizes for the wrong thing, and sacrifices usability and how we want folks to use this API long term. If perf is a huge issue, I'd much prefer we simply compile everything out like in Swift Log as an opt-in feature. But IMO the default should be high-convenience-fine-performance, and opt in should be top performance with reduced convenience. That's what we did for Swift Log traits, and I think it's a good general rule of thumb. Right now this proposal puts performance over everything else, and I don't think it's the right tradeoff. |
|
I'm also still not sure how to, in a library, put an explicitly provided tracer into the task local. Something that we need to support for sure, and is easy to do in Log/Metrics. |
@czechboy0 Now I am going to ask for examples :) Where do you see API usability sacrifice? If we go with the "always-checked-task-local-slot", the |
Case (3) sounds bad and it is a diversion from "task-local-overrides any global bootstrap" used by metrics/log, but it is not incorrect. If application decided to |
|
I continue to think that the inability to reliably use, and be able to reason about, At point A:
How do we achieve the same for tracing? Something that works reliably in the library regardless of what the app did, the library can locally reason about? The current withInstrument isn't it, because libraries can't actually adopt it unless they want to become the source of crashes, and holding this right requires coupling between libraries and apps. We don't have this problem for Log/Metrics, not sure why we'd create it here. What ergonomics we're missing (mostly from my first post)
I have ideas on how to achieve all these while making Tracing behave like Log/Metrics, but don't want to hijack this thread by proposing an alternative approach 🙂 |
|
More broadly - I think this proposal needs to be considered in the context of the accepted and implemented task locals for Log/Metrics. So much so that the proposal here should only diverge from that approach if there's a very good reason and won't hurt library and application owners who implement all three telemetry signals. Right now, this proposal seems to optimize for avoiding a task local lookup in withSpan, and works around that constraint. I don't think that's the right approach, however, and we should optimize for the ease of use of majority of users (for whom a task local lookup per span makes no measurable difference), and separately discuss other ways to make telemetry free for those who don't need it (like we did with traits in Log). |
|
Honza, you're continuing this discussion in the wrong place. Please move over to the review thread: https://forums.swift.org/t/proposal-sdt-0001-task-local-instrument/87826/4 Review threads are review threads because their point is to get involvement from the community in these discussions, and the PR discussion is likely completely missed out by folks interested in it. |
|
Apologies. Will use the forums thread next time, on our other repos we welcome discussion both on the forums and the proposal PR, I didn't realize that wasn't the case in this repo. Though - seems all the technical discussion happened here, not sure how to "move" a long back-and-forth now, or whether it's worth it. Vlad linked to this PR from there so hopefully nobody will miss it. |
This proposal adds a withInstrument(::) for task-local instrument scoping.
Motivation:
InstrumentationSystem.bootstrap(_:)installs an instrument once per process, so everywithSpan/startSpanand every read ofInstrumentationSystem.instrumentresolves through that single instrument. That fits one tracer for the whole application, but it leaves no way to bind a different instrument for a region of work — most acutely in tests, where a secondbootstrapcrashes and parallel tests cannot each install their own in-memory tracer.Modifications:
SDT-0001 proposal is added.
Result:
SDT-0001 proposal is ready for review.