Skip to content

Conversation

slashmo
Copy link
Collaborator

@slashmo slashmo commented Aug 30, 2025

Motivation

Library authors and end users integrating Swift Distributed Tracing need some form of test tracer implementation that they can use in unit tests. Up until now, Swift Distributed Tracing didn't provide a built-in solution to this problem, leading to TestTracer being redefined in many libraries and presumably some end-user applications. Most of these implementations share the common need of inspecting spans and context propagation to verify their code behaves as expected so they end up being extremely similar or straight up duplicates.

Therefore, I'd like to create a first-party TestTracer in a new TracingTestKit library. The name TracingTestKit aligns with MetricsTestKit from swift-metrics.

Modifications

  • Adds TracingTestKit library
  • Adds TestTracer to TracingTestKit to allow inspection of spans in unit tests

Remaining tasks

  • Add documentation to TracingTestKit

Result

Library users can now extensively test the parts of their code which make use of Swift Distributed Tracing without needing to build their own TestTracer type. Instead, they can now depend on TracingTestKit.

Future Direction

Using TestTracer in TracingTests

To keep this pull request focused on providing a fully unit-tested TestTracer implementation I opted not to use the new TestTracer in the existing TracingTests. IMHO, this can come in a follow-up PR.

Task-local TestTracer

@adam-fowler built a great task-local tracer for Hummingbird which allows parallel test execution while still using the global InstrumentationSystem: https://github.com/hummingbird-project/hummingbird/blob/3ae359b1bb1e72378ed43b59fdcd4d44cac5d7a4/Tests/HummingbirdTests/TestTracer.swift#L184
Now that we'd have a built-in TestTracer it'd be great to also add this to TracingTestKit.

@slashmo slashmo added the 🆕 semver/minor Adds new public API. label Aug 30, 2025
@slashmo slashmo force-pushed the feature/tracing-test-kit branch from 17f4c8d to 7df3df8 Compare August 30, 2025 13:29
Copy link
Member

@FranzBusch FranzBusch left a comment

Choose a reason for hiding this comment

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

Great idea! Overall can we add public API docs and some docs into our DocC catalog for this please?

Comment on lines +18 to +22
public struct TestSpan: Span {
public let context: ServiceContext
public let spanContext: TestSpanContext
public let kind: SpanKind
public let startInstant: any TracerInstant
Copy link
Member

Choose a reason for hiding this comment

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

Can we add some doc comments to this? Also can all of these properties be vars instead?

self.onEnd = onEnd
}

public var isRecording: Bool {
Copy link
Member

Choose a reason for hiding this comment

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

Same here. Can we add doc comments to all of those please?

Copy link
Member

Choose a reason for hiding this comment

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

These have docs on Span already so not sure if that's necessary here

Copy link
Contributor

@czechboy0 czechboy0 Sep 5, 2025

Choose a reason for hiding this comment

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

Then we could add // swift-format-ignore: AllPublicDeclarationsHaveDocumentation and enable AllPublicDeclarationsHaveDocumentation in the swift-format config file. I'd also like to see all public APIs having comments or opt out this way if they inherit them.

That said - probably worth doing in a separate PR.

Filed #182

}
}

public struct FinishedTestSpan: Sendable {
Copy link
Member

Choose a reason for hiding this comment

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

Can we move this into a new file, add doc comments and make all the properties vars instead?

Copy link
Member

Choose a reason for hiding this comment

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

Not sure this needs new file but +1 on the var, there's no reason not to :)

@_spi(Locking) import Instrumentation
import Testing
import Tracing
@testable import TracingTestKit
Copy link
Member

Choose a reason for hiding this comment

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

Do we need a testable import here?

Copy link
Member

Choose a reason for hiding this comment

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

Is it possible to add a test that uses the global withSpan APIs and the test tracer?

@slashmo
Copy link
Collaborator Author

slashmo commented Sep 1, 2025

Overall can we add public API docs and some docs into our DocC catalog for this please?

Thanks for the review. Yes, I'll add docs next but wanted to get some general feedback on the API first.

self.onEnd = onEnd
}

public var isRecording: Bool {
Copy link
Member

Choose a reason for hiding this comment

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

These have docs on Span already so not sure if that's necessary here

}
}

public struct FinishedTestSpan: Sendable {
Copy link
Member

Choose a reason for hiding this comment

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

Not sure this needs new file but +1 on the var, there's no reason not to :)

@_spi(Locking) import Instrumentation
import Tracing

public struct TestTracer: Tracer {
Copy link
Member

Choose a reason for hiding this comment

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

The implementation is fine, but we should call it some InMemoryStorageTracer, and rename the module as well perhaps. This isn't strictly required for tests but just accumulates in memory; you could imagine some silly use-cases that would want to use this and I don't necessarily want to taint them with the Test... word, WDYT?

errors: errors,
status: status
)
_isRecording.withValue { $0 = false }
Copy link
Member

Choose a reason for hiding this comment

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

This is racy with the assertIsRecording; you have to check + swap to false at the same time as asserting, otherwise you can get two tasks end()-ing they'll both pass the assert and then both try to set -- but we would have double-ended.

@@ -6,6 +6,7 @@ let package = Package(
products: [
.library(name: "Instrumentation", targets: ["Instrumentation"]),
.library(name: "Tracing", targets: ["Tracing"]),
.library(name: "TracingTestKit", targets: ["TracingTestKit"]),
Copy link
Member

Choose a reason for hiding this comment

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

I think we should call this InMemoryTracing maybe, and not have it be strictly called "just for testing" there may be various use cases to use this

@ktoso
Copy link
Member

ktoso commented Sep 10, 2025

We discussed this one a bit more -- just pending a rename and the cleanups and happy to merge afterwards!

@ktoso
Copy link
Member

ktoso commented Sep 11, 2025

I took a stab at pushing this over the finish line: #183

Gonna chat with @slashmo if he'll pick those over here or how to proceed

@ktoso ktoso closed this Sep 11, 2025
ktoso added a commit that referenced this pull request Sep 11, 2025
@slashmo slashmo deleted the feature/tracing-test-kit branch September 11, 2025 12:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
🆕 semver/minor Adds new public API.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants