@redthreadlabs/tracelog-client 1.8.0 → 2.0.0
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/dist/EventBuilder.d.ts +5 -7
- package/dist/EventBuilder.js +12 -19
- package/dist/LogClient.d.ts +51 -12
- package/dist/LogClient.js +215 -149
- package/dist/TraceLogClient.d.ts +71 -0
- package/dist/TraceLogClient.js +399 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +5 -3
- package/dist/types.d.ts +35 -22
- package/package.json +2 -2
package/dist/EventBuilder.d.ts
CHANGED
|
@@ -1,21 +1,19 @@
|
|
|
1
|
-
import { JsonValue,
|
|
2
|
-
export type EventEnqueuer = (event:
|
|
1
|
+
import { JsonValue, EventRecord } from './types';
|
|
2
|
+
export type EventEnqueuer = (event: EventRecord) => void;
|
|
3
3
|
export declare class EventBuilder {
|
|
4
4
|
private _enqueue;
|
|
5
5
|
private _type;
|
|
6
6
|
private _level;
|
|
7
7
|
private _message;
|
|
8
|
-
private _duration?;
|
|
9
8
|
private _error?;
|
|
10
|
-
private
|
|
9
|
+
private _labels?;
|
|
11
10
|
constructor(enqueue: EventEnqueuer, type: string);
|
|
12
11
|
info(message: string): this;
|
|
13
12
|
warn(message: string): this;
|
|
14
13
|
error(message: string): this;
|
|
15
14
|
debug(message: string): this;
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
withLabel(key: string, value: JsonValue): this;
|
|
16
|
+
withLabels(labels: Record<string, JsonValue>): this;
|
|
18
17
|
withError(err: any): this;
|
|
19
|
-
withDuration(ms: number): this;
|
|
20
18
|
send(): void;
|
|
21
19
|
}
|
package/dist/EventBuilder.js
CHANGED
|
@@ -29,16 +29,16 @@ class EventBuilder {
|
|
|
29
29
|
this._message = message;
|
|
30
30
|
return this;
|
|
31
31
|
}
|
|
32
|
-
|
|
33
|
-
if (!this.
|
|
34
|
-
this.
|
|
35
|
-
this.
|
|
32
|
+
withLabel(key, value) {
|
|
33
|
+
if (!this._labels)
|
|
34
|
+
this._labels = {};
|
|
35
|
+
this._labels[key] = value;
|
|
36
36
|
return this;
|
|
37
37
|
}
|
|
38
|
-
|
|
39
|
-
if (!this.
|
|
40
|
-
this.
|
|
41
|
-
Object.assign(this.
|
|
38
|
+
withLabels(labels) {
|
|
39
|
+
if (!this._labels)
|
|
40
|
+
this._labels = {};
|
|
41
|
+
Object.assign(this._labels, labels);
|
|
42
42
|
return this;
|
|
43
43
|
}
|
|
44
44
|
withError(err) {
|
|
@@ -73,31 +73,24 @@ class EventBuilder {
|
|
|
73
73
|
}
|
|
74
74
|
return this;
|
|
75
75
|
}
|
|
76
|
-
withDuration(ms) {
|
|
77
|
-
this._duration = ms;
|
|
78
|
-
return this;
|
|
79
|
-
}
|
|
80
76
|
send() {
|
|
81
77
|
const event = {
|
|
82
78
|
type: this._type,
|
|
83
|
-
timestamp: Date.now(),
|
|
79
|
+
timestamp: Date.now() * 1000, // epoch microseconds (the on-disk unit)
|
|
84
80
|
level: this._level,
|
|
85
81
|
message: this._message,
|
|
86
82
|
tz_offset: (0, util_1.tzOffsetMinutes)(),
|
|
87
83
|
};
|
|
88
|
-
if (this._duration !== undefined)
|
|
89
|
-
event.duration = this._duration;
|
|
90
84
|
if (this._error)
|
|
91
85
|
event.error = this._error;
|
|
92
|
-
if (this.
|
|
93
|
-
event.
|
|
86
|
+
if (this._labels)
|
|
87
|
+
event.context = { labels: this._labels };
|
|
94
88
|
this._enqueue(event);
|
|
95
89
|
}
|
|
96
90
|
}
|
|
97
91
|
exports.EventBuilder = EventBuilder;
|
|
98
92
|
// Stringify an error code (ShareDB and Node errors carry one) for the
|
|
99
|
-
// structured `error.code` field
|
|
100
|
-
// message text; a dedicated field is facetable downstream.
|
|
93
|
+
// structured `error.code` field — a dedicated field is facetable downstream.
|
|
101
94
|
function extractCode(code) {
|
|
102
95
|
if (typeof code === 'string' || typeof code === 'number') {
|
|
103
96
|
return String(code);
|
package/dist/LogClient.d.ts
CHANGED
|
@@ -1,28 +1,43 @@
|
|
|
1
1
|
import { EventBuilder } from './EventBuilder';
|
|
2
|
-
import {
|
|
2
|
+
import { EndOptions, LogClientOptions, RecordOptions, StartOptions } from './types';
|
|
3
3
|
export declare class LogClient {
|
|
4
4
|
private _opts;
|
|
5
5
|
private _eventBuffer;
|
|
6
|
-
private
|
|
7
|
-
private
|
|
6
|
+
private _transactionBuffer;
|
|
7
|
+
private _spanBuffer;
|
|
8
|
+
private _activeSpans;
|
|
9
|
+
/** Per-launch id; the join key between records and this lifetime's origin. */
|
|
10
|
+
private readonly _lifetimeId;
|
|
11
|
+
/** JSON of the last origin sent, to detect changes (null ⇒ not yet sent). */
|
|
12
|
+
private _lastOriginJson;
|
|
8
13
|
private _flushHandle;
|
|
9
14
|
private _persistHandle;
|
|
10
15
|
private _disposed;
|
|
11
16
|
private _flushing;
|
|
12
17
|
constructor(opts: LogClientOptions);
|
|
18
|
+
/** This LogClient's lifetime id (one per launch / process run). */
|
|
19
|
+
get lifetimeId(): string;
|
|
13
20
|
event(type?: string): EventBuilder;
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
/** Start a root timed operation (a trace root). Recorded as a transaction. */
|
|
22
|
+
startTransaction(name: string, opts?: StartOptions): Transaction;
|
|
23
|
+
/** Start a sub-operation under a transaction or span. Recorded as a span. */
|
|
24
|
+
startSpan(name: string, parent: Transaction | Span, opts?: StartOptions): Span;
|
|
25
|
+
/** Internal: end a live transaction/span by id (called by the handles). */
|
|
26
|
+
_end(id: string, opts?: EndOptions): void;
|
|
27
|
+
/** Record a complete root operation timed elsewhere → a transaction. */
|
|
28
|
+
recordTransaction(name: string, durationMs: number, opts?: RecordOptions): void;
|
|
29
|
+
/** Record a complete sub-operation timed elsewhere → a span under `parent`. */
|
|
30
|
+
recordSpan(name: string, durationMs: number, parent: Transaction | Span, opts?: RecordOptions): void;
|
|
31
|
+
private _belowThreshold;
|
|
32
|
+
private _buildOneShot;
|
|
23
33
|
flush(): Promise<void>;
|
|
24
34
|
dispose(): void;
|
|
25
35
|
private _enqueueEvent;
|
|
36
|
+
private _enqueueTransaction;
|
|
37
|
+
private _enqueueSpan;
|
|
38
|
+
private _afterEnqueue;
|
|
39
|
+
/** The origin to attach to this flush, or undefined if unchanged since last sent. */
|
|
40
|
+
private _takeOriginIfChanged;
|
|
26
41
|
private _sendInChunks;
|
|
27
42
|
private _sendChunkWithRetry;
|
|
28
43
|
private _sendChunk;
|
|
@@ -30,3 +45,27 @@ export declare class LogClient {
|
|
|
30
45
|
private _persistNow;
|
|
31
46
|
private _loadPersistedLogs;
|
|
32
47
|
}
|
|
48
|
+
/** A live root operation. Recorded as a `transaction` when ended. */
|
|
49
|
+
export declare class Transaction {
|
|
50
|
+
private readonly _client;
|
|
51
|
+
readonly id: string;
|
|
52
|
+
readonly traceId: string;
|
|
53
|
+
readonly type: string;
|
|
54
|
+
constructor(_client: LogClient, id: string, traceId: string, type: string);
|
|
55
|
+
/** A transaction is its own trace root. */
|
|
56
|
+
get transactionId(): string;
|
|
57
|
+
startSpan(name: string, opts?: StartOptions): Span;
|
|
58
|
+
end(opts?: EndOptions): void;
|
|
59
|
+
}
|
|
60
|
+
/** A live sub-operation. Recorded as a `span` when ended. */
|
|
61
|
+
export declare class Span {
|
|
62
|
+
private readonly _client;
|
|
63
|
+
readonly id: string;
|
|
64
|
+
readonly traceId: string;
|
|
65
|
+
readonly transactionId: string;
|
|
66
|
+
readonly parentId: string;
|
|
67
|
+
readonly type: string;
|
|
68
|
+
constructor(_client: LogClient, id: string, traceId: string, transactionId: string, parentId: string, type: string);
|
|
69
|
+
startSpan(name: string, opts?: StartOptions): Span;
|
|
70
|
+
end(opts?: EndOptions): void;
|
|
71
|
+
}
|
package/dist/LogClient.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.LogClient = void 0;
|
|
3
|
+
exports.Span = exports.Transaction = exports.LogClient = void 0;
|
|
4
4
|
const EventBuilder_1 = require("./EventBuilder");
|
|
5
5
|
const util_1 = require("./util");
|
|
6
6
|
// Level ordering for the optional getMinLevel gate. An event is dropped when
|
|
7
7
|
// its level ranks below the host-supplied minimum.
|
|
8
8
|
const LEVEL_RANK = { debug: 0, info: 1, warn: 2, error: 3 };
|
|
9
|
+
const DEFAULT_TYPE = 'app';
|
|
9
10
|
// Defaults (all overridable via LogClientOptions)
|
|
10
11
|
const DEFAULT_FLUSH_CADENCE_MS = 5000;
|
|
11
12
|
const DEFAULT_MAX_BUFFER_SIZE = 100;
|
|
@@ -18,8 +19,13 @@ const PERSIST_DEBOUNCE_MS = 100;
|
|
|
18
19
|
class LogClient {
|
|
19
20
|
constructor(opts) {
|
|
20
21
|
this._eventBuffer = [];
|
|
21
|
-
this.
|
|
22
|
-
this.
|
|
22
|
+
this._transactionBuffer = [];
|
|
23
|
+
this._spanBuffer = [];
|
|
24
|
+
this._activeSpans = new Map();
|
|
25
|
+
/** Per-launch id; the join key between records and this lifetime's origin. */
|
|
26
|
+
this._lifetimeId = randomHex(16);
|
|
27
|
+
/** JSON of the last origin sent, to detect changes (null ⇒ not yet sent). */
|
|
28
|
+
this._lastOriginJson = null;
|
|
23
29
|
this._flushHandle = null;
|
|
24
30
|
this._persistHandle = null;
|
|
25
31
|
this._disposed = false;
|
|
@@ -28,115 +34,120 @@ class LogClient {
|
|
|
28
34
|
this._loadPersistedLogs();
|
|
29
35
|
this._flushHandle = setInterval(() => this.flush(), opts.flushCadenceMs ?? DEFAULT_FLUSH_CADENCE_MS);
|
|
30
36
|
}
|
|
37
|
+
/** This LogClient's lifetime id (one per launch / process run). */
|
|
38
|
+
get lifetimeId() {
|
|
39
|
+
return this._lifetimeId;
|
|
40
|
+
}
|
|
31
41
|
// ---- Fluent event builder ----
|
|
32
42
|
event(type = 'client-log') {
|
|
33
43
|
return new EventBuilder_1.EventBuilder((evt) => this._enqueueEvent(evt), type);
|
|
34
44
|
}
|
|
35
|
-
// ----
|
|
36
|
-
|
|
45
|
+
// ---- Live traces: transactions & spans ----
|
|
46
|
+
/** Start a root timed operation (a trace root). Recorded as a transaction. */
|
|
47
|
+
startTransaction(name, opts) {
|
|
37
48
|
const id = randomHex(16);
|
|
38
|
-
const trace_id =
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
49
|
+
const trace_id = randomHex(32);
|
|
50
|
+
const type = opts?.type ?? DEFAULT_TYPE;
|
|
51
|
+
this._activeSpans.set(id, {
|
|
52
|
+
kind: 'transaction', id, trace_id, name, type,
|
|
53
|
+
startTime: now(), tzOffset: (0, util_1.tzOffsetMinutes)(), children: [],
|
|
54
|
+
});
|
|
55
|
+
return new Transaction(this, id, trace_id, type);
|
|
56
|
+
}
|
|
57
|
+
/** Start a sub-operation under a transaction or span. Recorded as a span. */
|
|
58
|
+
startSpan(name, parent, opts) {
|
|
59
|
+
const id = randomHex(16);
|
|
60
|
+
const type = opts?.type ?? DEFAULT_TYPE;
|
|
61
|
+
this._activeSpans.set(id, {
|
|
62
|
+
kind: 'span', id,
|
|
63
|
+
trace_id: parent.traceId,
|
|
64
|
+
transaction_id: parent.transactionId,
|
|
65
|
+
parent_id: parent.id,
|
|
66
|
+
name, type,
|
|
67
|
+
startTime: now(), tzOffset: (0, util_1.tzOffsetMinutes)(), children: [],
|
|
68
|
+
});
|
|
69
|
+
this._activeSpans.get(parent.id)?.children.push(id);
|
|
70
|
+
return new Span(this, id, parent.traceId, parent.transactionId, parent.id, type);
|
|
57
71
|
}
|
|
58
|
-
|
|
59
|
-
|
|
72
|
+
/** Internal: end a live transaction/span by id (called by the handles). */
|
|
73
|
+
_end(id, opts) {
|
|
74
|
+
const active = this._activeSpans.get(id);
|
|
60
75
|
if (!active)
|
|
61
76
|
return;
|
|
62
|
-
|
|
63
|
-
// Auto-close children that haven't been ended yet
|
|
77
|
+
// Auto-close any children not yet ended, so a forgotten child can't leak.
|
|
64
78
|
for (const childId of active.children) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
this.endPerf(childActive.token);
|
|
68
|
-
}
|
|
79
|
+
if (this._activeSpans.has(childId))
|
|
80
|
+
this._end(childId);
|
|
69
81
|
}
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
name:
|
|
75
|
-
type:
|
|
76
|
-
timestamp: Math.round(active.startTime),
|
|
82
|
+
const duration = Math.max(0, now() - active.startTime);
|
|
83
|
+
const base = {
|
|
84
|
+
id: active.id,
|
|
85
|
+
trace_id: active.trace_id,
|
|
86
|
+
name: active.name,
|
|
87
|
+
type: active.type,
|
|
88
|
+
timestamp: Math.round(active.startTime * 1000), // ms → µs
|
|
77
89
|
duration: Math.round(duration),
|
|
78
|
-
outcome: 'success',
|
|
90
|
+
outcome: opts?.outcome ?? 'success',
|
|
79
91
|
tz_offset: active.tzOffset,
|
|
92
|
+
...(opts?.labels && Object.keys(opts.labels).length > 0 ? { context: { labels: opts.labels } } : {}),
|
|
80
93
|
};
|
|
81
|
-
|
|
82
|
-
|
|
94
|
+
this._activeSpans.delete(id);
|
|
95
|
+
if (active.kind === 'transaction') {
|
|
96
|
+
this._enqueueTransaction(base);
|
|
83
97
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
87
|
-
this._perfBuffer.push(perf);
|
|
88
|
-
this._activePerfs.delete(token.id);
|
|
89
|
-
this._schedulePersist();
|
|
90
|
-
// Force flush if buffer is getting large
|
|
91
|
-
if (this._perfBuffer.length + this._eventBuffer.length >= (this._opts.maxBufferSize ?? DEFAULT_MAX_BUFFER_SIZE)) {
|
|
92
|
-
this.flush();
|
|
98
|
+
else {
|
|
99
|
+
this._enqueueSpan({ ...base, transaction_id: active.transaction_id, parent_id: active.parent_id });
|
|
93
100
|
}
|
|
94
101
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
102
|
+
// ---- One-shot (pre-measured) traces ----
|
|
103
|
+
/** Record a complete root operation timed elsewhere → a transaction. */
|
|
104
|
+
recordTransaction(name, durationMs, opts) {
|
|
105
|
+
if (this._disposed || this._belowThreshold(durationMs, opts))
|
|
106
|
+
return;
|
|
107
|
+
const id = randomHex(16);
|
|
108
|
+
this._enqueueTransaction(this._buildOneShot(id, randomHex(32), name, durationMs, opts));
|
|
109
|
+
}
|
|
110
|
+
/** Record a complete sub-operation timed elsewhere → a span under `parent`. */
|
|
111
|
+
recordSpan(name, durationMs, parent, opts) {
|
|
112
|
+
if (this._disposed || this._belowThreshold(durationMs, opts))
|
|
103
113
|
return;
|
|
104
|
-
if (!isFinite(durationMs) || durationMs < 0)
|
|
105
|
-
durationMs = 0;
|
|
106
114
|
const id = randomHex(16);
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
115
|
+
const rec = this._buildOneShot(id, parent.traceId, name, durationMs, opts);
|
|
116
|
+
rec.transaction_id = parent.transactionId;
|
|
117
|
+
rec.parent_id = parent.id;
|
|
118
|
+
this._enqueueSpan(rec);
|
|
119
|
+
}
|
|
120
|
+
_belowThreshold(durationMs, opts) {
|
|
121
|
+
return opts?.minDurationMs !== undefined && durationMs < opts.minDurationMs;
|
|
122
|
+
}
|
|
123
|
+
_buildOneShot(id, trace_id, name, durationMs, opts) {
|
|
124
|
+
const d = isFinite(durationMs) && durationMs > 0 ? durationMs : 0;
|
|
125
|
+
const rec = {
|
|
126
|
+
id, trace_id, name,
|
|
127
|
+
type: opts?.type ?? DEFAULT_TYPE,
|
|
128
|
+
// No live start, so back-compute it from the measured duration.
|
|
129
|
+
timestamp: Math.round((now() - d) * 1000), // ms → µs
|
|
130
|
+
duration: Math.round(d),
|
|
131
|
+
outcome: opts?.outcome ?? 'success',
|
|
118
132
|
tz_offset: (0, util_1.tzOffsetMinutes)(),
|
|
119
133
|
};
|
|
120
|
-
if (
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
this._perfBuffer.push(perf);
|
|
124
|
-
this._schedulePersist();
|
|
125
|
-
if (this._perfBuffer.length + this._eventBuffer.length >= (this._opts.maxBufferSize ?? DEFAULT_MAX_BUFFER_SIZE)) {
|
|
126
|
-
this.flush();
|
|
127
|
-
}
|
|
134
|
+
if (opts?.labels && Object.keys(opts.labels).length > 0)
|
|
135
|
+
rec.context = { labels: opts.labels };
|
|
136
|
+
return rec;
|
|
128
137
|
}
|
|
129
138
|
// ---- Transport ----
|
|
130
139
|
async flush() {
|
|
131
140
|
if (this._disposed || this._flushing)
|
|
132
141
|
return;
|
|
133
|
-
if (this._eventBuffer.length === 0 && this.
|
|
142
|
+
if (this._eventBuffer.length === 0 && this._transactionBuffer.length === 0 && this._spanBuffer.length === 0)
|
|
134
143
|
return;
|
|
135
144
|
this._flushing = true;
|
|
136
145
|
try {
|
|
137
146
|
const events = this._eventBuffer.splice(0);
|
|
138
|
-
const
|
|
139
|
-
|
|
147
|
+
const transactions = this._transactionBuffer.splice(0);
|
|
148
|
+
const spans = this._spanBuffer.splice(0);
|
|
149
|
+
const origin = this._takeOriginIfChanged();
|
|
150
|
+
await this._sendInChunks(events, transactions, spans, origin);
|
|
140
151
|
}
|
|
141
152
|
finally {
|
|
142
153
|
this._flushing = false;
|
|
@@ -155,109 +166,138 @@ class LogClient {
|
|
|
155
166
|
clearTimeout(this._persistHandle);
|
|
156
167
|
this._persistHandle = null;
|
|
157
168
|
}
|
|
158
|
-
// Persist anything remaining
|
|
159
169
|
this._persistNow();
|
|
160
170
|
}
|
|
161
|
-
// ---- Internal:
|
|
171
|
+
// ---- Internal: buffering ----
|
|
162
172
|
_enqueueEvent(event) {
|
|
163
173
|
if (this._disposed)
|
|
164
174
|
return;
|
|
165
|
-
// Level gate: drop events below the host-supplied minimum before
|
|
166
|
-
// buffer, persist, or ship. No callback ⇒ emit everything (back-compat).
|
|
175
|
+
// Level gate: drop events below the host-supplied minimum before buffering.
|
|
167
176
|
const minLevel = this._opts.getMinLevel?.();
|
|
168
177
|
if (minLevel && LEVEL_RANK[event.level] < LEVEL_RANK[minLevel])
|
|
169
178
|
return;
|
|
179
|
+
if (event.locale === undefined) {
|
|
180
|
+
const locale = this._opts.getLocale?.();
|
|
181
|
+
if (locale)
|
|
182
|
+
event.locale = locale;
|
|
183
|
+
}
|
|
170
184
|
this._eventBuffer.push(event);
|
|
185
|
+
this._afterEnqueue();
|
|
186
|
+
}
|
|
187
|
+
_enqueueTransaction(rec) {
|
|
188
|
+
if (this._disposed)
|
|
189
|
+
return;
|
|
190
|
+
this._transactionBuffer.push(rec);
|
|
191
|
+
this._afterEnqueue();
|
|
192
|
+
}
|
|
193
|
+
_enqueueSpan(rec) {
|
|
194
|
+
if (this._disposed)
|
|
195
|
+
return;
|
|
196
|
+
this._spanBuffer.push(rec);
|
|
197
|
+
this._afterEnqueue();
|
|
198
|
+
}
|
|
199
|
+
_afterEnqueue() {
|
|
171
200
|
this._schedulePersist();
|
|
172
|
-
|
|
201
|
+
const total = this._eventBuffer.length + this._transactionBuffer.length + this._spanBuffer.length;
|
|
202
|
+
if (total >= (this._opts.maxBufferSize ?? DEFAULT_MAX_BUFFER_SIZE))
|
|
173
203
|
this.flush();
|
|
204
|
+
}
|
|
205
|
+
/** The origin to attach to this flush, or undefined if unchanged since last sent. */
|
|
206
|
+
_takeOriginIfChanged() {
|
|
207
|
+
let origin;
|
|
208
|
+
try {
|
|
209
|
+
origin = this._opts.getOrigin();
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
return undefined;
|
|
174
213
|
}
|
|
214
|
+
const oj = JSON.stringify(origin);
|
|
215
|
+
if (oj === this._lastOriginJson)
|
|
216
|
+
return undefined;
|
|
217
|
+
this._lastOriginJson = oj;
|
|
218
|
+
return { ...origin, lifetime_id: this._lifetimeId };
|
|
175
219
|
}
|
|
176
220
|
// ---- Internal: chunked sending ----
|
|
177
|
-
async _sendInChunks(events,
|
|
221
|
+
async _sendInChunks(events, transactions, spans, origin) {
|
|
178
222
|
const maxChunkSize = this._opts.maxChunkSize ?? DEFAULT_MAX_CHUNK_SIZE;
|
|
179
223
|
const maxChunkBytes = this._opts.maxChunkBytes ?? DEFAULT_MAX_CHUNK_BYTES;
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
224
|
+
const work = [
|
|
225
|
+
...events.map((rec) => ({ k: 'e', rec })),
|
|
226
|
+
...transactions.map((rec) => ({ k: 't', rec })),
|
|
227
|
+
...spans.map((rec) => ({ k: 's', rec })),
|
|
228
|
+
];
|
|
229
|
+
let i = 0;
|
|
230
|
+
let isFirst = true;
|
|
231
|
+
while (i < work.length || (isFirst && origin)) {
|
|
232
|
+
if (!isFirst)
|
|
186
233
|
await delay(INTER_CHUNK_DELAY_MS);
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
if (estimatedBytes + itemBytes > maxChunkBytes && chunkEvents.length > 0)
|
|
196
|
-
break;
|
|
197
|
-
chunkEvents.push(events[eventIdx]);
|
|
198
|
-
estimatedBytes += itemBytes;
|
|
199
|
-
eventIdx++;
|
|
200
|
-
}
|
|
201
|
-
// Fill chunk with perfs
|
|
202
|
-
while (perfIdx < perfs.length && chunkEvents.length + chunkPerfs.length < maxChunkSize) {
|
|
203
|
-
const itemBytes = estimateJsonSize(perfs[perfIdx]);
|
|
204
|
-
if (estimatedBytes + itemBytes > maxChunkBytes && (chunkEvents.length + chunkPerfs.length) > 0)
|
|
234
|
+
const ce = [];
|
|
235
|
+
const ct = [];
|
|
236
|
+
const cs = [];
|
|
237
|
+
let bytes = 200; // base overhead for the batch envelope
|
|
238
|
+
while (i < work.length && (ce.length + ct.length + cs.length) < maxChunkSize) {
|
|
239
|
+
const item = work[i];
|
|
240
|
+
const itemBytes = estimateJsonSize(item.rec);
|
|
241
|
+
if (bytes + itemBytes > maxChunkBytes && (ce.length + ct.length + cs.length) > 0)
|
|
205
242
|
break;
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
243
|
+
if (item.k === 'e')
|
|
244
|
+
ce.push(item.rec);
|
|
245
|
+
else if (item.k === 't')
|
|
246
|
+
ct.push(item.rec);
|
|
247
|
+
else
|
|
248
|
+
cs.push(item.rec);
|
|
249
|
+
bytes += itemBytes;
|
|
250
|
+
i++;
|
|
209
251
|
}
|
|
210
|
-
|
|
252
|
+
const chunkOrigin = isFirst ? origin : undefined;
|
|
253
|
+
isFirst = false;
|
|
254
|
+
if (ce.length === 0 && ct.length === 0 && cs.length === 0 && !chunkOrigin)
|
|
211
255
|
break;
|
|
212
|
-
await this._sendChunkWithRetry(
|
|
256
|
+
await this._sendChunkWithRetry(ce, ct, cs, chunkOrigin);
|
|
213
257
|
}
|
|
214
258
|
}
|
|
215
|
-
async _sendChunkWithRetry(events,
|
|
259
|
+
async _sendChunkWithRetry(events, transactions, spans, origin) {
|
|
216
260
|
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
217
261
|
try {
|
|
218
|
-
await this._sendChunk(events,
|
|
262
|
+
await this._sendChunk(events, transactions, spans, origin);
|
|
219
263
|
return;
|
|
220
264
|
}
|
|
221
|
-
catch
|
|
265
|
+
catch {
|
|
222
266
|
if (attempt < MAX_RETRIES) {
|
|
223
267
|
await delay(BASE_RETRY_DELAY_MS * Math.pow(2, attempt));
|
|
224
268
|
}
|
|
225
269
|
else {
|
|
226
|
-
// Max retries exceeded — put
|
|
270
|
+
// Max retries exceeded — put records back for persistence/retry, and
|
|
271
|
+
// re-arm the origin so it ships again on the next successful flush.
|
|
227
272
|
this._eventBuffer.push(...events);
|
|
228
|
-
this.
|
|
273
|
+
this._transactionBuffer.push(...transactions);
|
|
274
|
+
this._spanBuffer.push(...spans);
|
|
275
|
+
if (origin)
|
|
276
|
+
this._lastOriginJson = null;
|
|
229
277
|
this._schedulePersist();
|
|
230
278
|
}
|
|
231
279
|
}
|
|
232
280
|
}
|
|
233
281
|
}
|
|
234
|
-
async _sendChunk(events,
|
|
235
|
-
const batch = {
|
|
236
|
-
|
|
237
|
-
events,
|
|
238
|
-
perfs,
|
|
239
|
-
};
|
|
282
|
+
async _sendChunk(events, transactions, spans, origin) {
|
|
283
|
+
const batch = { events, transactions, spans };
|
|
284
|
+
batch.lifetime_id = this._lifetimeId;
|
|
240
285
|
const userId = this._opts.getUserId?.();
|
|
241
|
-
const sessionRef = this._opts.getSessionRef?.();
|
|
242
286
|
const deviceId = this._opts.getDeviceId?.();
|
|
243
287
|
if (userId)
|
|
244
288
|
batch.user_id = userId;
|
|
245
|
-
if (sessionRef)
|
|
246
|
-
batch.session_ref = sessionRef;
|
|
247
289
|
if (deviceId)
|
|
248
290
|
batch.device_id = deviceId;
|
|
291
|
+
if (origin)
|
|
292
|
+
batch.origin = origin;
|
|
249
293
|
const headers = await Promise.resolve(this._opts.getAuthHeaders());
|
|
250
294
|
const response = await fetch(this._opts.endpoint, {
|
|
251
295
|
method: 'POST',
|
|
252
|
-
headers: {
|
|
253
|
-
'Content-Type': 'application/json',
|
|
254
|
-
...headers,
|
|
255
|
-
},
|
|
296
|
+
headers: { 'Content-Type': 'application/json', ...headers },
|
|
256
297
|
body: JSON.stringify(batch),
|
|
257
298
|
});
|
|
258
|
-
if (!response.ok)
|
|
299
|
+
if (!response.ok)
|
|
259
300
|
throw new Error(`Server returned ${response.status}`);
|
|
260
|
-
}
|
|
261
301
|
}
|
|
262
302
|
// ---- Internal: persistence ----
|
|
263
303
|
_schedulePersist() {
|
|
@@ -271,13 +311,14 @@ class LogClient {
|
|
|
271
311
|
_persistNow() {
|
|
272
312
|
if (!this._opts.persistLogs)
|
|
273
313
|
return;
|
|
274
|
-
if (this._eventBuffer.length === 0 && this.
|
|
314
|
+
if (this._eventBuffer.length === 0 && this._transactionBuffer.length === 0 && this._spanBuffer.length === 0) {
|
|
275
315
|
this._opts.persistLogs('').catch(() => { });
|
|
276
316
|
return;
|
|
277
317
|
}
|
|
278
318
|
const data = JSON.stringify({
|
|
279
319
|
events: this._eventBuffer,
|
|
280
|
-
|
|
320
|
+
transactions: this._transactionBuffer,
|
|
321
|
+
spans: this._spanBuffer,
|
|
281
322
|
});
|
|
282
323
|
this._opts.persistLogs(data).catch(() => { });
|
|
283
324
|
}
|
|
@@ -289,13 +330,12 @@ class LogClient {
|
|
|
289
330
|
if (!data)
|
|
290
331
|
return;
|
|
291
332
|
const parsed = JSON.parse(data);
|
|
292
|
-
if (Array.isArray(parsed.events))
|
|
333
|
+
if (Array.isArray(parsed.events))
|
|
293
334
|
this._eventBuffer.push(...parsed.events);
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
// Clear persisted data now that it's loaded
|
|
335
|
+
if (Array.isArray(parsed.transactions))
|
|
336
|
+
this._transactionBuffer.push(...parsed.transactions);
|
|
337
|
+
if (Array.isArray(parsed.spans))
|
|
338
|
+
this._spanBuffer.push(...parsed.spans);
|
|
299
339
|
this._opts.persistLogs?.('').catch(() => { });
|
|
300
340
|
}
|
|
301
341
|
catch {
|
|
@@ -304,6 +344,35 @@ class LogClient {
|
|
|
304
344
|
}
|
|
305
345
|
}
|
|
306
346
|
exports.LogClient = LogClient;
|
|
347
|
+
// ---- Trace handles ----
|
|
348
|
+
/** A live root operation. Recorded as a `transaction` when ended. */
|
|
349
|
+
class Transaction {
|
|
350
|
+
constructor(_client, id, traceId, type) {
|
|
351
|
+
this._client = _client;
|
|
352
|
+
this.id = id;
|
|
353
|
+
this.traceId = traceId;
|
|
354
|
+
this.type = type;
|
|
355
|
+
}
|
|
356
|
+
/** A transaction is its own trace root. */
|
|
357
|
+
get transactionId() { return this.id; }
|
|
358
|
+
startSpan(name, opts) { return this._client.startSpan(name, this, opts); }
|
|
359
|
+
end(opts) { this._client._end(this.id, opts); }
|
|
360
|
+
}
|
|
361
|
+
exports.Transaction = Transaction;
|
|
362
|
+
/** A live sub-operation. Recorded as a `span` when ended. */
|
|
363
|
+
class Span {
|
|
364
|
+
constructor(_client, id, traceId, transactionId, parentId, type) {
|
|
365
|
+
this._client = _client;
|
|
366
|
+
this.id = id;
|
|
367
|
+
this.traceId = traceId;
|
|
368
|
+
this.transactionId = transactionId;
|
|
369
|
+
this.parentId = parentId;
|
|
370
|
+
this.type = type;
|
|
371
|
+
}
|
|
372
|
+
startSpan(name, opts) { return this._client.startSpan(name, this, opts); }
|
|
373
|
+
end(opts) { this._client._end(this.id, opts); }
|
|
374
|
+
}
|
|
375
|
+
exports.Span = Span;
|
|
307
376
|
// ---- Helpers ----
|
|
308
377
|
function randomHex(length) {
|
|
309
378
|
const chars = '0123456789abcdef';
|
|
@@ -315,8 +384,6 @@ function randomHex(length) {
|
|
|
315
384
|
}
|
|
316
385
|
function now() {
|
|
317
386
|
if (typeof performance !== 'undefined' && performance.now) {
|
|
318
|
-
// Use performance.now() for high-res timing, but we need wall-clock for timestamps
|
|
319
|
-
// Store the offset on first call
|
|
320
387
|
if (!now._offset) {
|
|
321
388
|
now._offset = Date.now() - performance.now();
|
|
322
389
|
}
|
|
@@ -328,6 +395,5 @@ function delay(ms) {
|
|
|
328
395
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
329
396
|
}
|
|
330
397
|
function estimateJsonSize(obj) {
|
|
331
|
-
// Fast estimate — avoid full serialization during chunking
|
|
332
398
|
return JSON.stringify(obj).length;
|
|
333
399
|
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { EventBuilder } from './EventBuilder';
|
|
2
|
+
import { EndOptions, LogClientOptions, RecordOptions, StartOptions } from './types';
|
|
3
|
+
export declare class TracelogClient {
|
|
4
|
+
private _opts;
|
|
5
|
+
private _eventBuffer;
|
|
6
|
+
private _transactionBuffer;
|
|
7
|
+
private _spanBuffer;
|
|
8
|
+
private _activeSpans;
|
|
9
|
+
/** Per-launch id; the join key between records and this lifetime's origin. */
|
|
10
|
+
private readonly _lifetimeId;
|
|
11
|
+
/** JSON of the last origin sent, to detect changes (null ⇒ not yet sent). */
|
|
12
|
+
private _lastOriginJson;
|
|
13
|
+
private _flushHandle;
|
|
14
|
+
private _persistHandle;
|
|
15
|
+
private _disposed;
|
|
16
|
+
private _flushing;
|
|
17
|
+
constructor(opts: LogClientOptions);
|
|
18
|
+
/** This TracelogClient's lifetime id (one per launch / process run). */
|
|
19
|
+
get lifetimeId(): string;
|
|
20
|
+
event(type?: string): EventBuilder;
|
|
21
|
+
/** Start a root timed operation (a trace root). Recorded as a transaction. */
|
|
22
|
+
startTransaction(name: string, opts?: StartOptions): Transaction;
|
|
23
|
+
/** Start a sub-operation under a transaction or span. Recorded as a span. */
|
|
24
|
+
startSpan(name: string, parent: Transaction | Span, opts?: StartOptions): Span;
|
|
25
|
+
/** Internal: end a live transaction/span by id (called by the handles). */
|
|
26
|
+
_end(id: string, opts?: EndOptions): void;
|
|
27
|
+
/** Record a complete root operation timed elsewhere → a transaction. */
|
|
28
|
+
recordTransaction(name: string, durationMs: number, opts?: RecordOptions): void;
|
|
29
|
+
/** Record a complete sub-operation timed elsewhere → a span under `parent`. */
|
|
30
|
+
recordSpan(name: string, durationMs: number, parent: Transaction | Span, opts?: RecordOptions): void;
|
|
31
|
+
private _belowThreshold;
|
|
32
|
+
private _buildOneShot;
|
|
33
|
+
flush(): Promise<void>;
|
|
34
|
+
dispose(): void;
|
|
35
|
+
private _enqueueEvent;
|
|
36
|
+
private _enqueueTransaction;
|
|
37
|
+
private _enqueueSpan;
|
|
38
|
+
private _afterEnqueue;
|
|
39
|
+
/** The origin to attach to this flush, or undefined if unchanged since last sent. */
|
|
40
|
+
private _takeOriginIfChanged;
|
|
41
|
+
private _sendInChunks;
|
|
42
|
+
private _sendChunkWithRetry;
|
|
43
|
+
private _sendChunk;
|
|
44
|
+
private _schedulePersist;
|
|
45
|
+
private _persistNow;
|
|
46
|
+
private _loadPersistedLogs;
|
|
47
|
+
}
|
|
48
|
+
/** A live root operation. Recorded as a `transaction` when ended. */
|
|
49
|
+
export declare class Transaction {
|
|
50
|
+
private readonly _client;
|
|
51
|
+
readonly id: string;
|
|
52
|
+
readonly traceId: string;
|
|
53
|
+
readonly type: string;
|
|
54
|
+
constructor(_client: TracelogClient, id: string, traceId: string, type: string);
|
|
55
|
+
/** A transaction is its own trace root. */
|
|
56
|
+
get transactionId(): string;
|
|
57
|
+
startSpan(name: string, opts?: StartOptions): Span;
|
|
58
|
+
end(opts?: EndOptions): void;
|
|
59
|
+
}
|
|
60
|
+
/** A live sub-operation. Recorded as a `span` when ended. */
|
|
61
|
+
export declare class Span {
|
|
62
|
+
private readonly _client;
|
|
63
|
+
readonly id: string;
|
|
64
|
+
readonly traceId: string;
|
|
65
|
+
readonly transactionId: string;
|
|
66
|
+
readonly parentId: string;
|
|
67
|
+
readonly type: string;
|
|
68
|
+
constructor(_client: TracelogClient, id: string, traceId: string, transactionId: string, parentId: string, type: string);
|
|
69
|
+
startSpan(name: string, opts?: StartOptions): Span;
|
|
70
|
+
end(opts?: EndOptions): void;
|
|
71
|
+
}
|
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Span = exports.Transaction = exports.TracelogClient = void 0;
|
|
4
|
+
const EventBuilder_1 = require("./EventBuilder");
|
|
5
|
+
const util_1 = require("./util");
|
|
6
|
+
// Level ordering for the optional getMinLevel gate. An event is dropped when
|
|
7
|
+
// its level ranks below the host-supplied minimum.
|
|
8
|
+
const LEVEL_RANK = { debug: 0, info: 1, warn: 2, error: 3 };
|
|
9
|
+
const DEFAULT_TYPE = 'app';
|
|
10
|
+
// Defaults (all overridable via LogClientOptions)
|
|
11
|
+
const DEFAULT_FLUSH_CADENCE_MS = 5000;
|
|
12
|
+
const DEFAULT_MAX_BUFFER_SIZE = 100;
|
|
13
|
+
const DEFAULT_MAX_CHUNK_SIZE = 50;
|
|
14
|
+
const DEFAULT_MAX_CHUNK_BYTES = 512 * 1024;
|
|
15
|
+
const INTER_CHUNK_DELAY_MS = 200;
|
|
16
|
+
const MAX_RETRIES = 3;
|
|
17
|
+
const BASE_RETRY_DELAY_MS = 1000;
|
|
18
|
+
const PERSIST_DEBOUNCE_MS = 100;
|
|
19
|
+
class TracelogClient {
|
|
20
|
+
constructor(opts) {
|
|
21
|
+
this._eventBuffer = [];
|
|
22
|
+
this._transactionBuffer = [];
|
|
23
|
+
this._spanBuffer = [];
|
|
24
|
+
this._activeSpans = new Map();
|
|
25
|
+
/** Per-launch id; the join key between records and this lifetime's origin. */
|
|
26
|
+
this._lifetimeId = randomHex(16);
|
|
27
|
+
/** JSON of the last origin sent, to detect changes (null ⇒ not yet sent). */
|
|
28
|
+
this._lastOriginJson = null;
|
|
29
|
+
this._flushHandle = null;
|
|
30
|
+
this._persistHandle = null;
|
|
31
|
+
this._disposed = false;
|
|
32
|
+
this._flushing = false;
|
|
33
|
+
this._opts = opts;
|
|
34
|
+
this._loadPersistedLogs();
|
|
35
|
+
this._flushHandle = setInterval(() => this.flush(), opts.flushCadenceMs ?? DEFAULT_FLUSH_CADENCE_MS);
|
|
36
|
+
}
|
|
37
|
+
/** This TracelogClient's lifetime id (one per launch / process run). */
|
|
38
|
+
get lifetimeId() {
|
|
39
|
+
return this._lifetimeId;
|
|
40
|
+
}
|
|
41
|
+
// ---- Fluent event builder ----
|
|
42
|
+
event(type = 'client-log') {
|
|
43
|
+
return new EventBuilder_1.EventBuilder((evt) => this._enqueueEvent(evt), type);
|
|
44
|
+
}
|
|
45
|
+
// ---- Live traces: transactions & spans ----
|
|
46
|
+
/** Start a root timed operation (a trace root). Recorded as a transaction. */
|
|
47
|
+
startTransaction(name, opts) {
|
|
48
|
+
const id = randomHex(16);
|
|
49
|
+
const trace_id = randomHex(32);
|
|
50
|
+
const type = opts?.type ?? DEFAULT_TYPE;
|
|
51
|
+
this._activeSpans.set(id, {
|
|
52
|
+
kind: 'transaction', id, trace_id, name, type,
|
|
53
|
+
startTime: now(), tzOffset: (0, util_1.tzOffsetMinutes)(), children: [],
|
|
54
|
+
});
|
|
55
|
+
return new Transaction(this, id, trace_id, type);
|
|
56
|
+
}
|
|
57
|
+
/** Start a sub-operation under a transaction or span. Recorded as a span. */
|
|
58
|
+
startSpan(name, parent, opts) {
|
|
59
|
+
const id = randomHex(16);
|
|
60
|
+
const type = opts?.type ?? DEFAULT_TYPE;
|
|
61
|
+
this._activeSpans.set(id, {
|
|
62
|
+
kind: 'span', id,
|
|
63
|
+
trace_id: parent.traceId,
|
|
64
|
+
transaction_id: parent.transactionId,
|
|
65
|
+
parent_id: parent.id,
|
|
66
|
+
name, type,
|
|
67
|
+
startTime: now(), tzOffset: (0, util_1.tzOffsetMinutes)(), children: [],
|
|
68
|
+
});
|
|
69
|
+
this._activeSpans.get(parent.id)?.children.push(id);
|
|
70
|
+
return new Span(this, id, parent.traceId, parent.transactionId, parent.id, type);
|
|
71
|
+
}
|
|
72
|
+
/** Internal: end a live transaction/span by id (called by the handles). */
|
|
73
|
+
_end(id, opts) {
|
|
74
|
+
const active = this._activeSpans.get(id);
|
|
75
|
+
if (!active)
|
|
76
|
+
return;
|
|
77
|
+
// Auto-close any children not yet ended, so a forgotten child can't leak.
|
|
78
|
+
for (const childId of active.children) {
|
|
79
|
+
if (this._activeSpans.has(childId))
|
|
80
|
+
this._end(childId);
|
|
81
|
+
}
|
|
82
|
+
const duration = Math.max(0, now() - active.startTime);
|
|
83
|
+
const base = {
|
|
84
|
+
id: active.id,
|
|
85
|
+
trace_id: active.trace_id,
|
|
86
|
+
name: active.name,
|
|
87
|
+
type: active.type,
|
|
88
|
+
timestamp: Math.round(active.startTime * 1000), // ms → µs
|
|
89
|
+
duration: Math.round(duration),
|
|
90
|
+
outcome: opts?.outcome ?? 'success',
|
|
91
|
+
tz_offset: active.tzOffset,
|
|
92
|
+
...(opts?.labels && Object.keys(opts.labels).length > 0 ? { context: { labels: opts.labels } } : {}),
|
|
93
|
+
};
|
|
94
|
+
this._activeSpans.delete(id);
|
|
95
|
+
if (active.kind === 'transaction') {
|
|
96
|
+
this._enqueueTransaction(base);
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
this._enqueueSpan({ ...base, transaction_id: active.transaction_id, parent_id: active.parent_id });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// ---- One-shot (pre-measured) traces ----
|
|
103
|
+
/** Record a complete root operation timed elsewhere → a transaction. */
|
|
104
|
+
recordTransaction(name, durationMs, opts) {
|
|
105
|
+
if (this._disposed || this._belowThreshold(durationMs, opts))
|
|
106
|
+
return;
|
|
107
|
+
const id = randomHex(16);
|
|
108
|
+
this._enqueueTransaction(this._buildOneShot(id, randomHex(32), name, durationMs, opts));
|
|
109
|
+
}
|
|
110
|
+
/** Record a complete sub-operation timed elsewhere → a span under `parent`. */
|
|
111
|
+
recordSpan(name, durationMs, parent, opts) {
|
|
112
|
+
if (this._disposed || this._belowThreshold(durationMs, opts))
|
|
113
|
+
return;
|
|
114
|
+
const id = randomHex(16);
|
|
115
|
+
const rec = this._buildOneShot(id, parent.traceId, name, durationMs, opts);
|
|
116
|
+
rec.transaction_id = parent.transactionId;
|
|
117
|
+
rec.parent_id = parent.id;
|
|
118
|
+
this._enqueueSpan(rec);
|
|
119
|
+
}
|
|
120
|
+
_belowThreshold(durationMs, opts) {
|
|
121
|
+
return opts?.minDurationMs !== undefined && durationMs < opts.minDurationMs;
|
|
122
|
+
}
|
|
123
|
+
_buildOneShot(id, trace_id, name, durationMs, opts) {
|
|
124
|
+
const d = isFinite(durationMs) && durationMs > 0 ? durationMs : 0;
|
|
125
|
+
const rec = {
|
|
126
|
+
id, trace_id, name,
|
|
127
|
+
type: opts?.type ?? DEFAULT_TYPE,
|
|
128
|
+
// No live start, so back-compute it from the measured duration.
|
|
129
|
+
timestamp: Math.round((now() - d) * 1000), // ms → µs
|
|
130
|
+
duration: Math.round(d),
|
|
131
|
+
outcome: opts?.outcome ?? 'success',
|
|
132
|
+
tz_offset: (0, util_1.tzOffsetMinutes)(),
|
|
133
|
+
};
|
|
134
|
+
if (opts?.labels && Object.keys(opts.labels).length > 0)
|
|
135
|
+
rec.context = { labels: opts.labels };
|
|
136
|
+
return rec;
|
|
137
|
+
}
|
|
138
|
+
// ---- Transport ----
|
|
139
|
+
async flush() {
|
|
140
|
+
if (this._disposed || this._flushing)
|
|
141
|
+
return;
|
|
142
|
+
if (this._eventBuffer.length === 0 && this._transactionBuffer.length === 0 && this._spanBuffer.length === 0)
|
|
143
|
+
return;
|
|
144
|
+
this._flushing = true;
|
|
145
|
+
try {
|
|
146
|
+
const events = this._eventBuffer.splice(0);
|
|
147
|
+
const transactions = this._transactionBuffer.splice(0);
|
|
148
|
+
const spans = this._spanBuffer.splice(0);
|
|
149
|
+
const origin = this._takeOriginIfChanged();
|
|
150
|
+
await this._sendInChunks(events, transactions, spans, origin);
|
|
151
|
+
}
|
|
152
|
+
finally {
|
|
153
|
+
this._flushing = false;
|
|
154
|
+
this._schedulePersist();
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
dispose() {
|
|
158
|
+
if (this._disposed)
|
|
159
|
+
return;
|
|
160
|
+
this._disposed = true;
|
|
161
|
+
if (this._flushHandle) {
|
|
162
|
+
clearInterval(this._flushHandle);
|
|
163
|
+
this._flushHandle = null;
|
|
164
|
+
}
|
|
165
|
+
if (this._persistHandle) {
|
|
166
|
+
clearTimeout(this._persistHandle);
|
|
167
|
+
this._persistHandle = null;
|
|
168
|
+
}
|
|
169
|
+
this._persistNow();
|
|
170
|
+
}
|
|
171
|
+
// ---- Internal: buffering ----
|
|
172
|
+
_enqueueEvent(event) {
|
|
173
|
+
if (this._disposed)
|
|
174
|
+
return;
|
|
175
|
+
// Level gate: drop events below the host-supplied minimum before buffering.
|
|
176
|
+
const minLevel = this._opts.getMinLevel?.();
|
|
177
|
+
if (minLevel && LEVEL_RANK[event.level] < LEVEL_RANK[minLevel])
|
|
178
|
+
return;
|
|
179
|
+
if (event.locale === undefined) {
|
|
180
|
+
const locale = this._opts.getLocale?.();
|
|
181
|
+
if (locale)
|
|
182
|
+
event.locale = locale;
|
|
183
|
+
}
|
|
184
|
+
this._eventBuffer.push(event);
|
|
185
|
+
this._afterEnqueue();
|
|
186
|
+
}
|
|
187
|
+
_enqueueTransaction(rec) {
|
|
188
|
+
if (this._disposed)
|
|
189
|
+
return;
|
|
190
|
+
this._transactionBuffer.push(rec);
|
|
191
|
+
this._afterEnqueue();
|
|
192
|
+
}
|
|
193
|
+
_enqueueSpan(rec) {
|
|
194
|
+
if (this._disposed)
|
|
195
|
+
return;
|
|
196
|
+
this._spanBuffer.push(rec);
|
|
197
|
+
this._afterEnqueue();
|
|
198
|
+
}
|
|
199
|
+
_afterEnqueue() {
|
|
200
|
+
this._schedulePersist();
|
|
201
|
+
const total = this._eventBuffer.length + this._transactionBuffer.length + this._spanBuffer.length;
|
|
202
|
+
if (total >= (this._opts.maxBufferSize ?? DEFAULT_MAX_BUFFER_SIZE))
|
|
203
|
+
this.flush();
|
|
204
|
+
}
|
|
205
|
+
/** The origin to attach to this flush, or undefined if unchanged since last sent. */
|
|
206
|
+
_takeOriginIfChanged() {
|
|
207
|
+
let origin;
|
|
208
|
+
try {
|
|
209
|
+
origin = this._opts.getOrigin();
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
return undefined;
|
|
213
|
+
}
|
|
214
|
+
const oj = JSON.stringify(origin);
|
|
215
|
+
if (oj === this._lastOriginJson)
|
|
216
|
+
return undefined;
|
|
217
|
+
this._lastOriginJson = oj;
|
|
218
|
+
return { ...origin, lifetime_id: this._lifetimeId };
|
|
219
|
+
}
|
|
220
|
+
// ---- Internal: chunked sending ----
|
|
221
|
+
async _sendInChunks(events, transactions, spans, origin) {
|
|
222
|
+
const maxChunkSize = this._opts.maxChunkSize ?? DEFAULT_MAX_CHUNK_SIZE;
|
|
223
|
+
const maxChunkBytes = this._opts.maxChunkBytes ?? DEFAULT_MAX_CHUNK_BYTES;
|
|
224
|
+
const work = [
|
|
225
|
+
...events.map((rec) => ({ k: 'e', rec })),
|
|
226
|
+
...transactions.map((rec) => ({ k: 't', rec })),
|
|
227
|
+
...spans.map((rec) => ({ k: 's', rec })),
|
|
228
|
+
];
|
|
229
|
+
let i = 0;
|
|
230
|
+
let isFirst = true;
|
|
231
|
+
while (i < work.length || (isFirst && origin)) {
|
|
232
|
+
if (!isFirst)
|
|
233
|
+
await delay(INTER_CHUNK_DELAY_MS);
|
|
234
|
+
const ce = [];
|
|
235
|
+
const ct = [];
|
|
236
|
+
const cs = [];
|
|
237
|
+
let bytes = 200; // base overhead for the batch envelope
|
|
238
|
+
while (i < work.length && (ce.length + ct.length + cs.length) < maxChunkSize) {
|
|
239
|
+
const item = work[i];
|
|
240
|
+
const itemBytes = estimateJsonSize(item.rec);
|
|
241
|
+
if (bytes + itemBytes > maxChunkBytes && (ce.length + ct.length + cs.length) > 0)
|
|
242
|
+
break;
|
|
243
|
+
if (item.k === 'e')
|
|
244
|
+
ce.push(item.rec);
|
|
245
|
+
else if (item.k === 't')
|
|
246
|
+
ct.push(item.rec);
|
|
247
|
+
else
|
|
248
|
+
cs.push(item.rec);
|
|
249
|
+
bytes += itemBytes;
|
|
250
|
+
i++;
|
|
251
|
+
}
|
|
252
|
+
const chunkOrigin = isFirst ? origin : undefined;
|
|
253
|
+
isFirst = false;
|
|
254
|
+
if (ce.length === 0 && ct.length === 0 && cs.length === 0 && !chunkOrigin)
|
|
255
|
+
break;
|
|
256
|
+
await this._sendChunkWithRetry(ce, ct, cs, chunkOrigin);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
async _sendChunkWithRetry(events, transactions, spans, origin) {
|
|
260
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
261
|
+
try {
|
|
262
|
+
await this._sendChunk(events, transactions, spans, origin);
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
if (attempt < MAX_RETRIES) {
|
|
267
|
+
await delay(BASE_RETRY_DELAY_MS * Math.pow(2, attempt));
|
|
268
|
+
}
|
|
269
|
+
else {
|
|
270
|
+
// Max retries exceeded — put records back for persistence/retry, and
|
|
271
|
+
// re-arm the origin so it ships again on the next successful flush.
|
|
272
|
+
this._eventBuffer.push(...events);
|
|
273
|
+
this._transactionBuffer.push(...transactions);
|
|
274
|
+
this._spanBuffer.push(...spans);
|
|
275
|
+
if (origin)
|
|
276
|
+
this._lastOriginJson = null;
|
|
277
|
+
this._schedulePersist();
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
async _sendChunk(events, transactions, spans, origin) {
|
|
283
|
+
const batch = { events, transactions, spans };
|
|
284
|
+
batch.lifetime_id = this._lifetimeId;
|
|
285
|
+
const userId = this._opts.getUserId?.();
|
|
286
|
+
const deviceId = this._opts.getDeviceId?.();
|
|
287
|
+
if (userId)
|
|
288
|
+
batch.user_id = userId;
|
|
289
|
+
if (deviceId)
|
|
290
|
+
batch.device_id = deviceId;
|
|
291
|
+
if (origin)
|
|
292
|
+
batch.origin = origin;
|
|
293
|
+
const headers = await Promise.resolve(this._opts.getAuthHeaders());
|
|
294
|
+
const response = await fetch(this._opts.endpoint, {
|
|
295
|
+
method: 'POST',
|
|
296
|
+
headers: { 'Content-Type': 'application/json', ...headers },
|
|
297
|
+
body: JSON.stringify(batch),
|
|
298
|
+
});
|
|
299
|
+
if (!response.ok)
|
|
300
|
+
throw new Error(`Server returned ${response.status}`);
|
|
301
|
+
}
|
|
302
|
+
// ---- Internal: persistence ----
|
|
303
|
+
_schedulePersist() {
|
|
304
|
+
if (this._persistHandle || !this._opts.persistLogs)
|
|
305
|
+
return;
|
|
306
|
+
this._persistHandle = setTimeout(() => {
|
|
307
|
+
this._persistHandle = null;
|
|
308
|
+
this._persistNow();
|
|
309
|
+
}, PERSIST_DEBOUNCE_MS);
|
|
310
|
+
}
|
|
311
|
+
_persistNow() {
|
|
312
|
+
if (!this._opts.persistLogs)
|
|
313
|
+
return;
|
|
314
|
+
if (this._eventBuffer.length === 0 && this._transactionBuffer.length === 0 && this._spanBuffer.length === 0) {
|
|
315
|
+
this._opts.persistLogs('').catch(() => { });
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
const data = JSON.stringify({
|
|
319
|
+
events: this._eventBuffer,
|
|
320
|
+
transactions: this._transactionBuffer,
|
|
321
|
+
spans: this._spanBuffer,
|
|
322
|
+
});
|
|
323
|
+
this._opts.persistLogs(data).catch(() => { });
|
|
324
|
+
}
|
|
325
|
+
async _loadPersistedLogs() {
|
|
326
|
+
if (!this._opts.loadPersistedLogs)
|
|
327
|
+
return;
|
|
328
|
+
try {
|
|
329
|
+
const data = await this._opts.loadPersistedLogs();
|
|
330
|
+
if (!data)
|
|
331
|
+
return;
|
|
332
|
+
const parsed = JSON.parse(data);
|
|
333
|
+
if (Array.isArray(parsed.events))
|
|
334
|
+
this._eventBuffer.push(...parsed.events);
|
|
335
|
+
if (Array.isArray(parsed.transactions))
|
|
336
|
+
this._transactionBuffer.push(...parsed.transactions);
|
|
337
|
+
if (Array.isArray(parsed.spans))
|
|
338
|
+
this._spanBuffer.push(...parsed.spans);
|
|
339
|
+
this._opts.persistLogs?.('').catch(() => { });
|
|
340
|
+
}
|
|
341
|
+
catch {
|
|
342
|
+
// Ignore parse errors from corrupted persisted data
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
exports.TracelogClient = TracelogClient;
|
|
347
|
+
// ---- Trace handles ----
|
|
348
|
+
/** A live root operation. Recorded as a `transaction` when ended. */
|
|
349
|
+
class Transaction {
|
|
350
|
+
constructor(_client, id, traceId, type) {
|
|
351
|
+
this._client = _client;
|
|
352
|
+
this.id = id;
|
|
353
|
+
this.traceId = traceId;
|
|
354
|
+
this.type = type;
|
|
355
|
+
}
|
|
356
|
+
/** A transaction is its own trace root. */
|
|
357
|
+
get transactionId() { return this.id; }
|
|
358
|
+
startSpan(name, opts) { return this._client.startSpan(name, this, opts); }
|
|
359
|
+
end(opts) { this._client._end(this.id, opts); }
|
|
360
|
+
}
|
|
361
|
+
exports.Transaction = Transaction;
|
|
362
|
+
/** A live sub-operation. Recorded as a `span` when ended. */
|
|
363
|
+
class Span {
|
|
364
|
+
constructor(_client, id, traceId, transactionId, parentId, type) {
|
|
365
|
+
this._client = _client;
|
|
366
|
+
this.id = id;
|
|
367
|
+
this.traceId = traceId;
|
|
368
|
+
this.transactionId = transactionId;
|
|
369
|
+
this.parentId = parentId;
|
|
370
|
+
this.type = type;
|
|
371
|
+
}
|
|
372
|
+
startSpan(name, opts) { return this._client.startSpan(name, this, opts); }
|
|
373
|
+
end(opts) { this._client._end(this.id, opts); }
|
|
374
|
+
}
|
|
375
|
+
exports.Span = Span;
|
|
376
|
+
// ---- Helpers ----
|
|
377
|
+
function randomHex(length) {
|
|
378
|
+
const chars = '0123456789abcdef';
|
|
379
|
+
let result = '';
|
|
380
|
+
for (let i = 0; i < length; i++) {
|
|
381
|
+
result += chars[Math.floor(Math.random() * 16)];
|
|
382
|
+
}
|
|
383
|
+
return result;
|
|
384
|
+
}
|
|
385
|
+
function now() {
|
|
386
|
+
if (typeof performance !== 'undefined' && performance.now) {
|
|
387
|
+
if (!now._offset) {
|
|
388
|
+
now._offset = Date.now() - performance.now();
|
|
389
|
+
}
|
|
390
|
+
return now._offset + performance.now();
|
|
391
|
+
}
|
|
392
|
+
return Date.now();
|
|
393
|
+
}
|
|
394
|
+
function delay(ms) {
|
|
395
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
396
|
+
}
|
|
397
|
+
function estimateJsonSize(obj) {
|
|
398
|
+
return JSON.stringify(obj).length;
|
|
399
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { TracelogClient, Transaction, Span } from './TracelogClient';
|
|
2
2
|
export { EventBuilder } from './EventBuilder';
|
|
3
|
-
export {
|
|
3
|
+
export { RecordBatch, EventRecord, TransactionRecord, SpanRecord, RecordContext, RecordOrigin, RecordKind, Outcome, StartOptions, EndOptions, RecordOptions, LogClientOptions, LogLevel, JsonValue, } from './types';
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.EventBuilder = exports.
|
|
4
|
-
var
|
|
5
|
-
Object.defineProperty(exports, "
|
|
3
|
+
exports.EventBuilder = exports.Span = exports.Transaction = exports.TracelogClient = void 0;
|
|
4
|
+
var TracelogClient_1 = require("./TracelogClient");
|
|
5
|
+
Object.defineProperty(exports, "TracelogClient", { enumerable: true, get: function () { return TracelogClient_1.TracelogClient; } });
|
|
6
|
+
Object.defineProperty(exports, "Transaction", { enumerable: true, get: function () { return TracelogClient_1.Transaction; } });
|
|
7
|
+
Object.defineProperty(exports, "Span", { enumerable: true, get: function () { return TracelogClient_1.Span; } });
|
|
6
8
|
var EventBuilder_1 = require("./EventBuilder");
|
|
7
9
|
Object.defineProperty(exports, "EventBuilder", { enumerable: true, get: function () { return EventBuilder_1.EventBuilder; } });
|
package/dist/types.d.ts
CHANGED
|
@@ -1,45 +1,58 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
export type { JsonValue, LogLevel,
|
|
3
|
-
export
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
1
|
+
import type { JsonValue, LogLevel, RecordOrigin } from '@redthreadlabs/tracelog-schema';
|
|
2
|
+
export type { JsonValue, LogLevel, RecordBatch, EventRecord, TransactionRecord, SpanRecord, RecordContext, RecordOrigin, RecordKind, } from '@redthreadlabs/tracelog-schema';
|
|
3
|
+
export type Outcome = 'success' | 'failure' | 'unknown';
|
|
4
|
+
/** Options when starting a live transaction or span. */
|
|
5
|
+
export interface StartOptions {
|
|
6
|
+
/** Span/transaction type, e.g. 'db', 'app'. Default: 'app'. */
|
|
7
|
+
type?: string;
|
|
8
|
+
}
|
|
9
|
+
/** Options when ending a live transaction or span. */
|
|
10
|
+
export interface EndOptions {
|
|
11
|
+
labels?: Record<string, JsonValue>;
|
|
12
|
+
outcome?: Outcome;
|
|
13
|
+
}
|
|
14
|
+
/** Options when recording a pre-measured transaction or span in one call. */
|
|
15
|
+
export interface RecordOptions {
|
|
16
|
+
type?: string;
|
|
17
|
+
labels?: Record<string, JsonValue>;
|
|
18
|
+
outcome?: Outcome;
|
|
19
|
+
/** Drop the record if its duration is below this (ms). */
|
|
20
|
+
minDurationMs?: number;
|
|
12
21
|
}
|
|
13
22
|
export interface LogClientOptions {
|
|
14
23
|
/** Server endpoint URL for log submission */
|
|
15
24
|
endpoint: string;
|
|
16
25
|
/** Returns auth headers (e.g. { Authorization: 'Bearer ...' }) */
|
|
17
26
|
getAuthHeaders: () => Promise<Record<string, string>> | Record<string, string>;
|
|
18
|
-
/**
|
|
19
|
-
|
|
20
|
-
|
|
27
|
+
/**
|
|
28
|
+
* The current RecordOrigin (service + environment). The SDK sends it as a
|
|
29
|
+
* `metadata` record on the first batch of this lifetime and again whenever it
|
|
30
|
+
* changes; it fills in `lifetime_id`.
|
|
31
|
+
*/
|
|
32
|
+
getOrigin: () => RecordOrigin;
|
|
33
|
+
/** Returns current user ID, if logged in (→ batch.user_id). */
|
|
21
34
|
getUserId?: () => string | undefined;
|
|
22
|
-
/** Returns
|
|
23
|
-
getSessionRef?: () => string | undefined;
|
|
24
|
-
/** Returns native device identifier */
|
|
35
|
+
/** Returns the consumer's opaque device/installation id (→ batch.device_id). */
|
|
25
36
|
getDeviceId?: () => string | undefined;
|
|
37
|
+
/** Returns the UI locale at event time; stamped onto each event. */
|
|
38
|
+
getLocale?: () => string | undefined;
|
|
26
39
|
/**
|
|
27
40
|
* Returns the minimum level to emit. Events whose level ranks below this
|
|
28
41
|
* (debug < info < warn < error) are dropped before buffering — they never
|
|
29
42
|
* persist or ship. Called once per event, so keep it cheap and synchronous.
|
|
30
|
-
* Omit to emit every level (default).
|
|
43
|
+
* Omit to emit every level (default). Transactions/spans are unaffected.
|
|
31
44
|
*/
|
|
32
45
|
getMinLevel?: () => LogLevel;
|
|
33
46
|
/** Flush cadence in ms. Default: 5000 */
|
|
34
47
|
flushCadenceMs?: number;
|
|
35
|
-
/** Max
|
|
48
|
+
/** Max records buffered before forced flush. Default: 100 */
|
|
36
49
|
maxBufferSize?: number;
|
|
37
|
-
/** Max
|
|
50
|
+
/** Max records per HTTP request. Default: 50 */
|
|
38
51
|
maxChunkSize?: number;
|
|
39
52
|
/** Max bytes per HTTP request. Default: 524288 (512KB) */
|
|
40
53
|
maxChunkBytes?: number;
|
|
41
|
-
/** Persist pending
|
|
54
|
+
/** Persist pending records (e.g. to AsyncStorage). Called with JSON string. */
|
|
42
55
|
persistLogs?: (data: string) => Promise<void>;
|
|
43
|
-
/** Load previously persisted
|
|
56
|
+
/** Load previously persisted records. Returns JSON string or null. */
|
|
44
57
|
loadPersistedLogs?: () => Promise<string | null>;
|
|
45
58
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@redthreadlabs/tracelog-client",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Lightweight logging client for tracelog — works in React Native and browsers",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"registry": "https://registry.npmjs.org/",
|
|
@@ -21,6 +21,6 @@
|
|
|
21
21
|
"typescript": "latest"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@redthreadlabs/tracelog-schema": "^0.
|
|
24
|
+
"@redthreadlabs/tracelog-schema": "^0.4.0"
|
|
25
25
|
}
|
|
26
26
|
}
|