Repository: apple/swift-distributed-tracing (swift)

Issue:
SpanAttributes subscript should use concrete type and not an existential
Currently `SpanAttributes` uses an existential for its subscript API. 

```swift
public struct SpanAttributes {
  public subscript(_ name: String) -> SpanAttributeConvertible? { get set }
}
```

Swift Logging uses the concrete type `Logger.Metadata.Value` instead:
```swift
public struct Logger {
    subscript(metadataKey metadataKey: String) -> Logger.MetadataValue? { get set }

    public enum MetadataValue {
        case string(String)
        case stringConvertible(CustomStringConvertible & Sendable)
        case dictionary(Metadata)
        case array([Metadata.Value])
    }
}
```

I think that we should change the SpanAttributes subscript API to return a `SpanAttribute`. This way this would work like Logging and we remove an existential from the API boundary

Pull request description:
Provide explicit set/get to avoid SpanAttributeConvertible existential wrapping, to those who care about such overhead

**Motivation:**

Provide more control and allow avoiding the existential SpanAtrributeConvertible access when using the subscript.

**Modifications:**

additional API to avoid the existential

**Result:**
There is a way to use the API completely avoiding existentials

resolves https://github.com/apple/swift-distributed-tracing/issues/82

Patch:
diff --git a/Sources/Tracing/SpanProtocol.swift b/Sources/Tracing/SpanProtocol.swift
index 7ec5d1ef..30eef12d 100644
--- a/Sources/Tracing/SpanProtocol.swift
+++ b/Sources/Tracing/SpanProtocol.swift
@@ -537,6 +537,16 @@ extension SpanAttributes {
         }
     }
 
+    /// Similar to `subscript(_:)` however returns the stored `SpanAttribute` rather than going through `SpanAttributeConvertible`.
+    public func get(_ name: String) -> SpanAttribute? {
+        self._attributes[name]
+    }
+
+    /// Similar to `subscript(_:)` however accepts a `SpanAttribute` rather than going through `SpanAttributeConvertible`.
+    public mutating func set(_ name: String, value: SpanAttribute?) {
+        self._attributes[name] = value
+    }
+
     /// - Parameter callback: The function to call for each attribute.
     public func forEach(_ callback: (String, SpanAttribute) -> Void) {
         self._attributes.forEach {


Test patch:
diff --git a/Tests/TracingTests/SpanTests.swift b/Tests/TracingTests/SpanTests.swift
index 85d6009e..1ddd03b1 100644
--- a/Tests/TracingTests/SpanTests.swift
+++ b/Tests/TracingTests/SpanTests.swift
@@ -181,15 +181,48 @@ final class SpanTests: XCTestCase {
 
         XCTAssertEqual(child.links.count, 1)
         XCTAssertEqual(child.links[0].baggage[TestBaggageContextKey.self], "test")
-        #if swift(>=5.2)
         XCTAssertEqual(child.links[0].attributes.sampleHttp.statusCode, 418)
-        #endif
         guard case .some(.int64(let statusCode)) = child.links[0].attributes["http.status_code"]?.toSpanAttribute() else {
             XCTFail("Expected int value for http.status_code")
             return
         }
         XCTAssertEqual(statusCode, 418)
     }
+
+    func testSpanAttributeSetterGetter() {
+        var parentBaggage = Baggage.topLevel
+        parentBaggage[TestBaggageContextKey.self] = "test"
+
+        let parent = TestSpan(
+            operationName: "client",
+            startTime: .now(),
+            baggage: parentBaggage,
+            kind: .client,
+            onEnd: { _ in }
+        )
+        let childBaggage = Baggage.topLevel
+        let child = TestSpan(
+            operationName: "server",
+            startTime: .now(),
+            baggage: childBaggage,
+            kind: .server,
+            onEnd: { _ in }
+        )
+
+        var attributes = SpanAttributes()
+        attributes.set("http.status_code", value: .int32(418))
+        child.addLink(parent, attributes: attributes)
+
+        XCTAssertEqual(child.links.count, 1)
+        XCTAssertEqual(child.links[0].baggage[TestBaggageContextKey.self], "test")
+        XCTAssertEqual(child.links[0].attributes.sampleHttp.statusCode, 418)
+        guard case .some(.int32(let statusCode)) = child.links[0].attributes["http.status_code"]?.toSpanAttribute() else {
+            XCTFail("Expected int value for http.status_code")
+            return
+        }
+        XCTAssertEqual(statusCode, 418)
+        XCTAssertEqual(attributes.get("http.status_code"), SpanAttribute.int32(418))
+    }
 }
 
 // ==== ----------------------------------------------------------------------------------------------------------------