Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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

],
dependencies: [
.package(url: "https://github.com/apple/swift-service-context.git", from: "1.1.0")
Expand Down Expand Up @@ -44,6 +45,18 @@ let package = Package(
.target(name: "Tracing")
]
),
.target(
name: "TracingTestKit",
dependencies: [
.target(name: "Tracing")
]
),
.testTarget(
name: "TracingTestKitTests",
dependencies: [
.target(name: "TracingTestKit")
]
),

// ==== --------------------------------------------------------------------------------------------------------
// MARK: Wasm Support
Expand Down
165 changes: 165 additions & 0 deletions Sources/TracingTestKit/TestSpan.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift Distributed Tracing open source project
//
// Copyright (c) 2025 Apple Inc. and the Swift Distributed Tracing project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of Swift Distributed Tracing project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

@_spi(Locking) import Instrumentation
import Tracing

public struct TestSpan: Span {
public let context: ServiceContext
public let spanContext: TestSpanContext
public let kind: SpanKind
public let startInstant: any TracerInstant
Comment on lines +18 to +22
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?


init(
operationName: String,
context: ServiceContext,
spanContext: TestSpanContext,
kind: SpanKind,
startInstant: any TracerInstant,
onEnd: @escaping @Sendable (FinishedTestSpan) -> Void
) {
self._operationName = LockedValueBox(operationName)
self.context = context
self.spanContext = spanContext
self.kind = kind
self.startInstant = startInstant
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

_isRecording.withValue { $0 }
}

public var operationName: String {
get {
_operationName.withValue { $0 }
}
nonmutating set {
assertIsRecording()
_operationName.withValue { $0 = newValue }
}
}

public var attributes: SpanAttributes {
get {
_attributes.withValue { $0 }
}
nonmutating set {
assertIsRecording()
_attributes.withValue { $0 = newValue }
}
}

public var events: [SpanEvent] {
_events.withValue { $0 }
}

public func addEvent(_ event: SpanEvent) {
assertIsRecording()
_events.withValue { $0.append(event) }
}

public var links: [SpanLink] {
_links.withValue { $0 }
}

public func addLink(_ link: SpanLink) {
assertIsRecording()
_links.withValue { $0.append(link) }
}

public var errors: [RecordedError] {
_errors.withValue { $0 }
}

public func recordError(
_ error: any Error,
attributes: SpanAttributes,
at instant: @autoclosure () -> some TracerInstant
) {
assertIsRecording()
_errors.withValue {
$0.append(RecordedError(error: error, attributes: attributes, instant: instant()))
}
}

public var status: SpanStatus? {
_status.withValue { $0 }
}

public func setStatus(_ status: SpanStatus) {
assertIsRecording()
_status.withValue { $0 = status }
}

public func end(at instant: @autoclosure () -> some TracerInstant) {
assertIsRecording()
let finishedSpan = FinishedTestSpan(
operationName: operationName,
context: context,
kind: kind,
spanContext: spanContext,
startInstant: startInstant,
endInstant: instant(),
attributes: attributes,
events: events,
links: links,
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.

onEnd(finishedSpan)
}

public struct RecordedError: Sendable {
public let error: Error
public let attributes: SpanAttributes
public let instant: any TracerInstant
}

private let _operationName: LockedValueBox<String>
private let _attributes = LockedValueBox<SpanAttributes>([:])
private let _events = LockedValueBox<[SpanEvent]>([])
private let _links = LockedValueBox<[SpanLink]>([])
private let _errors = LockedValueBox<[RecordedError]>([])
private let _status = LockedValueBox<SpanStatus?>(nil)
private let _isRecording = LockedValueBox<Bool>(true)
private let onEnd: @Sendable (FinishedTestSpan) -> Void

private func assertIsRecording(
file: StaticString = #file,
line: UInt = #line
) {
assert(
_isRecording.withValue { $0 } == true,
"Attempted to mutate already ended span.",
file: file,
line: line
)
}
}

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 :)

public let operationName: String
public let context: ServiceContext
public let kind: SpanKind
public let spanContext: TestSpanContext
public let startInstant: any TracerInstant
public let endInstant: any TracerInstant
public let attributes: SpanAttributes
public let events: [SpanEvent]
public let links: [SpanLink]
public let errors: [TestSpan.RecordedError]
public let status: SpanStatus?
}
42 changes: 42 additions & 0 deletions Sources/TracingTestKit/TestSpanContext.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift Distributed Tracing open source project
//
// Copyright (c) 2025 Apple Inc. and the Swift Distributed Tracing project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of Swift Distributed Tracing project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

import ServiceContextModule

public struct TestSpanContext: Sendable, Hashable {
public let traceID: String
public let spanID: String
public let parentSpanID: String?

public init(traceID: String, spanID: String, parentSpanID: String?) {
self.traceID = traceID
self.spanID = spanID
self.parentSpanID = parentSpanID
}
}

extension ServiceContext {
var testSpanContext: TestSpanContext? {
get {
self[TestSpanContextKey.self]
}
set {
self[TestSpanContextKey.self] = newValue
}
}
}

private struct TestSpanContextKey: ServiceContextKey {
typealias Value = TestSpanContext
}
Loading