diff --git a/DatadogSDKTesting.xcodeproj/project.pbxproj b/DatadogSDKTesting.xcodeproj/project.pbxproj index c00c8a50..be40c9fe 100644 --- a/DatadogSDKTesting.xcodeproj/project.pbxproj +++ b/DatadogSDKTesting.xcodeproj/project.pbxproj @@ -44,6 +44,7 @@ A7B747C02C60E1E7009B4B93 /* SwiftUtilsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7B747BF2C60E1E7009B4B93 /* SwiftUtilsTests.swift */; }; A7CC6D862C07D624003C13BC /* Tags.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7CC6D852C07D624003C13BC /* Tags.swift */; }; A7CC6D882C0F3F83003C13BC /* Tags+ObjC.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7CC6D872C0F3F83003C13BC /* Tags+ObjC.swift */; }; + A7D23AF82C9B10A8006AB41D /* SpanMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7D23AF72C9B10A8006AB41D /* SpanMetadata.swift */; }; A7E0056D2C766EF7004D4B78 /* CoreTelephony.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A7E0056C2C766EF7004D4B78 /* CoreTelephony.framework */; platformFilters = (ios, maccatalyst, macos, ); }; A7E232E52BC6E80A0087D8F8 /* CodeCoverage.h in Headers */ = {isa = PBXBuildFile; fileRef = A7E232E42BC6E80A0087D8F8 /* CodeCoverage.h */; settings = {ATTRIBUTES = (Public, ); }; }; A7E232F52BC6EB470087D8F8 /* CoverageExporterJson.cpp in Sources */ = {isa = PBXBuildFile; fileRef = A7E232EA2BC6EB460087D8F8 /* CoverageExporterJson.cpp */; }; @@ -336,6 +337,7 @@ A7C292162B9B2DE0009C2979 /* DatadogSDKTesting.podspec */ = {isa = PBXFileReference; lastKnownFileType = text; path = DatadogSDKTesting.podspec; sourceTree = ""; }; A7CC6D852C07D624003C13BC /* Tags.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tags.swift; sourceTree = ""; }; A7CC6D872C0F3F83003C13BC /* Tags+ObjC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Tags+ObjC.swift"; sourceTree = ""; }; + A7D23AF72C9B10A8006AB41D /* SpanMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpanMetadata.swift; sourceTree = ""; }; A7E0056C2C766EF7004D4B78 /* CoreTelephony.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreTelephony.framework; path = System/Library/Frameworks/CoreTelephony.framework; sourceTree = SDKROOT; }; A7E232DC2BC6E68B0087D8F8 /* CDatadogSDKTesting.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CDatadogSDKTesting.framework; sourceTree = BUILT_PRODUCTS_DIR; }; A7E232E42BC6E80A0087D8F8 /* CodeCoverage.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CodeCoverage.h; sourceTree = ""; }; @@ -848,6 +850,7 @@ E143CEFE27D7790200F4018A /* SpansExporter.swift */, E143CEFF27D7790200F4018A /* SpanEncoder.swift */, E1EBBD4627F1FFE70059BC3E /* SpanSanitizer.swift */, + A7D23AF72C9B10A8006AB41D /* SpanMetadata.swift */, ); path = Spans; sourceTree = ""; @@ -1585,6 +1588,7 @@ A7FC26462BA1ECEF00067E26 /* FilesOrchestrator.swift in Sources */, A7FC26482BA1ECEF00067E26 /* FileWriter.swift in Sources */, A7FC263C2BA1ECCF00067E26 /* DataCompression.swift in Sources */, + A7D23AF82C9B10A8006AB41D /* SpanMetadata.swift in Sources */, A7FC261A2BA1EC5A00067E26 /* DataUploadStatus.swift in Sources */, A7FC263A2BA1ECCF00067E26 /* EncodableValue.swift in Sources */, A7FC26352BA1ECCF00067E26 /* PerformancePreset.swift in Sources */, diff --git a/Sources/DatadogSDKTesting/DDTags.swift b/Sources/DatadogSDKTesting/DDTags.swift index 8963f399..1e7279f4 100644 --- a/Sources/DatadogSDKTesting/DDTags.swift +++ b/Sources/DatadogSDKTesting/DDTags.swift @@ -61,6 +61,10 @@ internal enum DDTestTags { static let testSkippedByITR = "test.skipped_by_itr" } +internal enum DDHostTags { + static let hostVCPUCount = "_dd.host.vcpu_count" +} + internal enum DDOSTags { static let osPlatform = "os.platform" static let osArchitecture = "os.architecture" diff --git a/Sources/DatadogSDKTesting/DDTest.swift b/Sources/DatadogSDKTesting/DDTest.swift index cab8e78f..1c8bf349 100644 --- a/Sources/DatadogSDKTesting/DDTest.swift +++ b/Sources/DatadogSDKTesting/DDTest.swift @@ -58,6 +58,10 @@ public class DDTest: NSObject { DDTestMonitor.tracer.addPropagationsHeadersToEnvironment() span.addTags(from: DDTestMonitor.env) + + for metric in DDTestMonitor.baseMetrics { + span.setAttribute(key: metric.key, value: metric.value) + } let functionName = suite.name + "." + name if let functionInfo = DDTestModule.bundleFunctionInfo[functionName] { diff --git a/Sources/DatadogSDKTesting/DDTestModule.swift b/Sources/DatadogSDKTesting/DDTestModule.swift index d249f69c..943b07ae 100644 --- a/Sources/DatadogSDKTesting/DDTestModule.swift +++ b/Sources/DatadogSDKTesting/DDTestModule.swift @@ -139,6 +139,8 @@ public class DDTestModule: NSObject, Encodable { ] meta.merge(DDTestMonitor.baseConfigurationTags) { _, new in new } + metrics.merge(DDTestMonitor.baseMetrics) { _, new in new } + meta.merge(defaultAttributes) { _, new in new } meta.merge(DDTestMonitor.env.gitAttributes) { _, new in new } meta.merge(DDTestMonitor.env.ciAttributes) { _, new in new } diff --git a/Sources/DatadogSDKTesting/DDTestMonitor.swift b/Sources/DatadogSDKTesting/DDTestMonitor.swift index 0c634b4a..7644de94 100644 --- a/Sources/DatadogSDKTesting/DDTestMonitor.swift +++ b/Sources/DatadogSDKTesting/DDTestMonitor.swift @@ -75,6 +75,10 @@ internal class DDTestMonitor { DDRuntimeTags.runtimeVersion: env.platform.runtimeVersion, DDUISettingsTags.uiSettingsLocalization: env.platform.localization, ] + + static var baseMetrics = [ + DDHostTags.hostVCPUCount: Double(env.platform.vCPUCount) + ] var coverageHelper: DDCoverageHelper? var gitUploader: GitUploader? diff --git a/Sources/DatadogSDKTesting/DDTestSuite.swift b/Sources/DatadogSDKTesting/DDTestSuite.swift index 3bd10578..04556936 100644 --- a/Sources/DatadogSDKTesting/DDTestSuite.swift +++ b/Sources/DatadogSDKTesting/DDTestSuite.swift @@ -14,6 +14,7 @@ public class DDTestSuite: NSObject, Encodable { let startTime: Date var duration: UInt64 var meta: [String: String] = [:] + var metrics: [String: Double] = [:] var status: DDTestStatus var unskippable: Bool = false var localization: String @@ -63,6 +64,8 @@ public class DDTestSuite: NSObject, Encodable { ] meta.merge(DDTestMonitor.baseConfigurationTags) { _, new in new } + metrics.merge(DDTestMonitor.baseMetrics) { _, new in new } + meta.merge(defaultAttributes) { _, new in new } meta.merge(DDTestMonitor.env.gitAttributes) { _, new in new } meta.merge(DDTestMonitor.env.ciAttributes) { _, new in new } @@ -112,6 +115,7 @@ extension DDTestSuite { case start case duration case meta + case metrics case error case name case resource @@ -126,6 +130,7 @@ extension DDTestSuite { try container.encode(startTime.timeIntervalSince1970.toNanoseconds, forKey: .start) try container.encode(duration, forKey: .duration) try container.encode(meta, forKey: .meta) + try container.encode(metrics, forKey: .metrics) try container.encode(status == .fail ? 1 : 0, forKey: .error) try container.encode("\(module.testFramework).suite", forKey: .name) try container.encode("\(name)", forKey: .resource) diff --git a/Sources/DatadogSDKTesting/DDTracer.swift b/Sources/DatadogSDKTesting/DDTracer.swift index 2c79b92e..5674d7bf 100644 --- a/Sources/DatadogSDKTesting/DDTracer.swift +++ b/Sources/DatadogSDKTesting/DDTracer.swift @@ -70,13 +70,13 @@ internal class DDTracer { let exporterConfiguration = ExporterConfiguration( serviceName: conf.service ?? env.git.repositoryName ?? "unknown-swift-repo", - libraryVersion: DDTestMonitor.tracerVersion, applicationName: identifier, applicationVersion: version, environment: env.environment, hostname: hostnameToReport, apiKey: conf.apiKey ?? "", endpoint: conf.endpoint.exporterEndpoint, + metadata: .init(libraryVersion: DDTestMonitor.tracerVersion), payloadCompression: payloadCompression, performancePreset: .instantDataDelivery, exporterId: String(SpanId.random().rawValue), @@ -373,3 +373,11 @@ internal class DDTracer { return eventsExporter?.endpointURLs() ?? Set() } } + +private extension SpanMetadata { + init(libraryVersion: String) { + self.init() + self[string: DDGenericTags.language] = "swift" + self[string: DDGenericTags.libraryVersion] = libraryVersion + } +} diff --git a/Sources/DatadogSDKTesting/Environment/Environment.swift b/Sources/DatadogSDKTesting/Environment/Environment.swift index 5a4dd4ba..284027cd 100644 --- a/Sources/DatadogSDKTesting/Environment/Environment.swift +++ b/Sources/DatadogSDKTesting/Environment/Environment.swift @@ -43,7 +43,8 @@ internal final class Environment { osArchitecture: PlatformUtils.getPlatformArchitecture(), osVersion: PlatformUtils.getDeviceVersion(), runtimeName: runtimeName, runtimeVersion: runtimeVersion, - localization: PlatformUtils.getLocalization()) + localization: PlatformUtils.getLocalization(), + vCPUCount: PlatformUtils.getCpuCount()) let ciInfo = ciReaders.reduce(nil) { (ci, reader) in @@ -220,6 +221,8 @@ internal extension Environment { let localization: String + let vCPUCount: Int + var debugDescription: String { """ Platform: @@ -230,6 +233,7 @@ internal extension Environment { OS Version: \(osVersion) Runtime Name: \(runtimeName) Runtime Version: \(runtimeVersion) + vCPU Count: \(vCPUCount) Localization: \(localization) """ } diff --git a/Sources/DatadogSDKTesting/Utils/PlatformUtils.swift b/Sources/DatadogSDKTesting/Utils/PlatformUtils.swift index d46a74f4..04d0081d 100644 --- a/Sources/DatadogSDKTesting/Utils/PlatformUtils.swift +++ b/Sources/DatadogSDKTesting/Utils/PlatformUtils.swift @@ -88,6 +88,10 @@ struct PlatformUtils { return (ProcessInfo.processInfo.processName, (Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String) ?? "") } } + + static func getCpuCount() -> Int { + ProcessInfo.processInfo.processorCount + } static func getAppearance() -> String { #if os(iOS) || os(tvOS) || os(watchOS) diff --git a/Sources/EventsExporter/ExporterConfiguration.swift b/Sources/EventsExporter/ExporterConfiguration.swift index e3922575..cfd25e52 100644 --- a/Sources/EventsExporter/ExporterConfiguration.swift +++ b/Sources/EventsExporter/ExporterConfiguration.swift @@ -13,10 +13,8 @@ internal struct ExporterError: Error, CustomStringConvertible { public struct ExporterConfiguration { /// The name of the service, resource, version,... that will be reported to the backend. var serviceName: String - var libraryVersion: String var applicationName: String var version: String - var environment: String var hostname: String? /// API key for authentication @@ -34,21 +32,26 @@ public struct ExporterConfiguration { /// Exporter ID for tracing var exporterId: String + var metadata: SpanMetadata + + var environment: String { + didSet { _setEnv() } + } + var logger: Logger var debug: Debug public init( - serviceName: String, libraryVersion: String, applicationName: String, - applicationVersion: String, environment: String, hostname: String?, apiKey: String, - endpoint: Endpoint, payloadCompression: Bool = true, source: String = "ios", + serviceName: String, applicationName: String, applicationVersion: String, + environment: String, hostname: String?, apiKey: String, + endpoint: Endpoint, metadata: SpanMetadata, + payloadCompression: Bool = true, source: String = "ios", performancePreset: PerformancePreset = .default, exporterId: String, logger: Logger, debug: Debug = .init() ) { self.serviceName = serviceName - self.libraryVersion = libraryVersion self.applicationName = applicationName self.version = applicationVersion - self.environment = environment self.hostname = hostname self.apiKey = apiKey self.endpoint = endpoint @@ -56,8 +59,15 @@ public struct ExporterConfiguration { self.source = source self.performancePreset = performancePreset self.exporterId = exporterId + self.metadata = metadata self.logger = logger self.debug = debug + self.environment = environment + _setEnv() + } + + private mutating func _setEnv() { + metadata[string: "env"] = environment } } diff --git a/Sources/EventsExporter/Spans/SpanMetadata.swift b/Sources/EventsExporter/Spans/SpanMetadata.swift new file mode 100644 index 00000000..92a35ff7 --- /dev/null +++ b/Sources/EventsExporter/Spans/SpanMetadata.swift @@ -0,0 +1,208 @@ +/* + * 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 2020-Present Datadog, Inc. + */ + +import Foundation + +public struct SpanMetadata { + private var meta: [String: [String: Value]] + + public subscript(bool key: String) -> Bool? { + get { self[bool: .generic, key] } + set { self[bool: .generic, key] = newValue } + } + + public subscript(string key: String) -> String? { + get { self[string: .generic, key] } + set { self[string: .generic, key] = newValue } + } + + public subscript(bool type: SpanType, key: String) -> Bool? { + get { self[type, key]?.bool } + set { self[type, key] = newValue.map { .bool($0) } } + } + + public subscript(string type: SpanType, key: String) -> String? { + get { self[type, key]?.string } + set { self[type, key] = newValue.map { .string($0) } } + } + + public subscript(type: SpanType, key: String) -> Value? { + get { meta[type.rawValue]?[key] } + set { + var tStorage = meta[type.rawValue] ?? [:] + tStorage[key] = newValue + meta[type.rawValue] = tStorage + } + } + + public init() { + self.meta = [:] + } + + public var metadata: [String: [String: Value]] { + meta.compactMapValues { + let mapped = $0.compactMapValues { $0.forMetadata } + return mapped.count > 0 ? mapped : nil + } + } +} + +public extension SpanMetadata { + struct SpanType: RawRepresentable, Encodable, Hashable, ExpressibleByStringLiteral { + public typealias StringLiteralType = String + public typealias RawValue = String + + public let rawValue: String + + public init(_ key: String) { + self.rawValue = key + } + + public init(stringLiteral value: String) { + self.init(value) + } + + public init?(rawValue: String) { + self.init(rawValue) + } + + public func encode(to encoder: Encoder) throws { + try rawValue.encode(to: encoder) + } + + @inlinable static var generic: SpanType { "*" } + } +} + +public extension SpanMetadata { + enum Value: Encodable, ExpressibleByNilLiteral, ExpressibleByStringLiteral, ExpressibleByIntegerLiteral, ExpressibleByFloatLiteral { + case int(Int) + case double(Double) + case string(String) + case bool(Bool) + case none(isNumber: Bool) + + public typealias StringLiteralType = String + public typealias IntegerLiteralType = Int + public typealias FloatLiteralType = Double + + public init(nilLiteral: ()) { + self = .none(isNumber: false) + } + + public init(integerLiteral value: Int) { + self = .int(value) + } + + public init(floatLiteral value: Double) { + self = .double(value) + } + + public init(stringLiteral value: String) { + self = .string(value) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + switch self { + case .none: try container.encodeNil() + case .int(let i): try container.encode(i) + case .double(let d): try container.encode(d) + case .string(let s): try container.encode(s) + case .bool(let b): try container.encode(b ? "true" : "false") + } + } + + public var bool: Bool? { + switch self { + case .bool(let b): return b + case .double(let d): return !d.isZero + case .int(let i): return i != 0 + case .string(let s): + switch s.lowercased() { + case "true", "1": return true + case "false", "0": return false + default: return nil + } + default: return nil + } + } + + public var int: Int? { + switch self { + case .int(let i): return i + case .double(let d): return Int(exactly: d) + case .bool(let b): return b ? 1 : 0 + default: return nil + } + } + + public var double: Double? { + switch self { + case .int(let i): return Double(exactly: i) + case .double(let d): return d + case .bool(let b): return b ? 1.0 : 0.0 + default: return nil + } + } + + public var string: String? { + switch self { + case .int(let i): return String(i, radix: 10) + case .double(let d): return String(d) + case .bool(let b): return b ? "true" : "false" + case .string(let s): return s + default: return nil + } + } + + public var forMetrics: Self? { + switch self { + case .double, .int: return self + case .none(isNumber: let n): return n ? self : nil + default: return nil + } + } + + public var forMetadata: Self? { + switch self { + case .string, .bool: return self + case .none(isNumber: let n): return n ? nil : self + default: return nil + } + } + } +} + +// Metrics +public extension SpanMetadata { + subscript(int key: String) -> Int? { + get { self[int: .generic, key] } + set { self[int: .generic, key] = newValue } + } + + subscript(double key: String) -> Double? { + get { self[double: .generic, key] } + set { self[double: .generic, key] = newValue } + } + + subscript(int type: SpanType, key: String) -> Int? { + get { self[type, key]?.int } + set { self[type, key] = newValue.map { .int($0) } } + } + + subscript(double type: SpanType, key: String) -> Double? { + get { self[type, key]?.double } + set { self[type, key] = newValue.map { .double($0) } } + } + + var metrics: [String: [String: Value]] { + meta.compactMapValues { + let mapped = $0.compactMapValues { $0.forMetrics } + return mapped.count > 0 ? mapped : nil + } + } +} diff --git a/Sources/EventsExporter/Spans/SpansExporter.swift b/Sources/EventsExporter/Spans/SpansExporter.swift index fd5535f4..e8e2979d 100644 --- a/Sources/EventsExporter/Spans/SpansExporter.swift +++ b/Sources/EventsExporter/Spans/SpansExporter.swift @@ -12,7 +12,7 @@ internal class SpansExporter { let configuration: ExporterConfiguration let spansStorage: FeatureStorage let spansUpload: FeatureUpload - let runtimeId = UUID().uuidString + let runtimeId: String init(config: ExporterConfiguration) throws { self.configuration = config @@ -22,16 +22,22 @@ internal class SpansExporter { performance: configuration.performancePreset, dateProvider: SystemDateProvider() ) - - let genericMetadata = """ - "*": { "env": "\(configuration.environment)", "runtime-id": "\(runtimeId)", "language": "swift", "library_version": "\(configuration.libraryVersion)"} - """ - + + var metadata = config.metadata + + self.runtimeId = metadata[string: "runtime-id"] ?? UUID().uuidString + metadata[string: "runtime-id"] = self.runtimeId + + let encodedMetadata = String(data: try JSONEncoder().encode(metadata.metadata), encoding: .utf8)! + let prefix = """ - {"version": 1, "metadata": { \(genericMetadata) }, "events": [ + { + "version": 1, + "metadata": \(encodedMetadata), + "events": [ """ - let suffix = "]}" + let suffix = "]\n}" let dataFormat = DataFormat(prefix: prefix, suffix: suffix, separator: ",") diff --git a/Sources/EventsExporter/Utils/EncodableValue.swift b/Sources/EventsExporter/Utils/EncodableValue.swift index 8c503d06..f23f5d24 100644 --- a/Sources/EventsExporter/Utils/EncodableValue.swift +++ b/Sources/EventsExporter/Utils/EncodableValue.swift @@ -8,9 +8,9 @@ import Foundation /// Type erasure `Encodable` wrapper. internal struct EncodableValue: Encodable { - let value: Encodable + let value: any Encodable - init(_ value: Encodable) { + init(_ value: any Encodable) { self.value = value } diff --git a/Tests/EventsExporter/EventsExporterTests.swift b/Tests/EventsExporter/EventsExporterTests.swift index 8708618e..43b9d3cb 100644 --- a/Tests/EventsExporter/EventsExporterTests.swift +++ b/Tests/EventsExporter/EventsExporterTests.swift @@ -47,7 +47,6 @@ class EventsExporterTests: XCTestCase { let instrumentationLibraryVersion = "semver:0.1.0" let exporterConfiguration = ExporterConfiguration(serviceName: "serviceName", - libraryVersion: "0.0", applicationName: "applicationName", applicationVersion: "applicationVersion", environment: "environment", @@ -57,6 +56,7 @@ class EventsExporterTests: XCTestCase { testsURL: URL(string: "http://localhost:33333/traces")!, logsURL: URL(string: "http://localhost:33333/logs")! ), + metadata: .init(), exporterId: "exporterId", logger: Log()) diff --git a/Tests/EventsExporter/Logs/LogsExporterTests.swift b/Tests/EventsExporter/Logs/LogsExporterTests.swift index a9b2d644..527f7955 100644 --- a/Tests/EventsExporter/Logs/LogsExporterTests.swift +++ b/Tests/EventsExporter/Logs/LogsExporterTests.swift @@ -37,7 +37,7 @@ class LogsExporterTests: XCTestCase { } } - let configuration = ExporterConfiguration(serviceName: "serviceName", libraryVersion: "0.0", + let configuration = ExporterConfiguration(serviceName: "serviceName", applicationName: "applicationName", applicationVersion: "applicationVersion", environment: "environment", @@ -47,6 +47,7 @@ class LogsExporterTests: XCTestCase { testsURL: URL(string: "http://localhost:33333/traces")!, logsURL: URL(string: "http://localhost:33333/logs")! ), + metadata: .init(), performancePreset: .instantDataDelivery, exporterId: "exporterId", logger: Log()) diff --git a/Tests/EventsExporter/Spans/SpansExporterTests.swift b/Tests/EventsExporter/Spans/SpansExporterTests.swift index e743bb35..91dbdbbe 100644 --- a/Tests/EventsExporter/Spans/SpansExporterTests.swift +++ b/Tests/EventsExporter/Spans/SpansExporterTests.swift @@ -38,7 +38,6 @@ class SpansExporterTests: XCTestCase { } let configuration = ExporterConfiguration(serviceName: "serviceName", - libraryVersion: "0.0", applicationName: "applicationName", applicationVersion: "applicationVersion", environment: "environment", @@ -48,6 +47,7 @@ class SpansExporterTests: XCTestCase { testsURL: URL(string: "http://localhost:33333/traces")!, logsURL: URL(string: "http://localhost:33333/logs")! ), + metadata: .init(), performancePreset: .instantDataDelivery, exporterId: "exporterId", logger: Log())