diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index 1a4d964fd..c7b2ce6e1 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -42,4 +42,4 @@ jobs: - name: Build run: | cd swift/ITSClient - swift test --no-parallel \ No newline at end of file + swift test --no-parallel --skip OpenTelemetryClientTests diff --git a/README.md b/README.md index 189506097..0b7775b39 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ by propagating the [W3C Trace Context][12] using MQTTv5 properties. Sent traces include the following parameters: -- `service_name` client app name (configurable) +- `service.name` client app name (configurable) - `span.status` Error if anything went wrong, Unset otherwise - `span.name` IoT3 Core MQTT Message - `span.kind` diff --git a/THIRD-PARTY.md b/THIRD-PARTY.md index de50f4455..6f795b564 100644 --- a/THIRD-PARTY.md +++ b/THIRD-PARTY.md @@ -179,3 +179,7 @@ #### MQTT NIO - [Source code](https://github.com/swift-server-community/mqtt-nio) + +#### opentelemetry-swift + +- [Source code](https://github.com/open-telemetry/opentelemetry-swift) diff --git a/licenses/LICENSE-opentelemetry-swift (Apache 2.0).txt b/licenses/LICENSE-opentelemetry-swift (Apache 2.0).txt new file mode 100644 index 000000000..261eeb9e9 --- /dev/null +++ b/licenses/LICENSE-opentelemetry-swift (Apache 2.0).txt @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/swift/ITSClient/Package.resolved b/swift/ITSClient/Package.resolved index f2281d6f7..a92b2e059 100644 --- a/swift/ITSClient/Package.resolved +++ b/swift/ITSClient/Package.resolved @@ -1,6 +1,15 @@ { - "originHash" : "bd5288df1fbedfe47af38ffab17343277d7d0be80f931c25af84caaeb2d3f3d9", + "originHash" : "ec284ca5fac3dac5fdf7e779cd882ab66e8cabd9efc13a683df3b2436fa540e2", "pins" : [ + { + "identity" : "grpc-swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/grpc/grpc-swift.git", + "state" : { + "revision" : "8c5e99d0255c373e0330730d191a3423c57373fb", + "version" : "1.24.2" + } + }, { "identity" : "mqtt-nio", "kind" : "remoteSourceControl", @@ -10,6 +19,24 @@ "version" : "2.11.0" } }, + { + "identity" : "opentelemetry-swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/open-telemetry/opentelemetry-swift.git", + "state" : { + "revision" : "f2315d8646432c02338960e85b5fe20417ad6d8d", + "version" : "1.12.1" + } + }, + { + "identity" : "opentracing-objc", + "kind" : "remoteSourceControl", + "location" : "https://github.com/undefinedlabs/opentracing-objc", + "state" : { + "revision" : "18c1a35ca966236cee0c5a714a51a73ff33384c1", + "version" : "0.5.2" + } + }, { "identity" : "swift-atomics", "kind" : "remoteSourceControl", @@ -28,6 +55,15 @@ "version" : "1.1.4" } }, + { + "identity" : "swift-http-types", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-http-types", + "state" : { + "revision" : "ef18d829e8b92d731ad27bb81583edd2094d1ce3", + "version" : "1.3.1" + } + }, { "identity" : "swift-log", "kind" : "remoteSourceControl", @@ -37,6 +73,15 @@ "version" : "1.6.2" } }, + { + "identity" : "swift-metrics", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-metrics.git", + "state" : { + "revision" : "e0165b53d49b413dd987526b641e05e246782685", + "version" : "2.5.0" + } + }, { "identity" : "swift-nio", "kind" : "remoteSourceControl", @@ -46,6 +91,24 @@ "version" : "2.77.0" } }, + { + "identity" : "swift-nio-extras", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-extras.git", + "state" : { + "revision" : "2e9746cfc57554f70b650b021b6ae4738abef3e6", + "version" : "1.24.1" + } + }, + { + "identity" : "swift-nio-http2", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-http2.git", + "state" : { + "revision" : "170f4ca06b6a9c57b811293cebcb96e81b661310", + "version" : "1.35.0" + } + }, { "identity" : "swift-nio-ssl", "kind" : "remoteSourceControl", @@ -64,6 +127,15 @@ "version" : "1.23.0" } }, + { + "identity" : "swift-protobuf", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-protobuf.git", + "state" : { + "revision" : "ebc7251dd5b37f627c93698e4374084d98409633", + "version" : "1.28.2" + } + }, { "identity" : "swift-system", "kind" : "remoteSourceControl", @@ -72,6 +144,15 @@ "revision" : "c8a44d836fe7913603e246acab7c528c2e780168", "version" : "1.4.0" } + }, + { + "identity" : "thrift-swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/undefinedlabs/Thrift-Swift", + "state" : { + "revision" : "18ff09e6b30e589ed38f90a1af23e193b8ecef8e", + "version" : "1.1.2" + } } ], "version" : 3 diff --git a/swift/ITSClient/Package.swift b/swift/ITSClient/Package.swift index d82121ecf..5762f5037 100644 --- a/swift/ITSClient/Package.swift +++ b/swift/ITSClient/Package.swift @@ -15,7 +15,8 @@ let package = Package( targets: ["ITSCore"]), ], dependencies: [ - .package(url: "https://github.com/swift-server-community/mqtt-nio.git", from: "2.11.0") + .package(url: "https://github.com/swift-server-community/mqtt-nio.git", from: "2.11.0"), + .package(url: "https://github.com/open-telemetry/opentelemetry-swift.git", from: "1.12.1") ], targets: [ .target( @@ -23,6 +24,7 @@ let package = Package( dependencies: [ .product(name: "MQTTNIO", package: "mqtt-nio"), + .product(name: "OpenTelemetryProtocolExporterHTTP", package: "opentelemetry-swift"), ] ), .testTarget( diff --git a/swift/ITSClient/Sources/ITSCore/Telemetry/OpenTelemetryClient.swift b/swift/ITSClient/Sources/ITSCore/Telemetry/OpenTelemetryClient.swift new file mode 100644 index 000000000..8852b8ab4 --- /dev/null +++ b/swift/ITSClient/Sources/ITSCore/Telemetry/OpenTelemetryClient.swift @@ -0,0 +1,159 @@ +/* + * Software Name : ITSClient + * SPDX-FileCopyrightText: Copyright (c) Orange SA + * SPDX-License-Identifier: MIT + * + * This software is distributed under the MIT license, + * see the "LICENSE.txt" file for more details or https://opensource.org/license/MIT/ + * + * Software description: Swift ITS client. + */ + +import Foundation +@preconcurrency import OpenTelemetryApi +@preconcurrency import OpenTelemetryProtocolExporterCommon +@preconcurrency import OpenTelemetryProtocolExporterHttp +@preconcurrency import OpenTelemetrySdk + +actor OpenTelemetryClient: TelemetryClient { + private let configuration: TelemetryClientConfiguration + private var tracer: Tracer? + private var tracerProvider: TracerProviderSdk? + private var spans = [SpanID: Span]() + + init(configuration: TelemetryClientConfiguration) { + self.configuration = configuration + } + + func start() { + guard tracer == nil else { return } + + var credentials: String? + if let user = configuration.user, let password = configuration.password { + credentials = "\(user):\(password)".data(using: .utf8)?.base64EncodedString() + } + let headers = credentials.map { [("Authorization", "Basic \($0)")] } + let otlpConfiguration = OtlpConfiguration(headers: headers) + let url = configuration.url.appendingPathComponent("v1/traces") + let httpTraceExporter = OtlpHttpTraceExporter(endpoint: url, config: otlpConfiguration) + let batchSpanProcessor = BatchSpanProcessor(spanExporter: httpTraceExporter, + scheduleDelay: configuration.scheduleDelay, + maxExportBatchSize: configuration.batchSize) + let resource = Resource(attributes: [ResourceAttributes.serviceName.rawValue: .string(configuration.serviceName)]) + let tracerProvider = TracerProviderSdk(resource: resource, + spanProcessors: [batchSpanProcessor]) + + OpenTelemetry.registerTracerProvider(tracerProvider: tracerProvider) + OpenTelemetry.registerPropagators(textPropagators: [W3CTraceContextPropagator()], + baggagePropagator: W3CBaggagePropagator()) + tracer = OpenTelemetry.instance.tracerProvider.get(instrumentationName: configuration.serviceName, + instrumentationVersion: nil) + self.tracerProvider = tracerProvider + } + + func stop() { + tracerProvider?.shutdown() + spans.removeAll() + tracerProvider = nil + tracer = nil + } + + func startSpan(name: String, type: SpanType, attributes: [String: Any]) -> SpanID? { + guard let tracer else { return nil } + + let kind = spanKind(from: type) + + let span = tracer.spanBuilder(spanName: name).setSpanKind(spanKind: kind).startSpan() + attributes.forEach { + span.setAttribute(key: $0.key, value: AttributeValue($0.value)) + } + + let spanID = save(span) + + return spanID + } + + func startSpan( + name: String, + type: SpanType, + attributes: [String: Any], + fromContext context: [String: String] + ) -> SpanID? { + guard let tracer else { return nil } + + let getter = OpenTelemetryGetter() + let extractedSpanContext = OpenTelemetry.instance + .propagators + .textMapPropagator + .extract(carrier: context, getter: getter) + + let spanBuilder = tracer.spanBuilder(spanName: name) + .setSpanKind(spanKind: spanKind(from: type)) + if let extractedSpanContext { + let spanContext = SpanContext.createFromRemoteParent(traceId: extractedSpanContext.traceId, + spanId: extractedSpanContext.spanId, + traceFlags: TraceFlags(), + traceState: TraceState()) + spanBuilder.addLink(spanContext: spanContext) + } + let span = spanBuilder.startSpan() + + attributes.forEach { + span.setAttribute(key: $0.key, value: AttributeValue($0.value)) + } + + let spanID = save(span) + + return spanID + } + + func stopSpan(spanID: SpanID, errorMessage: String?) { + guard let span = spans[spanID] else { return } + + if let errorMessage { + span.status = .error(description: errorMessage) + } + + span.end() + + spans.removeValue(forKey: spanID) + } + + func updateContext(withSpanID spanID: SpanID) -> [String: String] { + guard let span = spans[spanID] else { return [:] } + + var context = [String: String]() + let setter = OpenTelemetrySetter() + OpenTelemetry.instance.propagators.textMapPropagator.inject(spanContext: span.context, + carrier: &context, + setter: setter) + return context + } + + private func spanKind(from spanType: SpanType) -> SpanKind { + switch spanType { + case .consumer: return .consumer + case .producer: return .producer + } + } + + private func save(_ span: Span) -> SpanID { + let spanID = span.context.spanId.hexString + spans[spanID] = span + return spanID + } +} + +struct OpenTelemetrySetter: Setter { + func set(carrier: inout [String: String], key: String, value: String) { + carrier[key] = value + } +} + +struct OpenTelemetryGetter: Getter { + func get(carrier: [String: String], key: String) -> [String]? { + guard let value = carrier[key] else { return nil } + + return [value] + } +} diff --git a/swift/ITSClient/Sources/ITSCore/Telemetry/TelemetryClient.swift b/swift/ITSClient/Sources/ITSCore/Telemetry/TelemetryClient.swift new file mode 100644 index 000000000..fb960ba0a --- /dev/null +++ b/swift/ITSClient/Sources/ITSCore/Telemetry/TelemetryClient.swift @@ -0,0 +1,39 @@ +/* + * Software Name : ITSClient + * SPDX-FileCopyrightText: Copyright (c) Orange SA + * SPDX-License-Identifier: MIT + * + * This software is distributed under the MIT license, + * see the "LICENSE.txt" file for more details or https://opensource.org/license/MIT/ + * + * Software description: Swift ITS client. + */ + +import Foundation + +protocol TelemetryClient: Actor { + init(configuration: TelemetryClientConfiguration) + func start() + func stop() + func startSpan(name: String, type: SpanType, attributes: [String: Any]) -> SpanID? + func startSpan( + name: String, + type: SpanType, + attributes: [String: Any], + fromContext context: [String: String] + ) -> SpanID? + func stopSpan(spanID: SpanID, errorMessage: String?) + func updateContext(withSpanID spanID: SpanID) -> [String: String] +} + +extension TelemetryClient { + func stopSpan(spanID: SpanID) { + stopSpan(spanID: spanID, errorMessage: nil) + } +} + +typealias SpanID = String + +enum SpanType { + case consumer, producer +} diff --git a/swift/ITSClient/Sources/ITSCore/Telemetry/TelemetryClientConfiguration.swift b/swift/ITSClient/Sources/ITSCore/Telemetry/TelemetryClientConfiguration.swift new file mode 100644 index 000000000..e39a5d05f --- /dev/null +++ b/swift/ITSClient/Sources/ITSCore/Telemetry/TelemetryClientConfiguration.swift @@ -0,0 +1,37 @@ +/* + * Software Name : ITSClient + * SPDX-FileCopyrightText: Copyright (c) Orange SA + * SPDX-License-Identifier: MIT + * + * This software is distributed under the MIT license, + * see the "LICENSE.txt" file for more details or https://opensource.org/license/MIT/ + * + * Software description: Swift ITS client. + */ + +import Foundation + +struct TelemetryClientConfiguration { + let url: URL + let user: String? + let password: String? + let serviceName: String + let scheduleDelay: TimeInterval + let batchSize: Int + + init( + url: URL, + user: String? = nil, + password: String? = nil, + serviceName: String, + scheduleDelay: TimeInterval = 5, + batchSize: Int = 50 + ) { + self.url = url + self.user = user + self.password = password + self.serviceName = serviceName + self.scheduleDelay = scheduleDelay + self.batchSize = batchSize + } +} diff --git a/swift/ITSClient/Tests/ITSCoreTests/OpenTelemetryClientTests.swift b/swift/ITSClient/Tests/ITSCoreTests/OpenTelemetryClientTests.swift new file mode 100644 index 000000000..4f234e138 --- /dev/null +++ b/swift/ITSClient/Tests/ITSCoreTests/OpenTelemetryClientTests.swift @@ -0,0 +1,97 @@ +/* + * Software Name : ITSClient + * SPDX-FileCopyrightText: Copyright (c) Orange SA + * SPDX-License-Identifier: MIT + * + * This software is distributed under the MIT license, + * see the "LICENSE.txt" file for more details or https://opensource.org/license/MIT/ + * + * Software description: Swift ITS client. + */ + +import Foundation +import Testing +@testable import ITSCore + +struct OpenTelemetryClientTests { + private let telemetryClientConfiguration: TelemetryClientConfiguration + + init() throws { + let url = try #require(URL(string: "http://localhost:4318")) + telemetryClientConfiguration = TelemetryClientConfiguration(url: url, + serviceName: "its-tests-service") + } + + @Test("Send several consumer and producer spans") + func send_several_consumer_and_producer_spans() async throws { + let openTelemetryClient = OpenTelemetryClient(configuration: telemetryClientConfiguration) + + await openTelemetryClient.start() + await withThrowingTaskGroup(of: Void.self) { group in + for taskIndex in 0..<20 { + group.addTask { + if taskIndex.isMultiple(of: 2) { + try await startConsumerSpan(telemetryClient: openTelemetryClient) + } else { + try await startProducerSpan(telemetryClient: openTelemetryClient) + } + } + } + } + + await openTelemetryClient.stop() + try await Task.sleep(for: .seconds(0.1)) + } + + @Test("Send a producer and a child consumer span") + func send_producer_and_child_consumer_span() async throws { + let openTelemetryClient = OpenTelemetryClient(configuration: telemetryClientConfiguration) + + await openTelemetryClient.start() + let traceParent = try await startProducerSpan(telemetryClient: openTelemetryClient) + try await startConsumerSpan(telemetryClient: openTelemetryClient, + traceParent: traceParent) + + await openTelemetryClient.stop() + try await Task.sleep(for: .seconds(0.1)) + } + + @Test("Send a producer span with error") + func send_producer_span_with_error() async throws { + let openTelemetryClient = OpenTelemetryClient(configuration: telemetryClientConfiguration) + + await openTelemetryClient.start() + try await startProducerSpan(telemetryClient: openTelemetryClient, + errorMessage: "Test error") + + await openTelemetryClient.stop() + try await Task.sleep(for: .seconds(0.1)) + } + + private func startConsumerSpan(telemetryClient: TelemetryClient, traceParent: String? = nil) async throws { + let consumerAttributes = ["testAtribute": "consumer"] + let context = traceParent.map { ["traceparent": $0] } ?? [:] + let consumerSpanID = try #require(await telemetryClient.startSpan(name: "Consumer span", + type: .consumer, + attributes: consumerAttributes, + fromContext: context)) + try await Task.sleep(for: .seconds(0.1)) + await telemetryClient.stopSpan(spanID: consumerSpanID) + } + + @discardableResult + private func startProducerSpan( + telemetryClient: TelemetryClient, + errorMessage: String? = nil + ) async throws -> String? { + let producerAttributes = ["testAtribute": "producer"] + let producerSpanID = try #require(await telemetryClient.startSpan(name: "Producer span", + type: .producer, + attributes: producerAttributes)) + let context = await telemetryClient.updateContext(withSpanID: producerSpanID) + try await Task.sleep(for: .seconds(0.1)) + await telemetryClient.stopSpan(spanID: producerSpanID, errorMessage: errorMessage) + + return context["traceparent"] + } +}