@friggframework/core 2.0.0-next.112 → 2.0.0-next.113

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.
Files changed (64) hide show
  1. package/.eslintrc.json +18 -1
  2. package/CLAUDE.md +51 -12
  3. package/README.md +19 -22
  4. package/core/CLAUDE.md +28 -13
  5. package/core/Worker.js +32 -37
  6. package/core/create-handler.js +97 -197
  7. package/core/index.js +8 -1
  8. package/core/invocation-scope.js +225 -0
  9. package/core/secrets-to-env.js +7 -6
  10. package/database/config.js +0 -7
  11. package/database/encryption/README.md +5 -1
  12. package/database/encryption/encryption-schema-registry.js +3 -0
  13. package/database/encryption/logger.js +5 -2
  14. package/errors/fetch-error.js +48 -65
  15. package/handlers/app-definition-loader.js +38 -2
  16. package/handlers/app-handler-helpers.js +18 -4
  17. package/handlers/backend-utils.js +10 -13
  18. package/handlers/database-migration-handler.js +10 -1
  19. package/handlers/integration-event-dispatcher.js +2 -2
  20. package/handlers/routers/integration-webhook-routers.js +13 -6
  21. package/handlers/routers/websocket.js +27 -7
  22. package/handlers/workers/db-migration.js +12 -1
  23. package/handlers/workers/dlq-processor.js +64 -38
  24. package/index.js +19 -1
  25. package/integrations/integration-base.js +56 -38
  26. package/integrations/integration-router.js +21 -20
  27. package/integrations/use-cases/delete-integration-for-user.js +24 -13
  28. package/jest.config.js +2 -0
  29. package/logs/.eslintrc.json +26 -1
  30. package/logs/__fixtures__/events.js +82 -0
  31. package/logs/__fixtures__/matchers.js +45 -0
  32. package/logs/__fixtures__/secrets.js +59 -0
  33. package/logs/__fixtures__/vectors.js +329 -0
  34. package/logs/__fixtures__/with-env.js +45 -0
  35. package/logs/context.js +109 -0
  36. package/logs/contract-keys.js +67 -0
  37. package/logs/debug-shims.js +46 -0
  38. package/logs/denied-keys.js +21 -0
  39. package/logs/index.js +14 -3
  40. package/logs/jest-logger-setup.js +25 -0
  41. package/logs/levels.js +71 -0
  42. package/logs/logger-runtime.js +137 -0
  43. package/logs/logger.js +134 -42
  44. package/logs/record.js +276 -0
  45. package/logs/redact.js +328 -0
  46. package/logs/serialize.js +329 -0
  47. package/logs/sinks.js +93 -0
  48. package/logs/summarize-event.js +163 -0
  49. package/modules/module.js +32 -19
  50. package/modules/requester/oauth-2.js +28 -22
  51. package/modules/requester/requester.js +69 -6
  52. package/package.json +5 -6
  53. package/queues/queue-delivery.js +41 -0
  54. package/queues/queuer-util.js +1 -15
  55. package/telemetry/bind-telemetry-context.js +4 -0
  56. package/telemetry/instrument-handler.js +19 -2
  57. package/telemetry/no-op-telemetry.js +4 -0
  58. package/telemetry/otel-telemetry.js +25 -6
  59. package/telemetry/telemetry-context.js +19 -8
  60. package/telemetry/telemetry-runtime.js +17 -0
  61. package/telemetry/telemetry-service.js +1 -0
  62. package/types/errors/index.d.ts +24 -10
  63. package/types/logs/index.d.ts +61 -2
  64. package/user/repositories/user-repository-documentdb.js +10 -42
@@ -1,82 +1,65 @@
1
1
  const { BaseError } = require('./base-error');
2
- const { stripIndent } = require('common-tags');
3
-
4
- // TODO hide header values
2
+ const { redactUrl, scrubString } = require('../logs/redact');
5
3
 
6
4
  // Parameters names here are based on fetch. See:
7
5
  // https://developer.mozilla.org/en-US/docs/Web/API/fetch
8
6
 
9
- class FetchError extends BaseError {
10
- response = null;
11
-
12
- constructor(options = {}) {
13
- const { resource, init, response, responseBody } = options;
14
- const method = init?.method ?? 'GET';
15
- let initText = init
16
- ? init.body instanceof URLSearchParams
17
- ? (() => {
18
- init.body = init.body.toString();
19
- return JSON.stringify({ init }, null, 2);
20
- })()
21
- : JSON.stringify({ init }, null, 2)
22
- : '';
23
-
24
- let responseBodyText = '<response body is unavailable>';
25
- if (typeof responseBody === 'string') {
26
- responseBodyText = responseBody;
27
- } else if (responseBody) {
28
- responseBodyText = JSON.stringify(responseBody, null, 2);
29
- }
30
-
31
- const responseHeaders = {};
32
- if (response?.headers) {
33
- for (const [key, value] of response.headers) {
34
- responseHeaders[key] = value;
35
- }
36
- }
37
-
38
- let responseHeaderText = response
39
- ? JSON.stringify({ headers: responseHeaders }, null, 2)
40
- : '';
7
+ function resourceUrl(resource) {
8
+ if (resource === undefined || resource === null) return '';
9
+ if (typeof resource === 'object' && typeof resource.url === 'string') {
10
+ return redactUrl(resource.url);
11
+ }
12
+ return redactUrl(resource);
13
+ }
41
14
 
42
- // sanitize error reporting
43
- if (process.env.STAGE !== 'dev') {
44
- initText = false;
45
- responseHeaderText = false;
46
- responseBodyText = false;
47
- }
15
+ function causeLabel(cause) {
16
+ if (!cause || typeof cause !== 'object') return '';
17
+ const label = cause.code ?? cause.name;
18
+ return label === undefined || label === null ? '' : scrubString(String(label));
19
+ }
48
20
 
49
- const messageParts = [
50
- stripIndent`
51
- -----------------------------------------------------
52
- An error ocurred while fetching an external resource.
53
- -----------------------------------------------------
54
- >>> Request Details >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
55
- ${method} ${resource}
56
- `,
57
- initText,
58
- stripIndent`
59
- <<< Response Details <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
60
- ${response?.status} ${response?.statusText}
61
- `,
62
- responseHeaderText,
63
- responseBodyText,
64
- stripIndent`
65
- -----------------------------------------------------
66
- Stack Trace:
67
- `,
68
- ];
21
+ function buildMessage({ method, url, response, cause }) {
22
+ const outcome = response
23
+ ? response.status === undefined || response.status === null
24
+ ? ''
25
+ : String(response.status)
26
+ : causeLabel(cause);
27
+ return [method, url, outcome].filter(Boolean).join(' ');
28
+ }
69
29
 
70
- super(messageParts.filter(Boolean).join('\n'));
30
+ class FetchError extends BaseError {
31
+ constructor({ resource, init, response, cause, responseBody, body } = {}) {
32
+ const method = String(init?.method ?? 'GET').toUpperCase();
33
+ const url = resourceUrl(resource);
34
+ super(
35
+ buildMessage({ method, url, response, cause }),
36
+ cause ? { cause } : undefined
37
+ );
71
38
 
72
- this.response = response;
73
39
  this.statusCode = response?.status;
40
+ this.method = method;
41
+ this.url = url;
42
+ Object.defineProperty(this, 'response', {
43
+ value: response ?? null,
44
+ enumerable: false,
45
+ writable: true,
46
+ configurable: true,
47
+ });
48
+ Object.defineProperty(this, 'body', {
49
+ value: responseBody ?? body,
50
+ enumerable: false,
51
+ writable: true,
52
+ configurable: true,
53
+ });
74
54
  }
75
55
 
76
56
  static async create(options = {}) {
77
57
  const { response } = options;
78
- let responseBody = response?.bodyUsed ? null : await response?.text();
79
- if (!responseBody && options.body) responseBody = options.body;
58
+ let responseBody =
59
+ response && !response.bodyUsed && typeof response.text === 'function'
60
+ ? await response.text()
61
+ : null;
62
+ if (!responseBody) responseBody = options.responseBody ?? options.body;
80
63
  return new FetchError({ ...options, responseBody });
81
64
  }
82
65
  }
@@ -2,13 +2,38 @@ const { findNearestBackendPackageJson } = require('@friggframework/core/utils');
2
2
  const path = require('node:path');
3
3
  const fs = require('fs-extra');
4
4
  const { resolveTelemetryConfig } = require('../telemetry/telemetry-config');
5
+ const { registerDeniedKeys } = require('../logs/denied-keys');
6
+ const {
7
+ extractCredentialFieldsFromModules,
8
+ } = require('../database/encryption/encryption-schema-registry');
9
+ const {
10
+ getModulesDefinitionFromIntegrationClasses,
11
+ } = require('../integrations/utils/map-integration-dto');
12
+
13
+ // Handlers without a database never load the encryption registry, so the
14
+ // credential leaf keys are registered for redaction here too.
15
+ function registerCredentialLogKeys(appDefinition, integrations) {
16
+ try {
17
+ extractCredentialFieldsFromModules(
18
+ getModulesDefinitionFromIntegrationClasses(integrations)
19
+ );
20
+ } catch {
21
+ // An odd integration shape must not stop the app from loading.
22
+ }
23
+ const schema = appDefinition.encryption?.schema;
24
+ if (schema && typeof schema === 'object') {
25
+ for (const config of Object.values(schema)) {
26
+ registerDeniedKeys(config?.fields);
27
+ }
28
+ }
29
+ }
5
30
 
6
31
  /**
7
32
  * Loads the App definition from the nearest backend package
8
33
  * @function loadAppDefinition
9
34
  * @description Searches for the nearest backend package.json, loads the corresponding index.js file,
10
35
  * and extracts the application definition containing integrations and user configuration.
11
- * @returns {{integrations: Array<object>, userConfig: object | null, adminScripts: Array<object>, reports: Array<object>, admin: object, telemetry: object}} An object containing the application definition.
36
+ * @returns {{integrations: Array<object>, userConfig: object | null, adminScripts: Array<object>, reports: Array<object>, admin: object, telemetry: object, logging: object | null}} An object containing the application definition.
12
37
  * @throws {Error} Throws error if backend package.json cannot be found.
13
38
  * @throws {Error} Throws error if index.js file cannot be found in the backend directory.
14
39
  * @example
@@ -36,8 +61,11 @@ function loadAppDefinition() {
36
61
  adminScripts = [],
37
62
  reports = [],
38
63
  admin = {},
64
+ logging = null,
39
65
  } = appDefinition;
40
66
 
67
+ registerCredentialLogKeys(appDefinition, integrations);
68
+
41
69
  // Degrade consistently: an invalid telemetry block must never take down a
42
70
  // router bundle that loads the app definition at module scope (telemetry is
43
71
  // never allowed to break a handler). The singletons apply the same
@@ -59,7 +87,15 @@ function loadAppDefinition() {
59
87
  };
60
88
  }
61
89
 
62
- return { integrations, userConfig, adminScripts, reports, admin, telemetry };
90
+ return {
91
+ integrations,
92
+ userConfig,
93
+ adminScripts,
94
+ reports,
95
+ admin,
96
+ telemetry,
97
+ logging,
98
+ };
63
99
  }
64
100
 
65
101
  module.exports = {
@@ -1,10 +1,14 @@
1
- const { createHandler, flushDebugLog } = require('@friggframework/core');
1
+ const { createHandler } = require('@friggframework/core');
2
+ const { getLogger } = require('../logs');
3
+ const { summarizeExpressRequest } = require('../logs/summarize-event');
2
4
  const express = require('express');
3
5
  const bodyParser = require('body-parser');
4
6
  const cors = require('cors');
5
7
  const Boom = require('@hapi/boom');
6
8
  const serverlessHttp = require('serverless-http');
7
9
 
10
+ const log = getLogger('frigg.http');
11
+
8
12
  const createApp = (applyMiddleware) => {
9
13
  const app = express();
10
14
 
@@ -21,7 +25,7 @@ const createApp = (applyMiddleware) => {
21
25
 
22
26
  if (applyMiddleware) applyMiddleware(app);
23
27
 
24
- // Handle sending error response and logging server errors to console
28
+ // The express boundary: send the error response and log it one time.
25
29
  app.use((err, req, res, next) => {
26
30
  const boomError = err.isBoom ? err : Boom.boomify(err);
27
31
  const {
@@ -29,10 +33,20 @@ const createApp = (applyMiddleware) => {
29
33
  } = boomError;
30
34
 
31
35
  if (statusCode >= 500) {
32
- flushDebugLog(boomError);
36
+ log.error('Request failed', {
37
+ eventName: 'frigg.http.request_failed',
38
+ statusCode,
39
+ invocation: summarizeExpressRequest(req),
40
+ error: boomError,
41
+ });
33
42
  res.status(statusCode).json({ error: 'Internal Server Error' });
34
43
  } else {
35
- console.warn(`[Frigg] ${req.method} ${req.path} -> ${statusCode}: ${err.message}`);
44
+ // A client error needs no stack; the logger scrubs the reason.
45
+ log.warn('Request rejected', {
46
+ eventName: 'frigg.http.request_rejected',
47
+ statusCode,
48
+ reason: boomError.message,
49
+ });
36
50
  res.status(statusCode).json({ error: err.message });
37
51
  }
38
52
  });
@@ -1,4 +1,5 @@
1
1
  const { Router } = require('express');
2
+ const { runInContext, LOGGER_SCOPE_KEY } = require('../logs/context');
2
3
  const { Worker } = require('@friggframework/core');
3
4
  const {
4
5
  IntegrationEventDispatcher,
@@ -19,6 +20,7 @@ const {
19
20
  const {
20
21
  getModulesDefinitionFromIntegrationClasses,
21
22
  } = require('../integrations/utils/map-integration-dto');
23
+ const { processNotFound } = require('../integrations/use-cases/process-errors');
22
24
 
23
25
  const loadRouterFromObject = (IntegrationClass, routerObject) => {
24
26
  const router = Router();
@@ -37,12 +39,11 @@ const loadRouterFromObject = (IntegrationClass, routerObject) => {
37
39
  const dispatcher = new IntegrationEventDispatcher(
38
40
  integrationInstance
39
41
  );
40
- const result = await dispatcher.dispatchHttp({
41
- event,
42
- req,
43
- res,
44
- next,
45
- });
42
+ // Logs only: a route :integrationId is unauthenticated input.
43
+ const result = await runInContext(
44
+ { [LOGGER_SCOPE_KEY]: { integrationId: req.params?.integrationId } },
45
+ () => dispatcher.dispatchHttp({ event, req, res, next })
46
+ );
46
47
  res.json(result);
47
48
  } catch (error) {
48
49
  next(error);
@@ -149,7 +150,7 @@ const loadIntegrationForProcess = async (processId, integrationClass) => {
149
150
  const process = await processRepository.findById(processId);
150
151
 
151
152
  if (!process) {
152
- throw new Error(`Process not found: ${processId}`);
153
+ throw processNotFound(`Process not found: ${processId}`);
153
154
  }
154
155
 
155
156
  const instance = await getIntegrationInstance.execute(
@@ -164,7 +165,7 @@ const createQueueWorker = (integrationClass) => {
164
165
  const integrationName = integrationClass.Definition.name;
165
166
 
166
167
  class QueueWorker extends Worker {
167
- async _run(params, context) {
168
+ async _run(params, context, delivery) {
168
169
  const logCtx = {
169
170
  integration: integrationName,
170
171
  event: params.event,
@@ -254,6 +255,7 @@ const createQueueWorker = (integrationClass) => {
254
255
  event: params.event,
255
256
  data: params.data,
256
257
  context: context,
258
+ delivery,
257
259
  });
258
260
  console.log(
259
261
  `[QueueWorker] ${params.event} dispatched ok`,
@@ -273,11 +275,6 @@ const createQueueWorker = (integrationClass) => {
273
275
  return;
274
276
  }
275
277
 
276
- console.error(
277
- `Error in ${params.event} for ${integrationName}:`,
278
- error
279
- );
280
-
281
278
  // 4xx HTTP errors are permanent — the requester already
282
279
  // attempted token refresh (401) and backoff (429/5xx).
283
280
  // By the time a 4xx reaches here, retrying won't help.
@@ -24,6 +24,9 @@
24
24
 
25
25
  const { execFile } = require('child_process');
26
26
  const path = require('path');
27
+ const { getLogger } = require('../logs');
28
+
29
+ const log = getLogger('frigg.database.migration');
27
30
 
28
31
  /**
29
32
  * Execute Prisma migration command
@@ -153,7 +156,13 @@ exports.handler = async (event, context) => {
153
156
  console.log('='.repeat(60));
154
157
  console.log('Database Migration Handler');
155
158
  console.log('='.repeat(60));
156
- console.log('Event:', JSON.stringify(event, null, 2));
159
+ log.info('Database migration invoked', {
160
+ eventName: 'frigg.database.migration.invoked',
161
+ migrationId: event.migrationId,
162
+ dbType: process.env.DB_TYPE,
163
+ targetStage: process.env.STAGE,
164
+ action: event.command || 'deploy',
165
+ });
157
166
  console.log('Context:', JSON.stringify({
158
167
  functionName: context.functionName,
159
168
  functionVersion: context.functionVersion,
@@ -18,9 +18,9 @@ class IntegrationEventDispatcher {
18
18
  );
19
19
  }
20
20
 
21
- async dispatchJob({ event, data, context }) {
21
+ async dispatchJob({ event, data, context, delivery }) {
22
22
  return this._dispatch(event, (instance, handler) =>
23
- handler.call(instance, { data, context })
23
+ handler.call(instance, { data, context, delivery })
24
24
  );
25
25
  }
26
26
 
@@ -1,6 +1,7 @@
1
1
  const { createAppHandler } = require('./../app-handler-helpers');
2
2
  const { loadAppDefinition } = require('../app-definition-loader');
3
3
  const { Router } = require('express');
4
+ const { runInContext, LOGGER_SCOPE_KEY } = require('../../logs/context');
4
5
  const {
5
6
  IntegrationEventDispatcher,
6
7
  } = require('../integration-event-dispatcher');
@@ -55,12 +56,18 @@ for (const IntegrationClass of integrationClasses) {
55
56
  const dispatcher = new IntegrationEventDispatcher(
56
57
  integrationInstance
57
58
  );
58
- await dispatcher.dispatchHttp({
59
- event: 'WEBHOOK_RECEIVED',
60
- req,
61
- res,
62
- next,
63
- });
59
+ // Logs only: the id comes from an unauthenticated URL, so it must
60
+ // not reach the telemetry context that drives usage attribution.
61
+ await runInContext(
62
+ { [LOGGER_SCOPE_KEY]: { integrationId: req.params.integrationId } },
63
+ () =>
64
+ dispatcher.dispatchHttp({
65
+ event: 'WEBHOOK_RECEIVED',
66
+ req,
67
+ res,
68
+ next,
69
+ })
70
+ );
64
71
  } catch (error) {
65
72
  next(error);
66
73
  }
@@ -1,5 +1,8 @@
1
1
  const { createHandler } = require('@friggframework/core');
2
- const { createWebsocketConnectionRepository } = require('../../database/websocket-connection-repository-factory');
2
+ const { createWebsocketConnectionRepository } = require('../../websocket/repositories/websocket-connection-repository-factory');
3
+ const { getLogger } = require('../../logs');
4
+
5
+ const log = getLogger('frigg.websocket');
3
6
 
4
7
  const websocketConnectionRepository = createWebsocketConnectionRepository();
5
8
 
@@ -11,10 +14,16 @@ const handleWebSocketConnection = async (event, context) => {
11
14
  try {
12
15
  const connectionId = event.requestContext.connectionId;
13
16
  await websocketConnectionRepository.createConnection(connectionId);
14
- console.log(`Stored new connection: ${connectionId}`);
17
+ log.info('Stored new connection', {
18
+ eventName: 'frigg.websocket.connected',
19
+ connectionId,
20
+ });
15
21
  return { statusCode: 200, body: 'Connected.' };
16
22
  } catch (error) {
17
- console.error('Error storing connection:', error);
23
+ log.error('Error storing connection', {
24
+ eventName: 'frigg.websocket.connect_failed',
25
+ error,
26
+ });
18
27
  return { statusCode: 500, body: 'Error connecting.' };
19
28
  }
20
29
 
@@ -23,17 +32,28 @@ const handleWebSocketConnection = async (event, context) => {
23
32
  try {
24
33
  const connectionId = event.requestContext.connectionId;
25
34
  await websocketConnectionRepository.deleteConnection(connectionId);
26
- console.log(`Removed connection: ${connectionId}`);
35
+ log.info('Removed connection', {
36
+ eventName: 'frigg.websocket.disconnected',
37
+ connectionId,
38
+ });
27
39
  return { statusCode: 200, body: 'Disconnected.' };
28
40
  } catch (error) {
29
- console.error('Error removing connection:', error);
41
+ log.error('Error removing connection', {
42
+ eventName: 'frigg.websocket.disconnect_failed',
43
+ error,
44
+ });
30
45
  return { statusCode: 500, body: 'Error disconnecting.' };
31
46
  }
32
47
 
33
48
  case 'MESSAGE':
34
49
  // Handle incoming message
35
- const message = JSON.parse(event.body);
36
- console.log('Received message:', message);
50
+ // Parsed only to keep the old failure on a body that is not JSON.
51
+ JSON.parse(event.body);
52
+ log.info('Received message', {
53
+ eventName: 'frigg.websocket.message_received',
54
+ connectionId: event.requestContext.connectionId,
55
+ bodyLength: event.body?.length ?? 0,
56
+ });
37
57
 
38
58
  // Process the message and send a response
39
59
  const responseMessage = { message: 'Message received' };
@@ -55,6 +55,9 @@ const {
55
55
 
56
56
  // Inject prisma-runner as dependency
57
57
  const prismaRunner = require('../../database/utils/prisma-runner');
58
+ const { getLogger } = require('../../logs');
59
+
60
+ const log = getLogger('frigg.database.migration');
58
61
 
59
62
  // Use S3 repository for migration status tracking (no User table dependency)
60
63
  const bucketName = process.env.S3_BUCKET_NAME || process.env.MIGRATION_STATUS_BUCKET;
@@ -144,7 +147,6 @@ exports.handler = async (event, context) => {
144
147
  console.log('========================================');
145
148
  console.log('Database Migration Lambda Started');
146
149
  console.log('========================================');
147
- console.log('Event:', JSON.stringify(event, null, 2));
148
150
  console.log('Context:', JSON.stringify({
149
151
  requestId: context.requestId,
150
152
  functionName: context.functionName,
@@ -157,6 +159,15 @@ exports.handler = async (event, context) => {
157
159
  // Check for action parameter (direct invocation for status checks)
158
160
  const action = event.action || 'migrate'; // Default to migration
159
161
 
162
+ // targetStage, not stage: the record's own stage field would win.
163
+ log.info('Database migration invoked', {
164
+ eventName: 'frigg.database.migration.invoked',
165
+ migrationId,
166
+ dbType,
167
+ targetStage: stage,
168
+ action,
169
+ });
170
+
160
171
  // Handle checkStatus action
161
172
  if (action === 'checkStatus') {
162
173
  console.log(`\n========================================`);
@@ -5,6 +5,18 @@
5
5
  * This handler MUST NOT throw. If it throws, the message goes back to
6
6
  * the DLQ and creates an infinite loop. All errors are caught and logged.
7
7
  */
8
+ const crypto = require('node:crypto');
9
+ const { getLogger } = require('../../logs');
10
+ const {
11
+ summarizeLambdaEvent,
12
+ toScopeInvocation,
13
+ } = require('../../logs/summarize-event');
14
+ const {
15
+ runInvocationScope,
16
+ runMessageScope,
17
+ } = require('../../core/invocation-scope');
18
+
19
+ const log = getLogger('frigg.queue.dlq');
8
20
 
9
21
  function extractQueueName(eventSourceARN) {
10
22
  if (!eventSourceARN) return 'UNKNOWN';
@@ -12,50 +24,64 @@ function extractQueueName(eventSourceARN) {
12
24
  return parts[parts.length - 1] || 'UNKNOWN';
13
25
  }
14
26
 
15
- function parseMessageBody(body) {
27
+ // The body can hold credentials, so only its size and digest are logged.
28
+ function describeBody(body) {
29
+ const text = typeof body === 'string' ? body : String(body ?? '');
30
+ let parsed = true;
16
31
  try {
17
- const parsed = JSON.parse(body);
18
- return {
19
- event: parsed.event || 'UNKNOWN',
20
- integrationId: parsed.data?.integrationId || null,
21
- processId: parsed.data?.processId || null,
22
- data: parsed.data,
23
- };
24
- } catch (error) {
25
- console.warn('[DLQ] Failed to parse message body', { error: error.message, body });
26
- return {
27
- event: 'UNKNOWN',
28
- integrationId: null,
29
- processId: null,
30
- rawBody: body,
31
- };
32
+ JSON.parse(text);
33
+ } catch {
34
+ parsed = false;
32
35
  }
36
+ return {
37
+ parsed,
38
+ bodyLength: Buffer.byteLength(text),
39
+ bodySha256: crypto.createHash('sha256').update(text).digest('hex'),
40
+ };
33
41
  }
34
42
 
35
- async function dlqProcessor(event) {
43
+ function logRecord(record) {
44
+ const { parsed, ...body } = describeBody(record.body);
45
+ if (!parsed) {
46
+ log.warn('DLQ message body is not JSON', {
47
+ eventName: 'frigg.queue.dlq.body_unparsed',
48
+ ...body,
49
+ });
50
+ }
51
+ // messageId, receiveCount and the body ids come from the message scope.
52
+ log.error('Message reached the DLQ', {
53
+ eventName: 'frigg.queue.dlq.message_failed',
54
+ sentTimestamp: record.attributes?.SentTimestamp,
55
+ sourceQueue: extractQueueName(record.eventSourceARN),
56
+ ...body,
57
+ });
58
+ }
59
+
60
+ async function dlqProcessor(event, context) {
36
61
  if (!event?.Records?.length) return { batchItemFailures: [] };
37
62
 
38
- for (const record of event.Records) {
39
- try {
40
- const parsed = parseMessageBody(record.body);
41
-
42
- console.error('[DLQ] Failed message', {
43
- messageId: record.messageId,
44
- event: parsed.event,
45
- integrationId: parsed.integrationId,
46
- processId: parsed.processId,
47
- receiveCount: record.attributes?.ApproximateReceiveCount,
48
- sentTimestamp: record.attributes?.SentTimestamp,
49
- sourceQueue: extractQueueName(record.eventSourceARN),
50
- ...(parsed.rawBody !== undefined && { rawBody: parsed.rawBody }),
51
- });
52
- } catch (error) {
53
- console.error('[DLQ] Error processing DLQ record', {
54
- messageId: record.messageId,
55
- error: error.message,
56
- });
57
- }
58
- }
63
+ await runInvocationScope(
64
+ {
65
+ requestId: context?.awsRequestId,
66
+ handlerName: 'dlqProcessor',
67
+ invocation: toScopeInvocation(summarizeLambdaEvent(event)),
68
+ },
69
+ async () => {
70
+ for (const record of event.Records) {
71
+ await runMessageScope(record, async () => {
72
+ try {
73
+ logRecord(record);
74
+ } catch (error) {
75
+ log.error('DLQ record failed', {
76
+ eventName: 'frigg.queue.dlq.record_failed',
77
+ error,
78
+ });
79
+ }
80
+ });
81
+ }
82
+ },
83
+ { shouldUseDatabase: false, context }
84
+ );
59
85
 
60
86
  return { batchItemFailures: [] };
61
87
  }
package/index.js CHANGED
@@ -11,6 +11,7 @@ const {
11
11
  Worker,
12
12
  loadInstalledModules,
13
13
  createHandler,
14
+ runInvocationScope,
14
15
  } = require('./core/index');
15
16
  const {
16
17
  prisma,
@@ -78,7 +79,17 @@ const {
78
79
  } = require('./telemetry/index');
79
80
  const { createUsageRepository } = require('./usage/index');
80
81
  const { TimeoutCatcher } = require('./lambda/index');
81
- const { debug, initDebugLog, flushDebugLog } = require('./logs/index');
82
+ const {
83
+ getLogger,
84
+ createMemorySink,
85
+ resetLoggerForTests,
86
+ serializeError,
87
+ redactValue,
88
+ toSanitizedSurrogate,
89
+ debug,
90
+ initDebugLog,
91
+ flushDebugLog,
92
+ } = require('./logs/index');
82
93
  const {
83
94
  Credential,
84
95
  Entity,
@@ -108,6 +119,7 @@ module.exports = {
108
119
  Worker,
109
120
  loadInstalledModules,
110
121
  createHandler,
122
+ runInvocationScope,
111
123
 
112
124
  // database
113
125
  prisma,
@@ -175,6 +187,12 @@ module.exports = {
175
187
  TimeoutCatcher,
176
188
 
177
189
  // logs
190
+ getLogger,
191
+ createMemorySink,
192
+ resetLoggerForTests,
193
+ serializeError,
194
+ redactValue,
195
+ toSanitizedSurrogate,
178
196
  debug,
179
197
  initDebugLog,
180
198
  flushDebugLog,