Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
30 changes: 23 additions & 7 deletions android/src/main/java/com/opentelemetry/OpenTelemetryModule.kt
Original file line number Diff line number Diff line change
@@ -1,17 +1,33 @@
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 getter = RNTextMapGetter()
val currentContext = Context.current()
val extractedContext =
sdk.propagators.textMapPropagator.extract(currentContext, carrier, getter)
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

class 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