Skip to content

Commit

Permalink
adds OTelTraceIdRatioBasedSampler
Browse files Browse the repository at this point in the history
  • Loading branch information
t089 authored and slashmo committed Sep 14, 2024
1 parent 786f913 commit 02f0088
Show file tree
Hide file tree
Showing 6 changed files with 187 additions and 6 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Benchmark boilerplate generated by Benchmark

import Benchmark
import OTel

let benchmarks = {
Benchmark("OTelTraceIdRatioBasedSampler") { benchmark in

let sampler = OTelTraceIdRatioBasedSampler(ratio: 0.5)
for _ in benchmark.scaledIterations {
_ = sampler.samplingResult(
operationName: "some-op",
kind: .internal,
traceID: .random(),
attributes: [:],
links: [],
parentContext: .topLevel)
}
}
// Add additional benchmarks here
}
22 changes: 21 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ let package = Package(
.package(url: "https://github.com/apple/swift-nio.git", from: "2.0.0"),
.package(url: "https://github.com/apple/swift-atomics.git", from: "1.0.2"),
.package(url: "https://github.com/apple/swift-metrics.git", from: "2.4.1"),
.package(url: "https://github.com/slashmo/swift-w3c-trace-context.git", from: "1.0.0-beta.1"),
//.package(url: "https://github.com/slashmo/swift-w3c-trace-context.git", from: "1.0.0-beta.1"),
//.package(name: "swift-w3c-trace-context", path: "../swift-w3c-trace-context"),
.package(url: "https://github.com/t089/swift-w3c-trace-context.git", branch: "trace-id-bytes"),

// MARK: - OTLP

Expand All @@ -30,6 +32,9 @@ let package = Package(
// MARK: - Plugins

.package(url: "https://github.com/apple/swift-docc-plugin.git", from: "1.0.0"),

// MARK: - Benchmarks
.package(url: "https://github.com/ordo-one/package-benchmark", .upToNextMajor(from: "1.4.0")),
],
targets: [
.target(
Expand Down Expand Up @@ -110,3 +115,18 @@ let package = Package(
],
swiftLanguageVersions: [.version("6"), .v5]
)

// Benchmark of OTelTraceIdRatioBasedSamplerBenchmark
package.targets += [
.executableTarget(
name: "OTelTraceIdRatioBasedSamplerBenchmark",
dependencies: [
.product(name: "Benchmark", package: "package-benchmark"),
"OTel",
],
path: "Benchmarks/OTelTraceIdRatioBasedSamplerBenchmark",
plugins: [
.plugin(name: "BenchmarkPlugin", package: "package-benchmark")
]
),
]
23 changes: 20 additions & 3 deletions Sources/OTLPCore/Tracing/OTelFinishedSpan+Proto.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,22 @@
import struct Foundation.Data
import OTel
import Tracing
import W3CTraceContext

extension Opentelemetry_Proto_Trace_V1_Span {
/// Create a span from an `OTelFinishedSpan`.
///
/// - Parameter finishedSpan: The `OTelFinishedSpan` to cast.
public init(_ finishedSpan: OTelFinishedSpan) {
traceID = Data(finishedSpan.spanContext.traceID.bytes)
spanID = Data(finishedSpan.spanContext.spanID.bytes)
traceID = finishedSpan.spanContext.traceID.data
spanID = finishedSpan.spanContext.spanID.data

if let traceStateHeaderValue = finishedSpan.spanContext.traceStateHeaderValue {
self.traceState = traceStateHeaderValue
}

if let parentSpanID = finishedSpan.spanContext.parentSpanID {
self.parentSpanID = Data(parentSpanID.bytes)
self.parentSpanID = parentSpanID.data
}

name = finishedSpan.operationName
Expand All @@ -46,3 +47,19 @@ extension Opentelemetry_Proto_Trace_V1_Span {
links = finishedSpan.links.compactMap(Opentelemetry_Proto_Trace_V1_Span.Link.init)
}
}

extension TraceID {
var data: Data {
self.withUnsafeBytes {
Data($0)
}
}
}

extension SpanID {
var data: Data {
self.withUnsafeBytes {
Data($0)
}
}
}
4 changes: 2 additions & 2 deletions Sources/OTLPCore/Tracing/SpanLink+Proto.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ extension Opentelemetry_Proto_Trace_V1_Span.Link {
/// - Returns: `nil` if the `SpanLink`s context does not contain a span context.
public init?(_ link: SpanLink) {
guard let spanContext = link.context.spanContext else { return nil }
self.traceID = Data(spanContext.traceID.bytes)
self.spanID = Data(spanContext.spanID.bytes)
self.traceID = spanContext.traceID.data
self.spanID = spanContext.spanID.data
if let traceStateHeaderValue = spanContext.traceStateHeaderValue {
self.traceState = traceStateHeaderValue
}
Expand Down
53 changes: 53 additions & 0 deletions Sources/OTel/Tracing/Sampling/OTelTraceIdRatioBasedSampler.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import Tracing
import W3CTraceContext

/// An `OTelSampler` based on a given `TraceID` and `ratio`.
/// [Spec](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/sdk.md#traceidratiobased)
public struct OTelTraceIdRatioBasedSampler: OTelSampler, Equatable, Hashable, CustomStringConvertible {

let idUpperBound : UInt64
public let ratio: Double

public init(ratio: Double) {
precondition(ratio >= 0.0 && ratio <= 1.0, "ratio must be between 0.0 and 1.0")

self.ratio = ratio
if ratio == 0.0 {
self.idUpperBound = .min
} else if ratio == 1.0 {
self.idUpperBound = .max
} else {
self.idUpperBound = UInt64(ratio * Double(UInt64.max))
}
}

public func samplingResult(
operationName: String,
kind: SpanKind,
traceID: TraceID,
attributes: SpanAttributes,
links: [SpanLink],
parentContext: ServiceContext
) -> OTelSamplingResult {

let value = traceID.withUnsafeBytes { $0[8...].load(as: UInt64.self) }

if value < idUpperBound {
return .init(decision: .recordAndSample)
} else {
return .init(decision: .drop)
}
}

public static func ==(lhs: Self, rhs: Self) -> Bool {
lhs.idUpperBound == rhs.idUpperBound
}

public func hash(into hasher: inout Hasher) {
hasher.combine(self.idUpperBound)
}

public var description: String {
"TraceIdRatioBased{\(ratio)}"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
@testable import OTel
import XCTest
import Tracing

final class OTelTraceIdRatioBasedSamplerTests: XCTestCase {
func test_zero_ratio_does_not_sample() {
let sampler = OTelTraceIdRatioBasedSampler(ratio: 0.0)

let result = sampler.samplingResult(
operationName: "does-not-matter",
kind: .internal,
traceID: .allZeroes,
attributes: [:],
links: [],
parentContext: .topLevel
)

XCTAssertEqual(result, OTelSamplingResult(decision: .drop, attributes: [:]))
}

func test_one_ratio_does_sample() {
let sampler = OTelTraceIdRatioBasedSampler(ratio: 1.0)

let result = sampler.samplingResult(
operationName: "does-not-matter",
kind: .internal,
traceID: .allZeroes,
attributes: [:],
links: [],
parentContext: .topLevel
)

XCTAssertEqual(result, OTelSamplingResult(decision: .recordAndSample, attributes: [:]))
}


func test_different_ratios() {

let ratios = [0.0, 0.1, 0.25, 0.5, 0.75, 1.0]

for ratio in ratios {

let sampler = OTelTraceIdRatioBasedSampler(ratio: ratio)

let N = 100_000
var sampled = 0

for _ in 0 ..< N {
let result = sampler.samplingResult(
operationName: "does-not-matter",
kind: .internal,
traceID: .random(),
attributes: [:],
links: [],
parentContext: .topLevel
)

switch result.decision {
case .recordAndSample:
sampled += 1
default: break
}
}

let observedRatio = Double(sampled) / Double(N)

XCTAssertEqual(ratio, observedRatio, accuracy: 0.05, "Expected ratio \(ratio) but observed \(observedRatio)")
}
}
}

0 comments on commit 02f0088

Please sign in to comment.