Skip to content
Open
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
86 changes: 26 additions & 60 deletions Datadog/IntegrationUnitTests/Trace/HeadBasedSamplingTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,11 @@ class HeadBasedSamplingTests: XCTestCase {
parent.finish()

let spans = core.waitAndReturnSpanEvents()
XCTAssertEqual(spans.count, 3, "It must send all spans")

let allKept = spans.filter({ $0.isKept }).count == 3
let allDropped = spans.filter({ !$0.isKept }).count == 3
XCTAssertTrue(allKept || allDropped, "All spans must be either kept or dropped")
guard spans.isEmpty else {
XCTAssertEqual(spans.filter({ $0.isKept }).count, 3, "All spans must be either kept or dropped")
return
}
}

func testSamplingLocalTraceWithImplicitParent() throws {
Expand All @@ -82,11 +82,11 @@ class HeadBasedSamplingTests: XCTestCase {
parent.finish()

let spans = core.waitAndReturnSpanEvents()
XCTAssertEqual(spans.count, 3, "It must send all spans")

let allKept = spans.filter({ $0.isKept }).count == 3
let allDropped = spans.filter({ !$0.isKept }).count == 3
XCTAssertTrue(allKept || allDropped, "All spans must be either kept or dropped")
guard spans.isEmpty else {
XCTAssertEqual(spans.filter({ $0.isKept }).count, 3, "All spans must be either kept or dropped")
return
}
}

// MARK: - Distributed Tracing (through network instrumentation API)
Expand Down Expand Up @@ -154,18 +154,10 @@ class HeadBasedSamplingTests: XCTestCase {
let request = try sendURLSessionRequest(to: "https://foo.com/request", using: InstrumentedSessionDelegate())

// Then
let span = try XCTUnwrap(core.waitAndReturnSpanEvents().first, "It should send span event")
XCTAssertEqual(span.operationName, "urlsession.request")
XCTAssertEqual(span.samplingRate, 0, "Span must use distributed trace sample rate")
XCTAssertFalse(span.isKept, "Span must be dropped")

// Then
let expectedTraceIDField = span.traceID.toString(representation: .decimal)
let expectedSpanIDField = span.spanID.toString(representation: .decimal)
let expectedTagsField = "_dd.p.tid=\(span.traceID.idHiHex)"
XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.traceIDField), expectedTraceIDField)
XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.parentSpanIDField), expectedSpanIDField)
XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.tagsField), expectedTagsField)
XCTAssertNil(core.waitAndReturnSpanEvents().first, "It should not send span event")
XCTAssertNotNil(request.value(forHTTPHeaderField: TracingHTTPHeaders.traceIDField))
XCTAssertNotNil(request.value(forHTTPHeaderField: TracingHTTPHeaders.parentSpanIDField))
XCTAssertNotNil(request.value(forHTTPHeaderField: TracingHTTPHeaders.tagsField))
XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.samplingPriorityField), "0")
}

Expand Down Expand Up @@ -245,23 +237,13 @@ class HeadBasedSamplingTests: XCTestCase {

// Then
let spanEvents = core.waitAndReturnSpanEvents()
let activeSpan = try XCTUnwrap(spanEvents.first(where: { $0.operationName == "active.span" }))
let urlsessionSpan = try XCTUnwrap(spanEvents.first(where: { $0.operationName == "urlsession.request" }))

XCTAssertEqual(activeSpan.samplingRate, 0, "Span must use local trace sample rate")
XCTAssertFalse(activeSpan.isKept, "Span must not be sampled")
XCTAssertEqual(urlsessionSpan.samplingRate, 0, "Span must use local trace sample rate")
XCTAssertFalse(urlsessionSpan.isKept, "Span must not be sampled")
XCTAssertEqual(urlsessionSpan.traceID, activeSpan.traceID)
XCTAssertEqual(urlsessionSpan.parentID, activeSpan.spanID)
XCTAssertNil(spanEvents.first(where: { $0.operationName == "active.span" }))
XCTAssertNil(spanEvents.first(where: { $0.operationName == "urlsession.request" }))

// Then
let expectedTraceIDField = activeSpan.traceID.toString(representation: .decimal)
let expectedSpanIDField = urlsessionSpan.spanID.toString(representation: .decimal)
let expectedTagsField = "_dd.p.tid=\(activeSpan.traceID.idHiHex)"
XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.traceIDField), expectedTraceIDField)
XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.parentSpanIDField), expectedSpanIDField)
XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.tagsField), expectedTagsField)
XCTAssertNotNil(request.value(forHTTPHeaderField: TracingHTTPHeaders.traceIDField))
XCTAssertNotNil(request.value(forHTTPHeaderField: TracingHTTPHeaders.parentSpanIDField))
XCTAssertNotNil(request.value(forHTTPHeaderField: TracingHTTPHeaders.tagsField))
XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.samplingPriorityField), "0")
}

Expand Down Expand Up @@ -329,18 +311,12 @@ class HeadBasedSamplingTests: XCTestCase {
span.finish()

// Then
let networkSpan = try XCTUnwrap(core.waitAndReturnSpanEvents().first, "It should send span event")
XCTAssertEqual(networkSpan.operationName, "network.span")
XCTAssertEqual(networkSpan.samplingRate, 0, "Span must use local trace sample rate")
XCTAssertFalse(networkSpan.isKept, "Span must be dropped")
XCTAssertNil(core.waitAndReturnSpanEvents().first, "It should not send span event")

// Then
let expectedTraceIDField = networkSpan.traceID.toString(representation: .decimal)
let expectedSpanIDField = networkSpan.spanID.toString(representation: .decimal)
let expectedTagsField = "_dd.p.tid=\(networkSpan.traceID.idHiHex)"
XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.traceIDField), expectedTraceIDField)
XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.parentSpanIDField), expectedSpanIDField)
XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.tagsField), expectedTagsField)
XCTAssertNotNil(request.value(forHTTPHeaderField: TracingHTTPHeaders.traceIDField))
XCTAssertNotNil(request.value(forHTTPHeaderField: TracingHTTPHeaders.parentSpanIDField))
XCTAssertNotNil(request.value(forHTTPHeaderField: TracingHTTPHeaders.tagsField))
XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.samplingPriorityField), "0")
}

Expand Down Expand Up @@ -420,23 +396,13 @@ class HeadBasedSamplingTests: XCTestCase {

// Then
let spanEvents = core.waitAndReturnSpanEvents()
let activeSpan = try XCTUnwrap(spanEvents.first(where: { $0.operationName == "active.span" }))
let networkSpan = try XCTUnwrap(spanEvents.first(where: { $0.operationName == "network.span" }))

XCTAssertEqual(activeSpan.samplingRate, 0, "Span must use local trace sample rate")
XCTAssertFalse(activeSpan.isKept, "Span must be dropped")
XCTAssertEqual(networkSpan.samplingRate, 0, "Span must use local trace sample rate")
XCTAssertFalse(networkSpan.isKept, "Span must be dropped")
XCTAssertEqual(networkSpan.traceID, activeSpan.traceID)
XCTAssertEqual(networkSpan.parentID, activeSpan.spanID)
XCTAssertNil(spanEvents.first(where: { $0.operationName == "active.span" }))
XCTAssertNil(spanEvents.first(where: { $0.operationName == "network.span" }))

// Then
let expectedTraceIDField = activeSpan.traceID.toString(representation: .decimal)
let expectedSpanIDField = networkSpan.spanID.toString(representation: .decimal)
let expectedTagsField = "_dd.p.tid=\(activeSpan.traceID.idHiHex)"
XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.traceIDField), expectedTraceIDField)
XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.parentSpanIDField), expectedSpanIDField)
XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.tagsField), expectedTagsField)
XCTAssertNotNil(request.value(forHTTPHeaderField: TracingHTTPHeaders.traceIDField))
XCTAssertNotNil(request.value(forHTTPHeaderField: TracingHTTPHeaders.parentSpanIDField))
XCTAssertNotNil(request.value(forHTTPHeaderField: TracingHTTPHeaders.tagsField))
XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.samplingPriorityField), "0")
}

Expand Down
2 changes: 2 additions & 0 deletions DatadogCore/Tests/Objc/ObjcAPITests/DDTrace+apiTests.m
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ - (void)testDDTraceConfigurationAPI {
}

- (void)testDDTracerAPI {
id<OTSpan> rootSpan = [[DDTracer shared] startRootSpan:@"" tags:NULL startTime:NULL customSampleRate:NULL];
[rootSpan setActive];
[[DDTracer shared] startSpan:@""];
[[DDTracer shared] startSpan:@"" tags:@{}];
[[DDTracer shared] startSpan:@"" childOf:NULL];
Expand Down
5 changes: 5 additions & 0 deletions DatadogTrace/Sources/DDNoOps.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ internal class DDNoopTracer: OTTracer, OpenTelemetryApi.Tracer {
return DDNoopGlobals.span
}

func startRootSpan(operationName: String, tags: [String: any Encodable]?, startTime: Date?, customSampleRate: SampleRate?) -> any OTSpan {
warn()
return DDNoopGlobals.span
}

// MARK: - Open Telemetry

func spanBuilder(spanName: String) -> OpenTelemetryApi.SpanBuilder {
Expand Down
4 changes: 3 additions & 1 deletion DatadogTrace/Sources/DDSpan.swift
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,9 @@ internal final class DDSpan: OTSpan {
ddTracer.removeSpan(span: self)
activity.leave()
}
sendSpan(finishTime: time, sampler: ddTracer.localTraceSampler)
if self.ddContext.isKept {
sendSpan(finishTime: time, sampler: ddTracer.localTraceSampler)
}
}

// MARK: - Writing SpanEvent
Expand Down
11 changes: 8 additions & 3 deletions DatadogTrace/Sources/DatadogTracer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,14 @@ internal final class DatadogTracer: OTTracer, OpenTelemetryApi.Tracer {
)
}

func startRootSpan(operationName: String, tags: [String: Encodable]? = nil, startTime: Date? = nil) -> OTSpan {
startSpan(
spanContext: createSpanContext(parentSpanContext: nil, using: localTraceSampler),
func startRootSpan(operationName: String, tags: [String: Encodable]? = nil, startTime: Date? = nil, customSampleRate: SampleRate? = nil) -> OTSpan {
let sampler: Sampling = if let customSampleRate {
Sampler(samplingRate: customSampleRate)
} else {
localTraceSampler
}
return startSpan(
spanContext: createSpanContext(parentSpanContext: nil, using: sampler),
operationName: operationName,
tags: tags,
startTime: startTime
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ public protocol objc_OTTracer {
func startSpan(_ operationName: String, childOf parent: objc_OTSpanContext?) -> objc_OTSpan
func startSpan(_ operationName: String, childOf parent: objc_OTSpanContext?, tags: NSDictionary?) -> objc_OTSpan
func startSpan(_ operationName: String, childOf parent: objc_OTSpanContext?, tags: NSDictionary?, startTime: Date?) -> objc_OTSpan
func startRootSpan(_ operationName: String, tags: NSDictionary?, startTime: Date?, customSampleRate: NSNumber?) -> objc_OTSpan
func inject(_ spanContext: objc_OTSpanContext, format: String, carrier: Any) throws
func extractWithFormat(_ format: String, carrier: Any) throws
}
17 changes: 17 additions & 0 deletions DatadogTrace/Sources/Objc/Tracing/Trace+objc.swift
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,23 @@ public final class objc_Tracer: NSObject, objc_OTTracer {
)
}

public func startRootSpan(
_ operationName: String,
tags: NSDictionary?,
startTime: Date?,
customSampleRate: NSNumber?
) -> any objc_OTSpan {
return objc_SpanObjc(
objcTracer: self,
swiftSpan: swiftTracer.startRootSpan(
operationName: operationName,
tags: tags.flatMap { castTagsToSwift($0) },
startTime: startTime,
customSampleRate: customSampleRate?.floatValue
)
)
}

public func inject(_ spanContext: objc_OTSpanContext, format: String, carrier: Any) throws {
if let objcWriter = carrier as? objc_HTTPHeadersWriter, format == OT.formatTextMap {
guard let ddspanContext = spanContext.dd else {
Expand Down
36 changes: 21 additions & 15 deletions DatadogTrace/Sources/OpenTracing/OTTracer.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Foundation
import DatadogInternal

/// Tracer is the starting point for all OpenTracing instrumentation. Use it
/// to create OTSpans, inject/extract them between processes, and so on.
Expand All @@ -25,16 +26,18 @@ public protocol OTTracer {

/// Start a new root span with the given operation name.
/// - Parameters:
/// - operationName: the operation name for the newly-started span
/// - tags: a set of tag keys and values per `OTSpan#setTag:value:`, or `nil` to start with
/// an empty tag map
/// - startTime: an explicitly specified start timestamp for the `OTSpan`, or `nil` to use the
/// current walltime
/// - returns: a valid Span instance; it is the caller's responsibility to call `finish()`.
/// - operationName: the operation name for the newly-started span
/// - tags: a set of tag keys and values per `OTSpan#setTag:value:`, or `nil` to start with
/// an empty tag map
/// - startTime: an explicitly specified start timestamp for the `OTSpan`, or `nil` to use the
/// current walltime
/// - customSampleRate: a sample rate that overrides the general Tracing configuration’s sample rate.
/// - returns: a valid Span instance; it is the caller's responsibility to call `finish()`.
func startRootSpan(
operationName: String,
tags: [String: Encodable]?,
startTime: Date?
startTime: Date?,
customSampleRate: SampleRate?
) -> OTSpan

/// Transfer the span information into the carrier of the given format.
Expand Down Expand Up @@ -108,21 +111,24 @@ public extension OTTracer {

/// Start a new root span with the given operation name.
/// - Parameters:
/// - operationName: the operation name for the newly-started span
/// - tags: a set of tag keys and values per `OTSpan#setTag:value:`, or `nil` to start with
/// an empty tag map
/// - startTime: an explicitly specified start timestamp for the `OTSpan`, or `nil` to use the
/// current walltime
/// - returns: a valid Span instance; it is the caller's responsibility to call `finish()`.
/// - operationName: the operation name for the newly-started span
/// - tags: a set of tag keys and values per `OTSpan#setTag:value:`, or `nil` to start with
/// an empty tag map
/// - startTime: an explicitly specified start timestamp for the `OTSpan`, or `nil` to use the
/// current walltime
/// - customSampleRate: a sample rate that overrides the general Tracing configuration’s sample rate.
/// - returns: a valid Span instance; it is the caller's responsibility to call `finish()`.
func startRootSpan(
operationName: String,
tags: [String: Encodable]? = nil,
startTime: Date? = nil
startTime: Date? = nil,
customSampleRate: SampleRate? = nil
) -> OTSpan {
return self.startRootSpan(
operationName: operationName,
tags: tags,
startTime: startTime
startTime: startTime,
customSampleRate: customSampleRate
)
}
}
81 changes: 57 additions & 24 deletions DatadogTrace/Tests/DatadogTracer+SamplingTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,55 @@ class DatadogTracer_SamplingTests: XCTestCase {

// Then
let events = try XCTUnwrap(featureScope.spanEventsWritten())
XCTAssertEqual(events.filter({ $0.samplingRate == 0.42 }).count, 100, "All spans must encode sample rate")
XCTAssertTrue(events.allSatisfy({ $0.samplingRate == 0.42 }), "All kept spans must encode sample rate")
XCTAssertGreaterThan(events.filter({ $0.isKept }).count, 1, "Some spans should be kept")
XCTAssertGreaterThan(events.filter({ !$0.isKept }).count, 1, "Some spans should be dropped")
XCTAssertEqual(events.filter({ !$0.isKept }).count, 0, "Not kept spans should be dropped")
}

func testRecordingCustomSampleRateInRootSpanEvent() throws {
// When
let tracer = createTracer(sampleRate: 0)
(0..<10).forEach { _ in
let span = tracer.startRootSpan(operationName: .mockAny(), customSampleRate: 100)
span.finish()
}

// Then
let events = try XCTUnwrap(featureScope.spanEventsWritten())
XCTAssertEqual(events.filter({ $0.samplingRate == 1 }).count, 10)
XCTAssertEqual(events.filter({ $0.isKept }).count, 10)
}

func testRootSampleInOverridesTracerAndPropagatesToChildSpans() throws {
// When
let tracer = createTracer(sampleRate: 0)
let root = tracer.startRootSpan(operationName: .mockAny(), customSampleRate: 100)
let child = tracer.startSpan(operationName: .mockAny(), childOf: root.context)
let grandChild = tracer.startSpan(operationName: .mockAny(), childOf: child.context)
grandChild.finish()
child.finish()
root.finish()

// Then
let events = try XCTUnwrap(featureScope.spanEventsWritten())
XCTAssertEqual(events.count, 3)
XCTAssertEqual(events.filter({ $0.samplingRate == 1 }).count, 3)
XCTAssertEqual(events.filter({ $0.isKept }).count, 3)
}

func testRootSampleOutOverridesTracerAndPropagatesToChildSpans() throws {
// When
let tracer = createTracer(sampleRate: 100)
let root = tracer.startRootSpan(operationName: .mockAny(), customSampleRate: 0)
let child = tracer.startSpan(operationName: .mockAny(), childOf: root.context)
let grandChild = tracer.startSpan(operationName: .mockAny(), childOf: child.context)
grandChild.finish()
child.finish()
root.finish()

// Then
let events = try XCTUnwrap(featureScope.spanEventsWritten())
XCTAssertEqual(events.count, 0)
}

func testRecordingSampledSpan() throws {
Expand All @@ -59,28 +105,12 @@ class DatadogTracer_SamplingTests: XCTestCase {
span.finish()

// Then
let event = try XCTUnwrap(featureScope.spanEventsWritten().first)
XCTAssertEqual(event.samplingRate, 0)
XCTAssertFalse(event.isKept)
let event = try featureScope.spanEventsWritten().first
XCTAssertNil(event)
}

// MARK: - Head-based Sampling

func testRecordingSampleRateInChildSpanEvents() throws {
// When
let tracer = createTracer(sampleRate: 42)
let root = tracer.startSpan(operationName: .mockAny())
let child = tracer.startSpan(operationName: .mockAny(), childOf: root.context)
let grandChild = tracer.startSpan(operationName: .mockAny(), childOf: child.context)
grandChild.finish()
child.finish()
root.finish()

// Then
let events = try XCTUnwrap(featureScope.spanEventsWritten())
XCTAssertEqual(events.filter({ $0.samplingRate == 0.42 }).count, 3, "All spans must encode the same sample rate")
}

func testWhenRootSpanIsSampled_thenAllChildSpansMustBeSampledTheSameWay() throws {
// When
let tracer = createTracer(sampleRate: 50)
Expand All @@ -93,10 +123,13 @@ class DatadogTracer_SamplingTests: XCTestCase {

// Then
let events = try XCTUnwrap(featureScope.spanEventsWritten())
XCTAssertEqual(events.count, 3)
let allKept = events.filter({ $0.isKept }).count == 3
let allDropped = events.filter({ !$0.isKept }).count == 3
XCTAssertTrue(allKept || allDropped, "All spans must be either kept or dropped")

if events.isEmpty {
XCTAssert(true, "No spans were collected")
} else {
XCTAssertEqual(events.filter({ $0.samplingRate == 0.5 }).count, 3, "All spans must encode the same sample rate")
XCTAssertEqual(events.filter({ $0.isKept }).count, 3, "All spans must be kept")
}
}
}

Expand Down
Loading