fg-tracker-sdk 0.10.0 → 0.11.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 +18 -0
- package/README.md +53 -3
- package/dist/context.d.ts +9 -0
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +27 -0
- package/dist/context.js.map +1 -1
- package/dist/database.d.ts.map +1 -1
- package/dist/database.js +34 -5
- package/dist/database.js.map +1 -1
- package/dist/express.js +2 -2
- package/dist/express.js.map +1 -1
- package/dist/fetch.d.ts.map +1 -1
- package/dist/fetch.js +16 -8
- package/dist/fetch.js.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -3
- package/dist/index.js.map +1 -1
- package/dist/infra/aws.d.ts +45 -3
- package/dist/infra/aws.d.ts.map +1 -1
- package/dist/infra/aws.js +419 -29
- package/dist/infra/aws.js.map +1 -1
- package/dist/infra/lambda.d.ts +19 -0
- package/dist/infra/lambda.d.ts.map +1 -1
- package/dist/infra/lambda.js +61 -9
- package/dist/infra/lambda.js.map +1 -1
- package/dist/infra/messaging.d.ts +133 -0
- package/dist/infra/messaging.d.ts.map +1 -0
- package/dist/infra/messaging.js +366 -0
- package/dist/infra/messaging.js.map +1 -0
- package/dist/next.js +2 -2
- package/dist/next.js.map +1 -1
- package/dist/node-http.d.ts.map +1 -1
- package/dist/node-http.js +7 -3
- package/dist/node-http.js.map +1 -1
- package/dist/normalize.d.ts +32 -0
- package/dist/normalize.d.ts.map +1 -1
- package/dist/normalize.js +165 -10
- package/dist/normalize.js.map +1 -1
- package/dist/pg.d.ts +8 -3
- package/dist/pg.d.ts.map +1 -1
- package/dist/pg.js +512 -41
- package/dist/pg.js.map +1 -1
- package/dist/tracker.d.ts +8 -1
- package/dist/tracker.d.ts.map +1 -1
- package/dist/tracker.js +36 -5
- package/dist/tracker.js.map +1 -1
- package/dist/types.d.ts +17 -4
- package/dist/types.d.ts.map +1 -1
- package/package.json +4 -1
package/dist/infra/aws.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { TrackerCore } from '../tracker.js';
|
|
2
2
|
import type { InfraResourceRef } from './aggregator.js';
|
|
3
|
+
import { ReceivedMessages } from './messaging.js';
|
|
3
4
|
/**
|
|
4
5
|
* Instrumentação do AWS SDK v3 por middleware (CONTRATO.md §27.8).
|
|
5
6
|
* Tipos estruturais: o SDK não depende de `@aws-sdk/*`.
|
|
@@ -36,16 +37,57 @@ export declare function parseArn(arn: string): {
|
|
|
36
37
|
* Também aceita um ARN pronto.
|
|
37
38
|
*/
|
|
38
39
|
export declare function sqsQueueRef(queue: string, fallbackRegion?: string): InfraResourceRef;
|
|
40
|
+
export interface AwsInstrumentationOptions {
|
|
41
|
+
/**
|
|
42
|
+
* Propaga o contexto do trace nas mensagens: `traceparent`/`tracestate` em `MessageAttributes`
|
|
43
|
+
* (SQS `SendMessage`/`SendMessageBatch`) e no `Detail` (EventBridge `PutEvents`), e pede esses
|
|
44
|
+
* atributos no `ReceiveMessage`. Default `true` (salvo `propagateTraceTo: false` no tracker).
|
|
45
|
+
*/
|
|
46
|
+
propagation?: boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Limite de bytes por mensagem SQS usado para decidir se o contexto cabe. Default 1 MiB (cota do
|
|
49
|
+
* SQS). Use o `MaximumMessageSize` da fila se ele for menor; mesmo sem isso, numa fila padrão,
|
|
50
|
+
* uma recusa por tamanho com o contexto é repetida sem ele. Em fila FIFO (`.fifo`), sem esta
|
|
51
|
+
* opção o contexto só vai em mensagens de até 1 KiB (o menor `MaximumMessageSize` possível):
|
|
52
|
+
* reenviar uma entrada recusada do lote depois das aceitas mudaria a ordem do grupo.
|
|
53
|
+
*/
|
|
54
|
+
sqsMaxMessageBytes?: number;
|
|
55
|
+
/** Limite do pedido `PutEvents` (soma das entradas). Default 1 MiB. */
|
|
56
|
+
eventBridgeMaxRequestBytes?: number;
|
|
57
|
+
}
|
|
58
|
+
/** Mensagens recebidas por clients instrumentados deste tracker (contexto lido antes de retirar os atributos). */
|
|
59
|
+
export declare function receivedMessages(core: TrackerCore): ReceivedMessages;
|
|
60
|
+
/**
|
|
61
|
+
* Fila FIFO pelo nome (o sufixo `.fifo` é do QueueName): último segmento do caminho da URL (com ou
|
|
62
|
+
* sem barra final, query ou fragmento) ou do ARN. Na dúvida (URL que não dá para ler), o nome cru.
|
|
63
|
+
*/
|
|
64
|
+
export declare function isFifoQueue(queue: string): boolean;
|
|
39
65
|
/**
|
|
40
66
|
* Adiciona o middleware `fgTrackerInfra` (step `initialize`) ao client.
|
|
41
67
|
* - Agregados de infra (§27.4) para SQS, SNS, EventBridge, Kinesis, DynamoDB e Step Functions — nunca amostrados.
|
|
42
68
|
* - Span `http.client` por chamada (qualquer serviço) quando há um trace em andamento; respeita amostragem.
|
|
43
69
|
* - DynamoDB: injeta `ReturnConsumedCapacity: 'TOTAL'` quando não informado.
|
|
70
|
+
* - SQS/EventBridge (§16.1): span `custom` `SQS publish <fila>`/`EventBridge publish <barramento>` com o
|
|
71
|
+
* contexto propagado nas mensagens; `ReceiveMessage` pede o contexto e o guarda para o `trackMessage`.
|
|
44
72
|
*/
|
|
45
|
-
export declare function instrumentAwsClient<T extends AwsClientLike>(core: TrackerCore, client: T): T;
|
|
73
|
+
export declare function instrumentAwsClient<T extends AwsClientLike>(core: TrackerCore, client: T, options?: AwsInstrumentationOptions): T;
|
|
74
|
+
/** Forma da mensagem aceita pelo `trackMessage`: record do evento da Lambda ou `Message` do AWS SDK. */
|
|
75
|
+
export type QueueMessageLike = object;
|
|
76
|
+
export interface TrackMessageOptions {
|
|
77
|
+
/**
|
|
78
|
+
* A mensagem processada (record do evento SQS da Lambda ou `Message` do `ReceiveMessage`): o
|
|
79
|
+
* processamento continua o trace do produtor (pai remoto da própria mensagem), com o atraso de
|
|
80
|
+
* fila e as tentativas. Sem ela, o processamento é medido só localmente.
|
|
81
|
+
*/
|
|
82
|
+
record?: QueueMessageLike;
|
|
83
|
+
}
|
|
46
84
|
/**
|
|
47
85
|
* Mede o processamento de uma mensagem consumida (`processingMsSum`, `processingMsMax`, `errors`)
|
|
48
|
-
* e cria um span `custom` para os spans internos (AWS, db, fetch) formarem um trace.
|
|
86
|
+
* e cria um span `custom` `SQS process <fila>` para os spans internos (AWS, db, fetch) formarem um trace.
|
|
87
|
+
*
|
|
88
|
+
* Com `record`, o span continua o trace do produtor: raiz local com `remoteParentSpanId` (ou filho
|
|
89
|
+
* do span atual quando ele já está no mesmo trace, como no `wrapLambda` de uma mensagem só). Cada
|
|
90
|
+
* chamada tem o próprio contexto, inclusive em `Promise.all`; o contexto da invocação vira link.
|
|
49
91
|
*/
|
|
50
|
-
export declare function trackQueueMessage<T>(core: TrackerCore, queue: string, fn: () => T | Promise<T
|
|
92
|
+
export declare function trackQueueMessage<T>(core: TrackerCore, queue: string, fn: () => T | Promise<T>, record?: QueueMessageLike): Promise<T>;
|
|
51
93
|
//# sourceMappingURL=aws.d.ts.map
|
package/dist/infra/aws.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"aws.d.ts","sourceRoot":"","sources":["../../src/infra/aws.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"aws.d.ts","sourceRoot":"","sources":["../../src/infra/aws.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAkB,WAAW,EAAE,MAAM,eAAe,CAAC;AACjE,OAAO,KAAK,EAA8B,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACpF,OAAO,EAUL,gBAAgB,EAWjB,MAAM,gBAAgB,CAAC;AAExB;;;GAGG;AAEH,iGAAiG;AACjG,MAAM,WAAW,aAAa;IAC5B,eAAe,EAAE;QACf,GAAG,CAAC,UAAU,EAAE,CAAC,IAAI,EAAE,cAAc,EAAE,OAAO,EAAE,iBAAiB,KAAK,cAAc,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;KAC/H,CAAC;IACF,MAAM,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC7E;AAED,MAAM,MAAM,cAAc,GAAG,CAAC,IAAI,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC;IAAE,MAAM,EAAE,OAAO,CAAA;CAAE,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;AAE5J,MAAM,WAAW,iBAAiB;IAChC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AA2ED,gFAAgF;AAChF,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAIlG;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,cAAc,CAAC,EAAE,MAAM,GAAG,gBAAgB,CAgBpF;AAuPD,MAAM,WAAW,yBAAyB;IACxC;;;;OAIG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB;;;;;;OAMG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,uEAAuE;IACvE,0BAA0B,CAAC,EAAE,MAAM,CAAC;CACrC;AAID,kHAAkH;AAClH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,WAAW,GAAG,gBAAgB,CAOpE;AAoBD;;;GAGG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAYlD;AAqJD;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,CAAC,SAAS,aAAa,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,CAAC,EAAE,OAAO,GAAE,yBAA8B,GAAG,CAAC,CA0NrI;AAED,wGAAwG;AACxG,MAAM,MAAM,gBAAgB,GAAG,MAAM,CAAC;AAEtC,MAAM,WAAW,mBAAmB;IAClC;;;;OAIG;IACH,MAAM,CAAC,EAAE,gBAAgB,CAAC;CAC3B;AAED;;;;;;;GAOG;AACH,wBAAsB,iBAAiB,CAAC,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,CAAC,CAAC,CA0D5I"}
|
package/dist/infra/aws.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { EVENTBRIDGE_MAX_REQUEST_BYTES, eventBridgeEntryBytes, injectEventBridgeContext, injectSqsContext, isValidationCode, isValidationError, metaContext, planReceive, readSqsMessage, ReceivedMessages, SQS_MAX_BATCH_BYTES, SQS_MAX_MESSAGE_BYTES, SQS_MIN_MESSAGE_BYTES, sqsAttributesMd5, sqsMessageBytes, stripReceivedMessage, uniqueLinks, } from './messaging.js';
|
|
1
2
|
const PATCHED = Symbol.for('fg-tracker.aws.patched');
|
|
2
3
|
const SERVICE_IDS = {
|
|
3
4
|
sqs: 'sqs',
|
|
@@ -360,13 +361,205 @@ function httpStatus(value) {
|
|
|
360
361
|
const status = obj(obj(value).$metadata).httpStatusCode;
|
|
361
362
|
return typeof status === 'number' ? status : null;
|
|
362
363
|
}
|
|
364
|
+
const received = new WeakMap();
|
|
365
|
+
/** Mensagens recebidas por clients instrumentados deste tracker (contexto lido antes de retirar os atributos). */
|
|
366
|
+
export function receivedMessages(core) {
|
|
367
|
+
let store = received.get(core);
|
|
368
|
+
if (!store) {
|
|
369
|
+
store = new ReceivedMessages();
|
|
370
|
+
received.set(core, store);
|
|
371
|
+
}
|
|
372
|
+
return store;
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Fila FIFO pelo nome (o sufixo `.fifo` é do QueueName): último segmento do caminho da URL (com ou
|
|
376
|
+
* sem barra final, query ou fragmento) ou do ARN. Na dúvida (URL que não dá para ler), o nome cru.
|
|
377
|
+
*/
|
|
378
|
+
export function isFifoQueue(queue) {
|
|
379
|
+
let name = queue;
|
|
380
|
+
if (queue.startsWith('arn:'))
|
|
381
|
+
name = queue.split(':').pop() ?? queue;
|
|
382
|
+
else {
|
|
383
|
+
try {
|
|
384
|
+
name = new URL(queue).pathname;
|
|
385
|
+
}
|
|
386
|
+
catch {
|
|
387
|
+
name = queue.split(/[?#]/)[0] ?? queue;
|
|
388
|
+
}
|
|
389
|
+
name = name.split('/').filter(Boolean).pop() ?? name;
|
|
390
|
+
}
|
|
391
|
+
return name.endsWith('.fifo');
|
|
392
|
+
}
|
|
393
|
+
function sqsPublishPlan(args, operation, ref, configured) {
|
|
394
|
+
const input = obj(args.input);
|
|
395
|
+
const queueName = ref?.resourceName ?? str(input.QueueUrl) ?? 'queue';
|
|
396
|
+
// FIFO: reenviar uma entrada recusada depois das aceitas mudaria a ordem do grupo. Sem o limite
|
|
397
|
+
// da fila declarado, o contexto só entra onde nenhuma fila o recusa (o mínimo do SQS, 1 KiB),
|
|
398
|
+
// e entradas de lote recusadas não são reenviadas fora de ordem.
|
|
399
|
+
const fifo = isFifoQueue(str(input.QueueUrl) ?? queueName);
|
|
400
|
+
const limits = { messageBytes: fifo && !configured.declared ? SQS_MIN_MESSAGE_BYTES : configured.messageBytes };
|
|
401
|
+
const base = {
|
|
402
|
+
'messaging.system': 'aws_sqs',
|
|
403
|
+
'messaging.operation.type': 'send',
|
|
404
|
+
'messaging.operation.name': operation,
|
|
405
|
+
'messaging.destination.name': queueName,
|
|
406
|
+
...(ref?.resourceArn ? { 'aws.resourceArn': ref.resourceArn } : {}),
|
|
407
|
+
};
|
|
408
|
+
if (operation === 'SendMessage') {
|
|
409
|
+
return {
|
|
410
|
+
name: `SQS publish ${queueName}`,
|
|
411
|
+
attributes: base,
|
|
412
|
+
inject(ctx) {
|
|
413
|
+
const result = injectSqsContext(input, ctx, { messageBytes: limits.messageBytes, budgetBytes: Number.POSITIVE_INFINITY });
|
|
414
|
+
return {
|
|
415
|
+
args: result.injected ? { ...args, input: result.entry } : args,
|
|
416
|
+
injected: new Set(result.injected ? ['message'] : []),
|
|
417
|
+
skipped: result.skipped && result.skipped !== 'present' ? [result.skipped] : [],
|
|
418
|
+
};
|
|
419
|
+
},
|
|
420
|
+
restore(output, injected) {
|
|
421
|
+
if (!injected.has('message'))
|
|
422
|
+
return output;
|
|
423
|
+
const md5 = sqsAttributesMd5(input.MessageAttributes);
|
|
424
|
+
const out = { ...output };
|
|
425
|
+
if (md5)
|
|
426
|
+
out.MD5OfMessageAttributes = md5;
|
|
427
|
+
else
|
|
428
|
+
delete out.MD5OfMessageAttributes;
|
|
429
|
+
return out;
|
|
430
|
+
},
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
if (operation === 'SendMessageBatch') {
|
|
434
|
+
const entries = arr(input.Entries).map(obj);
|
|
435
|
+
const originals = new Map(entries.map((entry) => [String(entry.Id), entry]));
|
|
436
|
+
return {
|
|
437
|
+
name: `SQS publish ${queueName}`,
|
|
438
|
+
attributes: { ...base, 'messaging.batch.message_count': entries.length },
|
|
439
|
+
inject(ctx) {
|
|
440
|
+
// Orçamento agregado: a soma das entradas (atributos incluídos) também tem teto.
|
|
441
|
+
let remaining = SQS_MAX_BATCH_BYTES - entries.reduce((sum, entry) => sum + sqsMessageBytes(entry.MessageBody, entry.MessageAttributes), 0);
|
|
442
|
+
const injected = new Set();
|
|
443
|
+
const skipped = [];
|
|
444
|
+
const next = entries.map((entry) => {
|
|
445
|
+
const result = injectSqsContext(entry, ctx, { messageBytes: limits.messageBytes, budgetBytes: Math.max(0, remaining) });
|
|
446
|
+
if (result.injected) {
|
|
447
|
+
remaining -= result.addedBytes;
|
|
448
|
+
injected.add(String(entry.Id));
|
|
449
|
+
}
|
|
450
|
+
else if (result.skipped && result.skipped !== 'present')
|
|
451
|
+
skipped.push(result.skipped);
|
|
452
|
+
return result.entry;
|
|
453
|
+
});
|
|
454
|
+
return { args: injected.size > 0 ? { ...args, input: { ...input, Entries: next } } : args, injected, skipped };
|
|
455
|
+
},
|
|
456
|
+
restore(output, injected) {
|
|
457
|
+
if (injected.size === 0 || !Array.isArray(output.Successful))
|
|
458
|
+
return output;
|
|
459
|
+
return {
|
|
460
|
+
...output,
|
|
461
|
+
Successful: output.Successful.map((item) => {
|
|
462
|
+
const ok = obj(item);
|
|
463
|
+
const id = String(ok.Id);
|
|
464
|
+
if (!injected.has(id))
|
|
465
|
+
return item;
|
|
466
|
+
const md5 = sqsAttributesMd5(originals.get(id)?.MessageAttributes);
|
|
467
|
+
const out = { ...ok };
|
|
468
|
+
if (md5)
|
|
469
|
+
out.MD5OfMessageAttributes = md5;
|
|
470
|
+
else
|
|
471
|
+
delete out.MD5OfMessageAttributes;
|
|
472
|
+
return out;
|
|
473
|
+
}),
|
|
474
|
+
};
|
|
475
|
+
},
|
|
476
|
+
retryEntries(output, injected) {
|
|
477
|
+
if (fifo)
|
|
478
|
+
return null;
|
|
479
|
+
const failed = arr(output.Failed).map(obj).filter((item) => injected.has(String(item.Id)) && item.SenderFault === true && isValidationCode(item.Code));
|
|
480
|
+
if (failed.length === 0)
|
|
481
|
+
return null;
|
|
482
|
+
const retry = failed.map((item) => originals.get(String(item.Id))).filter(Boolean);
|
|
483
|
+
return {
|
|
484
|
+
args: { ...args, input: { ...input, Entries: retry } },
|
|
485
|
+
merge(first, retried) {
|
|
486
|
+
const retriedIds = new Set([...arr(retried.Successful), ...arr(retried.Failed)].map((item) => String(obj(item).Id)));
|
|
487
|
+
return {
|
|
488
|
+
...first,
|
|
489
|
+
Successful: [...arr(first.Successful), ...arr(retried.Successful)],
|
|
490
|
+
Failed: [...arr(first.Failed).filter((item) => !retriedIds.has(String(obj(item).Id))), ...arr(retried.Failed)],
|
|
491
|
+
};
|
|
492
|
+
},
|
|
493
|
+
};
|
|
494
|
+
},
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
return null;
|
|
498
|
+
}
|
|
499
|
+
function eventBridgePublishPlan(args, limits) {
|
|
500
|
+
const input = obj(args.input);
|
|
501
|
+
const entries = arr(input.Entries).map(obj);
|
|
502
|
+
if (entries.length === 0)
|
|
503
|
+
return null;
|
|
504
|
+
const bus = str(entries[0].EventBusName) ?? 'default';
|
|
505
|
+
const busName = bus.startsWith('arn:') ? bus.split('/').pop() || bus : bus;
|
|
506
|
+
return {
|
|
507
|
+
name: `EventBridge publish ${busName}`,
|
|
508
|
+
attributes: {
|
|
509
|
+
'messaging.system': 'aws_eventbridge',
|
|
510
|
+
'messaging.operation.type': 'send',
|
|
511
|
+
'messaging.operation.name': 'PutEvents',
|
|
512
|
+
'messaging.destination.name': busName,
|
|
513
|
+
'messaging.batch.message_count': entries.length,
|
|
514
|
+
},
|
|
515
|
+
inject(ctx) {
|
|
516
|
+
// "Menor que" o limite: sobra pelo menos 1 byte.
|
|
517
|
+
let remaining = limits.requestBytes - 1 - entries.reduce((sum, entry) => sum + eventBridgeEntryBytes(entry), 0);
|
|
518
|
+
const injected = new Set();
|
|
519
|
+
const skipped = [];
|
|
520
|
+
const next = entries.map((entry, index) => {
|
|
521
|
+
const result = injectEventBridgeContext(entry, ctx, Math.max(0, remaining));
|
|
522
|
+
if (result.injected) {
|
|
523
|
+
remaining -= result.addedBytes;
|
|
524
|
+
injected.add(String(index));
|
|
525
|
+
}
|
|
526
|
+
else if (result.skipped && result.skipped !== 'present')
|
|
527
|
+
skipped.push(result.skipped);
|
|
528
|
+
return result.entry;
|
|
529
|
+
});
|
|
530
|
+
return { args: injected.size > 0 ? { ...args, input: { ...input, Entries: next } } : args, injected, skipped };
|
|
531
|
+
},
|
|
532
|
+
restore: (output) => output,
|
|
533
|
+
retryEntries(output, injected) {
|
|
534
|
+
const results = arr(output.Entries).map(obj);
|
|
535
|
+
const indexes = results.flatMap((result, index) => (injected.has(String(index)) && isValidationCode(result.ErrorCode) ? [index] : []));
|
|
536
|
+
if (indexes.length === 0)
|
|
537
|
+
return null;
|
|
538
|
+
return {
|
|
539
|
+
args: { ...args, input: { ...input, Entries: indexes.map((index) => entries[index]) } },
|
|
540
|
+
// Resultados por posição: troca só as posições reenviadas.
|
|
541
|
+
merge(first, retried) {
|
|
542
|
+
const merged = arr(first.Entries).slice();
|
|
543
|
+
const again = arr(retried.Entries);
|
|
544
|
+
indexes.forEach((index, position) => {
|
|
545
|
+
if (again[position] !== undefined)
|
|
546
|
+
merged[index] = again[position];
|
|
547
|
+
});
|
|
548
|
+
return { ...first, Entries: merged, FailedEntryCount: merged.filter((item) => obj(item).ErrorCode).length };
|
|
549
|
+
},
|
|
550
|
+
};
|
|
551
|
+
},
|
|
552
|
+
};
|
|
553
|
+
}
|
|
363
554
|
/**
|
|
364
555
|
* Adiciona o middleware `fgTrackerInfra` (step `initialize`) ao client.
|
|
365
556
|
* - Agregados de infra (§27.4) para SQS, SNS, EventBridge, Kinesis, DynamoDB e Step Functions — nunca amostrados.
|
|
366
557
|
* - Span `http.client` por chamada (qualquer serviço) quando há um trace em andamento; respeita amostragem.
|
|
367
558
|
* - DynamoDB: injeta `ReturnConsumedCapacity: 'TOTAL'` quando não informado.
|
|
559
|
+
* - SQS/EventBridge (§16.1): span `custom` `SQS publish <fila>`/`EventBridge publish <barramento>` com o
|
|
560
|
+
* contexto propagado nas mensagens; `ReceiveMessage` pede o contexto e o guarda para o `trackMessage`.
|
|
368
561
|
*/
|
|
369
|
-
export function instrumentAwsClient(core, client) {
|
|
562
|
+
export function instrumentAwsClient(core, client, options = {}) {
|
|
370
563
|
const target = client;
|
|
371
564
|
if (!core.enabled || target[PATCHED])
|
|
372
565
|
return client;
|
|
@@ -375,6 +568,12 @@ export function instrumentAwsClient(core, client) {
|
|
|
375
568
|
return client;
|
|
376
569
|
}
|
|
377
570
|
target[PATCHED] = true;
|
|
571
|
+
const propagation = options.propagation ?? core.options.propagateTraceTo !== false;
|
|
572
|
+
const sqsLimits = {
|
|
573
|
+
messageBytes: Math.min(SQS_MAX_MESSAGE_BYTES, Math.max(SQS_MIN_MESSAGE_BYTES, options.sqsMaxMessageBytes ?? SQS_MAX_MESSAGE_BYTES)),
|
|
574
|
+
declared: options.sqsMaxMessageBytes !== undefined,
|
|
575
|
+
};
|
|
576
|
+
const eventBridgeLimits = { requestBytes: Math.min(EVENTBRIDGE_MAX_REQUEST_BYTES, Math.max(1024, options.eventBridgeMaxRequestBytes ?? EVENTBRIDGE_MAX_REQUEST_BYTES)) };
|
|
378
577
|
let regionPromise = null;
|
|
379
578
|
let endpointPromise = null;
|
|
380
579
|
const middleware = (next, context) => async (args) => {
|
|
@@ -401,22 +600,6 @@ export function instrumentAwsClient(core, client) {
|
|
|
401
600
|
catch {
|
|
402
601
|
info = {};
|
|
403
602
|
}
|
|
404
|
-
const parent = core.currentContext();
|
|
405
|
-
const resource = info.primary;
|
|
406
|
-
const span = parent
|
|
407
|
-
? core.startSpan('http.client', `AWS ${label} ${operation}${resource?.resourceName ? ` ${resource.resourceName}` : ''}`, {
|
|
408
|
-
'aws.service': label,
|
|
409
|
-
'aws.operation': operation,
|
|
410
|
-
...(resource?.resourceArn ? { 'aws.resourceArn': resource.resourceArn } : {}),
|
|
411
|
-
...(resource?.resourceName ? { 'aws.resourceName': resource.resourceName } : {}),
|
|
412
|
-
...(region ? { 'aws.region': region } : {}),
|
|
413
|
-
...(endpointHost ? { 'server.address': endpointHost, host: endpointHost } : {}),
|
|
414
|
-
})
|
|
415
|
-
: null;
|
|
416
|
-
if (span) {
|
|
417
|
-
span.method = 'POST';
|
|
418
|
-
span.route = `/${label}/${operation}`;
|
|
419
|
-
}
|
|
420
603
|
const safeRecord = (output, error) => {
|
|
421
604
|
try {
|
|
422
605
|
info.record?.(output, error);
|
|
@@ -425,24 +608,189 @@ export function instrumentAwsClient(core, client) {
|
|
|
425
608
|
/* agregação nunca quebra a chamada */
|
|
426
609
|
}
|
|
427
610
|
};
|
|
611
|
+
/** A chamada em si, com o span `http.client` quando há trace. Não registra agregados. */
|
|
612
|
+
/**
|
|
613
|
+
* `injected`: tentativa com o contexto que a instrumentação acrescentou. Se o serviço a recusa
|
|
614
|
+
* por validação, a culpa é da injeção (a chamada é repetida sem ela): o span registra a recusa
|
|
615
|
+
* sem `error`, para não virar issue da aplicação.
|
|
616
|
+
*/
|
|
617
|
+
const call = async (finalArgs, decorate, injected = false) => {
|
|
618
|
+
const parent = core.currentContext();
|
|
619
|
+
const resource = info.primary;
|
|
620
|
+
const span = parent
|
|
621
|
+
? core.startSpan('http.client', `AWS ${label} ${operation}${resource?.resourceName ? ` ${resource.resourceName}` : ''}`, {
|
|
622
|
+
'aws.service': label,
|
|
623
|
+
'aws.operation': operation,
|
|
624
|
+
...(resource?.resourceArn ? { 'aws.resourceArn': resource.resourceArn } : {}),
|
|
625
|
+
...(resource?.resourceName ? { 'aws.resourceName': resource.resourceName } : {}),
|
|
626
|
+
...(region ? { 'aws.region': region } : {}),
|
|
627
|
+
...(endpointHost ? { 'server.address': endpointHost, host: endpointHost } : {}),
|
|
628
|
+
})
|
|
629
|
+
: null;
|
|
630
|
+
if (span) {
|
|
631
|
+
span.method = 'POST';
|
|
632
|
+
span.route = `/${label}/${operation}`;
|
|
633
|
+
}
|
|
634
|
+
try {
|
|
635
|
+
const result = span ? await core.runInSpan(span, () => next(finalArgs)) : await next(finalArgs);
|
|
636
|
+
const output = obj(result?.output);
|
|
637
|
+
if (span) {
|
|
638
|
+
span.statusCode = httpStatus(output) ?? 200;
|
|
639
|
+
try {
|
|
640
|
+
decorate?.(span, output);
|
|
641
|
+
}
|
|
642
|
+
catch {
|
|
643
|
+
/* atributos são best-effort */
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
return result;
|
|
647
|
+
}
|
|
648
|
+
catch (err) {
|
|
649
|
+
if (span) {
|
|
650
|
+
span.statusCode = httpStatus(err);
|
|
651
|
+
if (injected && isValidationError(err)) {
|
|
652
|
+
span.setAttribute('messaging.propagation.rejected', true);
|
|
653
|
+
const name = err?.name;
|
|
654
|
+
if (typeof name === 'string')
|
|
655
|
+
span.setAttribute('error.type', name);
|
|
656
|
+
}
|
|
657
|
+
else {
|
|
658
|
+
span.setError(err);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
throw err;
|
|
662
|
+
}
|
|
663
|
+
finally {
|
|
664
|
+
span?.end();
|
|
665
|
+
}
|
|
666
|
+
};
|
|
667
|
+
// ReceiveMessage: pede o contexto, guarda-o por mensagem e devolve a resposta como o chamador pediu.
|
|
668
|
+
if (propagation && service === 'sqs' && operation === 'ReceiveMessage') {
|
|
669
|
+
let plan = null;
|
|
670
|
+
try {
|
|
671
|
+
plan = planReceive(obj(callArgs.input));
|
|
672
|
+
}
|
|
673
|
+
catch {
|
|
674
|
+
plan = null;
|
|
675
|
+
}
|
|
676
|
+
if (plan) {
|
|
677
|
+
const store = receivedMessages(core);
|
|
678
|
+
try {
|
|
679
|
+
// Os produtores das mensagens recebidas viram links do span da chamada (antes de ele fechar).
|
|
680
|
+
const result = await call({ ...callArgs, input: plan.input }, (span, output) => {
|
|
681
|
+
const contexts = arr(output.Messages).map((message) => metaContext(readSqsMessage(message)));
|
|
682
|
+
const { links, dropped } = uniqueLinks(contexts);
|
|
683
|
+
if (links.length > 0)
|
|
684
|
+
span.setAttribute('links', links);
|
|
685
|
+
if (dropped > 0)
|
|
686
|
+
span.setAttribute('links.dropped', dropped);
|
|
687
|
+
});
|
|
688
|
+
const output = obj(result?.output);
|
|
689
|
+
let finalResult = result;
|
|
690
|
+
if (Array.isArray(output.Messages)) {
|
|
691
|
+
const messages = output.Messages.map((message) => {
|
|
692
|
+
const record = obj(message);
|
|
693
|
+
try {
|
|
694
|
+
store.remember(record, readSqsMessage(record));
|
|
695
|
+
return stripReceivedMessage(record, plan);
|
|
696
|
+
}
|
|
697
|
+
catch {
|
|
698
|
+
return message;
|
|
699
|
+
}
|
|
700
|
+
});
|
|
701
|
+
finalResult = { ...result, output: { ...output, Messages: messages } };
|
|
702
|
+
}
|
|
703
|
+
safeRecord(obj(finalResult.output), null);
|
|
704
|
+
return finalResult;
|
|
705
|
+
}
|
|
706
|
+
catch (err) {
|
|
707
|
+
safeRecord(null, err);
|
|
708
|
+
throw err;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
let publish = null;
|
|
713
|
+
if (propagation && core.currentContext()) {
|
|
714
|
+
try {
|
|
715
|
+
if (service === 'sqs')
|
|
716
|
+
publish = sqsPublishPlan(callArgs, operation, info.primary, sqsLimits);
|
|
717
|
+
else if (service === 'eventbridge' && operation === 'PutEvents')
|
|
718
|
+
publish = eventBridgePublishPlan(callArgs, eventBridgeLimits);
|
|
719
|
+
}
|
|
720
|
+
catch {
|
|
721
|
+
publish = null;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
if (!publish) {
|
|
725
|
+
try {
|
|
726
|
+
const result = await call(callArgs);
|
|
727
|
+
safeRecord(obj(result?.output), null);
|
|
728
|
+
return result;
|
|
729
|
+
}
|
|
730
|
+
catch (err) {
|
|
731
|
+
safeRecord(null, err);
|
|
732
|
+
throw err;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
const plan = publish;
|
|
736
|
+
const publishSpan = core.startSpan('custom', plan.name, { ...plan.attributes, ...(region ? { 'aws.region': region } : {}) });
|
|
428
737
|
try {
|
|
429
|
-
const result =
|
|
738
|
+
const result = await core.runInSpan(publishSpan, async () => {
|
|
739
|
+
let injection;
|
|
740
|
+
try {
|
|
741
|
+
injection = plan.inject(publishSpan.context());
|
|
742
|
+
}
|
|
743
|
+
catch {
|
|
744
|
+
injection = { args: callArgs, injected: new Set(), skipped: [] };
|
|
745
|
+
}
|
|
746
|
+
publishSpan.setAttribute('messaging.propagation.injected', injection.injected.size);
|
|
747
|
+
if (injection.skipped.length > 0) {
|
|
748
|
+
publishSpan.setAttribute('messaging.propagation.skipped', injection.skipped.length);
|
|
749
|
+
publishSpan.setAttribute('messaging.propagation.skip_reasons', [...new Set(injection.skipped)]);
|
|
750
|
+
}
|
|
751
|
+
let res;
|
|
752
|
+
try {
|
|
753
|
+
res = await call(injection.args, undefined, injection.injected.size > 0);
|
|
754
|
+
}
|
|
755
|
+
catch (err) {
|
|
756
|
+
// Recusa de validação com o contexto: nada foi publicado; repete como o usuário montou.
|
|
757
|
+
if (injection.injected.size === 0 || !isValidationError(err))
|
|
758
|
+
throw err;
|
|
759
|
+
publishSpan.setAttribute('messaging.propagation.fallback', 'request');
|
|
760
|
+
const plain = await call(callArgs);
|
|
761
|
+
return plain;
|
|
762
|
+
}
|
|
763
|
+
let output = obj(res?.output);
|
|
764
|
+
try {
|
|
765
|
+
const retry = plan.retryEntries?.(output, injection.injected);
|
|
766
|
+
if (retry) {
|
|
767
|
+
publishSpan.setAttribute('messaging.propagation.fallback', 'entries');
|
|
768
|
+
// Falha no reenvio: fica a resposta original, com as entradas recusadas.
|
|
769
|
+
const again = await call(retry.args).then((value) => obj(value?.output), () => null);
|
|
770
|
+
if (again)
|
|
771
|
+
output = retry.merge(output, again);
|
|
772
|
+
}
|
|
773
|
+
output = plan.restore(output, injection.injected);
|
|
774
|
+
}
|
|
775
|
+
catch {
|
|
776
|
+
/* a resposta original continua valendo */
|
|
777
|
+
}
|
|
778
|
+
return { ...res, output };
|
|
779
|
+
});
|
|
430
780
|
const output = obj(result?.output);
|
|
431
781
|
safeRecord(output, null);
|
|
432
|
-
|
|
433
|
-
|
|
782
|
+
const messageId = str(output.MessageId);
|
|
783
|
+
if (messageId)
|
|
784
|
+
publishSpan.setAttribute('messaging.message.id', messageId);
|
|
434
785
|
return result;
|
|
435
786
|
}
|
|
436
787
|
catch (err) {
|
|
437
788
|
safeRecord(null, err);
|
|
438
|
-
|
|
439
|
-
span.statusCode = httpStatus(err);
|
|
440
|
-
span.setError(err);
|
|
441
|
-
}
|
|
789
|
+
publishSpan.setError(err);
|
|
442
790
|
throw err;
|
|
443
791
|
}
|
|
444
792
|
finally {
|
|
445
|
-
|
|
793
|
+
publishSpan.end();
|
|
446
794
|
}
|
|
447
795
|
};
|
|
448
796
|
client.middlewareStack.add(middleware, { step: 'initialize', name: 'fgTrackerInfra', override: true, tags: ['FG_TRACKER'] });
|
|
@@ -450,18 +798,60 @@ export function instrumentAwsClient(core, client) {
|
|
|
450
798
|
}
|
|
451
799
|
/**
|
|
452
800
|
* Mede o processamento de uma mensagem consumida (`processingMsSum`, `processingMsMax`, `errors`)
|
|
453
|
-
* e cria um span `custom` para os spans internos (AWS, db, fetch) formarem um trace.
|
|
801
|
+
* e cria um span `custom` `SQS process <fila>` para os spans internos (AWS, db, fetch) formarem um trace.
|
|
802
|
+
*
|
|
803
|
+
* Com `record`, o span continua o trace do produtor: raiz local com `remoteParentSpanId` (ou filho
|
|
804
|
+
* do span atual quando ele já está no mesmo trace, como no `wrapLambda` de uma mensagem só). Cada
|
|
805
|
+
* chamada tem o próprio contexto, inclusive em `Promise.all`; o contexto da invocação vira link.
|
|
454
806
|
*/
|
|
455
|
-
export async function trackQueueMessage(core, queue, fn) {
|
|
807
|
+
export async function trackQueueMessage(core, queue, fn, record) {
|
|
456
808
|
if (!core.enabled)
|
|
457
809
|
return fn();
|
|
458
810
|
const ref = sqsQueueRef(queue, process.env.AWS_REGION || process.env.AWS_DEFAULT_REGION || undefined);
|
|
459
811
|
core.infra?.learnAccount(ref.resourceArn);
|
|
460
|
-
|
|
812
|
+
let meta;
|
|
813
|
+
if (record) {
|
|
814
|
+
try {
|
|
815
|
+
const own = readSqsMessage(record);
|
|
816
|
+
const stashed = receivedMessages(core).lookup(record);
|
|
817
|
+
meta = {
|
|
818
|
+
traceparent: own.traceparent ?? stashed?.traceparent,
|
|
819
|
+
tracestate: own.tracestate ?? stashed?.tracestate,
|
|
820
|
+
sentTimestamp: own.sentTimestamp ?? stashed?.sentTimestamp,
|
|
821
|
+
receiveCount: own.receiveCount ?? stashed?.receiveCount,
|
|
822
|
+
messageId: own.messageId ?? stashed?.messageId,
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
catch {
|
|
826
|
+
meta = undefined;
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
const attributes = {
|
|
461
830
|
'messaging.system': 'aws_sqs',
|
|
831
|
+
'messaging.operation.type': 'process',
|
|
832
|
+
'messaging.destination.name': ref.resourceName ?? queue,
|
|
462
833
|
...(ref.resourceArn ? { 'aws.resourceArn': ref.resourceArn } : {}),
|
|
463
834
|
...(ref.region ? { 'aws.region': ref.region } : {}),
|
|
464
|
-
}
|
|
835
|
+
};
|
|
836
|
+
if (meta?.messageId)
|
|
837
|
+
attributes['messaging.message.id'] = meta.messageId;
|
|
838
|
+
if (meta?.sentTimestamp !== undefined)
|
|
839
|
+
attributes['messaging.message.receive.latency'] = Math.max(0, Date.now() - meta.sentTimestamp);
|
|
840
|
+
if (meta?.receiveCount !== undefined)
|
|
841
|
+
attributes['messaging.message.retry.count'] = Math.max(0, meta.receiveCount - 1);
|
|
842
|
+
const remote = metaContext(meta);
|
|
843
|
+
const current = core.currentContext();
|
|
844
|
+
const name = `SQS process ${ref.resourceName ?? queue}`;
|
|
845
|
+
let span;
|
|
846
|
+
if (remote && current?.traceId !== remote.traceId) {
|
|
847
|
+
// Lote de mensagens de produtores diferentes: a invocação fica no próprio trace, como link.
|
|
848
|
+
if (current)
|
|
849
|
+
attributes.links = [{ traceId: current.traceId, spanId: current.spanId }];
|
|
850
|
+
span = core.startRemoteRoot('custom', name, attributes, remote);
|
|
851
|
+
}
|
|
852
|
+
else {
|
|
853
|
+
span = core.startSpan('custom', name, attributes);
|
|
854
|
+
}
|
|
465
855
|
const started = performance.now();
|
|
466
856
|
let failed = false;
|
|
467
857
|
try {
|