fg-tracker-sdk 0.8.1 → 0.10.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/CHANGELOG.md +26 -0
- package/README.md +37 -12
- package/dist/cli/release.js +1 -1
- package/dist/cli/sourcemaps.js +1 -1
- package/dist/context.d.ts +7 -0
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js.map +1 -1
- package/dist/exit-hooks.d.ts +23 -0
- package/dist/exit-hooks.d.ts.map +1 -0
- package/dist/exit-hooks.js +88 -0
- package/dist/exit-hooks.js.map +1 -0
- package/dist/fetch.d.ts +1 -1
- package/dist/fetch.js +1 -1
- package/dist/global-errors.d.ts +8 -5
- package/dist/global-errors.d.ts.map +1 -1
- package/dist/global-errors.js +59 -10
- package/dist/global-errors.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -5
- package/dist/index.js.map +1 -1
- package/dist/infra/aggregator.d.ts +5 -0
- package/dist/infra/aggregator.d.ts.map +1 -1
- package/dist/infra/aggregator.js.map +1 -1
- package/dist/infra/lambda.d.ts +1 -1
- package/dist/infra/lambda.d.ts.map +1 -1
- package/dist/infra/lambda.js +3 -2
- package/dist/infra/lambda.js.map +1 -1
- package/dist/logs/nest-logger.d.ts +1 -1
- package/dist/logs/nest-logger.js +1 -1
- package/dist/logs/shipper.d.ts +5 -4
- package/dist/logs/shipper.d.ts.map +1 -1
- package/dist/logs/shipper.js +170 -52
- package/dist/logs/shipper.js.map +1 -1
- package/dist/logs/types.d.ts +19 -3
- package/dist/logs/types.d.ts.map +1 -1
- package/dist/monitors.d.ts.map +1 -1
- package/dist/monitors.js +3 -0
- package/dist/monitors.js.map +1 -1
- package/dist/scrub.d.ts +5 -0
- package/dist/scrub.d.ts.map +1 -1
- package/dist/scrub.js +17 -1
- package/dist/scrub.js.map +1 -1
- package/dist/tracker.d.ts +101 -6
- package/dist/tracker.d.ts.map +1 -1
- package/dist/tracker.js +468 -92
- package/dist/tracker.js.map +1 -1
- package/dist/transport.d.ts +115 -0
- package/dist/transport.d.ts.map +1 -0
- package/dist/transport.js +323 -0
- package/dist/transport.js.map +1 -0
- package/dist/types.d.ts +50 -5
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
package/dist/tracker.js
CHANGED
|
@@ -1,14 +1,27 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
1
2
|
import { performance } from 'node:perf_hooks';
|
|
2
3
|
import { EventLoopMonitor } from './event-loop.js';
|
|
3
4
|
import { als, formatTraceparent, newSpanId, newTraceId, TRACEPARENT_HEADER } from './context.js';
|
|
4
5
|
import { toSpanError } from './normalize.js';
|
|
5
|
-
import { DEFAULT_SCRUB_FIELDS, matchesAny, scrubAttributes } from './scrub.js';
|
|
6
|
+
import { DEFAULT_SCRUB_FIELDS, matchesAny, scrubAttributes, testPattern } from './scrub.js';
|
|
6
7
|
import { API_KEY_HEADER, DEFAULT_ENDPOINT, readEnv } from './constants.js';
|
|
8
|
+
import { registerExitFlush } from './exit-hooks.js';
|
|
7
9
|
import { InfraAggregator } from './infra/aggregator.js';
|
|
10
|
+
import { byteLength, DEFAULT_MAX_BODY_BYTES, DEFAULT_REQUEST_TIMEOUT_MS, fitRecord, HttpSender, MIN_BODY_BYTES, safeStringify, Deadlines, truncateText, } from './transport.js';
|
|
8
11
|
const DEFAULT_FLUSH_INTERVAL_MS = 2000;
|
|
9
12
|
const DEFAULT_MAX_BATCH = 200;
|
|
10
13
|
const DEFAULT_MAX_QUEUE = 5000;
|
|
14
|
+
/** Teto de memória da fila de spans (JSON já serializado). */
|
|
15
|
+
const DEFAULT_MAX_QUEUE_BYTES = 32 * 1024 * 1024;
|
|
16
|
+
const DEFAULT_SHUTDOWN_TIMEOUT_MS = 10_000;
|
|
11
17
|
const MAX_SPANS_PER_REQUEST = 1000;
|
|
18
|
+
/**
|
|
19
|
+
* `attributes` e `error` acima de 64 KiB (JSON) fazem a API recusar o span
|
|
20
|
+
* (`attributes_too_large`/`error_too_large`, CONTRATO.md §4): o SDK corta antes, com margem.
|
|
21
|
+
*/
|
|
22
|
+
const MAX_SPAN_FIELD_BYTES = 60 * 1024;
|
|
23
|
+
/** Raízes locais não amostradas com erro num descendente, guardadas até fecharem (só com `sendUnsampledErrors`). */
|
|
24
|
+
const MAX_KEPT_ROOTS = 1000;
|
|
12
25
|
/** Limites de agregados de infra (CONTRATO.md §27.4: ≤ 500 por requisição). */
|
|
13
26
|
const MAX_INFRA_PER_REQUEST = 500;
|
|
14
27
|
const MAX_INFRA_PENDING = 2000;
|
|
@@ -66,6 +79,14 @@ export function resolveOptions(options) {
|
|
|
66
79
|
flushIntervalMs: Math.max(100, options.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS),
|
|
67
80
|
maxBatchSize: Math.max(1, options.maxBatchSize ?? DEFAULT_MAX_BATCH),
|
|
68
81
|
maxQueueSize: Math.max(10, options.maxQueueSize ?? DEFAULT_MAX_QUEUE),
|
|
82
|
+
maxQueueBytes: Math.max(1024 * 1024, options.maxQueueBytes ?? DEFAULT_MAX_QUEUE_BYTES),
|
|
83
|
+
maxBatchBytes: Math.min(DEFAULT_MAX_BODY_BYTES, Math.max(MIN_BODY_BYTES, options.maxBatchBytes ?? DEFAULT_MAX_BODY_BYTES)),
|
|
84
|
+
requestTimeoutMs: Math.max(100, options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS),
|
|
85
|
+
shutdownTimeoutMs: Math.max(0, options.shutdownTimeoutMs ?? DEFAULT_SHUTDOWN_TIMEOUT_MS),
|
|
86
|
+
// Com `fetch` próprio o corpo continua string (compatível com quem lê `init.body`), salvo pedido explícito.
|
|
87
|
+
compression: options.compression ?? (options.fetch ? 'none' : 'gzip'),
|
|
88
|
+
exitHooks: options.exitHooks ?? true,
|
|
89
|
+
sendUnsampledErrors: options.sendUnsampledErrors ?? false,
|
|
69
90
|
enabled: (options.enabled ?? true) && Boolean(options.apiKey),
|
|
70
91
|
debug: options.debug ?? false,
|
|
71
92
|
fetch: options.fetch ?? globalThis.fetch,
|
|
@@ -84,7 +105,7 @@ export function shouldPropagate(targets, url) {
|
|
|
84
105
|
return false;
|
|
85
106
|
return targets.some((target) => {
|
|
86
107
|
if (target instanceof RegExp)
|
|
87
|
-
return target
|
|
108
|
+
return testPattern(target, url.href);
|
|
88
109
|
const pattern = target.trim();
|
|
89
110
|
if (!pattern)
|
|
90
111
|
return false;
|
|
@@ -97,20 +118,52 @@ export function shouldPropagate(targets, url) {
|
|
|
97
118
|
* Núcleo do tracker: constrói spans, mantém a fila e faz o envio em lote.
|
|
98
119
|
* As integrações (express, pg, next, fetch) ficam em arquivos separados e só
|
|
99
120
|
* usam `startSpan` / `runInSpan`.
|
|
121
|
+
*
|
|
122
|
+
* Envio (CONTRATO.md §4.3): lotes limitados por bytes (`maxBatchBytes`) e por 1000 spans, uma
|
|
123
|
+
* requisição por vez, timeout de 10 s, backoff exponencial com jitter e `Retry-After` em
|
|
124
|
+
* rede/408/429/5xx, 413 divide o lote, outro 4xx descarta. Spans recusados num 202 parcial não
|
|
125
|
+
* são reenviados (a recusa se repetiria); agregados de infra em `retryableIds` voltam para a fila.
|
|
100
126
|
*/
|
|
101
127
|
export class TrackerCore {
|
|
102
128
|
options;
|
|
103
129
|
queue = [];
|
|
130
|
+
queueBytes = 0;
|
|
104
131
|
timer = null;
|
|
105
132
|
flushing = null;
|
|
106
133
|
warnedFailure = false;
|
|
134
|
+
warnedRejected = false;
|
|
107
135
|
stopped = false;
|
|
136
|
+
/** Prazo do encerramento em andamento (epoch ms): nenhum envio passa dele. */
|
|
137
|
+
/** Prazos de encerramento ativos; vale o menor (`deadline`). */
|
|
138
|
+
deadlines = new Deadlines();
|
|
139
|
+
get deadline() {
|
|
140
|
+
return this.deadlines.current;
|
|
141
|
+
}
|
|
142
|
+
/** Spans descartados por limite de fila ou por 4xx desde o último aviso. */
|
|
143
|
+
dropped = 0;
|
|
144
|
+
sender;
|
|
145
|
+
/** Teto atual do corpo: começa em `maxBatchBytes` e cai pela metade a cada 413. */
|
|
146
|
+
batchBytes;
|
|
147
|
+
/** Teto de itens por lote: só cai quando um 413 chega com o teto de bytes já no mínimo. */
|
|
148
|
+
batchItems = Number.POSITIVE_INFINITY;
|
|
149
|
+
/** Enfileiramentos desde a última tentativa de envio na saída do processo. */
|
|
150
|
+
enqueuedSinceExit = 0;
|
|
151
|
+
unregisterExit;
|
|
152
|
+
/**
|
|
153
|
+
* Raízes locais (por `spanId`) de traces não amostrados em que um descendente falhou: quando
|
|
154
|
+
* fecham, também são enviadas (`sendUnsampledErrors`). A chave é a raiz, não o trace: duas
|
|
155
|
+
* requisições do mesmo trace distribuído têm raízes locais diferentes.
|
|
156
|
+
*/
|
|
157
|
+
keptRoots = new Set();
|
|
108
158
|
/** Spans ainda abertos, por spanId — permite `currentSpan()` a partir do contexto assíncrono. */
|
|
109
159
|
active = new Map();
|
|
110
160
|
/** Agregados de infra (nunca amostrados); `null` quando `infra.enabled === false`. */
|
|
111
161
|
infra;
|
|
112
162
|
/** Agregados já fechados aguardando envio (retentativa após falha). */
|
|
113
163
|
infraPending = [];
|
|
164
|
+
/** Identidade dos agregados deste processo: `<emissor>:<sequência>` (dedupe no servidor). */
|
|
165
|
+
emitterId = randomBytes(8).toString('hex');
|
|
166
|
+
infraSeq = 0;
|
|
114
167
|
/** A janela de infra venceu: o próximo flush fecha o agregador e envia junto dos spans. */
|
|
115
168
|
infraDue = false;
|
|
116
169
|
infraTimer = null;
|
|
@@ -124,10 +177,26 @@ export class TrackerCore {
|
|
|
124
177
|
eventLoop;
|
|
125
178
|
constructor(options) {
|
|
126
179
|
this.options = resolveOptions(options);
|
|
180
|
+
this.batchBytes = this.options.maxBatchBytes;
|
|
181
|
+
this.sender = new HttpSender({
|
|
182
|
+
fetch: (input, init) => this.options.fetch(input, init),
|
|
183
|
+
url: `${this.options.endpoint}/v1/ingest`,
|
|
184
|
+
headers: { [API_KEY_HEADER]: this.options.apiKey },
|
|
185
|
+
timeoutMs: this.options.requestTimeoutMs,
|
|
186
|
+
compression: this.options.compression,
|
|
187
|
+
});
|
|
127
188
|
this.eventLoop = this.options.enabled && this.options.measureEventLoopLag ? new EventLoopMonitor() : null;
|
|
128
189
|
this.infra =
|
|
129
|
-
this.options.enabled && this.options.infraEnabled
|
|
190
|
+
this.options.enabled && this.options.infraEnabled
|
|
191
|
+
? new InfraAggregator({
|
|
192
|
+
onRecord: () => {
|
|
193
|
+
this.enqueuedSinceExit += 1;
|
|
194
|
+
this.ensureInfraTimer();
|
|
195
|
+
},
|
|
196
|
+
})
|
|
197
|
+
: null;
|
|
130
198
|
if (!this.options.enabled) {
|
|
199
|
+
this.unregisterExit = () => undefined;
|
|
131
200
|
if (!options.apiKey && (options.enabled ?? true)) {
|
|
132
201
|
console.warn('[fg-tracker] apiKey ausente: tracker desabilitado.');
|
|
133
202
|
}
|
|
@@ -139,10 +208,18 @@ export class TrackerCore {
|
|
|
139
208
|
// Não segura o processo vivo só por causa do timer.
|
|
140
209
|
if (typeof this.timer === 'object' && this.timer && 'unref' in this.timer)
|
|
141
210
|
this.timer.unref();
|
|
211
|
+
this.unregisterExit = this.options.exitHooks ? registerExitFlush(this) : () => undefined;
|
|
142
212
|
}
|
|
143
213
|
get enabled() {
|
|
144
214
|
return this.options.enabled && !this.stopped;
|
|
145
215
|
}
|
|
216
|
+
/** Spans aguardando envio. */
|
|
217
|
+
get queueSize() {
|
|
218
|
+
return this.queue.length;
|
|
219
|
+
}
|
|
220
|
+
get exitTimeoutMs() {
|
|
221
|
+
return this.options.shutdownTimeoutMs;
|
|
222
|
+
}
|
|
146
223
|
currentContext() {
|
|
147
224
|
return als.getStore();
|
|
148
225
|
}
|
|
@@ -153,11 +230,21 @@ export class TrackerCore {
|
|
|
153
230
|
const parentSpanId = parentCtx?.spanId ?? null;
|
|
154
231
|
const attrs = this.decorate(attributes);
|
|
155
232
|
// Amostragem: decidida uma vez por trace (raiz) e herdada pelos filhos.
|
|
156
|
-
|
|
157
|
-
|
|
233
|
+
// Excluído por `ignoreTransactions` (raiz ou herdado): nunca enviado, nem por `sendUnsampledErrors`.
|
|
234
|
+
// Decidido uma vez e reaproveitado pela amostragem.
|
|
235
|
+
const ignored = parentCtx ? parentCtx.ignored === true : matchesAny(name, this.options.ignoreTransactions);
|
|
236
|
+
const sampled = parentCtx ? parentCtx.sampled !== false : !ignored && this.decideSampling({ kind, name, attributes: attrs }, true);
|
|
237
|
+
const spanId = newSpanId();
|
|
238
|
+
const localRootId = parentCtx ? (parentCtx.localRootId ?? this.localRootOf(parentCtx.spanId)) : spanId;
|
|
239
|
+
const span = new ActiveSpanImpl(this, traceId, spanId, parentSpanId, kind, name, attrs, sampled, ignored, localRootId);
|
|
158
240
|
this.active.set(span.spanId, span);
|
|
159
241
|
return span;
|
|
160
242
|
}
|
|
243
|
+
/** Raiz local de um span ainda aberto (sobe pelos pais abertos); senão o próprio id. */
|
|
244
|
+
localRootOf(spanId) {
|
|
245
|
+
const open = this.active.get(spanId);
|
|
246
|
+
return open ? open.localRootId : spanId;
|
|
247
|
+
}
|
|
161
248
|
decorate(attributes) {
|
|
162
249
|
const attrs = { ...attributes };
|
|
163
250
|
if (this.options.serviceName && attrs.serviceName === undefined)
|
|
@@ -172,8 +259,8 @@ export class TrackerCore {
|
|
|
172
259
|
return attrs;
|
|
173
260
|
}
|
|
174
261
|
/** `ignoreTransactions` → nunca; `tracesSampler` → fração/boolean; senão `tracesSampleRate`. */
|
|
175
|
-
decideSampling(context) {
|
|
176
|
-
if (matchesAny(context.name, this.options.ignoreTransactions))
|
|
262
|
+
decideSampling(context, exclusionChecked = false) {
|
|
263
|
+
if (!exclusionChecked && matchesAny(context.name, this.options.ignoreTransactions))
|
|
177
264
|
return false;
|
|
178
265
|
let rate = this.options.tracesSampleRate;
|
|
179
266
|
if (this.options.tracesSampler) {
|
|
@@ -200,8 +287,10 @@ export class TrackerCore {
|
|
|
200
287
|
startRemoteRoot(kind, name, attributes, remote) {
|
|
201
288
|
const attrs = this.decorate({ ...attributes, remoteParentSpanId: remote.spanId });
|
|
202
289
|
// A decisão do serviço a montante (flag do traceparent) vale aqui, salvo `ignoreTransactions`.
|
|
203
|
-
const
|
|
204
|
-
const
|
|
290
|
+
const ignored = matchesAny(name, this.options.ignoreTransactions);
|
|
291
|
+
const sampled = remote.sampled === false || ignored ? false : this.decideSampling({ kind, name, attributes: attrs, parentSampled: remote.sampled }, true);
|
|
292
|
+
const spanId = newSpanId();
|
|
293
|
+
const span = new ActiveSpanImpl(this, remote.traceId, spanId, null, kind, name, attrs, sampled, ignored, spanId);
|
|
205
294
|
this.active.set(span.spanId, span);
|
|
206
295
|
return span;
|
|
207
296
|
}
|
|
@@ -253,9 +342,39 @@ export class TrackerCore {
|
|
|
253
342
|
/** Chamado por ActiveSpanImpl.end(): amostragem, ignoreErrors, scrubbing e beforeSend. */
|
|
254
343
|
finishSpan(span, payload) {
|
|
255
344
|
this.active.delete(span.spanId);
|
|
256
|
-
if (
|
|
345
|
+
if (span.sampled) {
|
|
346
|
+
this.emit(payload);
|
|
257
347
|
return;
|
|
258
|
-
|
|
348
|
+
}
|
|
349
|
+
if (!this.options.sendUnsampledErrors || span.ignored)
|
|
350
|
+
return;
|
|
351
|
+
// Trace não amostrado: só o que falhou (e a raiz local dele, que fecha depois) é enviado.
|
|
352
|
+
// O que já fechou antes do erro foi descartado e não volta.
|
|
353
|
+
if (this.isFailure(payload)) {
|
|
354
|
+
if (span.parentSpanId !== null)
|
|
355
|
+
this.keepRoot(span.localRootId);
|
|
356
|
+
else
|
|
357
|
+
this.keptRoots.delete(span.spanId);
|
|
358
|
+
this.emit(markKeptByError(payload));
|
|
359
|
+
}
|
|
360
|
+
else if (span.parentSpanId === null && this.keptRoots.delete(span.spanId)) {
|
|
361
|
+
this.emit(markKeptByError(payload));
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
isFailure(payload) {
|
|
365
|
+
if ((payload.statusCode ?? 0) >= 500)
|
|
366
|
+
return true;
|
|
367
|
+
if (!payload.error)
|
|
368
|
+
return false;
|
|
369
|
+
return !matchesAny(`${payload.error.name}: ${payload.error.message}`, this.options.ignoreErrors);
|
|
370
|
+
}
|
|
371
|
+
keepRoot(localRootId) {
|
|
372
|
+
this.keptRoots.add(localRootId);
|
|
373
|
+
if (this.keptRoots.size > MAX_KEPT_ROOTS) {
|
|
374
|
+
const oldest = this.keptRoots.values().next().value;
|
|
375
|
+
if (oldest !== undefined)
|
|
376
|
+
this.keptRoots.delete(oldest);
|
|
377
|
+
}
|
|
259
378
|
}
|
|
260
379
|
/**
|
|
261
380
|
* Registra um span que já terminou, sem ter existido um `ActiveSpanImpl`
|
|
@@ -267,9 +386,14 @@ export class TrackerCore {
|
|
|
267
386
|
* nada vira trace órfão.
|
|
268
387
|
*/
|
|
269
388
|
recordSpan(input) {
|
|
270
|
-
if (!this.enabled
|
|
389
|
+
if (!this.enabled)
|
|
390
|
+
return;
|
|
391
|
+
if (input.parent.ignored === true)
|
|
392
|
+
return;
|
|
393
|
+
const unsampled = input.parent.sampled === false;
|
|
394
|
+
if (unsampled && (input.error === undefined || !this.options.sendUnsampledErrors))
|
|
271
395
|
return;
|
|
272
|
-
|
|
396
|
+
const payload = {
|
|
273
397
|
traceId: input.parent.traceId,
|
|
274
398
|
spanId: newSpanId(),
|
|
275
399
|
parentSpanId: input.parent.spanId,
|
|
@@ -283,7 +407,14 @@ export class TrackerCore {
|
|
|
283
407
|
durationMs: input.durationMs,
|
|
284
408
|
attributes: this.decorate(input.attributes ?? {}),
|
|
285
409
|
error: input.error === undefined ? null : toSpanError(input.error),
|
|
286
|
-
}
|
|
410
|
+
};
|
|
411
|
+
if (!unsampled) {
|
|
412
|
+
this.emit(payload);
|
|
413
|
+
}
|
|
414
|
+
else if (this.isFailure(payload)) {
|
|
415
|
+
this.keepRoot(input.parent.localRootId ?? this.localRootOf(input.parent.spanId));
|
|
416
|
+
this.emit(markKeptByError(payload));
|
|
417
|
+
}
|
|
287
418
|
}
|
|
288
419
|
/** ignoreErrors, scrubbing e beforeSend antes de entrar na fila. */
|
|
289
420
|
emit(payload) {
|
|
@@ -291,8 +422,16 @@ export class TrackerCore {
|
|
|
291
422
|
if (out.error && matchesAny(`${out.error.name}: ${out.error.message}`, this.options.ignoreErrors)) {
|
|
292
423
|
out = { ...out, error: null, attributes: { ...out.attributes, ignoredError: out.error.name } };
|
|
293
424
|
}
|
|
294
|
-
if (this.options.scrubFields.length > 0)
|
|
295
|
-
|
|
425
|
+
if (this.options.scrubFields.length > 0) {
|
|
426
|
+
try {
|
|
427
|
+
out = { ...out, attributes: scrubAttributes(out.attributes, this.options.scrubFields) };
|
|
428
|
+
}
|
|
429
|
+
catch (err) {
|
|
430
|
+
this.dropped += 1;
|
|
431
|
+
this.warnOnce(`span descartado: falha ao limpar atributos (${err?.message ?? err})`);
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
296
435
|
if (this.options.beforeSend) {
|
|
297
436
|
try {
|
|
298
437
|
const result = this.options.beforeSend(out);
|
|
@@ -308,22 +447,40 @@ export class TrackerCore {
|
|
|
308
447
|
}
|
|
309
448
|
/** Executa `fn` com `span` como contexto pai das operações internas. */
|
|
310
449
|
runInSpan(span, fn) {
|
|
311
|
-
return als.run({ traceId: span.traceId, spanId: span.spanId, sampled: span.sampled }, fn);
|
|
450
|
+
return als.run({ traceId: span.traceId, spanId: span.spanId, sampled: span.sampled, localRootId: span.localRootId, ...(span.ignored ? { ignored: true } : {}) }, fn);
|
|
312
451
|
}
|
|
313
452
|
enqueue(span) {
|
|
314
453
|
if (!this.enabled)
|
|
315
454
|
return;
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
455
|
+
let item;
|
|
456
|
+
try {
|
|
457
|
+
item = serializeSpan(span);
|
|
458
|
+
}
|
|
459
|
+
catch (err) {
|
|
460
|
+
// Roda no `span.end()`, dentro da operação da aplicação: nunca lança para ela.
|
|
461
|
+
this.dropped += 1;
|
|
462
|
+
this.warnOnce(`span descartado: não foi possível serializar (${err?.message ?? err})`);
|
|
463
|
+
return;
|
|
320
464
|
}
|
|
465
|
+
this.queue.push(item);
|
|
466
|
+
this.queueBytes += item.bytes;
|
|
467
|
+
this.enqueuedSinceExit += 1;
|
|
468
|
+
this.trimQueue();
|
|
321
469
|
if (this.queue.length >= this.options.maxBatchSize)
|
|
322
470
|
void this.flush(false);
|
|
323
471
|
}
|
|
472
|
+
/** Acima dos limites (spans ou bytes), descarta os mais antigos para não crescer sem limite. */
|
|
473
|
+
trimQueue() {
|
|
474
|
+
while (this.queue.length > 1 && (this.queue.length > this.options.maxQueueSize || this.queueBytes > this.options.maxQueueBytes)) {
|
|
475
|
+
const removed = this.queue.shift();
|
|
476
|
+
this.queueBytes -= removed.bytes;
|
|
477
|
+
this.dropped += 1;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
324
480
|
/**
|
|
325
481
|
* Envia a fila. `includeInfra` (default `true`) também fecha a janela de infra e envia os
|
|
326
482
|
* agregados no mesmo lote; o timer de spans chama com `false` para respeitar a janela de 60 s/5 s.
|
|
483
|
+
* Não espera o backoff: em backoff, volta na hora e o próximo flush tenta.
|
|
327
484
|
*/
|
|
328
485
|
async flush(includeInfra = true) {
|
|
329
486
|
if (!this.options.enabled)
|
|
@@ -338,41 +495,192 @@ export class TrackerCore {
|
|
|
338
495
|
this.infraDue = false;
|
|
339
496
|
return;
|
|
340
497
|
}
|
|
341
|
-
this.flushing = this.
|
|
498
|
+
this.flushing = this.drain(false).finally(() => {
|
|
342
499
|
this.flushing = null;
|
|
343
500
|
});
|
|
344
501
|
return this.flushing;
|
|
345
502
|
}
|
|
503
|
+
/**
|
|
504
|
+
* Envia o que der até `deadline`, sem esperar backoff (fim de invocação da Lambda). Aborta a
|
|
505
|
+
* requisição em andamento no prazo: nada fica pendurado no congelamento do ambiente.
|
|
506
|
+
*/
|
|
507
|
+
async flushUntil(deadline) {
|
|
508
|
+
if (!this.options.enabled)
|
|
509
|
+
return;
|
|
510
|
+
const release = this.deadlines.add(deadline);
|
|
511
|
+
this.sender.abortAt(this.deadline ?? deadline);
|
|
512
|
+
try {
|
|
513
|
+
await this.flush(true);
|
|
514
|
+
}
|
|
515
|
+
finally {
|
|
516
|
+
release();
|
|
517
|
+
}
|
|
518
|
+
}
|
|
346
519
|
hasInfraToSend() {
|
|
347
520
|
return this.infraPending.length > 0 || (this.infraDue && Boolean(this.infra?.hasData()));
|
|
348
521
|
}
|
|
349
|
-
|
|
522
|
+
/**
|
|
523
|
+
* Laço de envio. `waitBackoff` (encerramento) espera o backoff enquanto couber no prazo;
|
|
524
|
+
* sem ele, uma falha devolve o lote à fila e encerra o laço.
|
|
525
|
+
*/
|
|
526
|
+
async drain(waitBackoff) {
|
|
350
527
|
for (;;) {
|
|
351
528
|
if (this.infraDue && this.infra) {
|
|
352
529
|
this.infraDue = false;
|
|
353
530
|
if (this.infra.hasData())
|
|
354
531
|
this.pushInfraPending(this.infra.drain());
|
|
355
532
|
}
|
|
356
|
-
|
|
357
|
-
const infra = this.infraPending.splice(0, MAX_INFRA_PER_REQUEST);
|
|
358
|
-
if (batch.length === 0 && infra.length === 0)
|
|
533
|
+
if (this.queue.length === 0 && this.infraPending.length === 0)
|
|
359
534
|
return;
|
|
360
|
-
const
|
|
361
|
-
if (
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
535
|
+
const now = Date.now();
|
|
536
|
+
if (this.deadline !== null && now >= this.deadline)
|
|
537
|
+
return;
|
|
538
|
+
const wait = this.sender.waitMs(now);
|
|
539
|
+
if (wait > 0) {
|
|
540
|
+
if (!waitBackoff || this.deadline === null || now + wait >= this.deadline)
|
|
541
|
+
return;
|
|
542
|
+
await this.deadlines.sleep(wait);
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
545
|
+
const batch = this.takeBatch();
|
|
546
|
+
const result = await this.sender.post(batch.body, this.deadline ?? undefined);
|
|
547
|
+
if (!this.handleResult(batch, result) && !waitBackoff)
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
/**
|
|
552
|
+
* Monta o próximo lote: agregados (até 500) e depois spans (até 1000), todos dentro do teto de
|
|
553
|
+
* bytes e do teto de itens (este só cai depois de 413 com o teto de bytes no mínimo). Sempre leva
|
|
554
|
+
* pelo menos um item.
|
|
555
|
+
*/
|
|
556
|
+
takeBatch() {
|
|
557
|
+
// Tamanho exato do corpo de `buildIngestBody`: `{"traces":[` + grupos + `]` + `,"infra":[…]` + `}`.
|
|
558
|
+
let bytes = 13;
|
|
559
|
+
let items = 0;
|
|
560
|
+
const fits = (size) => items === 0 || (bytes + size <= this.batchBytes && items < this.batchItems);
|
|
561
|
+
const infraJsons = [];
|
|
562
|
+
while (infraJsons.length < this.infraPending.length && infraJsons.length < MAX_INFRA_PER_REQUEST) {
|
|
563
|
+
const json = safeStringify(this.infraPending[infraJsons.length]);
|
|
564
|
+
// Primeiro agregado: `,"infra":[` + json + `]`; os seguintes: `,` + json.
|
|
565
|
+
const size = byteLength(json) + (infraJsons.length === 0 ? 11 : 1);
|
|
566
|
+
if (!fits(size))
|
|
567
|
+
break;
|
|
568
|
+
bytes += size;
|
|
569
|
+
items += 1;
|
|
570
|
+
infraJsons.push(json);
|
|
571
|
+
}
|
|
572
|
+
const infra = this.infraPending.splice(0, infraJsons.length);
|
|
573
|
+
const traces = new Set();
|
|
574
|
+
let count = 0;
|
|
575
|
+
while (count < this.queue.length && count < MAX_SPANS_PER_REQUEST) {
|
|
576
|
+
const next = this.queue[count];
|
|
577
|
+
// Span novo num trace já presente: `,<json>`; trace novo: `,{"traceId":"…","spans":[<json>]}`.
|
|
578
|
+
const size = traces.has(next.traceId) ? next.bytes + 1 : next.bytes + byteLength(JSON.stringify(next.traceId)) + 26;
|
|
579
|
+
if (!fits(size))
|
|
580
|
+
break;
|
|
581
|
+
bytes += size;
|
|
582
|
+
items += 1;
|
|
583
|
+
traces.add(next.traceId);
|
|
584
|
+
count += 1;
|
|
585
|
+
}
|
|
586
|
+
const spans = this.queue.splice(0, count);
|
|
587
|
+
for (const span of spans)
|
|
588
|
+
this.queueBytes -= span.bytes;
|
|
589
|
+
const body = buildIngestBody(spans, infraJsons.length > 0 ? `[${infraJsons.join(',')}]` : '');
|
|
590
|
+
return { spans, infra, body, bytes: byteLength(body) };
|
|
591
|
+
}
|
|
592
|
+
/** Aplica o resultado do envio. Devolve `false` quando o lote voltou para a fila (backoff). */
|
|
593
|
+
handleResult(batch, result) {
|
|
594
|
+
switch (result.type) {
|
|
595
|
+
case 'ok':
|
|
596
|
+
if (this.warnedFailure) {
|
|
597
|
+
this.warnedFailure = false;
|
|
598
|
+
if (this.options.debug)
|
|
599
|
+
console.info('[fg-tracker] envio restabelecido.');
|
|
366
600
|
}
|
|
367
|
-
if (
|
|
368
|
-
|
|
369
|
-
this.trimInfraPending();
|
|
601
|
+
if (this.options.debug) {
|
|
602
|
+
console.info(`[fg-tracker] enviados ${batch.spans.length} spans${batch.infra.length > 0 ? ` e ${batch.infra.length} agregados de infra` : ''} (${batch.bytes} bytes).`);
|
|
370
603
|
}
|
|
371
|
-
|
|
604
|
+
if (this.readIngestResponse(batch, result.body)) {
|
|
605
|
+
// Agregados com falha transitória na API (banco): voltam para a fila e esperam o
|
|
606
|
+
// backoff, que continua crescendo enquanto a falha durar (o 2xx não zera a sequência).
|
|
607
|
+
this.sender.noteFailure({ type: 'retry', status: result.status, retryAfterMs: null, reason: 'infra retryable' });
|
|
608
|
+
return false;
|
|
609
|
+
}
|
|
610
|
+
this.sender.noteSuccess();
|
|
611
|
+
return true;
|
|
612
|
+
case 'too_large': {
|
|
613
|
+
const items = batch.spans.length + batch.infra.length;
|
|
614
|
+
if (items <= 1) {
|
|
615
|
+
this.dropped += batch.spans.length;
|
|
616
|
+
this.warnOnce(`ingest respondeu 413 para um item de ${batch.bytes} bytes (descartado).`);
|
|
617
|
+
return true;
|
|
618
|
+
}
|
|
619
|
+
// Divide: o mesmo lote volta para a fila e sai em pedaços de metade do tamanho (spans e
|
|
620
|
+
// agregados contam). Abaixo do mínimo de bytes, divide pela metade em número de itens: cada
|
|
621
|
+
// 413 encolhe o lote, então o laço termina, e só um item sozinho acima do limite se perde.
|
|
622
|
+
const half = Math.floor(batch.bytes / 2);
|
|
623
|
+
if (half >= MIN_BODY_BYTES)
|
|
624
|
+
this.batchBytes = Math.min(this.batchBytes, half);
|
|
625
|
+
else
|
|
626
|
+
this.batchItems = Math.max(1, Math.min(this.batchItems, Math.floor(items / 2)));
|
|
627
|
+
this.requeue(batch);
|
|
628
|
+
return true;
|
|
629
|
+
}
|
|
630
|
+
case 'rejected':
|
|
631
|
+
// 4xx = configuração (chave inválida, payload malformado): reenviar daria o mesmo erro.
|
|
632
|
+
this.sender.noteSuccess();
|
|
633
|
+
this.dropped += batch.spans.length;
|
|
634
|
+
this.warnOnce(`ingest respondeu ${result.status} (lote descartado)`);
|
|
635
|
+
return true;
|
|
636
|
+
case 'retry': {
|
|
637
|
+
const delay = this.sender.noteFailure(result);
|
|
638
|
+
this.warnOnce(`falha ao enviar para ${this.options.endpoint}/v1/ingest: ${result.reason} (nova tentativa em ${Math.round(delay / 1000)} s)`);
|
|
639
|
+
this.requeue(batch);
|
|
640
|
+
return false;
|
|
372
641
|
}
|
|
373
642
|
}
|
|
374
643
|
}
|
|
644
|
+
/** Devolve o lote para a frente da fila (os limites de memória continuam valendo). */
|
|
645
|
+
requeue(batch) {
|
|
646
|
+
this.queue.unshift(...batch.spans);
|
|
647
|
+
for (const span of batch.spans)
|
|
648
|
+
this.queueBytes += span.bytes;
|
|
649
|
+
this.trimQueue();
|
|
650
|
+
if (batch.infra.length > 0) {
|
|
651
|
+
this.infraPending.unshift(...batch.infra);
|
|
652
|
+
this.trimInfraPending();
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* 202 da ingestão: `live`/`liveIntervalMs` (infra ao vivo), spans recusados (não reenvia: a
|
|
657
|
+
* recusa se repetiria) e agregados de infra com falha transitória (`retryableIds`, reenvia só
|
|
658
|
+
* esses). Devolve `true` quando algum agregado voltou para a fila.
|
|
659
|
+
*/
|
|
660
|
+
readIngestResponse(batch, body) {
|
|
661
|
+
if (!body || typeof body !== 'object')
|
|
662
|
+
return false;
|
|
663
|
+
const response = body;
|
|
664
|
+
if (typeof response.rejected === 'number' && response.rejected > 0 && !this.warnedRejected) {
|
|
665
|
+
this.warnedRejected = true;
|
|
666
|
+
const first = Array.isArray(response.errors) ? response.errors[0]?.reason : undefined;
|
|
667
|
+
console.warn(`[fg-tracker] a API recusou ${response.rejected} span(s)${typeof first === 'string' ? ` (ex.: ${first})` : ''}; recusas não são reenviadas.`);
|
|
668
|
+
}
|
|
669
|
+
this.readLiveFlag(response);
|
|
670
|
+
const retryable = response.infra?.retryableIds;
|
|
671
|
+
if (!Array.isArray(retryable) || retryable.length === 0 || batch.infra.length === 0)
|
|
672
|
+
return false;
|
|
673
|
+
const ids = new Set(retryable.filter((id) => typeof id === 'string'));
|
|
674
|
+
const again = batch.infra.filter((aggregate) => aggregate.id !== undefined && ids.has(aggregate.id));
|
|
675
|
+
if (again.length === 0)
|
|
676
|
+
return false;
|
|
677
|
+
this.infraPending.unshift(...again);
|
|
678
|
+
this.trimInfraPending();
|
|
679
|
+
return true;
|
|
680
|
+
}
|
|
375
681
|
pushInfraPending(items) {
|
|
682
|
+
for (const item of items)
|
|
683
|
+
item.id ??= `${this.emitterId}:${++this.infraSeq}`;
|
|
376
684
|
this.infraPending.push(...items);
|
|
377
685
|
this.trimInfraPending();
|
|
378
686
|
}
|
|
@@ -408,17 +716,8 @@ export class TrackerCore {
|
|
|
408
716
|
this.infraTimer.unref();
|
|
409
717
|
}
|
|
410
718
|
/** Lê `live`/`liveIntervalMs` da resposta da ingestão (piggyback, sem chamada extra). */
|
|
411
|
-
|
|
412
|
-
if (!this.infra
|
|
413
|
-
return;
|
|
414
|
-
let body;
|
|
415
|
-
try {
|
|
416
|
-
body = await res.json();
|
|
417
|
-
}
|
|
418
|
-
catch {
|
|
419
|
-
return;
|
|
420
|
-
}
|
|
421
|
-
if (!body || typeof body !== 'object')
|
|
719
|
+
readLiveFlag(body) {
|
|
720
|
+
if (!this.infra)
|
|
422
721
|
return;
|
|
423
722
|
const { live, liveIntervalMs } = body;
|
|
424
723
|
if (typeof live !== 'boolean')
|
|
@@ -451,55 +750,61 @@ export class TrackerCore {
|
|
|
451
750
|
return null;
|
|
452
751
|
}
|
|
453
752
|
}
|
|
454
|
-
async send(spans, infra = []) {
|
|
455
|
-
const byTrace = new Map();
|
|
456
|
-
for (const span of spans) {
|
|
457
|
-
const { traceId, ...rest } = span;
|
|
458
|
-
let list = byTrace.get(traceId);
|
|
459
|
-
if (!list) {
|
|
460
|
-
list = [];
|
|
461
|
-
byTrace.set(traceId, list);
|
|
462
|
-
}
|
|
463
|
-
list.push(rest);
|
|
464
|
-
}
|
|
465
|
-
const traces = [...byTrace].map(([traceId, s]) => ({ traceId, spans: s }));
|
|
466
|
-
// Sem agregados o body continua idêntico ao das versões anteriores.
|
|
467
|
-
const body = JSON.stringify(infra.length > 0 ? { traces, infra } : { traces });
|
|
468
|
-
try {
|
|
469
|
-
const res = await this.options.fetch(`${this.options.endpoint}/v1/ingest`, {
|
|
470
|
-
method: 'POST',
|
|
471
|
-
headers: { 'content-type': 'application/json', [API_KEY_HEADER]: this.options.apiKey },
|
|
472
|
-
body,
|
|
473
|
-
});
|
|
474
|
-
if (!res.ok) {
|
|
475
|
-
// 4xx = problema de configuração (chave inválida, payload rejeitado): não faz sentido reenviar.
|
|
476
|
-
const retry = res.status >= 500 || res.status === 429;
|
|
477
|
-
this.warnOnce(`ingest respondeu ${res.status}${retry ? ' (vai tentar de novo)' : ' (lote descartado)'}`);
|
|
478
|
-
return !retry;
|
|
479
|
-
}
|
|
480
|
-
if (this.warnedFailure) {
|
|
481
|
-
this.warnedFailure = false;
|
|
482
|
-
if (this.options.debug)
|
|
483
|
-
console.info('[fg-tracker] envio restabelecido.');
|
|
484
|
-
}
|
|
485
|
-
await this.readLiveFlag(res);
|
|
486
|
-
if (this.options.debug) {
|
|
487
|
-
console.info(`[fg-tracker] enviados ${spans.length} spans em ${byTrace.size} traces${infra.length > 0 ? ` e ${infra.length} agregados de infra` : ''}.`);
|
|
488
|
-
}
|
|
489
|
-
return true;
|
|
490
|
-
}
|
|
491
|
-
catch (err) {
|
|
492
|
-
this.warnOnce(`falha ao enviar para ${this.options.endpoint}/v1/ingest: ${err?.message ?? err}`);
|
|
493
|
-
return false;
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
753
|
warnOnce(message) {
|
|
497
754
|
if (this.warnedFailure)
|
|
498
755
|
return;
|
|
499
756
|
this.warnedFailure = true;
|
|
500
757
|
console.warn(`[fg-tracker] ${message}`);
|
|
501
758
|
}
|
|
502
|
-
|
|
759
|
+
hasPendingForExit() {
|
|
760
|
+
if (!this.enabled)
|
|
761
|
+
return false;
|
|
762
|
+
// Um lote já saiu da fila e está em voo: a saída espera ele (até o prazo).
|
|
763
|
+
if (this.flushing)
|
|
764
|
+
return true;
|
|
765
|
+
return this.enqueuedSinceExit > 0 && (this.queue.length > 0 || this.hasInfraToSend() || this.infra?.hasData() === true);
|
|
766
|
+
}
|
|
767
|
+
/** Saída do processo sem `shutdown()` (`beforeExit`, SIGTERM/SIGINT): envia com prazo, sem parar o tracker. */
|
|
768
|
+
async flushForExit(deadline) {
|
|
769
|
+
this.enqueuedSinceExit = 0;
|
|
770
|
+
try {
|
|
771
|
+
await this.sendUntil(deadline);
|
|
772
|
+
}
|
|
773
|
+
catch {
|
|
774
|
+
/* a saída nunca lança */
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
/**
|
|
778
|
+
* Envia tudo o que der até `deadline`, esperando backoff/`Retry-After` quando cabe no prazo.
|
|
779
|
+
* A requisição em andamento é abortada no prazo.
|
|
780
|
+
*/
|
|
781
|
+
async sendUntil(deadline) {
|
|
782
|
+
if (!this.options.enabled)
|
|
783
|
+
return;
|
|
784
|
+
const release = this.deadlines.add(deadline);
|
|
785
|
+
this.sender.abortAt(this.deadline ?? deadline);
|
|
786
|
+
try {
|
|
787
|
+
// Outro envio (flush, saída do processo) em andamento: espera, sem iniciar um segundo laço.
|
|
788
|
+
while (this.flushing)
|
|
789
|
+
await this.flushing;
|
|
790
|
+
if (this.infra?.hasData())
|
|
791
|
+
this.infraDue = true;
|
|
792
|
+
if (this.queue.length === 0 && !this.hasInfraToSend())
|
|
793
|
+
return;
|
|
794
|
+
this.flushing = this.drain(true).finally(() => {
|
|
795
|
+
this.flushing = null;
|
|
796
|
+
});
|
|
797
|
+
await this.flushing;
|
|
798
|
+
}
|
|
799
|
+
finally {
|
|
800
|
+
release();
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
/**
|
|
804
|
+
* Para os timers e envia o que restou em até `timeoutMs` (default `shutdownTimeoutMs`, 10 s).
|
|
805
|
+
* Com a API muda ou fora, volta no prazo; o que não saiu é descartado (e contado em `dropped`).
|
|
806
|
+
*/
|
|
807
|
+
async shutdown(options = {}) {
|
|
503
808
|
if (this.timer) {
|
|
504
809
|
clearInterval(this.timer);
|
|
505
810
|
this.timer = null;
|
|
@@ -518,10 +823,73 @@ export class TrackerCore {
|
|
|
518
823
|
}
|
|
519
824
|
}
|
|
520
825
|
this.disposers.clear();
|
|
521
|
-
|
|
826
|
+
this.unregisterExit();
|
|
827
|
+
const timeoutMs = Math.max(0, options.timeoutMs ?? this.options.shutdownTimeoutMs);
|
|
828
|
+
await this.sendUntil(Date.now() + timeoutMs);
|
|
829
|
+
if (this.queue.length > 0 && this.options.enabled) {
|
|
830
|
+
this.dropped += this.queue.length;
|
|
831
|
+
this.warnOnce(`encerrado com ${this.queue.length} span(s) sem enviar.`);
|
|
832
|
+
}
|
|
522
833
|
this.stopped = true;
|
|
523
834
|
}
|
|
524
835
|
}
|
|
836
|
+
/** Atributos do span enviado só porque falhou num trace não amostrado (`sendUnsampledErrors`). */
|
|
837
|
+
function markKeptByError(span) {
|
|
838
|
+
return { ...span, attributes: { ...span.attributes, 'sampling.kept': 'error' } };
|
|
839
|
+
}
|
|
840
|
+
/** JSON do span (sem `traceId`), com `attributes`/`error` cortados abaixo do limite da API. */
|
|
841
|
+
function serializeSpan(span) {
|
|
842
|
+
const { traceId, ...rest } = span;
|
|
843
|
+
let json = safeStringify(rest);
|
|
844
|
+
let bytes = byteLength(json);
|
|
845
|
+
// Só um span grande pode ter `attributes` ou `error` acima do limite: o resto não paga a checagem.
|
|
846
|
+
if (bytes > MAX_SPAN_FIELD_BYTES) {
|
|
847
|
+
json = safeStringify({ ...rest, attributes: fitRecord(rest.attributes ?? {}, MAX_SPAN_FIELD_BYTES), error: fitError(rest.error) });
|
|
848
|
+
bytes = byteLength(json);
|
|
849
|
+
}
|
|
850
|
+
return { traceId, json, bytes };
|
|
851
|
+
}
|
|
852
|
+
/**
|
|
853
|
+
* Corta `error` até o JSON serializado caber no limite (o servidor mede `JSON.stringify(error)`:
|
|
854
|
+
* aspas, barras e controles crescem ao escapar). Encolhe primeiro o stack, depois a mensagem, e
|
|
855
|
+
* confere o tamanho final a cada passo.
|
|
856
|
+
*/
|
|
857
|
+
function fitError(error) {
|
|
858
|
+
if (!error || byteLength(safeStringify(error)) <= MAX_SPAN_FIELD_BYTES)
|
|
859
|
+
return error;
|
|
860
|
+
const name = truncateText(String(error.name), 256);
|
|
861
|
+
const message = String(error.message);
|
|
862
|
+
const stack = typeof error.stack === 'string' ? error.stack : undefined;
|
|
863
|
+
let messageBytes = 8 * 1024;
|
|
864
|
+
let stackBytes = stack === undefined ? 0 : MAX_SPAN_FIELD_BYTES - 10 * 1024;
|
|
865
|
+
for (;;) {
|
|
866
|
+
const out = { name, message: truncateText(message, messageBytes), truncated: true };
|
|
867
|
+
if (stack !== undefined && stackBytes > 0)
|
|
868
|
+
out.stack = truncateText(stack, stackBytes);
|
|
869
|
+
if (byteLength(safeStringify(out)) <= MAX_SPAN_FIELD_BYTES)
|
|
870
|
+
return out;
|
|
871
|
+
if (stackBytes > 0)
|
|
872
|
+
stackBytes = stackBytes > 256 ? Math.floor(stackBytes / 2) : 0;
|
|
873
|
+
else if (messageBytes > 0)
|
|
874
|
+
messageBytes = messageBytes > 64 ? Math.floor(messageBytes / 2) : 0;
|
|
875
|
+
else
|
|
876
|
+
return { name: truncateText(name, 64), message: '', truncated: true };
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
/** `{"traces":[{"traceId":…,"spans":[…]}…]}` a partir do JSON já pronto de cada span. */
|
|
880
|
+
function buildIngestBody(spans, infraJson) {
|
|
881
|
+
const groups = new Map();
|
|
882
|
+
for (const span of spans) {
|
|
883
|
+
const list = groups.get(span.traceId);
|
|
884
|
+
if (list)
|
|
885
|
+
list.push(span.json);
|
|
886
|
+
else
|
|
887
|
+
groups.set(span.traceId, [span.json]);
|
|
888
|
+
}
|
|
889
|
+
const traces = [...groups].map(([traceId, jsons]) => `{"traceId":${JSON.stringify(traceId)},"spans":[${jsons.join(',')}]}`).join(',');
|
|
890
|
+
// Sem agregados o body continua idêntico ao das versões anteriores.
|
|
891
|
+
return infraJson ? `{"traces":[${traces}],"infra":${infraJson}}` : `{"traces":[${traces}]}`;
|
|
892
|
+
}
|
|
525
893
|
export function userAttributes(user) {
|
|
526
894
|
if (!user)
|
|
527
895
|
return { 'user.id': undefined, 'user.email': undefined, 'user.name': undefined };
|
|
@@ -539,6 +907,8 @@ export class ActiveSpanImpl {
|
|
|
539
907
|
parentSpanId;
|
|
540
908
|
kind;
|
|
541
909
|
sampled;
|
|
910
|
+
ignored;
|
|
911
|
+
localRootId;
|
|
542
912
|
name;
|
|
543
913
|
method = null;
|
|
544
914
|
route = null;
|
|
@@ -548,13 +918,19 @@ export class ActiveSpanImpl {
|
|
|
548
918
|
startedAt;
|
|
549
919
|
startedPerf;
|
|
550
920
|
ended = false;
|
|
551
|
-
constructor(core, traceId, spanId, parentSpanId, kind, name, attributes, sampled = true
|
|
921
|
+
constructor(core, traceId, spanId, parentSpanId, kind, name, attributes, sampled = true,
|
|
922
|
+
/** Trace excluído por `ignoreTransactions`: nunca enviado (nem com `sendUnsampledErrors`). */
|
|
923
|
+
ignored = false,
|
|
924
|
+
/** `spanId` da raiz local deste ramo (o próprio, quando o span é raiz local). */
|
|
925
|
+
localRootId = spanId) {
|
|
552
926
|
this.core = core;
|
|
553
927
|
this.traceId = traceId;
|
|
554
928
|
this.spanId = spanId;
|
|
555
929
|
this.parentSpanId = parentSpanId;
|
|
556
930
|
this.kind = kind;
|
|
557
931
|
this.sampled = sampled;
|
|
932
|
+
this.ignored = ignored;
|
|
933
|
+
this.localRootId = localRootId;
|
|
558
934
|
this.name = name;
|
|
559
935
|
this.attributes = attributes;
|
|
560
936
|
this.startedAt = new Date();
|