diff --git a/android/src/main/java/com/opentelemetry/OpenTelemetry.kt b/android/src/main/java/com/opentelemetry/OpenTelemetry.kt index 5bfb567..98e94b5 100644 --- a/android/src/main/java/com/opentelemetry/OpenTelemetry.kt +++ b/android/src/main/java/com/opentelemetry/OpenTelemetry.kt @@ -3,6 +3,10 @@ package com.opentelemetry import android.content.Context import io.opentelemetry.api.GlobalOpenTelemetry import io.opentelemetry.api.OpenTelemetry +import io.opentelemetry.api.baggage.propagation.W3CBaggagePropagator; +import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator; +import io.opentelemetry.context.propagation.ContextPropagators; +import io.opentelemetry.context.propagation.TextMapPropagator; import io.opentelemetry.api.common.AttributeKey import io.opentelemetry.api.common.Attributes import io.opentelemetry.exporter.logging.LoggingMetricExporter @@ -46,6 +50,10 @@ class OpenTelemetry { ), ) + val contextPropagators = ContextPropagators.create( + TextMapPropagator.composite( + W3CTraceContextPropagator.getInstance(), W3CBaggagePropagator.getInstance())); + val logSpanExporter = if (options.debug) LoggingSpanExporter.create() else null val otlpSpanExporter = options.url?.let { OtlpGrpcSpanExporter.builder().setEndpoint(it).build() } @@ -75,6 +83,7 @@ class OpenTelemetry { OpenTelemetrySdk.builder() .setTracerProvider(tracerProvider) .setMeterProvider(meterProvider) + .setPropagators(contextPropagators) .buildAndRegisterGlobal() } } diff --git a/android/src/main/java/com/opentelemetry/OpenTelemetryModule.kt b/android/src/main/java/com/opentelemetry/OpenTelemetryModule.kt index 01c596b..9d66bb1 100644 --- a/android/src/main/java/com/opentelemetry/OpenTelemetryModule.kt +++ b/android/src/main/java/com/opentelemetry/OpenTelemetryModule.kt @@ -1,17 +1,32 @@ package com.opentelemetry import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.ReadableMap import com.facebook.react.module.annotations.ReactModule +import io.opentelemetry.context.Context @ReactModule(name = OpenTelemetryModule.NAME) class OpenTelemetryModule(reactContext: ReactApplicationContext) : - NativeOpenTelemetrySpec(reactContext) { + NativeOpenTelemetrySpec(reactContext) { - override fun getName(): String { - return NAME - } + override fun getName(): String { + return NAME + } - companion object { - const val NAME = "OpenTelemetry" - } + override fun setContext(carrier: ReadableMap) { + if (carrier.toHashMap().isEmpty()) { + Context.root().makeCurrent() + return + } + + val sdk = OpenTelemetry.get() + val currentContext = Context.current() + val extractedContext = + sdk.propagators.textMapPropagator.extract(currentContext, carrier, RNTextMapGetter) + extractedContext.makeCurrent() + } + + companion object { + const val NAME = "OpenTelemetry" + } } diff --git a/android/src/main/java/com/opentelemetry/RNTextMapGetter.kt b/android/src/main/java/com/opentelemetry/RNTextMapGetter.kt new file mode 100644 index 0000000..a7504a0 --- /dev/null +++ b/android/src/main/java/com/opentelemetry/RNTextMapGetter.kt @@ -0,0 +1,20 @@ +package com.opentelemetry + +import com.facebook.react.bridge.ReadableMap +import com.facebook.react.bridge.ReadableMapKeySetIterator +import io.opentelemetry.context.propagation.TextMapGetter + +object RNTextMapGetter : TextMapGetter { + override fun keys(carrier: ReadableMap): MutableIterable { + val iterator: ReadableMapKeySetIterator = carrier.keySetIterator() + val keys = mutableListOf() + while (iterator.hasNextKey()) { + keys.add(iterator.nextKey()) + } + return keys + } + + override fun get(carrier: ReadableMap?, key: String): String? { + return carrier?.getString(key) + } +} \ No newline at end of file diff --git a/example/src/App.tsx b/example/src/App.tsx index affcdcd..75e01c6 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -32,28 +32,29 @@ export default function App() { style={{ padding: 16, backgroundColor: "lightgray", borderRadius: 8 }} onPress={async () => { console.log("Starting a long, parent span..."); - const parentSpan = tracer.startSpan("my-js-homepage-span"); - const ctx = sdk.trace.setSpan(sdk.context.active(), parentSpan); - parentSpan.setAttributes({ - platform: "js", - userId: 123, - userType: "admin", - }); - const childSpan = tracer.startSpan( - "my-js-homepage-child-span", - { attributes: { type: "child" } }, - ctx - ); - childSpan.end(); + tracer.startActiveSpan("my-js-homepage-span", async (parentSpan) => { + parentSpan.setAttributes({ + platform: "js", + userId: 123, + userType: "admin", + }); + + const childSpan = tracer.startSpan( + "my-js-homepage-child-span", + { attributes: { type: "child" } }, + ); + childSpan.end(); - // sleep for a random 1-3 seconds - const sleepTime = Math.floor(Math.random() * 3000) + 1000; - console.log(`Sleeping for ${sleepTime}ms`); - await new Promise((resolve) => setTimeout(resolve, sleepTime)); + // sleep for a random 1-3 seconds + const sleepTime = Math.floor(Math.random() * 3000) + 1000; + console.log(`Sleeping for ${sleepTime}ms`); + await new Promise((resolve) => setTimeout(resolve, sleepTime)); + + console.log("Span ended"); + parentSpan.end(); + }); - parentSpan.end(); - console.log("Span ended"); }} > Start a span diff --git a/src/NativeOpenTelemetry.ts b/src/NativeOpenTelemetry.ts index ad37dcb..d66674b 100644 --- a/src/NativeOpenTelemetry.ts +++ b/src/NativeOpenTelemetry.ts @@ -1,5 +1,11 @@ import { TurboModuleRegistry, type TurboModule } from "react-native"; -export interface Spec extends TurboModule {} +type Carrier = { + [key: string]: string; +}; + +export interface Spec extends TurboModule { + setContext: (carrier: Carrier) => void; +} export default TurboModuleRegistry.getEnforcing("OpenTelemetry"); diff --git a/src/RNContextManager.native.ts b/src/RNContextManager.native.ts new file mode 100644 index 0000000..88c7271 --- /dev/null +++ b/src/RNContextManager.native.ts @@ -0,0 +1,113 @@ +import { ROOT_CONTEXT, propagation } from '@opentelemetry/api'; +import type {Context, ContextManager} from '@opentelemetry/api'; +import NATIVE from './NativeOpenTelemetry'; + +/** + * Stack Context Manager for managing the state in JS, + * enriched with native-sync capabilities. + */ +export class RNContextManager implements ContextManager { + private _enabled = false; + public _currentContext = ROOT_CONTEXT; + + // Bind a function to a given context. + // This is the same as the default helper. + private _bindFunction( + context = ROOT_CONTEXT, + target: T + ): T { + const manager = this; + const contextWrapper = function (this: unknown, ...args: unknown[]) { + return manager.with(context, () => target.apply(this, args)); + }; + Object.defineProperty(contextWrapper, 'length', { + enumerable: false, + configurable: true, + writable: false, + value: target.length, + }); + return contextWrapper as unknown as T; + } + + private _syncToNative() { + const carrier = {}; + propagation.inject(this._currentContext, carrier); + console.log({ carrier }); + NATIVE.setContext(carrier); + } + + /** + * Returns the active (current) context. + */ + active(): Context { + return this._currentContext; + } + + /** + * Binds the provided context to a target function (or object) so that when that target is called, + * the provided context is active during its execution. + */ + bind(context: Context, target: T): T { + if (context === undefined) { + context = this.active(); + } + if (typeof target === 'function') { + return this._bindFunction(context, target); + } + return target; + } + + /** + * Disables the context manager and resets the current context to ROOT_CONTEXT. + * You could also choose to sync this state to Native if desired. + */ + disable(): this { + this._currentContext = ROOT_CONTEXT; + this._enabled = false; + // Optionally, notify Native with the ROOT_CONTEXT: + this._syncToNative(); + return this; + } + + /** + * Enables the context manager and initializes the current context. + * Synchronizes the initial state from Native. + */ + enable(): this { + if (this._enabled) { + return this; + } + this._enabled = true; + // Load any native context into the JS side + // this._currentContext = NATIVE.getContext() ?? ROOT_CONTEXT; + this._currentContext = ROOT_CONTEXT; + return this; + } + + /** + * Executes the function [fn] with the provided [context] as the active context. + * Ensures that the updated context is sent to Native before and after the call. + */ + with ReturnType>( + context: Context | null, + fn: F, + thisArg?: ThisParameterType, + ...args: A + ): ReturnType { + const previousContext = this._currentContext; + // Set new active context (or fallback to ROOT_CONTEXT) + this._currentContext = context || ROOT_CONTEXT; + + // Sync the new active context to Native + this._syncToNative(); + + try { + return fn.call(thisArg, ...args); + } finally { + // Restore previous context + this._currentContext = previousContext; + // Re-sync the restored context back to Native + this._syncToNative(); + } + } +} diff --git a/src/RNContextManager.ts b/src/RNContextManager.ts new file mode 100644 index 0000000..a70a572 --- /dev/null +++ b/src/RNContextManager.ts @@ -0,0 +1 @@ +export { StackContextManager as RNContextManager } from "@opentelemetry/sdk-trace-web"; diff --git a/src/index.tsx b/src/index.tsx index 55427ae..03907dd 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -28,6 +28,7 @@ import { ATTR_SERVICE_VERSION, } from "@opentelemetry/semantic-conventions"; import type { Options } from "./types"; +import { RNContextManager } from "./RNContextManager"; export function openTelemetrySDK(options: Options = {}) { console.log("SDK", { options }); @@ -63,29 +64,29 @@ export function openTelemetrySDK(options: Options = {}) { const tracerProvider = new WebTracerProvider({ resource, - spanProcessors: [ - logSpanProcessor, - otlpSpanProcessor, - ].filter((processor) => processor !== null), + spanProcessors: [logSpanProcessor, otlpSpanProcessor].filter( + (processor) => processor !== null + ), }); - tracerProvider.register({ - propagator: new CompositePropagator({ - propagators: [ - new W3CBaggagePropagator(), - new W3CTraceContextPropagator(), - ], - }), + // Context + + const contextManager = new RNContextManager(); + + const propagator = new CompositePropagator({ + propagators: [new W3CBaggagePropagator(), new W3CTraceContextPropagator()], }); + tracerProvider.register({ contextManager, propagator }); + registerInstrumentations({ instrumentations: [ new FetchInstrumentation({ propagateTraceHeaderCorsUrls: /.*/, clearTimingResources: false, }), - ] - }) + ], + }); // Metrics