diff --git a/Datadog/Datadog.xcodeproj/project.pbxproj b/Datadog/Datadog.xcodeproj/project.pbxproj index 99d61151a3..330599b276 100644 --- a/Datadog/Datadog.xcodeproj/project.pbxproj +++ b/Datadog/Datadog.xcodeproj/project.pbxproj @@ -78,6 +78,10 @@ 3CBDE6882AA0B7F000F6A7B6 /* URLSessionTaskDelegate+Tracking.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CBDE6862AA0B7F000F6A7B6 /* URLSessionTaskDelegate+Tracking.swift */; }; 3CBDE68A2AA0C47300F6A7B6 /* URLSessionTask+Tracking.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CBDE6892AA0C47300F6A7B6 /* URLSessionTask+Tracking.swift */; }; 3CBDE68B2AA0C47300F6A7B6 /* URLSessionTask+Tracking.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CBDE6892AA0C47300F6A7B6 /* URLSessionTask+Tracking.swift */; }; + 3CC6AD182B4F07DD00015B18 /* OTelAttributeValue+Datadog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CC6AD172B4F07DC00015B18 /* OTelAttributeValue+Datadog.swift */; }; + 3CC6AD192B4F07DD00015B18 /* OTelAttributeValue+Datadog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CC6AD172B4F07DC00015B18 /* OTelAttributeValue+Datadog.swift */; }; + 3CC6AD1D2B4F07FA00015B18 /* OTelAttributeValue+DatadogTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CC6AD1A2B4F07E700015B18 /* OTelAttributeValue+DatadogTests.swift */; }; + 3CC6AD1E2B4F07FB00015B18 /* OTelAttributeValue+DatadogTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CC6AD1A2B4F07E700015B18 /* OTelAttributeValue+DatadogTests.swift */; }; 3CCCA5C42ABAF0F80029D7BD /* DDURLSessionInstrumentation+objc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CCCA5C32ABAF0F80029D7BD /* DDURLSessionInstrumentation+objc.swift */; }; 3CCCA5C52ABAF0F80029D7BD /* DDURLSessionInstrumentation+objc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CCCA5C32ABAF0F80029D7BD /* DDURLSessionInstrumentation+objc.swift */; }; 3CCCA5C72ABAF5230029D7BD /* DDURLSessionInstrumentationConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CCCA5C62ABAF5230029D7BD /* DDURLSessionInstrumentationConfigurationTests.swift */; }; @@ -1920,6 +1924,8 @@ 3CBDE6832AA092BC00F6A7B6 /* URLSessionTaskSwizzlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionTaskSwizzlerTests.swift; sourceTree = ""; }; 3CBDE6862AA0B7F000F6A7B6 /* URLSessionTaskDelegate+Tracking.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URLSessionTaskDelegate+Tracking.swift"; sourceTree = ""; }; 3CBDE6892AA0C47300F6A7B6 /* URLSessionTask+Tracking.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URLSessionTask+Tracking.swift"; sourceTree = ""; }; + 3CC6AD172B4F07DC00015B18 /* OTelAttributeValue+Datadog.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "OTelAttributeValue+Datadog.swift"; sourceTree = ""; }; + 3CC6AD1A2B4F07E700015B18 /* OTelAttributeValue+DatadogTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "OTelAttributeValue+DatadogTests.swift"; sourceTree = ""; }; 3CCCA5C32ABAF0F80029D7BD /* DDURLSessionInstrumentation+objc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DDURLSessionInstrumentation+objc.swift"; sourceTree = ""; }; 3CCCA5C62ABAF5230029D7BD /* DDURLSessionInstrumentationConfigurationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DDURLSessionInstrumentationConfigurationTests.swift; sourceTree = ""; }; 3CE119FE29F7BE0100202522 /* DatadogWebViewTracking.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = DatadogWebViewTracking.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -3114,6 +3120,7 @@ 3C6C7FDE2B459AAA006F5CBC /* OpenTelemetry */ = { isa = PBXGroup; children = ( + 3CC6AD172B4F07DC00015B18 /* OTelAttributeValue+Datadog.swift */, 3CB012DB2B482E0400557951 /* NOPOTelSpan.swift */, 3CB012DC2B482E0400557951 /* NOPOTelSpanBuilder.swift */, 3C6C7FE02B459AAA006F5CBC /* OTelSpan.swift */, @@ -3127,6 +3134,7 @@ 3C6C7FF12B459AB3006F5CBC /* OpenTelemetry */ = { isa = PBXGroup; children = ( + 3CC6AD1A2B4F07E700015B18 /* OTelAttributeValue+DatadogTests.swift */, 3C6C7FF22B459AB3006F5CBC /* OTelSpanId+DatadogTests.swift */, 3C6C7FF32B459AB3006F5CBC /* OTelTraceId+DatadogTests.swift */, 3C6C7FF42B459AB3006F5CBC /* OTelSpanTests.swift */, @@ -8324,6 +8332,7 @@ files = ( 61A2CC3C2A44BED30000FF25 /* Tracer.swift in Sources */, D2C1A50229C4C4CB00946C31 /* Casting.swift in Sources */, + 3CC6AD182B4F07DD00015B18 /* OTelAttributeValue+Datadog.swift in Sources */, D2C1A50C29C4C4CB00946C31 /* DDNoOps.swift in Sources */, D2C1A4FC29C4C4CB00946C31 /* RequestBuilder.swift in Sources */, D2C1A50D29C4C4CB00946C31 /* SpanTagsReducer.swift in Sources */, @@ -8366,6 +8375,7 @@ buildActionMask = 2147483647; files = ( D2C1A51E29C4C75700946C31 /* Casting+Tracing.swift in Sources */, + 3CC6AD1D2B4F07FA00015B18 /* OTelAttributeValue+DatadogTests.swift in Sources */, D2C1A52429C4C75700946C31 /* TracingURLSessionHandlerTests.swift in Sources */, 619CE75E2A458CE1005588CB /* TraceConfigurationTests.swift in Sources */, D2C1A52329C4C75700946C31 /* WarningsTests.swift in Sources */, @@ -8522,6 +8532,7 @@ files = ( 61A2CC3D2A44BED30000FF25 /* Tracer.swift in Sources */, D2C1A53829C4F2DF00946C31 /* Casting.swift in Sources */, + 3CC6AD192B4F07DD00015B18 /* OTelAttributeValue+Datadog.swift in Sources */, D2C1A53929C4F2DF00946C31 /* DDNoOps.swift in Sources */, D2C1A53A29C4F2DF00946C31 /* RequestBuilder.swift in Sources */, D2C1A53B29C4F2DF00946C31 /* SpanTagsReducer.swift in Sources */, @@ -8564,6 +8575,7 @@ buildActionMask = 2147483647; files = ( D2C1A55F29C4F2E800946C31 /* Casting+Tracing.swift in Sources */, + 3CC6AD1E2B4F07FB00015B18 /* OTelAttributeValue+DatadogTests.swift in Sources */, D2C1A56029C4F2E800946C31 /* TracingURLSessionHandlerTests.swift in Sources */, 619CE75F2A458CE1005588CB /* TraceConfigurationTests.swift in Sources */, D2C1A56129C4F2E800946C31 /* WarningsTests.swift in Sources */, diff --git a/DatadogTrace/Sources/OpenTelemetry/OTelAttributeValue+Datadog.swift b/DatadogTrace/Sources/OpenTelemetry/OTelAttributeValue+Datadog.swift new file mode 100644 index 0000000000..6d5ac98ea0 --- /dev/null +++ b/DatadogTrace/Sources/OpenTelemetry/OTelAttributeValue+Datadog.swift @@ -0,0 +1,71 @@ +/* +* 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 + +extension Dictionary where Key == String, Value == OpenTelemetryApi.AttributeValue { + /// Converts OpenTelemetry attributes to Datadog tags. This method is recursive + /// and will flatten nested attributes. Collection attributes are flattened to multiple + /// tags with `key.index` naming convention. If attribute value is an empty collection, + /// it will be converted to empty string. + var tags: [String: String] { + var tags: [String: String] = [:] + for (key, value) in self { + switch value { + case .bool(let value): + tags[key] = value.description + case .string(let value): + tags[key] = value + case .int(let value): + tags[key] = value.description + case .double(let value): + tags[key] = value.description + case .stringArray(let array): + if array.isEmpty { + tags[key] = "" + } else { + for (index, element) in array.enumerated() { + tags["\(key).\(index)"] = element + } + } + case .boolArray(let array): + if array.isEmpty { + tags[key] = "" + } else { + for (index, element) in array.enumerated() { + tags["\(key).\(index)"] = element.description + } + } + case .intArray(let array): + if array.isEmpty { + tags[key] = "" + } else { + for (index, element) in array.enumerated() { + tags["\(key).\(index)"] = element.description + } + } + case .doubleArray(let array): + if array.isEmpty { + tags[key] = "" + } else { + for (index, element) in array.enumerated() { + tags["\(key).\(index)"] = element.description + } + } + case .set(let set): + if set.labels.tags.isEmpty { + tags[key] = "" + } else { + for (nestedKey, nestedValue) in set.labels.tags { + tags["\(key).\(nestedKey)"] = nestedValue + } + } + } + } + return tags + } +} diff --git a/DatadogTrace/Sources/OpenTelemetry/OTelSpan.swift b/DatadogTrace/Sources/OpenTelemetry/OTelSpan.swift index 1ffcc037f0..601b8109bd 100644 --- a/DatadogTrace/Sources/OpenTelemetry/OTelSpan.swift +++ b/DatadogTrace/Sources/OpenTelemetry/OTelSpan.swift @@ -118,7 +118,7 @@ internal class OTelSpan: OpenTelemetryApi.Span { return } isRecording = false - tags = makeTags() + tags = attributes.tags } // if the span was already ended before, we don't want to end it again @@ -136,35 +136,6 @@ internal class OTelSpan: OpenTelemetryApi.Span { ddSpan.finish(at: time) } - private func makeTags() -> [String: String] { - var tags = [String: String]() - for (key, value) in attributes { - switch value { - case .string(let value): - tags[key] = value - case .bool(let value): - tags[key] = value.description - case .int(let value): - tags[key] = value.description - case .double(let value): - tags[key] = value.description - // swiftlint:disable unavailable_function - case .stringArray: - fatalError("Not implemented yet") - case .boolArray: - fatalError("Not implemented yet") - case .intArray: - fatalError("Not implemented yet") - case .doubleArray: - fatalError("Not implemented yet") - case .set: - fatalError("Not implemented yet") - // swiftlint:enable unavailable_function - } - } - return tags - } - var description: String { return "OTelSpan" } diff --git a/DatadogTrace/Tests/OpenTelemetry/OTelAttributeValue+DatadogTests.swift b/DatadogTrace/Tests/OpenTelemetry/OTelAttributeValue+DatadogTests.swift new file mode 100644 index 0000000000..7a0d69fffb --- /dev/null +++ b/DatadogTrace/Tests/OpenTelemetry/OTelAttributeValue+DatadogTests.swift @@ -0,0 +1,131 @@ +/* + * 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 + +final class OTelAttributeValueDatadogTests: XCTestCase { + func testTags_givenMultipleLevelsAttributes() { + // Given + let attributes = makeAttributes(level: 3) + + // When + let tags = attributes.tags + + let expectedTags = + [ + "key3-0": "true", + "key3-1": "value1", + "key3-2": "2", + "key3-3": "3.0", + "key3-4.0": "value4", + "key3-4.1": "value5", + "key3-5.0": "true", + "key3-5.1": "false", + "key3-6.0": "7", + "key3-6.1": "8", + "key3-7.0": "7.0", + "key3-7.1": "8.0", + "key3-8.key2-0": "true", + "key3-8.key2-1": "value1", + "key3-8.key2-2": "2", + "key3-8.key2-3": "3.0", + "key3-8.key2-4.0": "value4", + "key3-8.key2-4.1": "value5", + "key3-8.key2-5.0": "true", + "key3-8.key2-5.1": "false", + "key3-8.key2-6.0": "7", + "key3-8.key2-6.1": "8", + "key3-8.key2-7.0": "7.0", + "key3-8.key2-7.1": "8.0", + "key3-8.key2-8.key1-0": "true", + "key3-8.key2-8.key1-1": "value1", + "key3-8.key2-8.key1-2": "2", + "key3-8.key2-8.key1-3": "3.0", + "key3-8.key2-8.key1-4.0": "value4", + "key3-8.key2-8.key1-4.1": "value5", + "key3-8.key2-8.key1-5.0": "true", + "key3-8.key2-8.key1-5.1": "false", + "key3-8.key2-8.key1-6.0": "7", + "key3-8.key2-8.key1-6.1": "8", + "key3-8.key2-8.key1-7.0": "7.0", + "key3-8.key2-8.key1-7.1": "8.0", + "key3-8.key2-8.key1-8": "" // when recursion ends, empty string is returned + ] + + // Then + DDAssertDictionariesEqual(expectedTags, tags) + } + + func testTags_givenOneLevelAttributesWithEmptyCollections() { + // Given + let attributes: [String: OpenTelemetryApi.AttributeValue] = [ + "key1": .bool(true), + "key2": .string("value1"), + "key3": .int(2), + "key4": .double(3.0), + "key5": .stringArray(["value5", "value6"]), + "key6": .boolArray([true, false]), + "key7": .intArray([7, 8]), + "key8": .doubleArray([8.0, 9.0]), + "key9": .set(.init(labels: [:])), + "key10": .stringArray([]), + "key11": .boolArray([]), + "key12": .intArray([]), + "key13": .doubleArray([]), + ] + + // When + let tags = attributes.tags + + // Then + let expectedTags = + [ + "key1": "true", + "key2": "value1", + "key3": "2", + "key4": "3.0", + "key5.0": "value5", + "key5.1": "value6", + "key6.0": "true", + "key6.1": "false", + "key7.0": "7", + "key7.1": "8", + "key8.0": "8.0", + "key8.1": "9.0", + "key9": "", + "key10": "", + "key11": "", + "key12": "", + "key13": "", + ] + DDAssertDictionariesEqual(expectedTags, tags) + } + + // MARK: - Helpers + + func makeAttributes(level: UInt) -> [String: OpenTelemetryApi.AttributeValue] { + guard level > 0 else { + return [:] + } + + return [ + "key\(level)-0": .bool(true), + "key\(level)-1": .string("value1"), + "key\(level)-2": .int(2), + "key\(level)-3": .double(3.0), + "key\(level)-4": .stringArray(["value4", "value5"]), + "key\(level)-5": .boolArray([true, false]), + "key\(level)-6": .intArray([7, 8]), + "key\(level)-7": .doubleArray([7.0, 8.0]), + "key\(level)-8": .set(.init(labels: makeAttributes(level: level - 1))) + ] + } +}