@friggframework/core 2.0.0--canary.652.b14f3b5.0 → 2.0.0--canary.654.42e42b2.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.
Files changed (56) hide show
  1. package/CLAUDE.md +13 -12
  2. package/README.md +19 -22
  3. package/core/CLAUDE.md +25 -12
  4. package/core/Worker.js +42 -34
  5. package/core/create-handler.js +99 -197
  6. package/core/index.js +8 -1
  7. package/core/invocation-scope.js +224 -0
  8. package/core/secrets-to-env.js +7 -6
  9. package/database/config.js +0 -7
  10. package/database/encryption/README.md +5 -1
  11. package/database/encryption/encryption-schema-registry.js +3 -0
  12. package/errors/fetch-error.js +48 -65
  13. package/handlers/app-definition-loader.js +53 -2
  14. package/handlers/app-handler-helpers.js +16 -4
  15. package/handlers/database-migration-handler.js +10 -1
  16. package/handlers/routers/websocket.js +26 -6
  17. package/handlers/workers/db-migration.js +12 -1
  18. package/handlers/workers/dlq-processor.js +66 -38
  19. package/index.js +19 -1
  20. package/integrations/integration-base.js +56 -38
  21. package/integrations/integration-router.js +21 -20
  22. package/integrations/use-cases/delete-integration-for-user.js +24 -13
  23. package/jest.config.js +2 -0
  24. package/logs/.eslintrc.json +26 -1
  25. package/logs/__fixtures__/events.js +82 -0
  26. package/logs/__fixtures__/matchers.js +45 -0
  27. package/logs/__fixtures__/secrets.js +22 -0
  28. package/logs/__fixtures__/vectors.js +329 -0
  29. package/logs/__fixtures__/with-env.js +45 -0
  30. package/logs/context.js +92 -0
  31. package/logs/debug-shims.js +49 -0
  32. package/logs/index.js +14 -3
  33. package/logs/jest-logger-setup.js +25 -0
  34. package/logs/levels.js +71 -0
  35. package/logs/logger-runtime.js +142 -0
  36. package/logs/logger.js +132 -42
  37. package/logs/record.js +199 -0
  38. package/logs/redact.js +325 -0
  39. package/logs/serialize.js +322 -0
  40. package/logs/sinks.js +99 -0
  41. package/logs/summarize-event.js +136 -0
  42. package/modules/module.js +32 -19
  43. package/modules/requester/oauth-2.js +28 -22
  44. package/modules/requester/requester.js +69 -6
  45. package/package.json +5 -6
  46. package/queues/queuer-util.js +1 -15
  47. package/telemetry/bind-telemetry-context.js +4 -0
  48. package/telemetry/instrument-handler.js +19 -2
  49. package/telemetry/no-op-telemetry.js +4 -0
  50. package/telemetry/otel-telemetry.js +25 -6
  51. package/telemetry/telemetry-context.js +19 -8
  52. package/telemetry/telemetry-runtime.js +17 -0
  53. package/telemetry/telemetry-service.js +1 -0
  54. package/types/errors/index.d.ts +24 -10
  55. package/types/logs/index.d.ts +61 -2
  56. package/user/repositories/user-repository-documentdb.js +18 -42
@@ -0,0 +1,224 @@
1
+ const { runInContext } = require('../logs/context');
2
+ const { summarizeMessageBody } = require('../logs/summarize-event');
3
+ const { flushSinks, hasFlushableSinks } = require('../logs/logger-runtime');
4
+
5
+ // Bounds the tail latency telemetry adds to every warm invocation. Kept low so
6
+ // an unreachable OTLP endpoint (e.g. a VPC Lambda with no NAT/egress) costs at
7
+ // most this, not multiple seconds. Override with OTEL_FLUSH_TIMEOUT_MS.
8
+ const DEFAULT_FLUSH_TIMEOUT_MS =
9
+ Number(process.env.OTEL_FLUSH_TIMEOUT_MS) || 500;
10
+
11
+ // Time kept back from the Lambda deadline, so a slow flush never becomes a
12
+ // Lambda timeout.
13
+ const FLUSH_MARGIN_MS = 50;
14
+
15
+ // setTimeout fires at once for a delay above this.
16
+ const MAX_TIMER_MS = 2 ** 31 - 1;
17
+
18
+ /**
19
+ * Fold the invocation's buffered usage counters into the durable store, then
20
+ * clear the buffer. On an SQS redelivery (ApproximateReceiveCount > 1) we
21
+ * DISCARD rather than flush — the prior delivery already counted, and the usage
22
+ * accuracy contract is "approximate, skip obvious redeliveries". Fully guarded.
23
+ */
24
+ async function flushUsageRollup(subscriber, eventSummary, shouldUseDatabase) {
25
+ if (!subscriber) return;
26
+ try {
27
+ // Persisting usage requires a DB connection. DB-free handlers (e.g. the
28
+ // webhook-receipt route) never called connectPrisma, so drop the buffer
29
+ // instead of issuing a connectionless Prisma write.
30
+ if (!shouldUseDatabase) {
31
+ subscriber.discard();
32
+ return;
33
+ }
34
+ // Discard only when EVERY record in the batch is a redelivery. The buffer
35
+ // is invocation-scoped (not per-message), so discarding on *any*
36
+ // redelivery would drop the fresh records' counts too (silent
37
+ // under-count). For a mixed batch we flush: preserving fresh counts and
38
+ // at worst re-counting the one redelivered record is strictly better than
39
+ // losing fresh data for an approximate store. (Integration queue workers
40
+ // are batchSize:1 today, so a batch is all-or-nothing; this keeps it
41
+ // correct if batchSize is ever raised.)
42
+ const records = Array.isArray(eventSummary?.records)
43
+ ? eventSummary.records
44
+ : [];
45
+ const allRedelivered =
46
+ records.length > 0 &&
47
+ records.every((r) => Number(r.receiveCount) > 1);
48
+ if (allRedelivered) {
49
+ subscriber.discard();
50
+ } else {
51
+ await subscriber.flush();
52
+ }
53
+ } catch (_) {
54
+ // Usage rollup must never break the handler.
55
+ }
56
+ }
57
+
58
+ function isTelemetryEnabled(telemetry) {
59
+ try {
60
+ return Boolean(
61
+ telemetry &&
62
+ typeof telemetry.isEnabled === 'function' &&
63
+ telemetry.isEnabled()
64
+ );
65
+ } catch (_) {
66
+ return false;
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Flush telemetry before the Lambda container freezes. Because
72
+ * `callbackWaitsForEmptyEventLoop=false` stops the event loop the moment the
73
+ * handler returns, OTel's timer-driven batch processors would never fire — so
74
+ * spans/metrics must be flushed here. No timer of its own: the caller bounds it
75
+ * with `withDeadline`. Fully guarded, so a flush failure never breaks the
76
+ * handler.
77
+ */
78
+ async function flushTelemetry(telemetry, { signal } = {}) {
79
+ if (!isTelemetryEnabled(telemetry) || signal?.aborted) return;
80
+ try {
81
+ await telemetry.forceFlush();
82
+ } catch (_) {
83
+ // Telemetry flush must never break the handler.
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Run `fn(signal)` for at most `ms`. Owns the only timer and clears it when the
89
+ * work settles. Aborts the signal at the deadline. Never rejects, and a late
90
+ * rejection of the work is swallowed.
91
+ */
92
+ function withDeadline(ms, fn) {
93
+ const controller = new AbortController();
94
+ return new Promise((resolve) => {
95
+ let timer;
96
+ const finish = () => {
97
+ clearTimeout(timer);
98
+ resolve();
99
+ };
100
+ timer = setTimeout(() => {
101
+ controller.abort();
102
+ finish();
103
+ }, Math.min(ms, MAX_TIMER_MS));
104
+ let work;
105
+ try {
106
+ work = Promise.resolve(fn(controller.signal));
107
+ } catch (_) {
108
+ work = Promise.resolve();
109
+ }
110
+ work.then(finish, finish);
111
+ });
112
+ }
113
+
114
+ function remainingTimeMs(context) {
115
+ try {
116
+ const remaining = context?.getRemainingTimeInMillis?.();
117
+ return typeof remaining === 'number' ? remaining : Infinity;
118
+ } catch (_) {
119
+ return Infinity;
120
+ }
121
+ }
122
+
123
+ async function flushInvocation({
124
+ telemetry,
125
+ usageRollup,
126
+ eventSummary,
127
+ shouldUseDatabase,
128
+ flushTimeoutMs,
129
+ context,
130
+ }) {
131
+ // ADR-048 §11: usage first and unbounded, then one deadline for the rest.
132
+ await flushUsageRollup(usageRollup, eventSummary, shouldUseDatabase);
133
+
134
+ const flushes = [];
135
+ if (isTelemetryEnabled(telemetry)) {
136
+ flushes.push((signal) => flushTelemetry(telemetry, { signal }));
137
+ }
138
+ if (hasFlushableSinks()) {
139
+ flushes.push((signal) => flushSinks({ signal }));
140
+ }
141
+ if (!flushes.length) return;
142
+
143
+ const deadline = Math.min(
144
+ flushTimeoutMs,
145
+ remainingTimeMs(context) - FLUSH_MARGIN_MS
146
+ );
147
+ if (!(deadline > 0)) return;
148
+ await withDeadline(deadline, (signal) =>
149
+ Promise.allSettled(flushes.map((flush) => flush(signal)))
150
+ );
151
+ }
152
+
153
+ /**
154
+ * Open the logger scope for one invocation, run `fn` inside it, then flush.
155
+ * `fields` (requestId, handlerName, method, route, routeKey, invocation, ...)
156
+ * go into the scope's logger sub-object and never reach the telemetry bus.
157
+ */
158
+ async function runInvocationScope(fields, fn, opts = {}) {
159
+ const {
160
+ telemetry,
161
+ usageRollup,
162
+ eventSummary,
163
+ shouldUseDatabase = true,
164
+ flushTimeoutMs = DEFAULT_FLUSH_TIMEOUT_MS,
165
+ context,
166
+ } = opts;
167
+ return runInContext({ log: { ...(fields || {}) } }, async () => {
168
+ try {
169
+ return await fn();
170
+ } finally {
171
+ try {
172
+ await flushInvocation({
173
+ telemetry,
174
+ usageRollup,
175
+ eventSummary,
176
+ shouldUseDatabase,
177
+ flushTimeoutMs,
178
+ context,
179
+ });
180
+ } catch (_) {
181
+ // A flush must never change the handler result.
182
+ }
183
+ }
184
+ });
185
+ }
186
+
187
+ function toCount(value) {
188
+ const count = Number(value);
189
+ return value !== undefined && value !== null && Number.isFinite(count)
190
+ ? count
191
+ : undefined;
192
+ }
193
+
194
+ /**
195
+ * Open the logger scope for one SQS message: messageId, receiveCount and the
196
+ * Frigg ids the body carries. No flush; the invocation scope owns that.
197
+ */
198
+ function runMessageScope(record, fn) {
199
+ const { event, processId, integrationId } = summarizeMessageBody(
200
+ record?.body
201
+ );
202
+ return runInContext(
203
+ {
204
+ log: {
205
+ messageId: record?.messageId,
206
+ receiveCount: toCount(record?.attributes?.ApproximateReceiveCount),
207
+ integrationEvent: event,
208
+ processId,
209
+ integrationId,
210
+ },
211
+ },
212
+ fn
213
+ );
214
+ }
215
+
216
+ module.exports = {
217
+ runInvocationScope,
218
+ runMessageScope,
219
+ withDeadline,
220
+ flushTelemetry,
221
+ flushUsageRollup,
222
+ FLUSH_MARGIN_MS,
223
+ DEFAULT_FLUSH_TIMEOUT_MS,
224
+ };
@@ -1,5 +1,9 @@
1
+ const { getLogger } = require('../logs');
2
+
3
+ const log = getLogger('frigg.core.secrets');
4
+
1
5
  const getSecretValue = async () => {
2
- console.log('Fetching secrets...');
6
+ log.debug('Fetching secrets', { eventName: 'frigg.core.secrets.fetching' });
3
7
 
4
8
  const httpPort = process.env.PARAMETERS_SECRETS_EXTENSION_HTTP_PORT || 2773;
5
9
  const url = `http://localhost:${httpPort}/secretsmanager/get?secretId=${encodeURIComponent(
@@ -15,10 +19,7 @@ const getSecretValue = async () => {
15
19
  const response = await fetch(url, options);
16
20
 
17
21
  if (!response.ok) {
18
- const json = await response.json().catch((err) => err.message);
19
- console.error('Invalid response - response:', JSON.stringify(response));
20
- console.error('Invalid response - json:', json);
21
- throw new Error(`Invalid ${response.status} response`);
22
+ throw new Error(`Secrets fetch failed with ${response.status}`);
22
23
  }
23
24
 
24
25
  const result = await response.json();
@@ -44,7 +45,7 @@ const secretsToEnv = async () => {
44
45
  if (!process.env.SECRET_ARN) {
45
46
  return;
46
47
  }
47
- console.log('Secrets to env');
48
+ log.debug('Secrets to env', { eventName: 'frigg.core.secrets.to_env' });
48
49
 
49
50
  try {
50
51
  const secrets = await getSecretValue();
@@ -125,16 +125,9 @@ let cachedDbType = null;
125
125
  */
126
126
  const PRISMA_LOG_LEVEL = process.env.PRISMA_LOG_LEVEL || 'error,warn';
127
127
 
128
- /**
129
- * Enable Prisma query logging for performance monitoring
130
- * @type {boolean}
131
- */
132
- const PRISMA_QUERY_LOGGING = process.env.PRISMA_QUERY_LOGGING === 'true';
133
-
134
128
  module.exports = {
135
129
  getDatabaseType, // Export for testing and direct use
136
130
  PRISMA_LOG_LEVEL,
137
- PRISMA_QUERY_LOGGING,
138
131
  };
139
132
 
140
133
  /**
@@ -680,7 +680,11 @@ The encryption system uses **fail-fast error handling**:
680
680
 
681
681
  ### Logging Configuration
682
682
 
683
- Configure log verbosity with `FRIGG_LOG_LEVEL`:
683
+ `FRIGG_LOG_LEVEL` is the one Frigg log level. The level rules, the record
684
+ fields and the redaction rules are in the
685
+ [Logging guide](../../../../docs/guides/LOGGING.md). The encryption logger
686
+ still writes text lines through `console` with the same level names until it
687
+ moves to the structured logger (ADR-048, incremental adoption).
684
688
 
685
689
  ```bash
686
690
  # Production (minimal logging)
@@ -9,6 +9,7 @@
9
9
  */
10
10
 
11
11
  const { logger } = require('./logger');
12
+ const { addDeniedKeys } = require('../../logs/redact');
12
13
 
13
14
  /**
14
15
  * Core encryption schema (immutable - cannot be overridden by custom schemas)
@@ -120,6 +121,7 @@ function registerCustomSchema(schema) {
120
121
  }
121
122
 
122
123
  customSchema = { ...schema };
124
+ addDeniedKeys(Object.values(schema).flatMap((config) => config.fields));
123
125
  logger.info(
124
126
  `Registered custom encryption schema for models: ${Object.keys(customSchema).join(', ')}`
125
127
  );
@@ -149,6 +151,7 @@ function extractCredentialFieldsFromModules(moduleDefinitions) {
149
151
  }
150
152
  }
151
153
 
154
+ addDeniedKeys(fields);
152
155
  return [...new Set(fields)];
153
156
  }
154
157
 
@@ -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,53 @@ 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 { addDeniedKeys } = require('../logs/redact');
6
+ const { getLogger } = require('../logs');
7
+ const {
8
+ extractCredentialFieldsFromModules,
9
+ } = require('../database/encryption/encryption-schema-registry');
10
+ const {
11
+ getModulesDefinitionFromIntegrationClasses,
12
+ } = require('../integrations/utils/map-integration-dto');
13
+
14
+ // Handlers without a database never load the encryption registry, so the
15
+ // credential leaf keys are registered for redaction here too.
16
+ const warnedIgnoredKeys = new Set();
17
+
18
+ function warnIgnoredKeys(ignored) {
19
+ for (const key of ignored) {
20
+ if (warnedIgnoredKeys.has(key)) continue;
21
+ warnedIgnoredKeys.add(key);
22
+ getLogger('frigg.logger').warn(
23
+ 'Credential field name is a record field; it stays visible in logs',
24
+ { eventName: 'frigg.logger.denied_key_ignored', key }
25
+ );
26
+ }
27
+ }
28
+
29
+ function registerCredentialLogKeys(appDefinition, integrations) {
30
+ try {
31
+ const fields = extractCredentialFieldsFromModules(
32
+ getModulesDefinitionFromIntegrationClasses(integrations)
33
+ );
34
+ warnIgnoredKeys(addDeniedKeys(fields));
35
+ } catch {
36
+ // An odd integration shape must not stop the app from loading.
37
+ }
38
+ const schema = appDefinition.encryption?.schema;
39
+ if (schema && typeof schema === 'object') {
40
+ for (const config of Object.values(schema)) {
41
+ warnIgnoredKeys(addDeniedKeys(config?.fields));
42
+ }
43
+ }
44
+ }
5
45
 
6
46
  /**
7
47
  * Loads the App definition from the nearest backend package
8
48
  * @function loadAppDefinition
9
49
  * @description Searches for the nearest backend package.json, loads the corresponding index.js file,
10
50
  * 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.
51
+ * @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
52
  * @throws {Error} Throws error if backend package.json cannot be found.
13
53
  * @throws {Error} Throws error if index.js file cannot be found in the backend directory.
14
54
  * @example
@@ -36,8 +76,11 @@ function loadAppDefinition() {
36
76
  adminScripts = [],
37
77
  reports = [],
38
78
  admin = {},
79
+ logging = null,
39
80
  } = appDefinition;
40
81
 
82
+ registerCredentialLogKeys(appDefinition, integrations);
83
+
41
84
  // Degrade consistently: an invalid telemetry block must never take down a
42
85
  // router bundle that loads the app definition at module scope (telemetry is
43
86
  // never allowed to break a handler). The singletons apply the same
@@ -59,7 +102,15 @@ function loadAppDefinition() {
59
102
  };
60
103
  }
61
104
 
62
- return { integrations, userConfig, adminScripts, reports, admin, telemetry };
105
+ return {
106
+ integrations,
107
+ userConfig,
108
+ adminScripts,
109
+ reports,
110
+ admin,
111
+ telemetry,
112
+ logging,
113
+ };
63
114
  }
64
115
 
65
116
  module.exports = {
@@ -1,10 +1,13 @@
1
- const { createHandler, flushDebugLog } = require('@friggframework/core');
1
+ const { createHandler } = require('@friggframework/core');
2
+ const { getLogger } = require('../logs');
2
3
  const express = require('express');
3
4
  const bodyParser = require('body-parser');
4
5
  const cors = require('cors');
5
6
  const Boom = require('@hapi/boom');
6
7
  const serverlessHttp = require('serverless-http');
7
8
 
9
+ const log = getLogger('frigg.http');
10
+
8
11
  const createApp = (applyMiddleware) => {
9
12
  const app = express();
10
13
 
@@ -21,7 +24,7 @@ const createApp = (applyMiddleware) => {
21
24
 
22
25
  if (applyMiddleware) applyMiddleware(app);
23
26
 
24
- // Handle sending error response and logging server errors to console
27
+ // The express boundary: send the error response and log it one time.
25
28
  app.use((err, req, res, next) => {
26
29
  const boomError = err.isBoom ? err : Boom.boomify(err);
27
30
  const {
@@ -29,10 +32,19 @@ const createApp = (applyMiddleware) => {
29
32
  } = boomError;
30
33
 
31
34
  if (statusCode >= 500) {
32
- flushDebugLog(boomError);
35
+ log.error('Request failed', {
36
+ eventName: 'frigg.http.request_failed',
37
+ statusCode,
38
+ error: boomError,
39
+ });
33
40
  res.status(statusCode).json({ error: 'Internal Server Error' });
34
41
  } else {
35
- console.warn(`[Frigg] ${req.method} ${req.path} -> ${statusCode}: ${err.message}`);
42
+ // A client error needs no stack; the logger scrubs the reason.
43
+ log.warn('Request rejected', {
44
+ eventName: 'frigg.http.request_rejected',
45
+ statusCode,
46
+ reason: boomError.message,
47
+ });
36
48
  res.status(statusCode).json({ error: err.message });
37
49
  }
38
50
  });
@@ -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,
@@ -1,5 +1,8 @@
1
1
  const { createHandler } = require('@friggframework/core');
2
2
  const { createWebsocketConnectionRepository } = require('../../database/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' };