Skip to content

Commit

Permalink
RUMM-2334 Add Datadog Context Observer
Browse files Browse the repository at this point in the history
  • Loading branch information
maxep committed Sep 6, 2022
1 parent b274944 commit 75ca11b
Show file tree
Hide file tree
Showing 8 changed files with 55 additions and 31 deletions.
29 changes: 26 additions & 3 deletions Sources/Datadog/DatadogCore/Context/DatadogContextProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,19 @@

import Foundation

/// Comply to `DatadogContextObserver` protocol if you want to listen for new
/// value of the Datadog Context.
internal protocol DatadogContextObserver {
/// Receives notification for new Context.
///
/// This method will be called each time the context changed in the observed
/// provider. There is no garantee on which thread this method will be invoked.
///
/// - Parameter context: The new context value.
///
func notify(context: DatadogContext)
}

/// Provides thread-safe access to Datadog Context.
///
/// The context can be accessed asynchronously for reads and writes.
Expand Down Expand Up @@ -39,7 +52,9 @@ internal final class DatadogContextProvider {
/// The current `context`.
///
/// The value must be accessed from the `queue` only.
private var context: DatadogContext
private var context: DatadogContext {
didSet { observers.forEach { $0.notify(context: context) } }
}

/// The queue used to synchronize the access to the `DatadogContext`.
///
Expand All @@ -50,6 +65,7 @@ internal final class DatadogContextProvider {
qos: .utility
)

private var observers: [DatadogContextObserver]
private var subscriptions: [ContextValueSubscription]
private var reader: KeyPathContextValueReader<DatadogContext>

Expand All @@ -59,6 +75,7 @@ internal final class DatadogContextProvider {
/// - Parameter context: The inital context value.
init(context: DatadogContext) {
self.context = context
self.observers = []
self.subscriptions = []
self.reader = KeyPathContextValueReader()
}
Expand All @@ -71,11 +88,17 @@ internal final class DatadogContextProvider {
///
/// **Warning:** Must be called from the `queue`.
private func unsafeRead() -> DatadogContext {
var context = self.context
reader.read(to: &context)
reader.read(to: &self.context)
return context
}

/// Adds an observer to notify when the context changes.
///
/// - Parameter observer: The observer to add.
func add(observer: DatadogContextObserver) {
queue.async(flags: .barrier) { self.observers.append(observer) }
}

/// Reads to the `context` synchronously, by blocking the caller thread.
///
/// **Warning:** This method will block the caller thread by reading the context
Expand Down
6 changes: 6 additions & 0 deletions Sources/Datadog/DatadogCore/DatadogCore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,12 @@ extension DatadogCore: DatadogV1CoreProtocol {
}
}

extension DatadogCore: DatadogContextObserver {
func notify(context: DatadogContext) {
send(message: .context(context))
}
}

/// A v1 Feature with an associated stroage.
internal protocol V1Feature {
/// The feature's storage.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ internal class RUMUserActionScope: RUMScope, RUMContextProvider {
// MARK: - RUMContextProvider

var context: RUMContext {
// Per `RUMCurrentContext.activeViewContext`, we currently only get the context from the parent scope (`RUMViewScope`) when it's still active (`viewScopes.last`).
// We currently only get the context from the parent scope (`RUMViewScope`) when it's still active (`viewScopes.last`).
// This might change at some point and the following context might then hold the wrong active view's properties at that point as this is not checked inside `RUMViewScope.context`.
return parent.context
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/Datadog/Tracing/TracingV2Configuration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ internal func createTracingConfiguration(intake: URL) -> DatadogFeatureConfigura
return DatadogFeatureConfiguration(
name: "tracing",
requestBuilder: TracingRequestBuilder(intake: intake),
messageReceiver: NOPFeatureMessageReceiver()
messageReceiver: TracingMessageReceiver()
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ internal final class DatadogCoreMock: Flushable {
}

private let lock = NSLock()
private var _context: DatadogContext
private var _context: DatadogContext {
didSet { send(message: .context(_context)) }
}

init(context: DatadogContext = .mockAny()) {
_context = context
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ internal final class PassthroughCoreMock: DatadogV1CoreProtocol, FeatureV1Scope
}

private let lock = NSLock()
private var _context: DatadogContext
private var _context: DatadogContext {
didSet { send(message: .context(_context)) }
}

private func acquire<T>(_ block: () -> T) -> T {
lock.lock()
Expand Down
5 changes: 3 additions & 2 deletions Tests/DatadogTests/Datadog/Mocks/TracingFeatureMocks.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ extension TracingFeature {
/// Mocks the feature instance which performs uploads to mocked `DataUploadWorker`.
/// Use `TracingFeature.waitAndReturnSpanMatchers()` to inspect and assert recorded `Spans`.
static func mockByRecordingSpanMatchers(
configuration: FeaturesConfiguration.Tracing = .mockAny()
configuration: FeaturesConfiguration.Tracing = .mockAny(),
messageReceiver: FeatureMessageReceiver = TracingMessageReceiver()
) -> TracingFeature {
// Mock storage with `InMemoryWriter`, used later for retrieving recorded events back:
let interceptedStorage = FeatureStorage(
Expand All @@ -33,7 +34,7 @@ extension TracingFeature {
storage: interceptedStorage,
upload: .mockNoOp(),
configuration: configuration,
messageReceiver: NOPFeatureMessageReceiver()
messageReceiver: messageReceiver
)
}

Expand Down
34 changes: 12 additions & 22 deletions Tests/DatadogTests/Datadog/TracerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -663,53 +663,43 @@ class TracerTests: XCTestCase {
core.register(feature: rum)

// given
let tracer = Tracer.initialize(configuration: .init(), in: core).dd
Global.sharedTracer = Tracer.initialize(configuration: .init(), in: core).dd
defer { Global.sharedTracer = DDNoopTracer() }
Global.rum = RUMMonitor.initialize(in: core)
Global.rum.startView(viewController: mockView)
defer { Global.rum = DDNoopRUMMonitor() }

// when
let span = tracer.startSpan(operationName: "operation", tags: [:], startTime: Date())
let span = Global.sharedTracer.startSpan(operationName: "operation", tags: [:], startTime: Date())
span.finish()

// then
let spanMatcher = try tracing.waitAndReturnSpanMatchers(count: 1)[0]
XCTAssertEqual(
try spanMatcher.meta.custom(keyPath: "meta.\(RUMContextIntegration.Attributes.applicationID)"),
try spanMatcher.meta.custom(keyPath: "meta.application_id"),
rum.configuration.applicationID
)
XCTAssertValidRumUUID(try spanMatcher.meta.custom(keyPath: "meta.\(RUMContextIntegration.Attributes.sessionID)"))
XCTAssertValidRumUUID(try spanMatcher.meta.custom(keyPath: "meta.\(RUMContextIntegration.Attributes.viewID)"))
XCTAssertValidRumUUID(try spanMatcher.meta.custom(keyPath: "meta.session_id"))
XCTAssertValidRumUUID(try spanMatcher.meta.custom(keyPath: "meta.view.id"))
}

func testGivenBundlingWithRUMEnabledButRUMMonitorNotRegistered_whenSendingSpan_itPrintsWarning() throws {
let tracing: TracingFeature = .mockByRecordingSpanMatchers()
core.register(feature: tracing)

let rum: RUMFeature = .mockNoOp()
core.register(feature: rum)

let dd = DD.mockWith(logger: CoreLoggerMock())
defer { dd.reset() }

// given
let tracer = Tracer.initialize(configuration: .init(), in: core).dd
XCTAssertTrue(Global.rum is DDNoopRUMMonitor)
Global.sharedTracer = Tracer.initialize(configuration: .init(), in: core).dd
defer { Global.sharedTracer = DDNoopTracer() }

// when
let span = tracer.startSpan(operationName: "operation", tags: [:], startTime: Date())
let span = Global.sharedTracer.startSpan(operationName: "operation", tags: [:], startTime: Date())
span.finish()

// then
try XCTAssertTrue(
XCTUnwrap(dd.logger.warnLog?.message)
.contains("RUM feature is enabled, but no `RUMMonitor` is registered. The RUM integration with Tracing will not work.")
)

let spanMatcher = try tracing.waitAndReturnSpanMatchers(count: 1)[0]
XCTAssertNil(try? spanMatcher.meta.custom(keyPath: "meta.\(RUMContextIntegration.Attributes.applicationID)"))
XCTAssertNil(try? spanMatcher.meta.custom(keyPath: "meta.\(RUMContextIntegration.Attributes.sessionID)"))
XCTAssertNil(try? spanMatcher.meta.custom(keyPath: "meta.\(RUMContextIntegration.Attributes.viewID)"))
XCTAssertNil(try? spanMatcher.meta.custom(keyPath: "meta.application_id"))
XCTAssertNil(try? spanMatcher.meta.custom(keyPath: "meta.session_id"))
XCTAssertNil(try? spanMatcher.meta.custom(keyPath: "meta.view.id"))
}

// MARK: - Injecting span context into carrier
Expand Down

0 comments on commit 75ca11b

Please sign in to comment.