fg-tracker-sdk 0.10.0 → 0.13.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 +30 -0
- package/README.md +129 -35
- 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 +47 -10
- package/dist/fetch.js.map +1 -1
- package/dist/genai/bedrock.d.ts +22 -0
- package/dist/genai/bedrock.d.ts.map +1 -0
- package/dist/genai/bedrock.js +161 -0
- package/dist/genai/bedrock.js.map +1 -0
- package/dist/genai/call.d.ts +96 -0
- package/dist/genai/call.d.ts.map +1 -0
- package/dist/genai/call.js +260 -0
- package/dist/genai/call.js.map +1 -0
- package/dist/genai/config.d.ts +71 -0
- package/dist/genai/config.d.ts.map +1 -0
- package/dist/genai/config.js +110 -0
- package/dist/genai/config.js.map +1 -0
- package/dist/genai/http.d.ts +49 -0
- package/dist/genai/http.d.ts.map +1 -0
- package/dist/genai/http.js +795 -0
- package/dist/genai/http.js.map +1 -0
- package/dist/genai/semconv.d.ts +64 -0
- package/dist/genai/semconv.d.ts.map +1 -0
- package/dist/genai/semconv.js +52 -0
- package/dist/genai/semconv.js.map +1 -0
- package/dist/genai/sse.d.ts +26 -0
- package/dist/genai/sse.d.ts.map +1 -0
- package/dist/genai/sse.js +100 -0
- package/dist/genai/sse.js.map +1 -0
- package/dist/index.d.ts +7 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -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 +455 -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 +17 -1
- package/dist/tracker.d.ts.map +1 -1
- package/dist/tracker.js +50 -5
- package/dist/tracker.js.map +1 -1
- package/dist/types.d.ts +26 -4
- package/dist/types.d.ts.map +1 -1
- package/package.json +7 -1
package/dist/infra/aws.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { performance } from 'node:perf_hooks';
|
|
2
|
+
import { isBedrockRuntime, startBedrockCall } from '../genai/bedrock.js';
|
|
3
|
+
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
4
|
const PATCHED = Symbol.for('fg-tracker.aws.patched');
|
|
2
5
|
const SERVICE_IDS = {
|
|
3
6
|
sqs: 'sqs',
|
|
@@ -360,13 +363,205 @@ function httpStatus(value) {
|
|
|
360
363
|
const status = obj(obj(value).$metadata).httpStatusCode;
|
|
361
364
|
return typeof status === 'number' ? status : null;
|
|
362
365
|
}
|
|
366
|
+
const received = new WeakMap();
|
|
367
|
+
/** Mensagens recebidas por clients instrumentados deste tracker (contexto lido antes de retirar os atributos). */
|
|
368
|
+
export function receivedMessages(core) {
|
|
369
|
+
let store = received.get(core);
|
|
370
|
+
if (!store) {
|
|
371
|
+
store = new ReceivedMessages();
|
|
372
|
+
received.set(core, store);
|
|
373
|
+
}
|
|
374
|
+
return store;
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Fila FIFO pelo nome (o sufixo `.fifo` é do QueueName): último segmento do caminho da URL (com ou
|
|
378
|
+
* sem barra final, query ou fragmento) ou do ARN. Na dúvida (URL que não dá para ler), o nome cru.
|
|
379
|
+
*/
|
|
380
|
+
export function isFifoQueue(queue) {
|
|
381
|
+
let name = queue;
|
|
382
|
+
if (queue.startsWith('arn:'))
|
|
383
|
+
name = queue.split(':').pop() ?? queue;
|
|
384
|
+
else {
|
|
385
|
+
try {
|
|
386
|
+
name = new URL(queue).pathname;
|
|
387
|
+
}
|
|
388
|
+
catch {
|
|
389
|
+
name = queue.split(/[?#]/)[0] ?? queue;
|
|
390
|
+
}
|
|
391
|
+
name = name.split('/').filter(Boolean).pop() ?? name;
|
|
392
|
+
}
|
|
393
|
+
return name.endsWith('.fifo');
|
|
394
|
+
}
|
|
395
|
+
function sqsPublishPlan(args, operation, ref, configured) {
|
|
396
|
+
const input = obj(args.input);
|
|
397
|
+
const queueName = ref?.resourceName ?? str(input.QueueUrl) ?? 'queue';
|
|
398
|
+
// FIFO: reenviar uma entrada recusada depois das aceitas mudaria a ordem do grupo. Sem o limite
|
|
399
|
+
// da fila declarado, o contexto só entra onde nenhuma fila o recusa (o mínimo do SQS, 1 KiB),
|
|
400
|
+
// e entradas de lote recusadas não são reenviadas fora de ordem.
|
|
401
|
+
const fifo = isFifoQueue(str(input.QueueUrl) ?? queueName);
|
|
402
|
+
const limits = { messageBytes: fifo && !configured.declared ? SQS_MIN_MESSAGE_BYTES : configured.messageBytes };
|
|
403
|
+
const base = {
|
|
404
|
+
'messaging.system': 'aws_sqs',
|
|
405
|
+
'messaging.operation.type': 'send',
|
|
406
|
+
'messaging.operation.name': operation,
|
|
407
|
+
'messaging.destination.name': queueName,
|
|
408
|
+
...(ref?.resourceArn ? { 'aws.resourceArn': ref.resourceArn } : {}),
|
|
409
|
+
};
|
|
410
|
+
if (operation === 'SendMessage') {
|
|
411
|
+
return {
|
|
412
|
+
name: `SQS publish ${queueName}`,
|
|
413
|
+
attributes: base,
|
|
414
|
+
inject(ctx) {
|
|
415
|
+
const result = injectSqsContext(input, ctx, { messageBytes: limits.messageBytes, budgetBytes: Number.POSITIVE_INFINITY });
|
|
416
|
+
return {
|
|
417
|
+
args: result.injected ? { ...args, input: result.entry } : args,
|
|
418
|
+
injected: new Set(result.injected ? ['message'] : []),
|
|
419
|
+
skipped: result.skipped && result.skipped !== 'present' ? [result.skipped] : [],
|
|
420
|
+
};
|
|
421
|
+
},
|
|
422
|
+
restore(output, injected) {
|
|
423
|
+
if (!injected.has('message'))
|
|
424
|
+
return output;
|
|
425
|
+
const md5 = sqsAttributesMd5(input.MessageAttributes);
|
|
426
|
+
const out = { ...output };
|
|
427
|
+
if (md5)
|
|
428
|
+
out.MD5OfMessageAttributes = md5;
|
|
429
|
+
else
|
|
430
|
+
delete out.MD5OfMessageAttributes;
|
|
431
|
+
return out;
|
|
432
|
+
},
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
if (operation === 'SendMessageBatch') {
|
|
436
|
+
const entries = arr(input.Entries).map(obj);
|
|
437
|
+
const originals = new Map(entries.map((entry) => [String(entry.Id), entry]));
|
|
438
|
+
return {
|
|
439
|
+
name: `SQS publish ${queueName}`,
|
|
440
|
+
attributes: { ...base, 'messaging.batch.message_count': entries.length },
|
|
441
|
+
inject(ctx) {
|
|
442
|
+
// Orçamento agregado: a soma das entradas (atributos incluídos) também tem teto.
|
|
443
|
+
let remaining = SQS_MAX_BATCH_BYTES - entries.reduce((sum, entry) => sum + sqsMessageBytes(entry.MessageBody, entry.MessageAttributes), 0);
|
|
444
|
+
const injected = new Set();
|
|
445
|
+
const skipped = [];
|
|
446
|
+
const next = entries.map((entry) => {
|
|
447
|
+
const result = injectSqsContext(entry, ctx, { messageBytes: limits.messageBytes, budgetBytes: Math.max(0, remaining) });
|
|
448
|
+
if (result.injected) {
|
|
449
|
+
remaining -= result.addedBytes;
|
|
450
|
+
injected.add(String(entry.Id));
|
|
451
|
+
}
|
|
452
|
+
else if (result.skipped && result.skipped !== 'present')
|
|
453
|
+
skipped.push(result.skipped);
|
|
454
|
+
return result.entry;
|
|
455
|
+
});
|
|
456
|
+
return { args: injected.size > 0 ? { ...args, input: { ...input, Entries: next } } : args, injected, skipped };
|
|
457
|
+
},
|
|
458
|
+
restore(output, injected) {
|
|
459
|
+
if (injected.size === 0 || !Array.isArray(output.Successful))
|
|
460
|
+
return output;
|
|
461
|
+
return {
|
|
462
|
+
...output,
|
|
463
|
+
Successful: output.Successful.map((item) => {
|
|
464
|
+
const ok = obj(item);
|
|
465
|
+
const id = String(ok.Id);
|
|
466
|
+
if (!injected.has(id))
|
|
467
|
+
return item;
|
|
468
|
+
const md5 = sqsAttributesMd5(originals.get(id)?.MessageAttributes);
|
|
469
|
+
const out = { ...ok };
|
|
470
|
+
if (md5)
|
|
471
|
+
out.MD5OfMessageAttributes = md5;
|
|
472
|
+
else
|
|
473
|
+
delete out.MD5OfMessageAttributes;
|
|
474
|
+
return out;
|
|
475
|
+
}),
|
|
476
|
+
};
|
|
477
|
+
},
|
|
478
|
+
retryEntries(output, injected) {
|
|
479
|
+
if (fifo)
|
|
480
|
+
return null;
|
|
481
|
+
const failed = arr(output.Failed).map(obj).filter((item) => injected.has(String(item.Id)) && item.SenderFault === true && isValidationCode(item.Code));
|
|
482
|
+
if (failed.length === 0)
|
|
483
|
+
return null;
|
|
484
|
+
const retry = failed.map((item) => originals.get(String(item.Id))).filter(Boolean);
|
|
485
|
+
return {
|
|
486
|
+
args: { ...args, input: { ...input, Entries: retry } },
|
|
487
|
+
merge(first, retried) {
|
|
488
|
+
const retriedIds = new Set([...arr(retried.Successful), ...arr(retried.Failed)].map((item) => String(obj(item).Id)));
|
|
489
|
+
return {
|
|
490
|
+
...first,
|
|
491
|
+
Successful: [...arr(first.Successful), ...arr(retried.Successful)],
|
|
492
|
+
Failed: [...arr(first.Failed).filter((item) => !retriedIds.has(String(obj(item).Id))), ...arr(retried.Failed)],
|
|
493
|
+
};
|
|
494
|
+
},
|
|
495
|
+
};
|
|
496
|
+
},
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
return null;
|
|
500
|
+
}
|
|
501
|
+
function eventBridgePublishPlan(args, limits) {
|
|
502
|
+
const input = obj(args.input);
|
|
503
|
+
const entries = arr(input.Entries).map(obj);
|
|
504
|
+
if (entries.length === 0)
|
|
505
|
+
return null;
|
|
506
|
+
const bus = str(entries[0].EventBusName) ?? 'default';
|
|
507
|
+
const busName = bus.startsWith('arn:') ? bus.split('/').pop() || bus : bus;
|
|
508
|
+
return {
|
|
509
|
+
name: `EventBridge publish ${busName}`,
|
|
510
|
+
attributes: {
|
|
511
|
+
'messaging.system': 'aws_eventbridge',
|
|
512
|
+
'messaging.operation.type': 'send',
|
|
513
|
+
'messaging.operation.name': 'PutEvents',
|
|
514
|
+
'messaging.destination.name': busName,
|
|
515
|
+
'messaging.batch.message_count': entries.length,
|
|
516
|
+
},
|
|
517
|
+
inject(ctx) {
|
|
518
|
+
// "Menor que" o limite: sobra pelo menos 1 byte.
|
|
519
|
+
let remaining = limits.requestBytes - 1 - entries.reduce((sum, entry) => sum + eventBridgeEntryBytes(entry), 0);
|
|
520
|
+
const injected = new Set();
|
|
521
|
+
const skipped = [];
|
|
522
|
+
const next = entries.map((entry, index) => {
|
|
523
|
+
const result = injectEventBridgeContext(entry, ctx, Math.max(0, remaining));
|
|
524
|
+
if (result.injected) {
|
|
525
|
+
remaining -= result.addedBytes;
|
|
526
|
+
injected.add(String(index));
|
|
527
|
+
}
|
|
528
|
+
else if (result.skipped && result.skipped !== 'present')
|
|
529
|
+
skipped.push(result.skipped);
|
|
530
|
+
return result.entry;
|
|
531
|
+
});
|
|
532
|
+
return { args: injected.size > 0 ? { ...args, input: { ...input, Entries: next } } : args, injected, skipped };
|
|
533
|
+
},
|
|
534
|
+
restore: (output) => output,
|
|
535
|
+
retryEntries(output, injected) {
|
|
536
|
+
const results = arr(output.Entries).map(obj);
|
|
537
|
+
const indexes = results.flatMap((result, index) => (injected.has(String(index)) && isValidationCode(result.ErrorCode) ? [index] : []));
|
|
538
|
+
if (indexes.length === 0)
|
|
539
|
+
return null;
|
|
540
|
+
return {
|
|
541
|
+
args: { ...args, input: { ...input, Entries: indexes.map((index) => entries[index]) } },
|
|
542
|
+
// Resultados por posição: troca só as posições reenviadas.
|
|
543
|
+
merge(first, retried) {
|
|
544
|
+
const merged = arr(first.Entries).slice();
|
|
545
|
+
const again = arr(retried.Entries);
|
|
546
|
+
indexes.forEach((index, position) => {
|
|
547
|
+
if (again[position] !== undefined)
|
|
548
|
+
merged[index] = again[position];
|
|
549
|
+
});
|
|
550
|
+
return { ...first, Entries: merged, FailedEntryCount: merged.filter((item) => obj(item).ErrorCode).length };
|
|
551
|
+
},
|
|
552
|
+
};
|
|
553
|
+
},
|
|
554
|
+
};
|
|
555
|
+
}
|
|
363
556
|
/**
|
|
364
557
|
* Adiciona o middleware `fgTrackerInfra` (step `initialize`) ao client.
|
|
365
558
|
* - Agregados de infra (§27.4) para SQS, SNS, EventBridge, Kinesis, DynamoDB e Step Functions — nunca amostrados.
|
|
366
559
|
* - Span `http.client` por chamada (qualquer serviço) quando há um trace em andamento; respeita amostragem.
|
|
367
560
|
* - DynamoDB: injeta `ReturnConsumedCapacity: 'TOTAL'` quando não informado.
|
|
561
|
+
* - SQS/EventBridge (§16.1): span `custom` `SQS publish <fila>`/`EventBridge publish <barramento>` com o
|
|
562
|
+
* contexto propagado nas mensagens; `ReceiveMessage` pede o contexto e o guarda para o `trackMessage`.
|
|
368
563
|
*/
|
|
369
|
-
export function instrumentAwsClient(core, client) {
|
|
564
|
+
export function instrumentAwsClient(core, client, options = {}) {
|
|
370
565
|
const target = client;
|
|
371
566
|
if (!core.enabled || target[PATCHED])
|
|
372
567
|
return client;
|
|
@@ -375,12 +570,19 @@ export function instrumentAwsClient(core, client) {
|
|
|
375
570
|
return client;
|
|
376
571
|
}
|
|
377
572
|
target[PATCHED] = true;
|
|
573
|
+
const propagation = options.propagation ?? core.options.propagateTraceTo !== false;
|
|
574
|
+
const sqsLimits = {
|
|
575
|
+
messageBytes: Math.min(SQS_MAX_MESSAGE_BYTES, Math.max(SQS_MIN_MESSAGE_BYTES, options.sqsMaxMessageBytes ?? SQS_MAX_MESSAGE_BYTES)),
|
|
576
|
+
declared: options.sqsMaxMessageBytes !== undefined,
|
|
577
|
+
};
|
|
578
|
+
const eventBridgeLimits = { requestBytes: Math.min(EVENTBRIDGE_MAX_REQUEST_BYTES, Math.max(1024, options.eventBridgeMaxRequestBytes ?? EVENTBRIDGE_MAX_REQUEST_BYTES)) };
|
|
378
579
|
let regionPromise = null;
|
|
379
580
|
let endpointPromise = null;
|
|
380
581
|
const middleware = (next, context) => async (args) => {
|
|
381
582
|
if (!core.enabled)
|
|
382
583
|
return next(args);
|
|
383
584
|
const { service, label } = detectService(client, context);
|
|
585
|
+
const bedrock = isBedrockRuntime(label);
|
|
384
586
|
const operation = (context?.commandName ?? 'Unknown').replace(/Command$/, '');
|
|
385
587
|
let info = {};
|
|
386
588
|
let region;
|
|
@@ -401,22 +603,6 @@ export function instrumentAwsClient(core, client) {
|
|
|
401
603
|
catch {
|
|
402
604
|
info = {};
|
|
403
605
|
}
|
|
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
606
|
const safeRecord = (output, error) => {
|
|
421
607
|
try {
|
|
422
608
|
info.record?.(output, error);
|
|
@@ -425,24 +611,222 @@ export function instrumentAwsClient(core, client) {
|
|
|
425
611
|
/* agregação nunca quebra a chamada */
|
|
426
612
|
}
|
|
427
613
|
};
|
|
614
|
+
/** A chamada em si, com o span `http.client` quando há trace. Não registra agregados. */
|
|
615
|
+
/**
|
|
616
|
+
* `injected`: tentativa com o contexto que a instrumentação acrescentou. Se o serviço a recusa
|
|
617
|
+
* por validação, a culpa é da injeção (a chamada é repetida sem ela): o span registra a recusa
|
|
618
|
+
* sem `error`, para não virar issue da aplicação.
|
|
619
|
+
*/
|
|
620
|
+
const call = async (finalArgs, decorate, injected = false) => {
|
|
621
|
+
const parent = core.currentContext();
|
|
622
|
+
const resource = info.primary;
|
|
623
|
+
const startedPerf = performance.now();
|
|
624
|
+
const span = parent
|
|
625
|
+
? core.startSpan('http.client', `AWS ${label} ${operation}${resource?.resourceName ? ` ${resource.resourceName}` : ''}`, {
|
|
626
|
+
'aws.service': label,
|
|
627
|
+
'aws.operation': operation,
|
|
628
|
+
...(resource?.resourceArn ? { 'aws.resourceArn': resource.resourceArn } : {}),
|
|
629
|
+
...(resource?.resourceName ? { 'aws.resourceName': resource.resourceName } : {}),
|
|
630
|
+
...(region ? { 'aws.region': region } : {}),
|
|
631
|
+
...(endpointHost ? { 'server.address': endpointHost, host: endpointHost } : {}),
|
|
632
|
+
})
|
|
633
|
+
: null;
|
|
634
|
+
if (span) {
|
|
635
|
+
span.method = 'POST';
|
|
636
|
+
span.route = `/${label}/${operation}`;
|
|
637
|
+
}
|
|
638
|
+
// Bedrock Converse/ConverseStream: atributos GenAI no próprio span; o fim do span passa a ser do observador.
|
|
639
|
+
let genAi = null;
|
|
640
|
+
const genAiConfig = span && bedrock ? core.genAiFor(span) : null;
|
|
641
|
+
if (span && genAiConfig) {
|
|
642
|
+
try {
|
|
643
|
+
genAi = startBedrockCall(genAiConfig, span, operation, finalArgs?.input, startedPerf);
|
|
644
|
+
}
|
|
645
|
+
catch {
|
|
646
|
+
genAi = null;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
let handedOff = false;
|
|
650
|
+
try {
|
|
651
|
+
const result = span ? await core.runInSpan(span, () => next(finalArgs)) : await next(finalArgs);
|
|
652
|
+
const output = obj(result?.output);
|
|
653
|
+
if (span) {
|
|
654
|
+
span.statusCode = httpStatus(output) ?? 200;
|
|
655
|
+
try {
|
|
656
|
+
decorate?.(span, output);
|
|
657
|
+
}
|
|
658
|
+
catch {
|
|
659
|
+
/* atributos são best-effort */
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
if (genAi && span) {
|
|
663
|
+
handedOff = true;
|
|
664
|
+
try {
|
|
665
|
+
genAi.output(result);
|
|
666
|
+
}
|
|
667
|
+
catch {
|
|
668
|
+
span.end();
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
return result;
|
|
672
|
+
}
|
|
673
|
+
catch (err) {
|
|
674
|
+
if (span) {
|
|
675
|
+
span.statusCode = httpStatus(err);
|
|
676
|
+
if (genAi) {
|
|
677
|
+
handedOff = true;
|
|
678
|
+
try {
|
|
679
|
+
genAi.error(err);
|
|
680
|
+
}
|
|
681
|
+
catch {
|
|
682
|
+
span.end();
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
else if (injected && isValidationError(err)) {
|
|
686
|
+
span.setAttribute('messaging.propagation.rejected', true);
|
|
687
|
+
const name = err?.name;
|
|
688
|
+
if (typeof name === 'string')
|
|
689
|
+
span.setAttribute('error.type', name);
|
|
690
|
+
}
|
|
691
|
+
else {
|
|
692
|
+
span.setError(err);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
throw err;
|
|
696
|
+
}
|
|
697
|
+
finally {
|
|
698
|
+
// Com observador GenAI, quem fecha é ele (já fechou, ou fecha no fim do stream).
|
|
699
|
+
if (!handedOff)
|
|
700
|
+
span?.end();
|
|
701
|
+
}
|
|
702
|
+
};
|
|
703
|
+
// ReceiveMessage: pede o contexto, guarda-o por mensagem e devolve a resposta como o chamador pediu.
|
|
704
|
+
if (propagation && service === 'sqs' && operation === 'ReceiveMessage') {
|
|
705
|
+
let plan = null;
|
|
706
|
+
try {
|
|
707
|
+
plan = planReceive(obj(callArgs.input));
|
|
708
|
+
}
|
|
709
|
+
catch {
|
|
710
|
+
plan = null;
|
|
711
|
+
}
|
|
712
|
+
if (plan) {
|
|
713
|
+
const store = receivedMessages(core);
|
|
714
|
+
try {
|
|
715
|
+
// Os produtores das mensagens recebidas viram links do span da chamada (antes de ele fechar).
|
|
716
|
+
const result = await call({ ...callArgs, input: plan.input }, (span, output) => {
|
|
717
|
+
const contexts = arr(output.Messages).map((message) => metaContext(readSqsMessage(message)));
|
|
718
|
+
const { links, dropped } = uniqueLinks(contexts);
|
|
719
|
+
if (links.length > 0)
|
|
720
|
+
span.setAttribute('links', links);
|
|
721
|
+
if (dropped > 0)
|
|
722
|
+
span.setAttribute('links.dropped', dropped);
|
|
723
|
+
});
|
|
724
|
+
const output = obj(result?.output);
|
|
725
|
+
let finalResult = result;
|
|
726
|
+
if (Array.isArray(output.Messages)) {
|
|
727
|
+
const messages = output.Messages.map((message) => {
|
|
728
|
+
const record = obj(message);
|
|
729
|
+
try {
|
|
730
|
+
store.remember(record, readSqsMessage(record));
|
|
731
|
+
return stripReceivedMessage(record, plan);
|
|
732
|
+
}
|
|
733
|
+
catch {
|
|
734
|
+
return message;
|
|
735
|
+
}
|
|
736
|
+
});
|
|
737
|
+
finalResult = { ...result, output: { ...output, Messages: messages } };
|
|
738
|
+
}
|
|
739
|
+
safeRecord(obj(finalResult.output), null);
|
|
740
|
+
return finalResult;
|
|
741
|
+
}
|
|
742
|
+
catch (err) {
|
|
743
|
+
safeRecord(null, err);
|
|
744
|
+
throw err;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
let publish = null;
|
|
749
|
+
if (propagation && core.currentContext()) {
|
|
750
|
+
try {
|
|
751
|
+
if (service === 'sqs')
|
|
752
|
+
publish = sqsPublishPlan(callArgs, operation, info.primary, sqsLimits);
|
|
753
|
+
else if (service === 'eventbridge' && operation === 'PutEvents')
|
|
754
|
+
publish = eventBridgePublishPlan(callArgs, eventBridgeLimits);
|
|
755
|
+
}
|
|
756
|
+
catch {
|
|
757
|
+
publish = null;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
if (!publish) {
|
|
761
|
+
try {
|
|
762
|
+
const result = await call(callArgs);
|
|
763
|
+
safeRecord(obj(result?.output), null);
|
|
764
|
+
return result;
|
|
765
|
+
}
|
|
766
|
+
catch (err) {
|
|
767
|
+
safeRecord(null, err);
|
|
768
|
+
throw err;
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
const plan = publish;
|
|
772
|
+
const publishSpan = core.startSpan('custom', plan.name, { ...plan.attributes, ...(region ? { 'aws.region': region } : {}) });
|
|
428
773
|
try {
|
|
429
|
-
const result =
|
|
774
|
+
const result = await core.runInSpan(publishSpan, async () => {
|
|
775
|
+
let injection;
|
|
776
|
+
try {
|
|
777
|
+
injection = plan.inject(publishSpan.context());
|
|
778
|
+
}
|
|
779
|
+
catch {
|
|
780
|
+
injection = { args: callArgs, injected: new Set(), skipped: [] };
|
|
781
|
+
}
|
|
782
|
+
publishSpan.setAttribute('messaging.propagation.injected', injection.injected.size);
|
|
783
|
+
if (injection.skipped.length > 0) {
|
|
784
|
+
publishSpan.setAttribute('messaging.propagation.skipped', injection.skipped.length);
|
|
785
|
+
publishSpan.setAttribute('messaging.propagation.skip_reasons', [...new Set(injection.skipped)]);
|
|
786
|
+
}
|
|
787
|
+
let res;
|
|
788
|
+
try {
|
|
789
|
+
res = await call(injection.args, undefined, injection.injected.size > 0);
|
|
790
|
+
}
|
|
791
|
+
catch (err) {
|
|
792
|
+
// Recusa de validação com o contexto: nada foi publicado; repete como o usuário montou.
|
|
793
|
+
if (injection.injected.size === 0 || !isValidationError(err))
|
|
794
|
+
throw err;
|
|
795
|
+
publishSpan.setAttribute('messaging.propagation.fallback', 'request');
|
|
796
|
+
const plain = await call(callArgs);
|
|
797
|
+
return plain;
|
|
798
|
+
}
|
|
799
|
+
let output = obj(res?.output);
|
|
800
|
+
try {
|
|
801
|
+
const retry = plan.retryEntries?.(output, injection.injected);
|
|
802
|
+
if (retry) {
|
|
803
|
+
publishSpan.setAttribute('messaging.propagation.fallback', 'entries');
|
|
804
|
+
// Falha no reenvio: fica a resposta original, com as entradas recusadas.
|
|
805
|
+
const again = await call(retry.args).then((value) => obj(value?.output), () => null);
|
|
806
|
+
if (again)
|
|
807
|
+
output = retry.merge(output, again);
|
|
808
|
+
}
|
|
809
|
+
output = plan.restore(output, injection.injected);
|
|
810
|
+
}
|
|
811
|
+
catch {
|
|
812
|
+
/* a resposta original continua valendo */
|
|
813
|
+
}
|
|
814
|
+
return { ...res, output };
|
|
815
|
+
});
|
|
430
816
|
const output = obj(result?.output);
|
|
431
817
|
safeRecord(output, null);
|
|
432
|
-
|
|
433
|
-
|
|
818
|
+
const messageId = str(output.MessageId);
|
|
819
|
+
if (messageId)
|
|
820
|
+
publishSpan.setAttribute('messaging.message.id', messageId);
|
|
434
821
|
return result;
|
|
435
822
|
}
|
|
436
823
|
catch (err) {
|
|
437
824
|
safeRecord(null, err);
|
|
438
|
-
|
|
439
|
-
span.statusCode = httpStatus(err);
|
|
440
|
-
span.setError(err);
|
|
441
|
-
}
|
|
825
|
+
publishSpan.setError(err);
|
|
442
826
|
throw err;
|
|
443
827
|
}
|
|
444
828
|
finally {
|
|
445
|
-
|
|
829
|
+
publishSpan.end();
|
|
446
830
|
}
|
|
447
831
|
};
|
|
448
832
|
client.middlewareStack.add(middleware, { step: 'initialize', name: 'fgTrackerInfra', override: true, tags: ['FG_TRACKER'] });
|
|
@@ -450,18 +834,60 @@ export function instrumentAwsClient(core, client) {
|
|
|
450
834
|
}
|
|
451
835
|
/**
|
|
452
836
|
* 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.
|
|
837
|
+
* e cria um span `custom` `SQS process <fila>` para os spans internos (AWS, db, fetch) formarem um trace.
|
|
838
|
+
*
|
|
839
|
+
* Com `record`, o span continua o trace do produtor: raiz local com `remoteParentSpanId` (ou filho
|
|
840
|
+
* do span atual quando ele já está no mesmo trace, como no `wrapLambda` de uma mensagem só). Cada
|
|
841
|
+
* chamada tem o próprio contexto, inclusive em `Promise.all`; o contexto da invocação vira link.
|
|
454
842
|
*/
|
|
455
|
-
export async function trackQueueMessage(core, queue, fn) {
|
|
843
|
+
export async function trackQueueMessage(core, queue, fn, record) {
|
|
456
844
|
if (!core.enabled)
|
|
457
845
|
return fn();
|
|
458
846
|
const ref = sqsQueueRef(queue, process.env.AWS_REGION || process.env.AWS_DEFAULT_REGION || undefined);
|
|
459
847
|
core.infra?.learnAccount(ref.resourceArn);
|
|
460
|
-
|
|
848
|
+
let meta;
|
|
849
|
+
if (record) {
|
|
850
|
+
try {
|
|
851
|
+
const own = readSqsMessage(record);
|
|
852
|
+
const stashed = receivedMessages(core).lookup(record);
|
|
853
|
+
meta = {
|
|
854
|
+
traceparent: own.traceparent ?? stashed?.traceparent,
|
|
855
|
+
tracestate: own.tracestate ?? stashed?.tracestate,
|
|
856
|
+
sentTimestamp: own.sentTimestamp ?? stashed?.sentTimestamp,
|
|
857
|
+
receiveCount: own.receiveCount ?? stashed?.receiveCount,
|
|
858
|
+
messageId: own.messageId ?? stashed?.messageId,
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
catch {
|
|
862
|
+
meta = undefined;
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
const attributes = {
|
|
461
866
|
'messaging.system': 'aws_sqs',
|
|
867
|
+
'messaging.operation.type': 'process',
|
|
868
|
+
'messaging.destination.name': ref.resourceName ?? queue,
|
|
462
869
|
...(ref.resourceArn ? { 'aws.resourceArn': ref.resourceArn } : {}),
|
|
463
870
|
...(ref.region ? { 'aws.region': ref.region } : {}),
|
|
464
|
-
}
|
|
871
|
+
};
|
|
872
|
+
if (meta?.messageId)
|
|
873
|
+
attributes['messaging.message.id'] = meta.messageId;
|
|
874
|
+
if (meta?.sentTimestamp !== undefined)
|
|
875
|
+
attributes['messaging.message.receive.latency'] = Math.max(0, Date.now() - meta.sentTimestamp);
|
|
876
|
+
if (meta?.receiveCount !== undefined)
|
|
877
|
+
attributes['messaging.message.retry.count'] = Math.max(0, meta.receiveCount - 1);
|
|
878
|
+
const remote = metaContext(meta);
|
|
879
|
+
const current = core.currentContext();
|
|
880
|
+
const name = `SQS process ${ref.resourceName ?? queue}`;
|
|
881
|
+
let span;
|
|
882
|
+
if (remote && current?.traceId !== remote.traceId) {
|
|
883
|
+
// Lote de mensagens de produtores diferentes: a invocação fica no próprio trace, como link.
|
|
884
|
+
if (current)
|
|
885
|
+
attributes.links = [{ traceId: current.traceId, spanId: current.spanId }];
|
|
886
|
+
span = core.startRemoteRoot('custom', name, attributes, remote);
|
|
887
|
+
}
|
|
888
|
+
else {
|
|
889
|
+
span = core.startSpan('custom', name, attributes);
|
|
890
|
+
}
|
|
465
891
|
const started = performance.now();
|
|
466
892
|
let failed = false;
|
|
467
893
|
try {
|