Skip to content

feat(android): context sync #12

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions android/src/main/java/com/opentelemetry/OpenTelemetry.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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() }
Expand Down Expand Up @@ -75,6 +83,7 @@ class OpenTelemetry {
OpenTelemetrySdk.builder()
.setTracerProvider(tracerProvider)
.setMeterProvider(meterProvider)
.setPropagators(contextPropagators)
.buildAndRegisterGlobal()
}
}
Expand Down
29 changes: 22 additions & 7 deletions android/src/main/java/com/opentelemetry/OpenTelemetryModule.kt
Original file line number Diff line number Diff line change
@@ -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"
}
}
20 changes: 20 additions & 0 deletions android/src/main/java/com/opentelemetry/RNTextMapGetter.kt
Original file line number Diff line number Diff line change
@@ -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<ReadableMap> {
override fun keys(carrier: ReadableMap): MutableIterable<String> {
val iterator: ReadableMapKeySetIterator = carrier.keySetIterator()
val keys = mutableListOf<String>()
while (iterator.hasNextKey()) {
keys.add(iterator.nextKey())
}
return keys
}

override fun get(carrier: ReadableMap?, key: String): String? {
return carrier?.getString(key)
}
}
39 changes: 20 additions & 19 deletions example/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}}
>
<Text>Start a span</Text>
Expand Down
8 changes: 7 additions & 1 deletion src/NativeOpenTelemetry.ts
Original file line number Diff line number Diff line change
@@ -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<Spec>("OpenTelemetry");
113 changes: 113 additions & 0 deletions src/RNContextManager.native.ts
Original file line number Diff line number Diff line change
@@ -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<T extends Function>(
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<T>(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<A extends unknown[], F extends (...args: A) => ReturnType<F>>(
context: Context | null,
fn: F,
thisArg?: ThisParameterType<F>,
...args: A
): ReturnType<F> {
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();
}
}
}
1 change: 1 addition & 0 deletions src/RNContextManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { StackContextManager as RNContextManager } from "@opentelemetry/sdk-trace-web";
27 changes: 14 additions & 13 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand Down Expand Up @@ -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

Expand Down