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

Issue:
Allow mutation of span operation name
We might want to consider offering an API to rename an already started span. This is [part of the OTel spec](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/api.md#updatename) but comes with a disadvantage for sampling.

### Use-Case: HTTP Server

HTTP servers might want to rename a span after creation, because they can come up with a more relevant span in the meantime.

1. Immediately start span when receiving a request (`operationName` = `request.head.uri`)
2. Route request
  2.1. Route Not Found --> Record error
  2.2 Route Found --> Rename to route (`/users/1` becomes `/users/:userID`)
3. End span

This approach has one disadvantage. In a typical tracer with head-first sampling, sampling decisions based on the `operationName` of a span will potentially not be accurate as the sampler won't be invoked after the rename.

### Alternatives: Start spans after the fact

As mentioned in [OTel Spec: `Span.UpdateName`](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/api.md#updatename) an alternative to renaming an already started span would be to record the timestamp whenever the span should have been started, and then passing that timestamp to the `startSpan` call.

1. Get current timestamp
2. Route request
  2.1. Route Not Found --> Start span at previous timestamp (`operationName` = `request.head.uri`) --> Record error
  2.2 Route Found --> Start span at previous timestamp (`operationName` = `/users/:userID`)
3. End span

Pull request description:
**Motivation:**

The feature frozen otel specification allows for renaming spans after they have been started. This often is too late to impact sampling decisions, however we still can use to to provide better names for spans as time goes on.

**Modifications:**

add an operationName to the Span protocol.

A span has to be thread-safe already and we have mutating operations on it already so it does not change the complexity requirements on implementing a span.

**Result:**

Be closer to otel spec and more flexible in naming and renaming spans.
Resolves https://github.com/apple/swift-distributed-tracing/issues/48

Patch:
diff --git a/Sources/Tracing/NoOpTracer.swift b/Sources/Tracing/NoOpTracer.swift
index 97bfc35e..a85ce65a 100644
--- a/Sources/Tracing/NoOpTracer.swift
+++ b/Sources/Tracing/NoOpTracer.swift
@@ -29,7 +29,7 @@ public struct NoOpTracer: Tracer {
         file fileID: String,
         line: UInt
     ) -> Span {
-        NoOpSpan(baggage: baggage)
+        NoOpSpan(operationName: operationName, baggage: baggage)
     }
 
     public func forceFlush() {}
@@ -50,7 +50,18 @@ public struct NoOpTracer: Tracer {
         public let baggage: Baggage
         public let isRecording = false
 
-        public init(baggage: Baggage) {
+        private let _operationName: String
+        public var operationName: String {
+            get {
+                self._operationName
+            }
+            set {
+                // ignore
+            }
+        }
+
+        public init(operationName: String, baggage: Baggage) {
+            self._operationName = operationName
             self.baggage = baggage
         }
 
diff --git a/Sources/Tracing/Span.swift b/Sources/Tracing/Span.swift
index ad15b2c3..7efdf678 100644
--- a/Sources/Tracing/Span.swift
+++ b/Sources/Tracing/Span.swift
@@ -30,6 +30,23 @@ public protocol Span: AnyObject, _SwiftTracingSendableSpan {
     /// The read-only `Baggage` of this `Span`, set when starting this `Span`.
     var baggage: Baggage { get }
 
+    /// Returns the name of the operation this span represents.
+    ///
+    /// The name may be changed during the lifetime of a `Span`, this change
+    /// may or may not impact the sampling decision and actually emitting the span,
+    /// depending on how a backend decides to treat renames.
+    ///
+    /// This can still be useful when, for example, we want to immediately start
+    /// span when receiving a request but make it more precise as handling of the request proceeds.
+    /// For example, we can start a span immediately when a request is received in a server,
+    /// and update it to reflect the matched route, if it did match one:
+    ///
+    /// - 1) Start span with basic path (e.g. `operationName = request.head.uri` during `withSpan`)
+    ///   - 2.1) "Route Not Found" -> Record error
+    ///   - 2.2) "Route Found" -> Rename to route (`/users/1` becomes `/users/:userID`)
+    /// - 3) End span
+    var operationName: String { get set }
+
     /// Set the status.
     /// - Parameter status: The status of this `Span`.
     func setStatus(_ status: SpanStatus)


Test patch:
diff --git a/Tests/TracingTests/DynamicTracepointTracerTests.swift b/Tests/TracingTests/DynamicTracepointTracerTests.swift
index 0738b8f1..0a488c0a 100644
--- a/Tests/TracingTests/DynamicTracepointTracerTests.swift
+++ b/Tests/TracingTests/DynamicTracepointTracerTests.swift
@@ -152,7 +152,7 @@ final class DynamicTracepointTestTracer: Tracer {
     {
         let tracepoint = TracepointID(function: function, fileID: fileID, line: line)
         guard self.shouldRecord(tracepoint: tracepoint) else {
-            return NoOpTracer.NoOpSpan(baggage: baggage)
+            return NoOpTracer.NoOpSpan(operationName: operationName, baggage: baggage)
         }
 
         let span = TracepointSpan(
@@ -230,7 +230,6 @@ final class DynamicTracepointTestTracer: Tracer {
 extension DynamicTracepointTestTracer {
     /// Only intended to be used in single-threaded testing.
     final class TracepointSpan: Tracing.Span {
-        private let operationName: String
         private let kind: SpanKind
 
         private var status: SpanStatus?
@@ -238,6 +237,7 @@ extension DynamicTracepointTestTracer {
         private let startTime: DispatchWallTime
         private(set) var endTime: DispatchWallTime?
 
+        public var operationName: String
         private(set) var baggage: Baggage
         private(set) var isRecording: Bool = false
 
diff --git a/Tests/TracingTests/TestTracer.swift b/Tests/TracingTests/TestTracer.swift
index 397809f4..70f5cbe3 100644
--- a/Tests/TracingTests/TestTracer.swift
+++ b/Tests/TracingTests/TestTracer.swift
@@ -96,7 +96,6 @@ extension Baggage {
 
 /// Only intended to be used in single-threaded testing.
 final class TestSpan: Span {
-    private let operationName: String
     private let kind: SpanKind
 
     private var status: SpanStatus?
@@ -104,6 +103,7 @@ final class TestSpan: Span {
     private let startTime: DispatchWallTime
     private(set) var endTime: DispatchWallTime?
 
+    var operationName: String
     let baggage: Baggage
 
     private(set) var events = [SpanEvent]() {
diff --git a/Tests/TracingTests/TracedLockTests.swift b/Tests/TracingTests/TracedLockTests.swift
index e7caee5a..aeb5c3fe 100644
--- a/Tests/TracingTests/TracedLockTests.swift
+++ b/Tests/TracingTests/TracedLockTests.swift
@@ -98,7 +98,6 @@ private final class TracedLockPrintlnTracer: Tracer {
         Carrier == Extract.Carrier {}
 
     final class TracedLockPrintlnSpan: Span {
-        private let operationName: String
         private let kind: SpanKind
 
         private var status: SpanStatus?
@@ -106,6 +105,7 @@ private final class TracedLockPrintlnTracer: Tracer {
         private let startTime: DispatchWallTime
         private(set) var endTime: DispatchWallTime?
 
+        var operationName: String
         let baggage: Baggage
 
         private var links = [SpanLink]()