react-native-otel 0.1.0 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1137 -13
- package/lib/module/context/span-context.js +56 -5
- package/lib/module/context/span-context.js.map +1 -1
- package/lib/module/core/ids.js +21 -7
- package/lib/module/core/ids.js.map +1 -1
- package/lib/module/core/meter.js +101 -12
- package/lib/module/core/meter.js.map +1 -1
- package/lib/module/core/processor.js +28 -0
- package/lib/module/core/processor.js.map +1 -0
- package/lib/module/core/resource.js +1 -0
- package/lib/module/core/resource.js.map +1 -1
- package/lib/module/core/sampler.js +55 -0
- package/lib/module/core/sampler.js.map +1 -0
- package/lib/module/core/span.js +15 -1
- package/lib/module/core/span.js.map +1 -1
- package/lib/module/core/tracer.js +94 -5
- package/lib/module/core/tracer.js.map +1 -1
- package/lib/module/exporters/console-exporter.js +8 -1
- package/lib/module/exporters/console-exporter.js.map +1 -1
- package/lib/module/exporters/multi-exporter.js +57 -0
- package/lib/module/exporters/multi-exporter.js.map +1 -0
- package/lib/module/exporters/otlp-http-exporter.js +159 -25
- package/lib/module/exporters/otlp-http-exporter.js.map +1 -1
- package/lib/module/exporters/wal.js +129 -0
- package/lib/module/exporters/wal.js.map +1 -0
- package/lib/module/index.js +16 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/instrumentation/errors.js +21 -0
- package/lib/module/instrumentation/errors.js.map +1 -1
- package/lib/module/instrumentation/expo-router.js +76 -0
- package/lib/module/instrumentation/expo-router.js.map +1 -0
- package/lib/module/instrumentation/fetch.js +99 -0
- package/lib/module/instrumentation/fetch.js.map +1 -0
- package/lib/module/instrumentation/lifecycle.js +6 -1
- package/lib/module/instrumentation/lifecycle.js.map +1 -1
- package/lib/module/instrumentation/linking.js +65 -0
- package/lib/module/instrumentation/linking.js.map +1 -0
- package/lib/module/instrumentation/network.js +35 -0
- package/lib/module/instrumentation/network.js.map +1 -1
- package/lib/module/instrumentation/startup.js +46 -0
- package/lib/module/instrumentation/startup.js.map +1 -0
- package/lib/module/sdk.js +87 -5
- package/lib/module/sdk.js.map +1 -1
- package/lib/module/version.js +7 -0
- package/lib/module/version.js.map +1 -0
- package/lib/typescript/src/context/span-context.d.ts +14 -4
- package/lib/typescript/src/context/span-context.d.ts.map +1 -1
- package/lib/typescript/src/core/ids.d.ts.map +1 -1
- package/lib/typescript/src/core/meter.d.ts +12 -2
- package/lib/typescript/src/core/meter.d.ts.map +1 -1
- package/lib/typescript/src/core/processor.d.ts +29 -0
- package/lib/typescript/src/core/processor.d.ts.map +1 -0
- package/lib/typescript/src/core/resource.d.ts +3 -0
- package/lib/typescript/src/core/resource.d.ts.map +1 -1
- package/lib/typescript/src/core/sampler.d.ts +31 -0
- package/lib/typescript/src/core/sampler.d.ts.map +1 -0
- package/lib/typescript/src/core/span.d.ts +16 -0
- package/lib/typescript/src/core/span.d.ts.map +1 -1
- package/lib/typescript/src/core/tracer.d.ts +16 -6
- package/lib/typescript/src/core/tracer.d.ts.map +1 -1
- package/lib/typescript/src/exporters/console-exporter.d.ts.map +1 -1
- package/lib/typescript/src/exporters/multi-exporter.d.ts +28 -0
- package/lib/typescript/src/exporters/multi-exporter.d.ts.map +1 -0
- package/lib/typescript/src/exporters/otlp-http-exporter.d.ts +20 -2
- package/lib/typescript/src/exporters/otlp-http-exporter.d.ts.map +1 -1
- package/lib/typescript/src/exporters/types.d.ts +17 -3
- package/lib/typescript/src/exporters/types.d.ts.map +1 -1
- package/lib/typescript/src/exporters/wal.d.ts +21 -0
- package/lib/typescript/src/exporters/wal.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +15 -2
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/instrumentation/errors.d.ts.map +1 -1
- package/lib/typescript/src/instrumentation/expo-router.d.ts +31 -0
- package/lib/typescript/src/instrumentation/expo-router.d.ts.map +1 -0
- package/lib/typescript/src/instrumentation/fetch.d.ts +18 -0
- package/lib/typescript/src/instrumentation/fetch.d.ts.map +1 -0
- package/lib/typescript/src/instrumentation/lifecycle.d.ts.map +1 -1
- package/lib/typescript/src/instrumentation/linking.d.ts +23 -0
- package/lib/typescript/src/instrumentation/linking.d.ts.map +1 -0
- package/lib/typescript/src/instrumentation/network.d.ts.map +1 -1
- package/lib/typescript/src/instrumentation/startup.d.ts +16 -0
- package/lib/typescript/src/instrumentation/startup.d.ts.map +1 -0
- package/lib/typescript/src/sdk.d.ts +35 -0
- package/lib/typescript/src/sdk.d.ts.map +1 -1
- package/lib/typescript/src/version.d.ts +2 -0
- package/lib/typescript/src/version.d.ts.map +1 -0
- package/package.json +12 -2
- package/src/context/span-context.ts +61 -8
- package/src/core/ids.ts +21 -7
- package/src/core/meter.ts +136 -14
- package/src/core/processor.ts +33 -0
- package/src/core/resource.ts +6 -0
- package/src/core/sampler.ts +65 -0
- package/src/core/span.ts +28 -1
- package/src/core/tracer.ts +140 -19
- package/src/exporters/console-exporter.ts +18 -4
- package/src/exporters/multi-exporter.ts +59 -0
- package/src/exporters/otlp-http-exporter.ts +191 -29
- package/src/exporters/types.ts +24 -3
- package/src/exporters/wal.ts +145 -0
- package/src/index.ts +36 -1
- package/src/instrumentation/errors.ts +27 -0
- package/src/instrumentation/expo-router.ts +94 -0
- package/src/instrumentation/fetch.ts +134 -0
- package/src/instrumentation/lifecycle.ts +7 -1
- package/src/instrumentation/linking.ts +83 -0
- package/src/instrumentation/network.ts +39 -0
- package/src/instrumentation/startup.ts +49 -0
- package/src/sdk.ts +115 -4
- package/src/version.ts +6 -0
|
@@ -3,10 +3,16 @@ import type {
|
|
|
3
3
|
LogRecord,
|
|
4
4
|
MetricExporter,
|
|
5
5
|
MetricRecord,
|
|
6
|
+
CounterRecord,
|
|
7
|
+
HistogramRecord,
|
|
8
|
+
GaugeRecord,
|
|
6
9
|
} from './types';
|
|
7
10
|
import type { Attributes } from '../core/attributes';
|
|
8
11
|
import type { Resource } from '../core/resource';
|
|
9
12
|
import type { ReadonlySpan, SpanExporter } from '../core/span';
|
|
13
|
+
import type { StorageAdapter } from '../instrumentation/errors';
|
|
14
|
+
import { Wal, fetchWithRetry } from './wal';
|
|
15
|
+
import { SDK_VERSION } from '../version';
|
|
10
16
|
|
|
11
17
|
// ─── OTLP attribute value serialization ──────────────────────────────────────
|
|
12
18
|
|
|
@@ -79,6 +85,7 @@ export class OtlpHttpExporter implements SpanExporter {
|
|
|
79
85
|
private buffer: ReadonlySpan[] = [];
|
|
80
86
|
private resource_: Readonly<Resource> | undefined;
|
|
81
87
|
private timer_: ReturnType<typeof setInterval> | undefined;
|
|
88
|
+
private wal_: Wal<ReadonlySpan> | undefined;
|
|
82
89
|
|
|
83
90
|
constructor(options: OtlpHttpExporterOptions) {
|
|
84
91
|
this.tracesEndpoint = options.endpoint.replace(/\/$/, '') + '/v1/traces';
|
|
@@ -92,6 +99,8 @@ export class OtlpHttpExporter implements SpanExporter {
|
|
|
92
99
|
this.timer_ = setInterval(() => {
|
|
93
100
|
this.flush();
|
|
94
101
|
}, interval);
|
|
102
|
+
// Allow Node.js to exit cleanly in test environments without calling destroy().
|
|
103
|
+
if (typeof this.timer_.unref === 'function') this.timer_.unref();
|
|
95
104
|
}
|
|
96
105
|
|
|
97
106
|
// Called by OtelSDK.init() after buildResource() — not part of SpanExporter.
|
|
@@ -99,6 +108,20 @@ export class OtlpHttpExporter implements SpanExporter {
|
|
|
99
108
|
this.resource_ = resource;
|
|
100
109
|
}
|
|
101
110
|
|
|
111
|
+
// Called by OtelSDK.init() when a StorageAdapter is configured.
|
|
112
|
+
// Initialises the WAL and replays any undelivered batches from the previous session.
|
|
113
|
+
setStorage(storage: StorageAdapter): void {
|
|
114
|
+
this.wal_ = new Wal<ReadonlySpan>(storage, '@react-native-otel/wal/spans');
|
|
115
|
+
this.replayWal();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private replayWal(): void {
|
|
119
|
+
if (!this.wal_) return;
|
|
120
|
+
for (const batch of this.wal_.readAll()) {
|
|
121
|
+
this.deliverBatch(batch.data as ReadonlySpan[], batch.id);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
102
125
|
export(spans: ReadonlySpan[]): void {
|
|
103
126
|
this.buffer.push(...spans);
|
|
104
127
|
if (this.buffer.length >= this.batchSize) {
|
|
@@ -109,7 +132,12 @@ export class OtlpHttpExporter implements SpanExporter {
|
|
|
109
132
|
flush(): void {
|
|
110
133
|
if (this.buffer.length === 0) return;
|
|
111
134
|
const batch = this.buffer.splice(0);
|
|
112
|
-
this.
|
|
135
|
+
if (this.wal_) {
|
|
136
|
+
const id = this.wal_.write(batch);
|
|
137
|
+
this.deliverBatch(batch, id);
|
|
138
|
+
} else {
|
|
139
|
+
this.deliverBatch(batch, undefined);
|
|
140
|
+
}
|
|
113
141
|
}
|
|
114
142
|
|
|
115
143
|
// Clear the flush timer and send any remaining buffered spans.
|
|
@@ -121,30 +149,41 @@ export class OtlpHttpExporter implements SpanExporter {
|
|
|
121
149
|
this.flush();
|
|
122
150
|
}
|
|
123
151
|
|
|
124
|
-
private
|
|
152
|
+
private deliverBatch(spans: ReadonlySpan[], walId: string | undefined): void {
|
|
153
|
+
const body = this.buildBody(spans);
|
|
154
|
+
fetchWithRetry(this.tracesEndpoint, {
|
|
155
|
+
method: 'POST',
|
|
156
|
+
headers: this.headers,
|
|
157
|
+
body,
|
|
158
|
+
})
|
|
159
|
+
.then((success) => {
|
|
160
|
+
if (success && walId !== undefined) {
|
|
161
|
+
this.wal_?.delete(walId);
|
|
162
|
+
}
|
|
163
|
+
})
|
|
164
|
+
.catch(() => {
|
|
165
|
+
// Leave in WAL for next session
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
private buildBody(spans: ReadonlySpan[]): string {
|
|
125
170
|
const resourceAttrs = this.resource_
|
|
126
171
|
? toOtlpAttributes(this.resource_ as unknown as Record<string, unknown>)
|
|
127
172
|
: [];
|
|
128
173
|
|
|
129
|
-
|
|
174
|
+
return JSON.stringify({
|
|
130
175
|
resourceSpans: [
|
|
131
176
|
{
|
|
132
177
|
resource: { attributes: resourceAttrs },
|
|
133
178
|
scopeSpans: [
|
|
134
179
|
{
|
|
135
|
-
scope: { name: 'react-native-otel', version:
|
|
180
|
+
scope: { name: 'react-native-otel', version: SDK_VERSION },
|
|
136
181
|
spans: spans.map((s) => this.toOtlpSpan(s)),
|
|
137
182
|
},
|
|
138
183
|
],
|
|
139
184
|
},
|
|
140
185
|
],
|
|
141
186
|
});
|
|
142
|
-
|
|
143
|
-
fetch(this.tracesEndpoint, {
|
|
144
|
-
method: 'POST',
|
|
145
|
-
headers: this.headers,
|
|
146
|
-
body,
|
|
147
|
-
}).catch(() => {});
|
|
148
187
|
}
|
|
149
188
|
|
|
150
189
|
private toOtlpSpan(span: ReadonlySpan) {
|
|
@@ -163,6 +202,11 @@ export class OtlpHttpExporter implements SpanExporter {
|
|
|
163
202
|
timeUnixNano: msToNano(event.timestampMs),
|
|
164
203
|
attributes: toOtlpAttributes(event.attributes),
|
|
165
204
|
})),
|
|
205
|
+
links: span.links.map((link) => ({
|
|
206
|
+
traceId: link.traceId,
|
|
207
|
+
spanId: link.spanId,
|
|
208
|
+
attributes: link.attributes ? toOtlpAttributes(link.attributes) : [],
|
|
209
|
+
})),
|
|
166
210
|
droppedEventsCount: span.droppedEventsCount,
|
|
167
211
|
status: {
|
|
168
212
|
code: SPAN_STATUS_CODE[span.status] ?? 0,
|
|
@@ -175,18 +219,24 @@ export class OtlpHttpExporter implements SpanExporter {
|
|
|
175
219
|
|
|
176
220
|
// ─── Metric exporter ──────────────────────────────────────────────────────────
|
|
177
221
|
|
|
178
|
-
// OTLP aggregation temporality
|
|
222
|
+
// OTLP aggregation temporality constants
|
|
223
|
+
const AGGREGATION_TEMPORALITY_DELTA = 1;
|
|
179
224
|
const AGGREGATION_TEMPORALITY_CUMULATIVE = 2;
|
|
180
225
|
|
|
181
226
|
export interface OtlpHttpMetricExporterOptions {
|
|
182
227
|
endpoint: string;
|
|
183
228
|
headers?: Record<string, string>;
|
|
229
|
+
// How often to auto-flush buffered metrics in ms. Default: 30_000.
|
|
230
|
+
flushIntervalMs?: number;
|
|
184
231
|
}
|
|
185
232
|
|
|
186
233
|
export class OtlpHttpMetricExporter implements MetricExporter {
|
|
187
234
|
private readonly metricsEndpoint: string;
|
|
188
235
|
private readonly headers: Record<string, string>;
|
|
189
236
|
private resource_: Readonly<Resource> | undefined;
|
|
237
|
+
private wal_: Wal<MetricRecord> | undefined;
|
|
238
|
+
private timer_: ReturnType<typeof setInterval> | undefined;
|
|
239
|
+
private buffer_: MetricRecord[] = [];
|
|
190
240
|
|
|
191
241
|
constructor(options: OtlpHttpMetricExporterOptions) {
|
|
192
242
|
this.metricsEndpoint = options.endpoint.replace(/\/$/, '') + '/v1/metrics';
|
|
@@ -194,18 +244,82 @@ export class OtlpHttpMetricExporter implements MetricExporter {
|
|
|
194
244
|
'Content-Type': 'application/json',
|
|
195
245
|
...options.headers,
|
|
196
246
|
};
|
|
247
|
+
|
|
248
|
+
const interval = options.flushIntervalMs ?? 30_000;
|
|
249
|
+
this.timer_ = setInterval(() => {
|
|
250
|
+
this.flush();
|
|
251
|
+
}, interval);
|
|
252
|
+
// Allow Node.js to exit cleanly in test environments without calling destroy().
|
|
253
|
+
if (typeof this.timer_.unref === 'function') this.timer_.unref();
|
|
197
254
|
}
|
|
198
255
|
|
|
199
256
|
setResource(resource: Readonly<Resource>): void {
|
|
200
257
|
this.resource_ = resource;
|
|
201
258
|
}
|
|
202
259
|
|
|
260
|
+
setStorage(storage: StorageAdapter): void {
|
|
261
|
+
this.wal_ = new Wal<MetricRecord>(
|
|
262
|
+
storage,
|
|
263
|
+
'@react-native-otel/wal/metrics'
|
|
264
|
+
);
|
|
265
|
+
this.replayWal();
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
private replayWal(): void {
|
|
269
|
+
if (!this.wal_) return;
|
|
270
|
+
for (const batch of this.wal_.readAll()) {
|
|
271
|
+
this.deliverBatch(batch.data as MetricRecord[], batch.id);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
203
275
|
export(metrics: MetricRecord[]): void {
|
|
204
276
|
if (metrics.length === 0) return;
|
|
205
|
-
this.
|
|
277
|
+
this.buffer_.push(...metrics);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Drain buffered metrics and deliver them immediately.
|
|
281
|
+
// Called by the internal timer and also useful for testing / explicit flush.
|
|
282
|
+
flush(): void {
|
|
283
|
+
if (this.buffer_.length === 0) return;
|
|
284
|
+
const batch = this.buffer_.splice(0);
|
|
285
|
+
if (this.wal_) {
|
|
286
|
+
const id = this.wal_.write(batch);
|
|
287
|
+
this.deliverBatch(batch, id);
|
|
288
|
+
} else {
|
|
289
|
+
this.deliverBatch(batch, undefined);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Clear the flush timer and send any remaining buffered metrics.
|
|
294
|
+
destroy(): void {
|
|
295
|
+
if (this.timer_ !== undefined) {
|
|
296
|
+
clearInterval(this.timer_);
|
|
297
|
+
this.timer_ = undefined;
|
|
298
|
+
}
|
|
299
|
+
this.flush();
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
private deliverBatch(
|
|
303
|
+
metrics: MetricRecord[],
|
|
304
|
+
walId: string | undefined
|
|
305
|
+
): void {
|
|
306
|
+
const body = this.buildBody(metrics);
|
|
307
|
+
fetchWithRetry(this.metricsEndpoint, {
|
|
308
|
+
method: 'POST',
|
|
309
|
+
headers: this.headers,
|
|
310
|
+
body,
|
|
311
|
+
})
|
|
312
|
+
.then((success) => {
|
|
313
|
+
if (success && walId !== undefined) {
|
|
314
|
+
this.wal_?.delete(walId);
|
|
315
|
+
}
|
|
316
|
+
})
|
|
317
|
+
.catch(() => {
|
|
318
|
+
// Leave in WAL for next session
|
|
319
|
+
});
|
|
206
320
|
}
|
|
207
321
|
|
|
208
|
-
private
|
|
322
|
+
private buildBody(metrics: MetricRecord[]): string {
|
|
209
323
|
const resourceAttrs = this.resource_
|
|
210
324
|
? toOtlpAttributes(this.resource_ as unknown as Record<string, unknown>)
|
|
211
325
|
: [];
|
|
@@ -224,12 +338,11 @@ export class OtlpHttpMetricExporter implements MetricExporter {
|
|
|
224
338
|
const otlpMetrics = Array.from(byName.entries()).map(([name, records]) => {
|
|
225
339
|
const type = records[0]?.type;
|
|
226
340
|
|
|
227
|
-
// Counters → sum; histograms + gauges → gauge (no bucket data available).
|
|
228
341
|
if (type === 'counter') {
|
|
229
342
|
return {
|
|
230
343
|
name,
|
|
231
344
|
sum: {
|
|
232
|
-
dataPoints: records.map((r) => ({
|
|
345
|
+
dataPoints: (records as CounterRecord[]).map((r) => ({
|
|
233
346
|
asDouble: r.value,
|
|
234
347
|
startTimeUnixNano: msToNano(r.timestampMs),
|
|
235
348
|
timeUnixNano: msToNano(r.timestampMs),
|
|
@@ -241,10 +354,32 @@ export class OtlpHttpMetricExporter implements MetricExporter {
|
|
|
241
354
|
};
|
|
242
355
|
}
|
|
243
356
|
|
|
357
|
+
if (type === 'histogram') {
|
|
358
|
+
return {
|
|
359
|
+
name,
|
|
360
|
+
histogram: {
|
|
361
|
+
dataPoints: (records as HistogramRecord[]).map((r) => ({
|
|
362
|
+
count: String(r.count),
|
|
363
|
+
sum: r.sum,
|
|
364
|
+
// bucketCounts in OTLP are string-encoded uint64
|
|
365
|
+
bucketCounts: r.bucketCounts.map(String),
|
|
366
|
+
// explicitBounds does not include the implicit +Inf upper bound
|
|
367
|
+
explicitBounds: r.bucketBoundaries,
|
|
368
|
+
startTimeUnixNano: msToNano(r.timestampMs),
|
|
369
|
+
timeUnixNano: msToNano(r.timestampMs),
|
|
370
|
+
attributes: toOtlpAttributes(r.attributes),
|
|
371
|
+
})),
|
|
372
|
+
// Each flush window is independent — use DELTA semantics.
|
|
373
|
+
aggregationTemporality: AGGREGATION_TEMPORALITY_DELTA,
|
|
374
|
+
},
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// gauge
|
|
244
379
|
return {
|
|
245
380
|
name,
|
|
246
381
|
gauge: {
|
|
247
|
-
dataPoints: records.map((r) => ({
|
|
382
|
+
dataPoints: (records as GaugeRecord[]).map((r) => ({
|
|
248
383
|
asDouble: r.value,
|
|
249
384
|
timeUnixNano: msToNano(r.timestampMs),
|
|
250
385
|
attributes: toOtlpAttributes(r.attributes),
|
|
@@ -253,25 +388,19 @@ export class OtlpHttpMetricExporter implements MetricExporter {
|
|
|
253
388
|
};
|
|
254
389
|
});
|
|
255
390
|
|
|
256
|
-
|
|
391
|
+
return JSON.stringify({
|
|
257
392
|
resourceMetrics: [
|
|
258
393
|
{
|
|
259
394
|
resource: { attributes: resourceAttrs },
|
|
260
395
|
scopeMetrics: [
|
|
261
396
|
{
|
|
262
|
-
scope: { name: 'react-native-otel', version:
|
|
397
|
+
scope: { name: 'react-native-otel', version: SDK_VERSION },
|
|
263
398
|
metrics: otlpMetrics,
|
|
264
399
|
},
|
|
265
400
|
],
|
|
266
401
|
},
|
|
267
402
|
],
|
|
268
403
|
});
|
|
269
|
-
|
|
270
|
-
fetch(this.metricsEndpoint, {
|
|
271
|
-
method: 'POST',
|
|
272
|
-
headers: this.headers,
|
|
273
|
-
body,
|
|
274
|
-
}).catch(() => {});
|
|
275
404
|
}
|
|
276
405
|
}
|
|
277
406
|
|
|
@@ -301,6 +430,7 @@ export class OtlpHttpLogExporter implements LogExporter {
|
|
|
301
430
|
private buffer: LogRecord[] = [];
|
|
302
431
|
private resource_: Readonly<Resource> | undefined;
|
|
303
432
|
private timer_: ReturnType<typeof setInterval> | undefined;
|
|
433
|
+
private wal_: Wal<LogRecord> | undefined;
|
|
304
434
|
|
|
305
435
|
constructor(options: OtlpHttpLogExporterOptions) {
|
|
306
436
|
this.logsEndpoint = options.endpoint.replace(/\/$/, '') + '/v1/logs';
|
|
@@ -314,12 +444,27 @@ export class OtlpHttpLogExporter implements LogExporter {
|
|
|
314
444
|
this.timer_ = setInterval(() => {
|
|
315
445
|
this.flush();
|
|
316
446
|
}, interval);
|
|
447
|
+
// Allow Node.js to exit cleanly in test environments without calling destroy().
|
|
448
|
+
if (typeof this.timer_.unref === 'function') this.timer_.unref();
|
|
317
449
|
}
|
|
318
450
|
|
|
319
451
|
setResource(resource: Readonly<Resource>): void {
|
|
320
452
|
this.resource_ = resource;
|
|
321
453
|
}
|
|
322
454
|
|
|
455
|
+
// Called by OtelSDK.init() when a StorageAdapter is configured.
|
|
456
|
+
setStorage(storage: StorageAdapter): void {
|
|
457
|
+
this.wal_ = new Wal<LogRecord>(storage, '@react-native-otel/wal/logs');
|
|
458
|
+
this.replayWal();
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
private replayWal(): void {
|
|
462
|
+
if (!this.wal_) return;
|
|
463
|
+
for (const batch of this.wal_.readAll()) {
|
|
464
|
+
this.deliverBatch(batch.data as LogRecord[], batch.id);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
323
468
|
export(logs: LogRecord[]): void {
|
|
324
469
|
this.buffer.push(...logs);
|
|
325
470
|
if (this.buffer.length >= this.batchSize) {
|
|
@@ -330,7 +475,12 @@ export class OtlpHttpLogExporter implements LogExporter {
|
|
|
330
475
|
flush(): void {
|
|
331
476
|
if (this.buffer.length === 0) return;
|
|
332
477
|
const batch = this.buffer.splice(0);
|
|
333
|
-
this.
|
|
478
|
+
if (this.wal_) {
|
|
479
|
+
const id = this.wal_.write(batch);
|
|
480
|
+
this.deliverBatch(batch, id);
|
|
481
|
+
} else {
|
|
482
|
+
this.deliverBatch(batch, undefined);
|
|
483
|
+
}
|
|
334
484
|
}
|
|
335
485
|
|
|
336
486
|
destroy(): void {
|
|
@@ -341,7 +491,19 @@ export class OtlpHttpLogExporter implements LogExporter {
|
|
|
341
491
|
this.flush();
|
|
342
492
|
}
|
|
343
493
|
|
|
344
|
-
private
|
|
494
|
+
private deliverBatch(logs: LogRecord[], walId: string | undefined): void {
|
|
495
|
+
this.send(logs)
|
|
496
|
+
.then((success) => {
|
|
497
|
+
if (success && walId !== undefined) {
|
|
498
|
+
this.wal_?.delete(walId);
|
|
499
|
+
}
|
|
500
|
+
})
|
|
501
|
+
.catch(() => {
|
|
502
|
+
// Leave in WAL for next session
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
private send(logs: LogRecord[]): Promise<boolean> {
|
|
345
507
|
const resourceAttrs = this.resource_
|
|
346
508
|
? toOtlpAttributes(this.resource_ as unknown as Record<string, unknown>)
|
|
347
509
|
: [];
|
|
@@ -352,7 +514,7 @@ export class OtlpHttpLogExporter implements LogExporter {
|
|
|
352
514
|
resource: { attributes: resourceAttrs },
|
|
353
515
|
scopeLogs: [
|
|
354
516
|
{
|
|
355
|
-
scope: { name: 'react-native-otel', version:
|
|
517
|
+
scope: { name: 'react-native-otel', version: SDK_VERSION },
|
|
356
518
|
logRecords: logs.map((log) => ({
|
|
357
519
|
timeUnixNano: msToNano(log.timestampMs),
|
|
358
520
|
severityNumber: LOG_SEVERITY_NUMBER[log.severity] ?? 9,
|
|
@@ -368,10 +530,10 @@ export class OtlpHttpLogExporter implements LogExporter {
|
|
|
368
530
|
],
|
|
369
531
|
});
|
|
370
532
|
|
|
371
|
-
|
|
533
|
+
return fetchWithRetry(this.logsEndpoint, {
|
|
372
534
|
method: 'POST',
|
|
373
535
|
headers: this.headers,
|
|
374
536
|
body,
|
|
375
|
-
})
|
|
537
|
+
});
|
|
376
538
|
}
|
|
377
539
|
}
|
package/src/exporters/types.ts
CHANGED
|
@@ -3,14 +3,35 @@ import type { ReadonlySpan, SpanExporter } from '../core/span';
|
|
|
3
3
|
|
|
4
4
|
export type { ReadonlySpan, SpanExporter };
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
type: 'counter' | 'histogram' | 'gauge';
|
|
6
|
+
interface BaseMetricRecord {
|
|
8
7
|
name: string;
|
|
9
|
-
value: number;
|
|
10
8
|
timestampMs: number;
|
|
11
9
|
attributes: Attributes;
|
|
12
10
|
}
|
|
13
11
|
|
|
12
|
+
export interface CounterRecord extends BaseMetricRecord {
|
|
13
|
+
type: 'counter';
|
|
14
|
+
value: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface GaugeRecord extends BaseMetricRecord {
|
|
18
|
+
type: 'gauge';
|
|
19
|
+
value: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface HistogramRecord extends BaseMetricRecord {
|
|
23
|
+
type: 'histogram';
|
|
24
|
+
// Aggregated data for the flush window
|
|
25
|
+
count: number;
|
|
26
|
+
sum: number;
|
|
27
|
+
// Explicit bucket upper bounds (last bucket is +Inf, implicit)
|
|
28
|
+
bucketBoundaries: number[];
|
|
29
|
+
// Length is bucketBoundaries.length + 1 (last entry = +Inf bucket)
|
|
30
|
+
bucketCounts: number[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type MetricRecord = CounterRecord | GaugeRecord | HistogramRecord;
|
|
34
|
+
|
|
14
35
|
export interface LogRecord {
|
|
15
36
|
timestampMs: number;
|
|
16
37
|
severity: string;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import type { StorageAdapter } from '../instrumentation/errors';
|
|
2
|
+
|
|
3
|
+
const MAX_BATCHES = 3;
|
|
4
|
+
const MAX_ITEMS_PER_BATCH = 500;
|
|
5
|
+
|
|
6
|
+
// Circuit breaker: after this many consecutive delivery failures, pause for
|
|
7
|
+
// CIRCUIT_OPEN_MS before attempting again.
|
|
8
|
+
const CIRCUIT_BREAKER_THRESHOLD = 5;
|
|
9
|
+
const CIRCUIT_OPEN_MS = 60_000;
|
|
10
|
+
|
|
11
|
+
interface WalBatch<T> {
|
|
12
|
+
id: string;
|
|
13
|
+
timestamp: number;
|
|
14
|
+
data: T[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Write-ahead log backed by StorageAdapter.
|
|
18
|
+
// Persists undelivered export batches so they survive force-kills.
|
|
19
|
+
// Stores at most maxBatches batches; oldest are dropped when the limit is exceeded.
|
|
20
|
+
export class Wal<T> {
|
|
21
|
+
constructor(
|
|
22
|
+
private readonly storage: StorageAdapter,
|
|
23
|
+
private readonly storageKey: string,
|
|
24
|
+
private readonly maxBatches = MAX_BATCHES,
|
|
25
|
+
private readonly maxItems = MAX_ITEMS_PER_BATCH
|
|
26
|
+
) {}
|
|
27
|
+
|
|
28
|
+
// Persist a batch and return the id needed to delete it after delivery.
|
|
29
|
+
write(items: T[]): string {
|
|
30
|
+
const batches = this.readAll();
|
|
31
|
+
const id = `${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
|
|
32
|
+
batches.push({
|
|
33
|
+
id,
|
|
34
|
+
timestamp: Date.now(),
|
|
35
|
+
data: items.slice(0, this.maxItems),
|
|
36
|
+
});
|
|
37
|
+
// Keep only the most recent batches to cap storage growth.
|
|
38
|
+
const trimmed = batches.slice(-this.maxBatches);
|
|
39
|
+
this.storage.setSync(this.storageKey, JSON.stringify(trimmed));
|
|
40
|
+
return id;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Remove a successfully delivered batch from the WAL.
|
|
44
|
+
delete(id: string): void {
|
|
45
|
+
const remaining = this.readAll().filter((b) => b.id !== id);
|
|
46
|
+
if (remaining.length === 0) {
|
|
47
|
+
this.storage.deleteSync(this.storageKey);
|
|
48
|
+
} else {
|
|
49
|
+
this.storage.setSync(this.storageKey, JSON.stringify(remaining));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Return all pending batches (for session-start replay).
|
|
54
|
+
readAll(): WalBatch<T>[] {
|
|
55
|
+
const raw = this.storage.getSync(this.storageKey);
|
|
56
|
+
if (!raw) return [];
|
|
57
|
+
try {
|
|
58
|
+
return JSON.parse(raw) as WalBatch<T>[];
|
|
59
|
+
} catch {
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Per-endpoint circuit-breaker state.
|
|
66
|
+
// Key: URL string. Stores consecutive failure count and open-until timestamp.
|
|
67
|
+
interface CircuitState {
|
|
68
|
+
failures: number;
|
|
69
|
+
openUntil: number;
|
|
70
|
+
}
|
|
71
|
+
const circuitMap = new Map<string, CircuitState>();
|
|
72
|
+
|
|
73
|
+
function getCircuit(url: string): CircuitState {
|
|
74
|
+
let state = circuitMap.get(url);
|
|
75
|
+
if (!state) {
|
|
76
|
+
state = { failures: 0, openUntil: 0 };
|
|
77
|
+
circuitMap.set(url, state);
|
|
78
|
+
}
|
|
79
|
+
return state;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// The fetch implementation used for all OTLP delivery.
|
|
83
|
+
// Overridden by the SDK before installing fetch instrumentation so that
|
|
84
|
+
// exporter calls always use the original fetch — preventing infinite recursion.
|
|
85
|
+
let fetchImpl: typeof fetch | undefined;
|
|
86
|
+
|
|
87
|
+
export function setFetchImpl(impl: typeof fetch): void {
|
|
88
|
+
fetchImpl = impl;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Retry a fetch up to maxRetries times with exponential backoff + jitter.
|
|
92
|
+
// Returns true on success, false if all retries are exhausted.
|
|
93
|
+
// 4xx responses are not retried (they indicate a client-side problem).
|
|
94
|
+
// After CIRCUIT_BREAKER_THRESHOLD consecutive failures the circuit opens for
|
|
95
|
+
// CIRCUIT_OPEN_MS and all attempts are skipped until it closes.
|
|
96
|
+
export async function fetchWithRetry(
|
|
97
|
+
url: string,
|
|
98
|
+
options: RequestInit,
|
|
99
|
+
maxRetries = 3,
|
|
100
|
+
baseDelayMs = 500
|
|
101
|
+
): Promise<boolean> {
|
|
102
|
+
const circuit = getCircuit(url);
|
|
103
|
+
|
|
104
|
+
// Circuit open — bail out immediately without burning retries.
|
|
105
|
+
if (circuit.openUntil > Date.now()) {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Use the override if set (pre-patch fetch), otherwise fall back to global.
|
|
110
|
+
const doFetch = fetchImpl ?? globalThis.fetch;
|
|
111
|
+
|
|
112
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
113
|
+
try {
|
|
114
|
+
const response = await doFetch(url, options);
|
|
115
|
+
if (response.ok || (response.status >= 400 && response.status < 500)) {
|
|
116
|
+
// Reset circuit on success.
|
|
117
|
+
circuit.failures = 0;
|
|
118
|
+
circuit.openUntil = 0;
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
// 5xx — fall through to retry
|
|
122
|
+
} catch {
|
|
123
|
+
// Network error — fall through to retry
|
|
124
|
+
}
|
|
125
|
+
if (attempt < maxRetries - 1) {
|
|
126
|
+
// Jitter: scale by 0.5–1.0 to spread out retries under load.
|
|
127
|
+
const jitter = 0.5 + Math.random() * 0.5;
|
|
128
|
+
await new Promise<void>((r) =>
|
|
129
|
+
setTimeout(r, baseDelayMs * Math.pow(2, attempt) * jitter)
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// All retries exhausted — update circuit breaker.
|
|
135
|
+
circuit.failures += 1;
|
|
136
|
+
if (circuit.failures >= CIRCUIT_BREAKER_THRESHOLD) {
|
|
137
|
+
circuit.openUntil = Date.now() + CIRCUIT_OPEN_MS;
|
|
138
|
+
}
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Reset circuit breaker state for a URL (useful in tests).
|
|
143
|
+
export function resetCircuit(url: string): void {
|
|
144
|
+
circuitMap.delete(url);
|
|
145
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// SDK
|
|
2
2
|
export { otel } from './sdk';
|
|
3
|
-
export type { OtelConfig } from './sdk';
|
|
3
|
+
export type { OtelConfig, NetworkAdapter } from './sdk';
|
|
4
4
|
|
|
5
5
|
// Core
|
|
6
6
|
export { Span, NoopSpan } from './core/span';
|
|
@@ -8,17 +8,36 @@ export type {
|
|
|
8
8
|
SpanKind,
|
|
9
9
|
SpanStatus,
|
|
10
10
|
SpanEvent,
|
|
11
|
+
SpanLink,
|
|
11
12
|
ReadonlySpan,
|
|
13
|
+
SpanProcessor,
|
|
12
14
|
} from './core/span';
|
|
13
15
|
export { Tracer } from './core/tracer';
|
|
16
|
+
export type { SpanOptions } from './core/tracer';
|
|
14
17
|
export { Meter, Counter, Histogram, Gauge } from './core/meter';
|
|
18
|
+
export type { HistogramOptions } from './core/meter';
|
|
15
19
|
export { OtelLogger } from './core/log-record';
|
|
16
20
|
export type { LogSeverity } from './core/log-record';
|
|
17
21
|
export type { Attributes, AttributeValue } from './core/attributes';
|
|
18
22
|
export type { Resource } from './core/resource';
|
|
19
23
|
|
|
24
|
+
// Samplers
|
|
25
|
+
export type { Sampler } from './core/sampler';
|
|
26
|
+
export {
|
|
27
|
+
AlwaysOnSampler,
|
|
28
|
+
AlwaysOffSampler,
|
|
29
|
+
TraceIdRatioSampler,
|
|
30
|
+
} from './core/sampler';
|
|
31
|
+
|
|
32
|
+
// Processors
|
|
33
|
+
export { SimpleSpanProcessor, NoopSpanProcessor } from './core/processor';
|
|
34
|
+
|
|
35
|
+
// Version
|
|
36
|
+
export { SDK_VERSION } from './version';
|
|
37
|
+
|
|
20
38
|
// Context
|
|
21
39
|
export { spanContext } from './context/span-context';
|
|
40
|
+
export type { SpanContextManagerPublic } from './context/span-context';
|
|
22
41
|
|
|
23
42
|
// Exporters
|
|
24
43
|
export type {
|
|
@@ -43,6 +62,11 @@ export type {
|
|
|
43
62
|
OtlpHttpMetricExporterOptions,
|
|
44
63
|
OtlpHttpLogExporterOptions,
|
|
45
64
|
} from './exporters/otlp-http-exporter';
|
|
65
|
+
export {
|
|
66
|
+
MultiSpanExporter,
|
|
67
|
+
MultiMetricExporter,
|
|
68
|
+
MultiLogExporter,
|
|
69
|
+
} from './exporters/multi-exporter';
|
|
46
70
|
|
|
47
71
|
// Instrumentation
|
|
48
72
|
export { createNavigationInstrumentation } from './instrumentation/navigation';
|
|
@@ -56,6 +80,17 @@ export type {
|
|
|
56
80
|
} from './instrumentation/network';
|
|
57
81
|
export { installErrorInstrumentation } from './instrumentation/errors';
|
|
58
82
|
export type { StorageAdapter } from './instrumentation/errors';
|
|
83
|
+
export {
|
|
84
|
+
createFetchInstrumentation,
|
|
85
|
+
uninstallFetchInstrumentation,
|
|
86
|
+
} from './instrumentation/fetch';
|
|
87
|
+
export type { FetchInstrumentationOptions } from './instrumentation/fetch';
|
|
88
|
+
export { installStartupInstrumentation } from './instrumentation/startup';
|
|
89
|
+
export {
|
|
90
|
+
createLinkingInstrumentation,
|
|
91
|
+
recordPushNotification,
|
|
92
|
+
} from './instrumentation/linking';
|
|
93
|
+
export type { LinkingInstrumentation } from './instrumentation/linking';
|
|
59
94
|
|
|
60
95
|
// React
|
|
61
96
|
export { OtelProvider, OtelContext } from './react/OtelProvider';
|
|
@@ -92,4 +92,31 @@ export function installErrorInstrumentation(params: {
|
|
|
92
92
|
|
|
93
93
|
originalHandler?.(error, isFatal);
|
|
94
94
|
});
|
|
95
|
+
|
|
96
|
+
// Wire up unhandled Promise rejection tracking.
|
|
97
|
+
// globalThis.onunhandledrejection is available in Hermes (default RN engine since 0.70).
|
|
98
|
+
// Without this, async errors that are never .catch()-ed are silently swallowed.
|
|
99
|
+
const prevRejectionHandler = (globalThis as Record<string, unknown>)
|
|
100
|
+
.onunhandledrejection as ((event: { reason: unknown }) => void) | undefined;
|
|
101
|
+
|
|
102
|
+
(globalThis as Record<string, unknown>).onunhandledrejection = (event: {
|
|
103
|
+
reason: unknown;
|
|
104
|
+
}) => {
|
|
105
|
+
const reason = event.reason;
|
|
106
|
+
const error = reason instanceof Error ? reason : new Error(String(reason));
|
|
107
|
+
|
|
108
|
+
const span = tracer.startSpan(`unhandled_rejection.${error.name}`, {
|
|
109
|
+
kind: 'INTERNAL',
|
|
110
|
+
attributes: {
|
|
111
|
+
[ATTR_EXCEPTION_TYPE]: error.name,
|
|
112
|
+
[ATTR_EXCEPTION_MESSAGE]: error.message,
|
|
113
|
+
[ATTR_EXCEPTION_STACKTRACE]: error.stack ?? '',
|
|
114
|
+
'exception.unhandled_rejection': true,
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
span.setStatus('ERROR', error.message);
|
|
118
|
+
span.end();
|
|
119
|
+
|
|
120
|
+
prevRejectionHandler?.call(globalThis, event);
|
|
121
|
+
};
|
|
95
122
|
}
|