@friggframework/core 2.0.0--canary.652.b14f3b5.0 → 2.0.0--canary.654.be44e66.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/CLAUDE.md +13 -12
- package/README.md +19 -22
- package/core/CLAUDE.md +25 -12
- package/core/Worker.js +37 -34
- package/core/create-handler.js +97 -197
- package/core/index.js +8 -1
- package/core/invocation-scope.js +224 -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/errors/fetch-error.js +48 -65
- package/handlers/app-definition-loader.js +38 -2
- package/handlers/app-handler-helpers.js +15 -4
- package/handlers/database-migration-handler.js +10 -1
- package/handlers/routers/websocket.js +26 -6
- package/handlers/workers/db-migration.js +12 -1
- package/handlers/workers/dlq-processor.js +66 -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 +22 -0
- package/logs/__fixtures__/vectors.js +312 -0
- package/logs/__fixtures__/with-env.js +45 -0
- package/logs/context.js +92 -0
- package/logs/debug-shims.js +43 -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 +142 -0
- package/logs/logger.js +132 -42
- package/logs/record.js +199 -0
- package/logs/redact.js +293 -0
- package/logs/serialize.js +322 -0
- package/logs/sinks.js +99 -0
- package/logs/summarize-event.js +110 -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/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 +18 -42
package/CLAUDE.md
CHANGED
|
@@ -437,22 +437,24 @@ if (!userId) {
|
|
|
437
437
|
|
|
438
438
|
### 9. Logging System (`/logs`)
|
|
439
439
|
|
|
440
|
-
**Purpose**:
|
|
440
|
+
**Purpose**: One redacted JSON record per line to stdout (ADR-048). See `docs/guides/LOGGING.md`.
|
|
441
441
|
|
|
442
|
-
**
|
|
443
|
-
- `
|
|
444
|
-
- `
|
|
445
|
-
- `
|
|
442
|
+
**Rules**:
|
|
443
|
+
- `logs/` is a leaf: it requires only Node built-ins and sibling `logs/` files.
|
|
444
|
+
- Integrations and API modules use `this.logger`; core uses one `getLogger('frigg.<area>')` per module.
|
|
445
|
+
- Fixed message text; ids and counts go into fields. `frigg.*` records at WARN and above need an `eventName` (`<logger name>.<action>`); tests fail without it.
|
|
446
|
+
- Log an error one time, at the boundary (`createHandler`, the express middleware, `Worker.run`, the DLQ processor). Inner code throws with `cause`.
|
|
447
|
+
- Use `statusCode`, not `status` (reserved key). `body`, `payload`, `response` are dropped at INFO and above.
|
|
448
|
+
- Tests assert on `createMemorySink()` records by `eventName`, not on console spies.
|
|
449
|
+
- `debug`, `initDebugLog`, `flushDebugLog` are deprecated shims.
|
|
446
450
|
|
|
447
451
|
**Usage**:
|
|
448
452
|
|
|
449
453
|
```javascript
|
|
450
|
-
const {
|
|
454
|
+
const { getLogger } = require('../logs');
|
|
455
|
+
const log = getLogger('frigg.core.sync');
|
|
451
456
|
|
|
452
|
-
|
|
453
|
-
debug('Processing request', { userId, action });
|
|
454
|
-
// ... your code ...
|
|
455
|
-
flushDebugLog(); // On error
|
|
457
|
+
log.warn('Sync skipped', { eventName: 'frigg.core.sync.skipped', processId });
|
|
456
458
|
```
|
|
457
459
|
|
|
458
460
|
### 10. Lambda Utilities (`/lambda`)
|
|
@@ -660,8 +662,7 @@ Use test doubles from `@friggframework/test` package for consistent mocking.
|
|
|
660
662
|
### Optional
|
|
661
663
|
|
|
662
664
|
- `SECRET_ARN` - AWS Secrets Manager ARN for auto-injection
|
|
663
|
-
- `
|
|
664
|
-
- `LOG_LEVEL` - Logging level (debug, info, warn, error)
|
|
665
|
+
- `FRIGG_LOG_LEVEL` - Minimum log level (`TRACE` … `FATAL`, default `INFO`; `DEBUG` on local runs)
|
|
665
666
|
|
|
666
667
|
## Version Information
|
|
667
668
|
|
package/README.md
CHANGED
|
@@ -107,10 +107,6 @@ FRIGG_ENCRYPTION_KEY=your-256-bit-encryption-key
|
|
|
107
107
|
AWS_REGION=us-east-1
|
|
108
108
|
AWS_ACCESS_KEY_ID=your-access-key
|
|
109
109
|
AWS_SECRET_ACCESS_KEY=your-secret-key
|
|
110
|
-
|
|
111
|
-
# Logging
|
|
112
|
-
DEBUG=frigg:*
|
|
113
|
-
LOG_LEVEL=info
|
|
114
110
|
```
|
|
115
111
|
|
|
116
112
|
## Core Components
|
|
@@ -255,10 +251,12 @@ const {
|
|
|
255
251
|
// Custom business logic error
|
|
256
252
|
throw new RequiredPropertyError('userId is required');
|
|
257
253
|
|
|
258
|
-
// API communication error
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
254
|
+
// API communication error. The message is built from the method, the
|
|
255
|
+
// sanitized URL and the status; the response body stays off the message.
|
|
256
|
+
throw await FetchError.create({
|
|
257
|
+
resource: 'https://api.example.com/contacts',
|
|
258
|
+
init: { method: 'GET' },
|
|
259
|
+
response,
|
|
262
260
|
});
|
|
263
261
|
|
|
264
262
|
// Base error with custom properties
|
|
@@ -270,25 +268,25 @@ throw new BaseError('Integration failed', {
|
|
|
270
268
|
|
|
271
269
|
### 6. Logging (`/logs`)
|
|
272
270
|
|
|
273
|
-
|
|
271
|
+
One redacted JSON record per line to stdout, with the correlation ids of
|
|
272
|
+
ADR-011. See the [Logging guide](../../docs/guides/LOGGING.md).
|
|
274
273
|
|
|
275
274
|
**Usage:**
|
|
276
275
|
```javascript
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
// Log debug information
|
|
283
|
-
debug('Processing webhook payload', {
|
|
284
|
-
eventType: 'contact.created',
|
|
285
|
-
payload: webhookData
|
|
276
|
+
// In an integration or an API module
|
|
277
|
+
this.logger.info('Contact batch started', {
|
|
278
|
+
eventName: 'integration.hubspot.batch_started',
|
|
279
|
+
batchSize: contacts.length,
|
|
286
280
|
});
|
|
287
281
|
|
|
288
|
-
//
|
|
289
|
-
|
|
282
|
+
// In core code
|
|
283
|
+
const { getLogger } = require('@friggframework/core');
|
|
284
|
+
const log = getLogger('frigg.core.sync');
|
|
285
|
+
log.warn('Sync skipped', { eventName: 'frigg.core.sync.skipped', processId });
|
|
290
286
|
```
|
|
291
287
|
|
|
288
|
+
`debug`, `initDebugLog` and `flushDebugLog` are deprecated shims.
|
|
289
|
+
|
|
292
290
|
### 7. User Management (`/user`)
|
|
293
291
|
|
|
294
292
|
Comprehensive user authentication and authorization system supporting both individual and organizational users.
|
|
@@ -1013,8 +1011,7 @@ const {
|
|
|
1013
1011
|
| `MONGO_URI` | Yes | MongoDB connection string |
|
|
1014
1012
|
| `FRIGG_ENCRYPTION_KEY` | Yes | 256-bit encryption key |
|
|
1015
1013
|
| `AWS_REGION` | No | AWS region for services |
|
|
1016
|
-
| `
|
|
1017
|
-
| `LOG_LEVEL` | No | Logging level (debug, info, warn, error) |
|
|
1014
|
+
| `FRIGG_LOG_LEVEL` | No | Minimum log level: `TRACE`, `DEBUG`, `INFO` (default), `WARN`, `ERROR`, `FATAL`. Local runs default to `DEBUG` |
|
|
1018
1015
|
|
|
1019
1016
|
## License
|
|
1020
1017
|
|
package/core/CLAUDE.md
CHANGED
|
@@ -21,13 +21,13 @@ This file provides guidance to Claude Code when working with the Frigg Framework
|
|
|
21
21
|
- **Database Connection Management**: Automatic MongoDB connection with pooling
|
|
22
22
|
- **Secrets Management**: AWS Secrets Manager integration via `SECRET_ARN` env var
|
|
23
23
|
- **Error Sanitization**: Prevents internal details from leaking to end users
|
|
24
|
-
- **
|
|
24
|
+
- **Invocation Scope**: `runInvocationScope` puts `requestId`, `route` and the redacted event summary on every record, then flushes usage, telemetry and sinks
|
|
25
25
|
- **Connection Optimization**: `context.callbackWaitsForEmptyEventLoop = false` for reuse
|
|
26
26
|
|
|
27
27
|
**Handler Configuration Options**:
|
|
28
28
|
```javascript
|
|
29
29
|
const handler = createHandler({
|
|
30
|
-
eventName: 'MyIntegration', //
|
|
30
|
+
eventName: 'MyIntegration', // Logged as handlerName
|
|
31
31
|
isUserFacingResponse: true, // true = sanitize errors, false = pass through
|
|
32
32
|
method: async (event, context) => {}, // Your Lambda function logic
|
|
33
33
|
shouldUseDatabase: true // false = skip MongoDB connection
|
|
@@ -129,7 +129,8 @@ class MyIntegration extends Delegate {
|
|
|
129
129
|
### Lambda Handler Lifecycle
|
|
130
130
|
1. **Pre-Execution Setup**:
|
|
131
131
|
```javascript
|
|
132
|
-
|
|
132
|
+
runInvocationScope({ requestId, handlerName, method, route, invocation }, …); // Logger scope for the invocation
|
|
133
|
+
log.info('Handler invoked', { eventName: 'frigg.handler.invoked' });
|
|
133
134
|
await secretsToEnv(); // Secrets Manager injection
|
|
134
135
|
await parametersToEnv(); // SSM Parameter Store fetch (only when SSM_PARAMETER_PREFIX + FRIGG_SSM_OFFLOADED_KEYS are set)
|
|
135
136
|
context.callbackWaitsForEmptyEventLoop = false; // Connection pooling
|
|
@@ -149,12 +150,20 @@ class MyIntegration extends Delegate {
|
|
|
149
150
|
|
|
150
151
|
4. **Error Handling & Cleanup**:
|
|
151
152
|
```javascript
|
|
152
|
-
|
|
153
|
-
//
|
|
153
|
+
// One record at the boundary: WARN frigg.handler.rejected (client-safe),
|
|
154
|
+
// ERROR frigg.handler.failed, or ERROR frigg.handler.halted (halt, no retry).
|
|
155
|
+
// Sanitized error response for user-facing endpoints; server-to-server
|
|
156
|
+
// errors are rethrown as a sanitized surrogate (toSanitizedSurrogate).
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
5. **Flush** (in `runInvocationScope`'s `finally`):
|
|
160
|
+
```javascript
|
|
161
|
+
// Usage rollup first (unbounded), then telemetry + log sinks in parallel
|
|
162
|
+
// under one deadline: min(flushTimeoutMs, remaining time - 50 ms).
|
|
154
163
|
```
|
|
155
164
|
|
|
156
165
|
### SQS Job Processing Lifecycle
|
|
157
|
-
1. **Batch Processing**: Process all records in `event.Records` sequentially
|
|
166
|
+
1. **Batch Processing**: Process all records in `event.Records` sequentially, each inside `runMessageScope` (adds `messageId`, `receiveCount`, `processId`, `integrationId`, `integrationEvent` to its records)
|
|
158
167
|
2. **Message Parsing**: JSON.parse message body for parameters
|
|
159
168
|
3. **Validation**: Run custom validation on parsed parameters
|
|
160
169
|
4. **Execution**: Call `_run()` method with validated parameters
|
|
@@ -205,7 +214,9 @@ const handler = createHandler({
|
|
|
205
214
|
- Logs full error details internally
|
|
206
215
|
|
|
207
216
|
2. **Server-to-Server Errors**: `isUserFacingResponse: false`
|
|
208
|
-
-
|
|
217
|
+
- Logs one ERROR, then rethrows a sanitized surrogate for AWS handling:
|
|
218
|
+
a fresh `Error` with the sanitized `name`, `message`, `stack`, plus
|
|
219
|
+
`statusCode` and `code`. `instanceof` checks and custom properties are gone
|
|
209
220
|
- Used for SQS, SNS, and internal API calls
|
|
210
221
|
- Enables proper retry mechanisms
|
|
211
222
|
|
|
@@ -214,12 +225,14 @@ const handler = createHandler({
|
|
|
214
225
|
- Prevents infinite retries for known issues
|
|
215
226
|
- Used for graceful degradation scenarios
|
|
216
227
|
|
|
217
|
-
###
|
|
228
|
+
### Logging Strategy
|
|
218
229
|
```javascript
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
230
|
+
const { getLogger } = require('../logs');
|
|
231
|
+
const log = getLogger('frigg.core.sync');
|
|
232
|
+
log.info('Sync started', { eventName: 'frigg.core.sync.started', processId });
|
|
233
|
+
// Throw with cause; the boundary logs the error one time.
|
|
222
234
|
```
|
|
235
|
+
See `docs/guides/LOGGING.md`.
|
|
223
236
|
|
|
224
237
|
## Integration Development Patterns
|
|
225
238
|
|
|
@@ -646,7 +659,7 @@ describe('Health Handler', () => {
|
|
|
646
659
|
const { createHandler } = require('@friggframework/core/core');
|
|
647
660
|
|
|
648
661
|
const testHandler = createHandler({
|
|
649
|
-
isUserFacingResponse: false, //
|
|
662
|
+
isUserFacingResponse: false, // Rethrows a sanitized surrogate (name, message, statusCode, code; not the original instance)
|
|
650
663
|
shouldUseDatabase: false, // Mock/skip DB in tests
|
|
651
664
|
method: yourTestMethod
|
|
652
665
|
});
|
package/core/Worker.js
CHANGED
|
@@ -2,6 +2,7 @@ const { SQSClient, GetQueueUrlCommand, SendMessageCommand } = require('@aws-sdk/
|
|
|
2
2
|
const _ = require('lodash');
|
|
3
3
|
const { RequiredPropertyError } = require('../errors');
|
|
4
4
|
const { get } = require('../assertions');
|
|
5
|
+
const { runMessageScope } = require('./invocation-scope');
|
|
5
6
|
|
|
6
7
|
const sqs = new SQSClient({ region: process.env.AWS_REGION });
|
|
7
8
|
|
|
@@ -25,45 +26,47 @@ class Worker {
|
|
|
25
26
|
);
|
|
26
27
|
|
|
27
28
|
for (const record of records) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
event: parsedEvent,
|
|
39
|
-
receiveCount: record.attributes?.ApproximateReceiveCount,
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
try {
|
|
43
|
-
const runParams = JSON.parse(record.body);
|
|
44
|
-
this._validateParams(runParams);
|
|
45
|
-
await this._run(runParams, context);
|
|
46
|
-
console.log(`[Worker] record success`, {
|
|
29
|
+
await runMessageScope(record, async () => {
|
|
30
|
+
// Log record entry with SQS-provided attributes useful for tracing
|
|
31
|
+
// delivery history (ApproximateReceiveCount for retries, etc.).
|
|
32
|
+
let parsedEvent;
|
|
33
|
+
try {
|
|
34
|
+
parsedEvent = JSON.parse(record.body)?.event;
|
|
35
|
+
} catch {
|
|
36
|
+
parsedEvent = undefined;
|
|
37
|
+
}
|
|
38
|
+
console.log(`[Worker] record begin`, {
|
|
47
39
|
messageId: record.messageId,
|
|
48
|
-
event:
|
|
40
|
+
event: parsedEvent,
|
|
41
|
+
receiveCount: record.attributes?.ApproximateReceiveCount,
|
|
49
42
|
});
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
console.warn(`[Worker] record halted (discarded, no retry)`, {
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
const runParams = JSON.parse(record.body);
|
|
46
|
+
this._validateParams(runParams);
|
|
47
|
+
await this._run(runParams, context);
|
|
48
|
+
console.log(`[Worker] record success`, {
|
|
57
49
|
messageId: record.messageId,
|
|
58
|
-
event:
|
|
59
|
-
reason: error.message,
|
|
60
|
-
statusCode: error.statusCode,
|
|
50
|
+
event: runParams?.event,
|
|
61
51
|
});
|
|
62
|
-
|
|
52
|
+
} catch (error) {
|
|
53
|
+
if (error.isHaltError) {
|
|
54
|
+
// HaltError means "discard this message, don't retry".
|
|
55
|
+
// Treat as success so SQS deletes it from the queue.
|
|
56
|
+
// Logged explicitly — silent discards made prod debugging
|
|
57
|
+
// extremely hard; keep this visible.
|
|
58
|
+
console.warn(`[Worker] record halted (discarded, no retry)`, {
|
|
59
|
+
messageId: record.messageId,
|
|
60
|
+
event: parsedEvent,
|
|
61
|
+
reason: error.message,
|
|
62
|
+
statusCode: error.statusCode,
|
|
63
|
+
});
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
console.error(`[Worker] Failed to process record ${record.messageId}:`, error);
|
|
67
|
+
batchItemFailures.push({ itemIdentifier: record.messageId });
|
|
63
68
|
}
|
|
64
|
-
|
|
65
|
-
batchItemFailures.push({ itemIdentifier: record.messageId });
|
|
66
|
-
}
|
|
69
|
+
});
|
|
67
70
|
}
|
|
68
71
|
|
|
69
72
|
if (batchItemFailures.length > 0) {
|
package/core/create-handler.js
CHANGED
|
@@ -2,7 +2,15 @@
|
|
|
2
2
|
// REMOVING FOR NOW UNTIL WE ADD WEBPACK BACK IN
|
|
3
3
|
// require('source-map-support').install();
|
|
4
4
|
|
|
5
|
-
const {
|
|
5
|
+
const { getLogger, toSanitizedSurrogate } = require('../logs');
|
|
6
|
+
const {
|
|
7
|
+
summarizeLambdaEvent,
|
|
8
|
+
toScopeInvocation,
|
|
9
|
+
} = require('../logs/summarize-event');
|
|
10
|
+
const {
|
|
11
|
+
runInvocationScope,
|
|
12
|
+
DEFAULT_FLUSH_TIMEOUT_MS,
|
|
13
|
+
} = require('./invocation-scope');
|
|
6
14
|
const { secretsToEnv } = require('./secrets-to-env');
|
|
7
15
|
const { parametersToEnv } = require('./parameters-to-env');
|
|
8
16
|
const {
|
|
@@ -11,123 +19,7 @@ const {
|
|
|
11
19
|
getPluginTelemetrySubscribers,
|
|
12
20
|
} = require('../telemetry/telemetry-runtime');
|
|
13
21
|
|
|
14
|
-
|
|
15
|
-
// an unreachable OTLP endpoint (e.g. a VPC Lambda with no NAT/egress) costs at
|
|
16
|
-
// most this, not multiple seconds. Override with OTEL_FLUSH_TIMEOUT_MS.
|
|
17
|
-
const DEFAULT_FLUSH_TIMEOUT_MS =
|
|
18
|
-
Number(process.env.OTEL_FLUSH_TIMEOUT_MS) || 500;
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Fold the invocation's buffered usage counters into the durable store, then
|
|
22
|
-
* clear the buffer. On an SQS redelivery (ApproximateReceiveCount > 1) we
|
|
23
|
-
* DISCARD rather than flush — the prior delivery already counted, and the usage
|
|
24
|
-
* accuracy contract is "approximate, skip obvious redeliveries". Fully guarded.
|
|
25
|
-
*/
|
|
26
|
-
async function flushUsageRollup(subscriber, eventSummary, shouldUseDatabase) {
|
|
27
|
-
if (!subscriber) return;
|
|
28
|
-
try {
|
|
29
|
-
// Persisting usage requires a DB connection. DB-free handlers (e.g. the
|
|
30
|
-
// webhook-receipt route) never called connectPrisma, so drop the buffer
|
|
31
|
-
// instead of issuing a connectionless Prisma write.
|
|
32
|
-
if (!shouldUseDatabase) {
|
|
33
|
-
subscriber.discard();
|
|
34
|
-
return;
|
|
35
|
-
}
|
|
36
|
-
// Discard only when EVERY record in the batch is a redelivery. The buffer
|
|
37
|
-
// is invocation-scoped (not per-message), so discarding on *any*
|
|
38
|
-
// redelivery would drop the fresh records' counts too (silent
|
|
39
|
-
// under-count). For a mixed batch we flush: preserving fresh counts and
|
|
40
|
-
// at worst re-counting the one redelivered record is strictly better than
|
|
41
|
-
// losing fresh data for an approximate store. (Integration queue workers
|
|
42
|
-
// are batchSize:1 today, so a batch is all-or-nothing; this keeps it
|
|
43
|
-
// correct if batchSize is ever raised.)
|
|
44
|
-
const records = Array.isArray(eventSummary?.records)
|
|
45
|
-
? eventSummary.records
|
|
46
|
-
: [];
|
|
47
|
-
const allRedelivered =
|
|
48
|
-
records.length > 0 &&
|
|
49
|
-
records.every((r) => Number(r.receiveCount) > 1);
|
|
50
|
-
if (allRedelivered) {
|
|
51
|
-
subscriber.discard();
|
|
52
|
-
} else {
|
|
53
|
-
await subscriber.flush();
|
|
54
|
-
}
|
|
55
|
-
} catch (_) {
|
|
56
|
-
// Usage rollup must never break the handler.
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Flush telemetry before the Lambda container freezes. Because
|
|
62
|
-
* `callbackWaitsForEmptyEventLoop=false` (below) stops the event loop the moment
|
|
63
|
-
* the handler returns, OTel's timer-driven batch processors would never fire —
|
|
64
|
-
* so spans/metrics must be flushed synchronously here. Bounded by a timeout so a
|
|
65
|
-
* stalled exporter can never block the response, and fully guarded so a flush
|
|
66
|
-
* failure never breaks the handler.
|
|
67
|
-
*/
|
|
68
|
-
async function flushTelemetry(telemetry, timeoutMs) {
|
|
69
|
-
try {
|
|
70
|
-
if (
|
|
71
|
-
!telemetry ||
|
|
72
|
-
typeof telemetry.isEnabled !== 'function' ||
|
|
73
|
-
!telemetry.isEnabled()
|
|
74
|
-
) {
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
let timer;
|
|
78
|
-
const deadline = new Promise((resolve) => {
|
|
79
|
-
timer = setTimeout(resolve, timeoutMs);
|
|
80
|
-
});
|
|
81
|
-
try {
|
|
82
|
-
await Promise.race([
|
|
83
|
-
Promise.resolve(telemetry.forceFlush()),
|
|
84
|
-
deadline,
|
|
85
|
-
]);
|
|
86
|
-
} finally {
|
|
87
|
-
clearTimeout(timer);
|
|
88
|
-
}
|
|
89
|
-
} catch (_) {
|
|
90
|
-
// Telemetry flush must never break the handler.
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// Best-effort extraction of correlation identifiers from a Lambda event.
|
|
95
|
-
// For SQS: pulls messageIds + parsed event/processId/integrationId from each
|
|
96
|
-
// record body. For HTTP: pulls method+path. Never throws.
|
|
97
|
-
const summarizeLambdaEvent = (event) => {
|
|
98
|
-
if (!event) return {};
|
|
99
|
-
if (Array.isArray(event.Records)) {
|
|
100
|
-
return {
|
|
101
|
-
source: 'sqs',
|
|
102
|
-
records: event.Records.map((r) => {
|
|
103
|
-
let parsed = {};
|
|
104
|
-
try {
|
|
105
|
-
const body = JSON.parse(r.body);
|
|
106
|
-
parsed = {
|
|
107
|
-
event: body?.event,
|
|
108
|
-
processId: body?.data?.processId,
|
|
109
|
-
integrationId: body?.data?.integrationId,
|
|
110
|
-
};
|
|
111
|
-
} catch {
|
|
112
|
-
// ignore unparseable bodies
|
|
113
|
-
}
|
|
114
|
-
return {
|
|
115
|
-
messageId: r.messageId,
|
|
116
|
-
receiveCount: r.attributes?.ApproximateReceiveCount,
|
|
117
|
-
...parsed,
|
|
118
|
-
};
|
|
119
|
-
}),
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
if (event.httpMethod || event.requestContext?.http) {
|
|
123
|
-
return {
|
|
124
|
-
source: 'http',
|
|
125
|
-
method: event.httpMethod || event.requestContext?.http?.method,
|
|
126
|
-
path: event.path || event.rawPath,
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
return { source: 'other' };
|
|
130
|
-
};
|
|
22
|
+
const log = getLogger('frigg.handler');
|
|
131
23
|
|
|
132
24
|
const createHandler = (optionByName = {}) => {
|
|
133
25
|
const {
|
|
@@ -157,95 +49,103 @@ const createHandler = (optionByName = {}) => {
|
|
|
157
49
|
// no-op after the first invocation.
|
|
158
50
|
getPluginTelemetrySubscribers();
|
|
159
51
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
52
|
+
const scopeFields = {
|
|
53
|
+
requestId: context?.awsRequestId,
|
|
54
|
+
handlerName: eventName,
|
|
55
|
+
method: eventSummary.method,
|
|
56
|
+
route: eventSummary.route,
|
|
57
|
+
routeKey: eventSummary.routeKey,
|
|
58
|
+
invocation: toScopeInvocation(eventSummary),
|
|
59
|
+
};
|
|
168
60
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
61
|
+
return runInvocationScope(
|
|
62
|
+
scopeFields,
|
|
63
|
+
async () => {
|
|
64
|
+
try {
|
|
65
|
+
log.info('Handler invoked', {
|
|
66
|
+
eventName: 'frigg.handler.invoked',
|
|
67
|
+
});
|
|
174
68
|
|
|
175
|
-
|
|
176
|
-
|
|
69
|
+
// If enabled (i.e. if SECRET_ARN is set in process.env) Fetch secrets from AWS Secrets Manager, and set them as environment variables.
|
|
70
|
+
await secretsToEnv();
|
|
177
71
|
|
|
178
|
-
|
|
179
|
-
|
|
72
|
+
// If enabled (i.e. if SSM_PARAMETER_PREFIX and FRIGG_SSM_OFFLOADED_KEYS are set) fetch offloaded params from SSM Parameter Store into process.env.
|
|
73
|
+
await parametersToEnv();
|
|
180
74
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
75
|
+
// Lazy-required so DB-free handlers never load the Prisma client.
|
|
76
|
+
if (shouldUseDatabase) {
|
|
77
|
+
const { connectPrisma } = require('../database/prisma');
|
|
78
|
+
await connectPrisma();
|
|
79
|
+
}
|
|
186
80
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
81
|
+
// Helps reuse the database connection. Lowers response times.
|
|
82
|
+
context.callbackWaitsForEmptyEventLoop = false;
|
|
83
|
+
|
|
84
|
+
// Run the Lambda
|
|
85
|
+
return await method(event, context);
|
|
86
|
+
} catch (error) {
|
|
87
|
+
// Don't leak implementation details to end users.
|
|
88
|
+
if (isUserFacingResponse) {
|
|
89
|
+
// Allow client-safe errors to pass through with their actual message
|
|
90
|
+
if (error.isClientSafe === true) {
|
|
91
|
+
const statusCode = error.statusCode || 400;
|
|
92
|
+
log.warn('Request rejected', {
|
|
93
|
+
eventName: 'frigg.handler.rejected',
|
|
94
|
+
statusCode,
|
|
95
|
+
error,
|
|
96
|
+
});
|
|
97
|
+
return {
|
|
98
|
+
statusCode,
|
|
99
|
+
body: JSON.stringify({
|
|
100
|
+
error: error.message,
|
|
101
|
+
}),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
log.error('Handler failed', {
|
|
106
|
+
eventName: 'frigg.handler.failed',
|
|
107
|
+
error,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// Hide other errors with generic message
|
|
111
|
+
return {
|
|
112
|
+
statusCode: 500,
|
|
113
|
+
body: JSON.stringify({
|
|
114
|
+
error: 'An Internal Error Occurred',
|
|
115
|
+
}),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
207
118
|
|
|
208
|
-
|
|
209
|
-
return {
|
|
210
|
-
statusCode: 500,
|
|
211
|
-
body: JSON.stringify({
|
|
212
|
-
error: 'An Internal Error Occurred',
|
|
213
|
-
}),
|
|
214
|
-
};
|
|
215
|
-
}
|
|
119
|
+
// Handle server-to-server responses.
|
|
216
120
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
console.warn(
|
|
226
|
-
`[createHandler] ${eventName}: halt error suppressed (no retry)`,
|
|
227
|
-
{
|
|
228
|
-
eventName,
|
|
229
|
-
errorName: error.name,
|
|
230
|
-
errorMessage: error.message,
|
|
231
|
-
statusCode: error.statusCode,
|
|
232
|
-
...eventSummary,
|
|
121
|
+
// Halt errors succeed and won't be retried, so the halt
|
|
122
|
+
// itself is the failure record.
|
|
123
|
+
if (error.isHaltError === true) {
|
|
124
|
+
log.error('Handler halted', {
|
|
125
|
+
eventName: 'frigg.handler.halted',
|
|
126
|
+
error,
|
|
127
|
+
});
|
|
128
|
+
return;
|
|
233
129
|
}
|
|
234
|
-
);
|
|
235
|
-
return;
|
|
236
|
-
}
|
|
237
130
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
131
|
+
log.error('Handler failed', {
|
|
132
|
+
eventName: 'frigg.handler.failed',
|
|
133
|
+
error,
|
|
134
|
+
});
|
|
135
|
+
// The Lambda runtime writes a rethrown error itself, so
|
|
136
|
+
// rethrow a sanitized copy (ADR-048 §6 item 11).
|
|
137
|
+
throw toSanitizedSurrogate(error);
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
telemetry: activeTelemetry,
|
|
142
|
+
usageRollup: activeUsageRollup,
|
|
245
143
|
eventSummary,
|
|
246
|
-
shouldUseDatabase
|
|
247
|
-
|
|
248
|
-
|
|
144
|
+
shouldUseDatabase,
|
|
145
|
+
flushTimeoutMs,
|
|
146
|
+
context,
|
|
147
|
+
}
|
|
148
|
+
);
|
|
249
149
|
};
|
|
250
150
|
};
|
|
251
151
|
|
package/core/index.js
CHANGED
|
@@ -2,5 +2,12 @@ const { Delegate } = require('./Delegate');
|
|
|
2
2
|
const { Worker } = require('./Worker');
|
|
3
3
|
const { loadInstalledModules } = require('./load-installed-modules');
|
|
4
4
|
const { createHandler } = require('./create-handler');
|
|
5
|
+
const { runInvocationScope } = require('./invocation-scope');
|
|
5
6
|
|
|
6
|
-
module.exports = {
|
|
7
|
+
module.exports = {
|
|
8
|
+
Delegate,
|
|
9
|
+
Worker,
|
|
10
|
+
loadInstalledModules,
|
|
11
|
+
createHandler,
|
|
12
|
+
runInvocationScope,
|
|
13
|
+
};
|