diff --git a/DatadogTrace/Sources/DDNoOps.swift b/DatadogTrace/Sources/DDNoOps.swift index fe71dc1fc3..b39f818020 100644 --- a/DatadogTrace/Sources/DDNoOps.swift +++ b/DatadogTrace/Sources/DDNoOps.swift @@ -49,7 +49,8 @@ internal class DDNoopTracer: OTTracer, OpenTelemetryApi.Tracer { // MARK: - Open Telemetry func spanBuilder(spanName: String) -> OpenTelemetryApi.SpanBuilder { - fatalError("Not implemented") + warn() + return OTelNoOpSpanBuilder() } } diff --git a/DatadogTrace/Sources/DatadogTracer.swift b/DatadogTrace/Sources/DatadogTracer.swift index ee26cb7414..cb371aa813 100644 --- a/DatadogTrace/Sources/DatadogTracer.swift +++ b/DatadogTrace/Sources/DatadogTracer.swift @@ -161,6 +161,14 @@ internal class DatadogTracer: OTTracer, OpenTelemetryApi.Tracer { // MARK: - OpenTelemetry func spanBuilder(spanName: String) -> OpenTelemetryApi.SpanBuilder { - OTelSpanBuilder() + OTelSpanBuilder( + active: false, + attributes: [:], + parent: .currentSpan, + spanKind: .client, + spanName: spanName, + startTime: nil, + tracer: self + ) } } diff --git a/DatadogTrace/Sources/OpenTelemetry/OTelNoOpSpan.swift b/DatadogTrace/Sources/OpenTelemetry/OTelNoOpSpan.swift index 31e08e4c67..c0f08291f7 100644 --- a/DatadogTrace/Sources/OpenTelemetry/OTelNoOpSpan.swift +++ b/DatadogTrace/Sources/OpenTelemetry/OTelNoOpSpan.swift @@ -7,21 +7,21 @@ import Foundation import OpenTelemetryApi -class OTelNoOpSpan: Span { +internal class OTelNoOpSpan: Span { var kind: OpenTelemetryApi.SpanKind = .internal var name: String = "" - var context: SpanContext = SpanContext.create( + var context = SpanContext.create( traceId: TraceId.invalid, spanId: SpanId.invalid, traceFlags: TraceFlags(), traceState: TraceState() ) - var isRecording: Bool = false + var isRecording = false - var status: Status = Status.unset + var status = Status.unset var description: String = "NoOpSpan" diff --git a/DatadogTrace/Sources/OpenTelemetry/OTelNoOpSpanBuilder.swift b/DatadogTrace/Sources/OpenTelemetry/OTelNoOpSpanBuilder.swift index ae2b8abe70..b4b0c4560d 100644 --- a/DatadogTrace/Sources/OpenTelemetry/OTelNoOpSpanBuilder.swift +++ b/DatadogTrace/Sources/OpenTelemetry/OTelNoOpSpanBuilder.swift @@ -7,40 +7,48 @@ import Foundation import OpenTelemetryApi -class NoOpSpanBuilder: SpanBuilder { - @discardableResult public func startSpan() -> Span { +internal class OTelNoOpSpanBuilder: SpanBuilder { + @discardableResult + func startSpan() -> Span { return OTelNoOpSpan() } - @discardableResult public func setParent(_ parent: Span) -> Self { + @discardableResult + func setParent(_ parent: Span) -> Self { return self } - @discardableResult public func setParent(_ parent: SpanContext) -> Self { + @discardableResult + func setParent(_ parent: SpanContext) -> Self { return self } - @discardableResult public func setNoParent() -> Self { + @discardableResult + func setNoParent() -> Self { return self } - @discardableResult public func addLink(spanContext: SpanContext) -> Self { + @discardableResult + func addLink(spanContext: SpanContext) -> Self { return self } - @discardableResult public func addLink(spanContext: SpanContext, attributes: [String: OpenTelemetryApi.AttributeValue]) -> Self { + @discardableResult + func addLink(spanContext: SpanContext, attributes: [String: OpenTelemetryApi.AttributeValue]) -> Self { return self } - @discardableResult public func setSpanKind(spanKind: SpanKind) -> Self { + @discardableResult + func setSpanKind(spanKind: SpanKind) -> Self { return self } - @discardableResult public func setStartTime(time: Date) -> Self { + @discardableResult + func setStartTime(time: Date) -> Self { return self } - public func setAttribute(key: String, value: OpenTelemetryApi.AttributeValue) -> Self { + func setAttribute(key: String, value: OpenTelemetryApi.AttributeValue) -> Self { return self } diff --git a/DatadogTrace/Sources/OpenTelemetry/OTelSpan.swift b/DatadogTrace/Sources/OpenTelemetry/OTelSpan.swift index aa83590685..b6d6f47380 100644 --- a/DatadogTrace/Sources/OpenTelemetry/OTelSpan.swift +++ b/DatadogTrace/Sources/OpenTelemetry/OTelSpan.swift @@ -7,33 +7,29 @@ import Foundation import OpenTelemetryApi -class OTelSpan: OpenTelemetryApi.Span { - private let tracer: DatadogTracer - private let queue: DispatchQueue +internal enum DatadogTagKeys: String { + case spanKind = "span.kind" + case errorMessage = "error.Message" +} + +internal class OTelSpan: OpenTelemetryApi.Span { private var _status: OpenTelemetryApi.Status private var _name: String - init( - context: OpenTelemetryApi.SpanContext, - kind: OpenTelemetryApi.SpanKind, - name: String, - tracer: DatadogTracer - ) { - self._name = name - self._status = .unset - self.context = context - self.isRecording = true - self.kind = kind - self.queue = tracer.queue - self.tracer = tracer - } - + var attributes: [String: OpenTelemetryApi.AttributeValue] + let context: OpenTelemetryApi.SpanContext var kind: OpenTelemetryApi.SpanKind + let nestedSpan: DDSpan + let tracer: DatadogTracer + let queue: DispatchQueue - var context: OpenTelemetryApi.SpanContext - + /// `isRecording` indicates whether the span is recording or not + /// and events can be added to it. var isRecording: Bool + /// `status` saves state of the code and description indicating + /// whether the span has recorded errors. This will be done by setting `error.message` + /// tag on the span. var status: OpenTelemetryApi.Status { get { queue.sync { @@ -42,11 +38,16 @@ class OTelSpan: OpenTelemetryApi.Span { } set { queue.sync { + guard isRecording else { + return + } + _status = newValue } } } + /// `name` of the span is akin to operation name in Datadog var name: String { get { queue.sync { @@ -55,33 +56,93 @@ class OTelSpan: OpenTelemetryApi.Span { } set { queue.sync { + guard isRecording else { + return + } _name = newValue } + nestedSpan.setOperationName(name) } } + init( + attributes: [String: OpenTelemetryApi.AttributeValue], + kind: OpenTelemetryApi.SpanKind, + name: String, + parentSpanID: OpenTelemetryApi.SpanId?, + spanContext: OpenTelemetryApi.SpanContext, + spanKind: OpenTelemetryApi.SpanKind, + startTime: Date, + tracer: DatadogTracer + ) { + self._name = name + self._status = .unset + self.attributes = attributes + self.context = spanContext + self.kind = kind + self.isRecording = true + self.queue = tracer.queue + self.tracer = tracer + self.nestedSpan = .init( + tracer: tracer, + context: .init( + traceID: context.traceId.toDatadog(), + spanID: context.spanId.toDatadog(), + parentSpanID: parentSpanID?.toDatadog(), + baggageItems: .init() + ), + operationName: name, + startTime: startTime, + tags: [:] + ) + } + + // swiftlint:disable unavailable_function func addEvent(name: String) { - fatalError("Not implemented") + fatalError("Not implemented yet") } func addEvent(name: String, timestamp: Date) { - fatalError("Not implemented") + fatalError("Not implemented yet") } - func addEvent(name: String, attributes: [String : OpenTelemetryApi.AttributeValue]) { - fatalError("Not implemented") + func addEvent(name: String, attributes: [String: OpenTelemetryApi.AttributeValue]) { + fatalError("Not implemented yet") } - func addEvent(name: String, attributes: [String : OpenTelemetryApi.AttributeValue], timestamp: Date) { - fatalError("Not implemented") + func addEvent(name: String, attributes: [String: OpenTelemetryApi.AttributeValue], timestamp: Date) { + fatalError("Not implemented yet") } + // swiftlint:enable unavailable_function func end() { - fatalError("Not implemented") + end(time: Date()) } func end(time: Date) { - fatalError("Not implemented") + queue.sync { + guard isRecording else { + return + } + isRecording = false + + // Attributes maps to tags in Datadog + for (key, value) in attributes { + nestedSpan.setTag(key: key, value: value.description) + } + + // If status is error, error.message tag is added + switch self._status { + case .error(description: let description): + nestedSpan.setTag(key: DatadogTagKeys.errorMessage.rawValue, value: description) + case .ok, .unset: + break + } + + // SpanKind maps to the `span.kind` tag in Datadog + nestedSpan.setTag(key: DatadogTagKeys.spanKind.rawValue, value: kind.rawValue) + } + nestedSpan.finish(at: time) } var description: String { @@ -89,6 +150,10 @@ class OTelSpan: OpenTelemetryApi.Span { } func setAttribute(key: String, value: OpenTelemetryApi.AttributeValue?) { - fatalError("Not implemented") + guard isRecording else { + return + } + + attributes[key] = value } } diff --git a/DatadogTrace/Sources/OpenTelemetry/OTelSpanBuilder.swift b/DatadogTrace/Sources/OpenTelemetry/OTelSpanBuilder.swift index 2f3b051883..93a4deb244 100644 --- a/DatadogTrace/Sources/OpenTelemetry/OTelSpanBuilder.swift +++ b/DatadogTrace/Sources/OpenTelemetry/OTelSpanBuilder.swift @@ -1,50 +1,140 @@ /* -* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. -* This product includes software developed at Datadog (https://www.datadoghq.com/). -* Copyright 2019-Present Datadog, Inc. -*/ + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-Present Datadog, Inc. + */ import Foundation import OpenTelemetryApi -class OTelSpanBuilder: OpenTelemetryApi.SpanBuilder { +internal class OTelSpanBuilder: OpenTelemetryApi.SpanBuilder { + var tracer: DatadogTracer + var spanName: String + var spanKind = SpanKind.client + var attributes: [String: OpenTelemetryApi.AttributeValue] + var startTime: Date? + var active: Bool + var parent: Parent + + enum Parent { + case currentSpan + case span(OpenTelemetryApi.Span) + case spanContext(OpenTelemetryApi.SpanContext) + case noParent + + func context() -> OpenTelemetryApi.SpanContext? { + switch self { + case .currentSpan: + return OpenTelemetry.instance.contextProvider.activeSpan?.context + case .span(let span): + return span.context + case .spanContext(let context): + return context + case .noParent: + return nil + } + } + } + + init( + active: Bool, + attributes: [String: OpenTelemetryApi.AttributeValue], + parent: Parent, + spanKind: SpanKind, + spanName: String, + startTime: Date?, + tracer: DatadogTracer + ) { + self.tracer = tracer + self.spanName = spanName + self.spanKind = spanKind + self.attributes = attributes + self.startTime = startTime + self.active = active + self.parent = parent + } + func setParent(_ parent: OpenTelemetryApi.Span) -> Self { - fatalError("Not implemented") + self.parent = .span(parent) + return self } func setParent(_ parent: OpenTelemetryApi.SpanContext) -> Self { - fatalError("Not implemented") + self.parent = .spanContext(parent) + return self } func setNoParent() -> Self { - fatalError("Not implemented") + self.parent = .noParent + return self } + // swiftlint:disable unavailable_function func addLink(spanContext: OpenTelemetryApi.SpanContext) -> Self { - fatalError("Not implemented") + fatalError("Not implemented yet") } - func addLink(spanContext: OpenTelemetryApi.SpanContext, attributes: [String : OpenTelemetryApi.AttributeValue]) -> Self { - fatalError("Not implemented") + func addLink(spanContext: OpenTelemetryApi.SpanContext, attributes: [String: OpenTelemetryApi.AttributeValue]) -> Self { + fatalError("Not implemented yet") } + // swiftlint:enable unavailable_function func setSpanKind(spanKind: OpenTelemetryApi.SpanKind) -> Self { - fatalError("Not implemented") + self.spanKind = spanKind + return self } func setStartTime(time: Date) -> Self { - fatalError("Not implemented") + self.startTime = time + return self } func setActive(_ active: Bool) -> Self { - fatalError("Not implemented") + self.active = active + return self } func startSpan() -> OpenTelemetryApi.Span { - fatalError("Not implemented") + let parentContext = parent.context() + let traceId: TraceId + let spanId = SpanId.random() + let traceState: TraceState + + if let parentContext = parentContext, parentContext.isValid { + traceId = parentContext.traceId + traceState = parentContext.traceState + } else { + traceId = TraceId.random() + traceState = .init() + } + + let spanContext = SpanContext.create( + traceId: traceId, + spanId: spanId, + traceFlags: TraceFlags(), + traceState: traceState + ) + + let createdSpan = OTelSpan( + attributes: attributes, + kind: spanKind, + name: spanName, + parentSpanID: parentContext?.spanId, + spanContext: spanContext, + spanKind: spanKind, + startTime: startTime ?? Date(), + tracer: tracer + ) + + if active { + OpenTelemetry.instance.contextProvider.setActiveSpan(createdSpan) + } + + return createdSpan } func setAttribute(key: String, value: OpenTelemetryApi.AttributeValue) -> Self { - fatalError("Not implemented") + attributes[key] = value + return self } } diff --git a/DatadogTrace/Sources/OpenTelemetry/OtelSpanId+Datadog.swift b/DatadogTrace/Sources/OpenTelemetry/OtelSpanId+Datadog.swift new file mode 100644 index 0000000000..6c8ff2c0b8 --- /dev/null +++ b/DatadogTrace/Sources/OpenTelemetry/OtelSpanId+Datadog.swift @@ -0,0 +1,20 @@ +/* +* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. +* This product includes software developed at Datadog (https://www.datadoghq.com/). +* Copyright 2019-Present Datadog, Inc. +*/ + +import Foundation +import OpenTelemetryApi +import DatadogInternal + +extension OpenTelemetryApi.SpanId { + /// Converts OpenTelemetry `SpanId` to Datadog `SpanID`. + /// - Returns: Datadog `SpanID`. + func toDatadog() -> SpanID { + var data = Data(count: 8) + self.copyBytesTo(dest: &data, destOffset: 0) + let integerLiteral = UInt64(bigEndian: data.withUnsafeBytes { $0.load(as: UInt64.self) }) + return .init(integerLiteral: integerLiteral) + } +} diff --git a/DatadogTrace/Sources/OpenTelemetry/OtelTraceId+Datadog.swift b/DatadogTrace/Sources/OpenTelemetry/OtelTraceId+Datadog.swift new file mode 100644 index 0000000000..9f34b8cf8d --- /dev/null +++ b/DatadogTrace/Sources/OpenTelemetry/OtelTraceId+Datadog.swift @@ -0,0 +1,20 @@ +/* +* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. +* This product includes software developed at Datadog (https://www.datadoghq.com/). +* Copyright 2019-Present Datadog, Inc. +*/ + +import Foundation +import OpenTelemetryApi +import DatadogInternal + +extension OpenTelemetryApi.TraceId { + /// Converts OpenTelemetry `TraceId` to Datadog `TraceID`. + /// - Returns: Datadog `TraceID` with only higher order bits considered. + func toDatadog() -> TraceID { + var data = Data(count: 16) + self.copyBytesTo(dest: &data, destOffset: 0) + let integerLiteral = UInt64(bigEndian: data.withUnsafeBytes { $0.load(as: UInt64.self) }) + return .init(integerLiteral: integerLiteral) + } +} diff --git a/DatadogTrace/Tests/OpenTelemetry/OtelSpanId+DatadogTests.swift b/DatadogTrace/Tests/OpenTelemetry/OtelSpanId+DatadogTests.swift new file mode 100644 index 0000000000..b1e2566259 --- /dev/null +++ b/DatadogTrace/Tests/OpenTelemetry/OtelSpanId+DatadogTests.swift @@ -0,0 +1,20 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-Present Datadog, Inc. + */ + +import XCTest +import TestUtilities +import DatadogInternal +import OpenTelemetryApi + +@testable import DatadogTrace + +class OtelSpanIdDatadogTests: XCTestCase { + func testToDatadog() { + let otelId = SpanId.random() + let ddId = otelId.toDatadog() + XCTAssertEqual(otelId.rawValue, ddId.rawValue) + } +} diff --git a/DatadogTrace/Tests/OpenTelemetry/OtelSpanTests.swift b/DatadogTrace/Tests/OpenTelemetry/OtelSpanTests.swift new file mode 100644 index 0000000000..80ebebb49a --- /dev/null +++ b/DatadogTrace/Tests/OpenTelemetry/OtelSpanTests.swift @@ -0,0 +1,100 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-Present Datadog, Inc. + */ + +import XCTest +import TestUtilities +import DatadogInternal + +@testable import DatadogTrace + +final class OtelSpanTests: XCTestCase { + func testSpanResourceNameDefault() { + let writeSpanExpectation = expectation(description: "write span event") + let core = PassthroughCoreMock(expectation: writeSpanExpectation) + + // Given + let tracer: DatadogTracer = .mockWith(core: core) + let span = tracer.spanBuilder(spanName: "OperationName").startSpan() + + // When + span.end() + + // Then + waitForExpectations(timeout: 0.5, handler: nil) + let events: [SpanEventsEnvelope] = core.events() + XCTAssertEqual(events.count, 1) + let recordedSpan = events.first!.spans.first! + XCTAssertEqual(recordedSpan.resource, "OperationName") + XCTAssertEqual(recordedSpan.operationName, "OperationName") + } + + func testSpanSetName() { + let writeSpanExpectation = expectation(description: "write span event") + let core = PassthroughCoreMock(expectation: writeSpanExpectation) + + // Given + let tracer: DatadogTracer = .mockWith(core: core) + let span = tracer.spanBuilder(spanName: "OperationName").startSpan() + + // When + span.name = "NewOperationName" + span.end() + + // Then + waitForExpectations(timeout: 0.5, handler: nil) + let events: [SpanEventsEnvelope] = core.events() + XCTAssertEqual(events.count, 1) + let recordedSpan = events.first!.spans.first! + XCTAssertEqual(recordedSpan.resource, "NewOperationName") + XCTAssertEqual(recordedSpan.operationName, "NewOperationName") + } + + func testSpanEnd() { + // Given + let (name, ignoredName) = ("trueName", "invalidName") + let (code, ignoredCode) = (200, 400) + let (message, ignoredMessage) = ("message", "ignoredMessage") + let (attributes, ignoredAttributes) = (["key": "value"], ["ignoredKey": "ignoredValue"]) + + let writeSpanExpectation = expectation(description: "write span event") + let core = PassthroughCoreMock(expectation: writeSpanExpectation) + + let tracer: DatadogTracer = .mockWith(core: core) + let span = tracer.spanBuilder(spanName: name).startSpan() + span.putHttpStatusCode(statusCode: code, reasonPhrase: message) + for (key, value) in attributes { + span.setAttribute(key: key, value: value) + } + XCTAssertTrue(span.isRecording) + + // When + span.end() + XCTAssertFalse(span.isRecording) + + // Then ignores + span.name = ignoredName + span.putHttpStatusCode(statusCode: ignoredCode, reasonPhrase: ignoredMessage) + for (key, value) in ignoredAttributes { + span.setAttribute(key: key, value: value) + } + + span.end() + + waitForExpectations(timeout: 0.5, handler: nil) + let events: [SpanEventsEnvelope] = core.events() + XCTAssertEqual(events.count, 1) + let recordedSpan = events.first!.spans.first! + + XCTAssertEqual(recordedSpan.resource, name) + XCTAssertEqual(recordedSpan.operationName, name) + let expectedTags = [ + "http.status_code": "200", + "key": "value", + "span.kind": "client", + ] + XCTAssertEqual(recordedSpan.tags, expectedTags) + } +} diff --git a/DatadogTrace/Tests/OpenTelemetry/OtelTraceId+DatadogTests.swift b/DatadogTrace/Tests/OpenTelemetry/OtelTraceId+DatadogTests.swift new file mode 100644 index 0000000000..749ec16f63 --- /dev/null +++ b/DatadogTrace/Tests/OpenTelemetry/OtelTraceId+DatadogTests.swift @@ -0,0 +1,20 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-Present Datadog, Inc. + */ + +import XCTest +import TestUtilities +import DatadogInternal +import OpenTelemetryApi + +@testable import DatadogTrace + +class OtelTraceIdDatadogTests: XCTestCase { + func testToDatadog_onlyHigherOrderBitsAreConsidered() { + let otelId = TraceId.random() + let ddId = otelId.toDatadog() + XCTAssertEqual(otelId.rawHigherLong, ddId.rawValue) + } +}