@friggframework/core 2.0.0-next.112 → 2.0.0-next.114
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/.eslintrc.json +18 -1
- package/CLAUDE.md +51 -12
- package/README.md +19 -22
- package/core/CLAUDE.md +28 -13
- package/core/Worker.js +32 -37
- package/core/create-handler.js +97 -197
- package/core/index.js +8 -1
- package/core/invocation-scope.js +225 -0
- package/core/secrets-to-env.js +7 -6
- package/database/config.js +0 -7
- package/database/encryption/README.md +5 -1
- package/database/encryption/encryption-schema-registry.js +3 -0
- package/database/encryption/logger.js +5 -2
- package/errors/fetch-error.js +58 -64
- package/handlers/app-definition-loader.js +38 -2
- package/handlers/app-handler-helpers.js +18 -4
- package/handlers/backend-utils.js +10 -13
- package/handlers/database-migration-handler.js +10 -1
- package/handlers/integration-event-dispatcher.js +2 -2
- package/handlers/routers/integration-webhook-routers.js +13 -6
- package/handlers/routers/websocket.js +27 -7
- package/handlers/workers/db-migration.js +12 -1
- package/handlers/workers/dlq-processor.js +64 -38
- package/index.js +19 -1
- package/integrations/integration-base.js +56 -38
- package/integrations/integration-router.js +21 -20
- package/integrations/use-cases/delete-integration-for-user.js +24 -13
- package/jest.config.js +2 -0
- package/logs/.eslintrc.json +26 -1
- package/logs/__fixtures__/events.js +82 -0
- package/logs/__fixtures__/matchers.js +45 -0
- package/logs/__fixtures__/secrets.js +59 -0
- package/logs/__fixtures__/vectors.js +329 -0
- package/logs/__fixtures__/with-env.js +45 -0
- package/logs/context.js +109 -0
- package/logs/contract-keys.js +67 -0
- package/logs/debug-shims.js +46 -0
- package/logs/denied-keys.js +21 -0
- package/logs/index.js +14 -3
- package/logs/jest-logger-setup.js +25 -0
- package/logs/levels.js +71 -0
- package/logs/logger-runtime.js +137 -0
- package/logs/logger.js +134 -42
- package/logs/record.js +276 -0
- package/logs/redact.js +328 -0
- package/logs/serialize.js +329 -0
- package/logs/sinks.js +93 -0
- package/logs/summarize-event.js +163 -0
- package/modules/module.js +32 -19
- package/modules/requester/oauth-2.js +28 -22
- package/modules/requester/requester.js +69 -6
- package/package.json +5 -6
- package/queues/queue-delivery.js +41 -0
- package/queues/queuer-util.js +1 -15
- package/telemetry/bind-telemetry-context.js +4 -0
- package/telemetry/instrument-handler.js +19 -2
- package/telemetry/no-op-telemetry.js +4 -0
- package/telemetry/otel-telemetry.js +25 -6
- package/telemetry/telemetry-context.js +19 -8
- package/telemetry/telemetry-runtime.js +17 -0
- package/telemetry/telemetry-service.js +1 -0
- package/types/errors/index.d.ts +24 -10
- package/types/logs/index.d.ts +61 -2
- package/user/repositories/user-repository-documentdb.js +10 -42
package/errors/fetch-error.js
CHANGED
|
@@ -1,82 +1,76 @@
|
|
|
1
1
|
const { BaseError } = require('./base-error');
|
|
2
|
-
const {
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
}
|
|
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
|
+
}
|
|
37
14
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
15
|
+
// node-fetch messages can contain part of the response body.
|
|
16
|
+
// Use a fixed phrase instead of the original message.
|
|
17
|
+
const NODE_FETCH_TYPE_PHRASES = {
|
|
18
|
+
'invalid-json': 'invalid json response body',
|
|
19
|
+
};
|
|
41
20
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
21
|
+
function causeLabel(cause) {
|
|
22
|
+
if (!cause || typeof cause !== 'object') return '';
|
|
23
|
+
const label = cause.code ?? cause.name;
|
|
24
|
+
const scrubbedLabel =
|
|
25
|
+
label === undefined || label === null ? '' : scrubString(String(label));
|
|
26
|
+
const phrase = Object.hasOwn(NODE_FETCH_TYPE_PHRASES, cause.type)
|
|
27
|
+
? NODE_FETCH_TYPE_PHRASES[cause.type]
|
|
28
|
+
: '';
|
|
29
|
+
return [scrubbedLabel, phrase].filter(Boolean).join(': ');
|
|
30
|
+
}
|
|
48
31
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
];
|
|
32
|
+
function buildMessage({ method, url, response, cause }) {
|
|
33
|
+
const outcome = response
|
|
34
|
+
? response.status === undefined || response.status === null
|
|
35
|
+
? ''
|
|
36
|
+
: String(response.status)
|
|
37
|
+
: causeLabel(cause);
|
|
38
|
+
return [method, url, outcome].filter(Boolean).join(' ');
|
|
39
|
+
}
|
|
69
40
|
|
|
70
|
-
|
|
41
|
+
class FetchError extends BaseError {
|
|
42
|
+
constructor({ resource, init, response, cause, responseBody, body } = {}) {
|
|
43
|
+
const method = String(init?.method ?? 'GET').toUpperCase();
|
|
44
|
+
const url = resourceUrl(resource);
|
|
45
|
+
super(
|
|
46
|
+
buildMessage({ method, url, response, cause }),
|
|
47
|
+
cause ? { cause } : undefined
|
|
48
|
+
);
|
|
71
49
|
|
|
72
|
-
this.response = response;
|
|
73
50
|
this.statusCode = response?.status;
|
|
51
|
+
this.method = method;
|
|
52
|
+
this.url = url;
|
|
53
|
+
Object.defineProperty(this, 'response', {
|
|
54
|
+
value: response ?? null,
|
|
55
|
+
enumerable: false,
|
|
56
|
+
writable: true,
|
|
57
|
+
configurable: true,
|
|
58
|
+
});
|
|
59
|
+
Object.defineProperty(this, 'body', {
|
|
60
|
+
value: responseBody ?? body,
|
|
61
|
+
enumerable: false,
|
|
62
|
+
writable: true,
|
|
63
|
+
configurable: true,
|
|
64
|
+
});
|
|
74
65
|
}
|
|
75
66
|
|
|
76
67
|
static async create(options = {}) {
|
|
77
68
|
const { response } = options;
|
|
78
|
-
let responseBody =
|
|
79
|
-
|
|
69
|
+
let responseBody =
|
|
70
|
+
response && !response.bodyUsed && typeof response.text === 'function'
|
|
71
|
+
? await response.text()
|
|
72
|
+
: null;
|
|
73
|
+
if (!responseBody) responseBody = options.responseBody ?? options.body;
|
|
80
74
|
return new FetchError({ ...options, responseBody });
|
|
81
75
|
}
|
|
82
76
|
}
|
|
@@ -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 {
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
req,
|
|
43
|
-
res,
|
|
44
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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('../../
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
36
|
-
|
|
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
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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 {
|
|
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,
|