TracingTestsUtilities.swift 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. /*
  2. * Copyright 2024, gRPC Authors All rights reserved.
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. import GRPCCore
  17. import NIOConcurrencyHelpers
  18. import Tracing
  19. final class TestTracer: Tracer {
  20. typealias Span = TestSpan
  21. private let testSpans: NIOLockedValueBox<[String: TestSpan]> = .init([:])
  22. func getEventsForTestSpan(ofOperationName operationName: String) -> [SpanEvent] {
  23. let span = self.testSpans.withLockedValue({ $0[operationName] })
  24. return span?.events ?? []
  25. }
  26. func extract<Carrier, Extract>(
  27. _ carrier: Carrier,
  28. into context: inout ServiceContextModule.ServiceContext,
  29. using extractor: Extract
  30. ) where Carrier == Extract.Carrier, Extract: Instrumentation.Extractor {
  31. let traceID = extractor.extract(key: TraceID.keyName, from: carrier)
  32. context[TraceID.self] = traceID
  33. }
  34. func inject<Carrier, Inject>(
  35. _ context: ServiceContextModule.ServiceContext,
  36. into carrier: inout Carrier,
  37. using injector: Inject
  38. ) where Carrier == Inject.Carrier, Inject: Instrumentation.Injector {
  39. if let traceID = context.traceID {
  40. injector.inject(traceID, forKey: TraceID.keyName, into: &carrier)
  41. }
  42. }
  43. func forceFlush() {
  44. // no-op
  45. }
  46. func startSpan<Instant>(
  47. _ operationName: String,
  48. context: @autoclosure () -> ServiceContext,
  49. ofKind kind: SpanKind,
  50. at instant: @autoclosure () -> Instant,
  51. function: String,
  52. file fileID: String,
  53. line: UInt
  54. ) -> TestSpan where Instant: TracerInstant {
  55. return self.testSpans.withLockedValue { testSpans in
  56. let span = TestSpan(context: context(), operationName: operationName)
  57. testSpans[operationName] = span
  58. return span
  59. }
  60. }
  61. }
  62. struct TestSpan: Span, Sendable {
  63. private struct State {
  64. var context: ServiceContextModule.ServiceContext
  65. var operationName: String
  66. var attributes: Tracing.SpanAttributes
  67. var status: Tracing.SpanStatus?
  68. var events: [Tracing.SpanEvent] = []
  69. }
  70. private let state: NIOLockedValueBox<State>
  71. let isRecording: Bool
  72. var context: ServiceContextModule.ServiceContext {
  73. self.state.withLockedValue { $0.context }
  74. }
  75. var operationName: String {
  76. get { self.state.withLockedValue { $0.operationName } }
  77. nonmutating set { self.state.withLockedValue { $0.operationName = newValue } }
  78. }
  79. var attributes: Tracing.SpanAttributes {
  80. get { self.state.withLockedValue { $0.attributes } }
  81. nonmutating set { self.state.withLockedValue { $0.attributes = newValue } }
  82. }
  83. var events: [Tracing.SpanEvent] {
  84. self.state.withLockedValue { $0.events }
  85. }
  86. init(
  87. context: ServiceContextModule.ServiceContext,
  88. operationName: String,
  89. attributes: Tracing.SpanAttributes = [:],
  90. isRecording: Bool = true
  91. ) {
  92. let state = State(context: context, operationName: operationName, attributes: attributes)
  93. self.state = NIOLockedValueBox(state)
  94. self.isRecording = isRecording
  95. }
  96. func setStatus(_ status: Tracing.SpanStatus) {
  97. self.state.withLockedValue { $0.status = status }
  98. }
  99. func addEvent(_ event: Tracing.SpanEvent) {
  100. self.state.withLockedValue { $0.events.append(event) }
  101. }
  102. func recordError<Instant>(
  103. _ error: any Error,
  104. attributes: Tracing.SpanAttributes,
  105. at instant: @autoclosure () -> Instant
  106. ) where Instant: Tracing.TracerInstant {
  107. self.setStatus(
  108. .init(
  109. code: .error,
  110. message: "Error: \(error), attributes: \(attributes), at instant: \(instant())"
  111. )
  112. )
  113. }
  114. func addLink(_ link: Tracing.SpanLink) {
  115. self.state.withLockedValue {
  116. $0.context.spanLinks?.append(link)
  117. }
  118. }
  119. func end<Instant>(at instant: @autoclosure () -> Instant) where Instant: Tracing.TracerInstant {
  120. self.setStatus(.init(code: .ok, message: "Ended at instant: \(instant())"))
  121. }
  122. }
  123. enum TraceID: ServiceContextModule.ServiceContextKey {
  124. typealias Value = String
  125. static let keyName = "trace-id"
  126. }
  127. enum ServiceContextSpanLinksKey: ServiceContextModule.ServiceContextKey {
  128. typealias Value = [SpanLink]
  129. static let keyName = "span-links"
  130. }
  131. extension ServiceContext {
  132. var traceID: String? {
  133. get {
  134. self[TraceID.self]
  135. }
  136. set {
  137. self[TraceID.self] = newValue
  138. }
  139. }
  140. var spanLinks: [SpanLink]? {
  141. get {
  142. self[ServiceContextSpanLinksKey.self]
  143. }
  144. set {
  145. self[ServiceContextSpanLinksKey.self] = newValue
  146. }
  147. }
  148. }
  149. @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
  150. struct TestWriter<WriterElement: Sendable>: RPCWriterProtocol {
  151. typealias Element = WriterElement
  152. private let streamContinuation: AsyncStream<Element>.Continuation
  153. init(streamContinuation: AsyncStream<Element>.Continuation) {
  154. self.streamContinuation = streamContinuation
  155. }
  156. func write(_ element: WriterElement) {
  157. self.streamContinuation.yield(element)
  158. }
  159. func write(contentsOf elements: some Sequence<Self.Element>) {
  160. elements.forEach { element in
  161. self.write(element)
  162. }
  163. }
  164. }