@friggframework/core 2.0.0--canary.654.5821f44.0 → 2.0.0--canary.654.e9530b8.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/core/create-handler.js +7 -6
- package/core/invocation-scope.js +12 -15
- package/logs/context.js +17 -7
- package/logs/contract-keys.js +67 -0
- package/logs/logger-runtime.js +6 -11
- package/logs/logger.js +15 -13
- package/logs/record.js +105 -95
- package/logs/redact.js +30 -35
- package/logs/serialize.js +21 -14
- package/logs/sinks.js +16 -22
- package/package.json +5 -5
package/core/create-handler.js
CHANGED
|
@@ -6,7 +6,7 @@ const { getLogger, toSanitizedSurrogate } = require('../logs');
|
|
|
6
6
|
const {
|
|
7
7
|
summarizeLambdaEvent,
|
|
8
8
|
toScopeInvocation,
|
|
9
|
-
|
|
9
|
+
toRequestDetails,
|
|
10
10
|
} = require('../logs/summarize-event');
|
|
11
11
|
const {
|
|
12
12
|
runInvocationScope,
|
|
@@ -58,8 +58,9 @@ const createHandler = (optionByName = {}) => {
|
|
|
58
58
|
routeKey: eventSummary.routeKey,
|
|
59
59
|
invocation: toScopeInvocation(eventSummary),
|
|
60
60
|
};
|
|
61
|
-
//
|
|
62
|
-
|
|
61
|
+
// Path, query keys and header names go only on the entry and failure
|
|
62
|
+
// records; the logger adds them to the scope's invocation.
|
|
63
|
+
const requestDetails = toRequestDetails(eventSummary);
|
|
63
64
|
|
|
64
65
|
return runInvocationScope(
|
|
65
66
|
scopeFields,
|
|
@@ -67,7 +68,7 @@ const createHandler = (optionByName = {}) => {
|
|
|
67
68
|
try {
|
|
68
69
|
log.info('Handler invoked', {
|
|
69
70
|
eventName: 'frigg.handler.invoked',
|
|
70
|
-
invocation:
|
|
71
|
+
invocation: requestDetails,
|
|
71
72
|
});
|
|
72
73
|
|
|
73
74
|
// If enabled (i.e. if SECRET_ARN is set in process.env) Fetch secrets from AWS Secrets Manager, and set them as environment variables.
|
|
@@ -109,7 +110,7 @@ const createHandler = (optionByName = {}) => {
|
|
|
109
110
|
if (!isUserFacingResponse && error.isHaltError === true) {
|
|
110
111
|
log.error('Handler halted', {
|
|
111
112
|
eventName: 'frigg.handler.halted',
|
|
112
|
-
invocation:
|
|
113
|
+
invocation: requestDetails,
|
|
113
114
|
error,
|
|
114
115
|
});
|
|
115
116
|
return;
|
|
@@ -117,7 +118,7 @@ const createHandler = (optionByName = {}) => {
|
|
|
117
118
|
|
|
118
119
|
log.error('Handler failed', {
|
|
119
120
|
eventName: 'frigg.handler.failed',
|
|
120
|
-
invocation:
|
|
121
|
+
invocation: requestDetails,
|
|
121
122
|
error,
|
|
122
123
|
});
|
|
123
124
|
|
package/core/invocation-scope.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const { runInContext } = require('../logs/context');
|
|
1
|
+
const { runInContext, LOGGER_SCOPE_KEY } = require('../logs/context');
|
|
2
2
|
const { summarizeMessageBody } = require('../logs/summarize-event');
|
|
3
3
|
const { flushSinks, hasFlushableSinks } = require('../logs/logger-runtime');
|
|
4
4
|
|
|
@@ -164,22 +164,19 @@ async function runInvocationScope(fields, fn, opts = {}) {
|
|
|
164
164
|
flushTimeoutMs = DEFAULT_FLUSH_TIMEOUT_MS,
|
|
165
165
|
context,
|
|
166
166
|
} = opts;
|
|
167
|
-
return runInContext({
|
|
167
|
+
return runInContext({ [LOGGER_SCOPE_KEY]: { ...(fields || {}) } }, async () => {
|
|
168
168
|
try {
|
|
169
169
|
return await fn();
|
|
170
170
|
} finally {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
} catch (_) {
|
|
181
|
-
// A flush must never change the handler result.
|
|
182
|
-
}
|
|
171
|
+
// Every step is guarded, so a flush never changes the result.
|
|
172
|
+
await flushInvocation({
|
|
173
|
+
telemetry,
|
|
174
|
+
usageRollup,
|
|
175
|
+
eventSummary,
|
|
176
|
+
shouldUseDatabase,
|
|
177
|
+
flushTimeoutMs,
|
|
178
|
+
context,
|
|
179
|
+
});
|
|
183
180
|
}
|
|
184
181
|
});
|
|
185
182
|
}
|
|
@@ -201,7 +198,7 @@ function runMessageScope(record, fn) {
|
|
|
201
198
|
);
|
|
202
199
|
return runInContext(
|
|
203
200
|
{
|
|
204
|
-
|
|
201
|
+
[LOGGER_SCOPE_KEY]: {
|
|
205
202
|
messageId: record?.messageId,
|
|
206
203
|
receiveCount: toCount(record?.attributes?.ApproximateReceiveCount),
|
|
207
204
|
integrationEvent: event,
|
package/logs/context.js
CHANGED
|
@@ -64,15 +64,24 @@ function getContext() {
|
|
|
64
64
|
return als().getStore() ?? null;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
// The store is frozen per scope, so one scope object per store is enough.
|
|
68
|
+
const EMPTY_SCOPE = Object.freeze({});
|
|
69
|
+
const scopeByStore = new WeakMap();
|
|
70
|
+
|
|
67
71
|
function getLoggerScope() {
|
|
68
72
|
const store = getContext();
|
|
69
|
-
if (!store) return
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
73
|
+
if (!store) return EMPTY_SCOPE;
|
|
74
|
+
let scope = scopeByStore.get(store);
|
|
75
|
+
if (!scope) {
|
|
76
|
+
scope = {};
|
|
77
|
+
for (const key of INTEGRATION_KEYS) {
|
|
78
|
+
if (store[key] !== undefined) scope[key] = store[key];
|
|
79
|
+
}
|
|
80
|
+
if (isPlainObject(store[LOGGER_SCOPE_KEY])) {
|
|
81
|
+
Object.assign(scope, store[LOGGER_SCOPE_KEY]);
|
|
82
|
+
}
|
|
83
|
+
scope = Object.freeze(scope);
|
|
84
|
+
scopeByStore.set(store, scope);
|
|
76
85
|
}
|
|
77
86
|
return scope;
|
|
78
87
|
}
|
|
@@ -89,4 +98,5 @@ module.exports = {
|
|
|
89
98
|
getContext,
|
|
90
99
|
getLoggerScope,
|
|
91
100
|
hasOnlyLoggerKeys,
|
|
101
|
+
isPlainObject,
|
|
92
102
|
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// Field names of the record contract (ADR-048 §3). A leaf: no requires.
|
|
2
|
+
|
|
3
|
+
const REQUIRED_KEYS = ['timestamp', 'level', 'message', 'logger'];
|
|
4
|
+
const RESOURCE_KEYS = ['appName', 'stage'];
|
|
5
|
+
const TRACE_KEYS = ['trace_id', 'span_id', 'trace_flags'];
|
|
6
|
+
const DROPPED_KEYS_FIELD = 'droppedKeys';
|
|
7
|
+
const OWNED_KEYS = new Set([
|
|
8
|
+
...REQUIRED_KEYS,
|
|
9
|
+
...RESOURCE_KEYS,
|
|
10
|
+
...TRACE_KEYS,
|
|
11
|
+
DROPPED_KEYS_FIELD,
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
// Set by the scope builders (logs/context.js, core/invocation-scope.js,
|
|
15
|
+
// core/create-handler.js) and the Module and IntegrationBase bindings.
|
|
16
|
+
const SCOPE_KEYS = [
|
|
17
|
+
'integrationId',
|
|
18
|
+
'integrationType',
|
|
19
|
+
'userId',
|
|
20
|
+
'version',
|
|
21
|
+
'entityId',
|
|
22
|
+
'credentialId',
|
|
23
|
+
'requestId',
|
|
24
|
+
'messageId',
|
|
25
|
+
'receiveCount',
|
|
26
|
+
'processId',
|
|
27
|
+
'integrationEvent',
|
|
28
|
+
'handlerName',
|
|
29
|
+
'method',
|
|
30
|
+
'route',
|
|
31
|
+
'routeKey',
|
|
32
|
+
'invocation',
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
const CONTRACT_KEYS = [
|
|
36
|
+
...new Set([...OWNED_KEYS, ...SCOPE_KEYS, 'eventName', 'statusCode', 'error']),
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
const RESERVED_KEYS = new Set([
|
|
40
|
+
'tenantId',
|
|
41
|
+
'type',
|
|
42
|
+
'time',
|
|
43
|
+
'record',
|
|
44
|
+
'errorType',
|
|
45
|
+
'errorMessage',
|
|
46
|
+
'stackTrace',
|
|
47
|
+
'service',
|
|
48
|
+
'env',
|
|
49
|
+
'host',
|
|
50
|
+
'source',
|
|
51
|
+
'status',
|
|
52
|
+
'severity',
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
const PAYLOAD_KEYS = new Set(['body', 'rawBody', 'payload', 'response']);
|
|
56
|
+
|
|
57
|
+
module.exports = {
|
|
58
|
+
REQUIRED_KEYS,
|
|
59
|
+
RESOURCE_KEYS,
|
|
60
|
+
TRACE_KEYS,
|
|
61
|
+
DROPPED_KEYS_FIELD,
|
|
62
|
+
OWNED_KEYS,
|
|
63
|
+
SCOPE_KEYS,
|
|
64
|
+
CONTRACT_KEYS,
|
|
65
|
+
RESERVED_KEYS,
|
|
66
|
+
PAYLOAD_KEYS,
|
|
67
|
+
};
|
package/logs/logger-runtime.js
CHANGED
|
@@ -69,16 +69,11 @@ function hasFlushableSinks() {
|
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
async function flushSinks({ signal } = {}) {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
} catch (error) {
|
|
78
|
-
return Promise.reject(error);
|
|
79
|
-
}
|
|
80
|
-
});
|
|
81
|
-
await Promise.allSettled(flushes);
|
|
72
|
+
await Promise.allSettled(
|
|
73
|
+
getSinks()
|
|
74
|
+
.filter((sink) => typeof sink.flush === 'function')
|
|
75
|
+
.map(async (sink) => sink.flush({ signal }))
|
|
76
|
+
);
|
|
82
77
|
}
|
|
83
78
|
|
|
84
79
|
function setSpanContextProvider(provider) {
|
|
@@ -117,7 +112,7 @@ function resetLoggerForTests({ level, sinks, trackViolations } = {}) {
|
|
|
117
112
|
const s = state();
|
|
118
113
|
s.config = null;
|
|
119
114
|
s.levelOverride = parseLevel(level);
|
|
120
|
-
|
|
115
|
+
setSinks(sinks);
|
|
121
116
|
s.spanContextProvider = null;
|
|
122
117
|
s.warned.clear();
|
|
123
118
|
s.violations.length = 0;
|
package/logs/logger.js
CHANGED
|
@@ -5,31 +5,33 @@ const { buildRecord, DEFAULT_LOGGER_NAME } = require('./record');
|
|
|
5
5
|
|
|
6
6
|
const LOGGER_OWN_NAME = 'frigg.logger';
|
|
7
7
|
|
|
8
|
-
function copyBindings(bindings) {
|
|
9
|
-
|
|
10
|
-
if (!bindings || typeof bindings !== 'object') return copy;
|
|
8
|
+
function copyBindings(bindings, into = {}) {
|
|
9
|
+
if (!bindings || typeof bindings !== 'object') return into;
|
|
11
10
|
for (const key of Object.keys(bindings)) {
|
|
12
11
|
try {
|
|
13
|
-
|
|
12
|
+
into[key] = bindings[key];
|
|
14
13
|
} catch {
|
|
15
14
|
// A throwing getter drops only that binding.
|
|
16
15
|
}
|
|
17
16
|
}
|
|
18
|
-
return
|
|
17
|
+
return into;
|
|
19
18
|
}
|
|
20
19
|
|
|
20
|
+
// One plain object per record. Static sources were copied at child() time.
|
|
21
21
|
function evaluateBindings(sources) {
|
|
22
22
|
const merged = {};
|
|
23
23
|
for (const source of sources) {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
24
|
+
if (typeof source !== 'function') {
|
|
25
|
+
Object.assign(merged, source);
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
let value;
|
|
29
|
+
try {
|
|
30
|
+
value = source();
|
|
31
|
+
} catch {
|
|
32
|
+
continue;
|
|
31
33
|
}
|
|
32
|
-
|
|
34
|
+
copyBindings(value, merged);
|
|
33
35
|
}
|
|
34
36
|
return merged;
|
|
35
37
|
}
|
package/logs/record.js
CHANGED
|
@@ -1,42 +1,17 @@
|
|
|
1
1
|
const { LEVELS } = require('./levels');
|
|
2
|
-
const { serializeValue, serializeError } = require('./serialize');
|
|
2
|
+
const { serializeValue, serializeError, isError } = require('./serialize');
|
|
3
|
+
const { isPlainObject } = require('./context');
|
|
4
|
+
const {
|
|
5
|
+
RESOURCE_KEYS,
|
|
6
|
+
DROPPED_KEYS_FIELD,
|
|
7
|
+
OWNED_KEYS,
|
|
8
|
+
RESERVED_KEYS,
|
|
9
|
+
PAYLOAD_KEYS,
|
|
10
|
+
} = require('./contract-keys');
|
|
3
11
|
|
|
4
12
|
const MAX_RECORD_BYTES = 16 * 1024;
|
|
5
|
-
const REQUIRED_KEYS = ['timestamp', 'level', 'message', 'logger'];
|
|
6
|
-
const RESOURCE_KEYS = ['appName', 'stage'];
|
|
7
|
-
const TRACE_KEYS = ['trace_id', 'span_id', 'trace_flags'];
|
|
8
|
-
const DROPPED_KEYS_FIELD = 'droppedKeys';
|
|
9
|
-
const OWNED_KEYS = new Set([
|
|
10
|
-
...REQUIRED_KEYS,
|
|
11
|
-
...RESOURCE_KEYS,
|
|
12
|
-
...TRACE_KEYS,
|
|
13
|
-
DROPPED_KEYS_FIELD,
|
|
14
|
-
]);
|
|
15
|
-
const RESERVED_KEYS = new Set([
|
|
16
|
-
'tenantId',
|
|
17
|
-
'type',
|
|
18
|
-
'time',
|
|
19
|
-
'record',
|
|
20
|
-
'errorType',
|
|
21
|
-
'errorMessage',
|
|
22
|
-
'stackTrace',
|
|
23
|
-
'service',
|
|
24
|
-
'env',
|
|
25
|
-
'host',
|
|
26
|
-
'source',
|
|
27
|
-
'status',
|
|
28
|
-
'severity',
|
|
29
|
-
]);
|
|
30
|
-
const PAYLOAD_KEYS = new Set(['body', 'rawBody', 'payload', 'response']);
|
|
31
13
|
const DEFAULT_LOGGER_NAME = 'frigg.unknown';
|
|
32
14
|
|
|
33
|
-
function isError(value) {
|
|
34
|
-
return (
|
|
35
|
-
value instanceof Error ||
|
|
36
|
-
Object.prototype.toString.call(value) === '[object Error]'
|
|
37
|
-
);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
15
|
function deepFreeze(value) {
|
|
41
16
|
if (value && typeof value === 'object' && !Object.isFrozen(value)) {
|
|
42
17
|
for (const key of Object.keys(value)) deepFreeze(value[key]);
|
|
@@ -64,39 +39,31 @@ function readFields(source) {
|
|
|
64
39
|
return result;
|
|
65
40
|
}
|
|
66
41
|
|
|
42
|
+
// Returns the message text. An Error message is serialized once; when the
|
|
43
|
+
// call site has no `error`, that result becomes the record's `error`.
|
|
67
44
|
function buildMessage(message, callSite) {
|
|
68
45
|
if (typeof message === 'string') {
|
|
69
|
-
return serializeValue(message);
|
|
46
|
+
return { text: serializeValue(message) };
|
|
70
47
|
}
|
|
71
|
-
if (message === undefined || message === null) return '';
|
|
48
|
+
if (message === undefined || message === null) return { text: '' };
|
|
72
49
|
if (isError(message)) {
|
|
73
50
|
const error = serializeError(message);
|
|
74
|
-
|
|
75
|
-
|
|
51
|
+
const text = typeof error?.message === 'string' ? error.message : '';
|
|
52
|
+
if (callSite.error !== undefined) return { text };
|
|
53
|
+
callSite.error = message;
|
|
54
|
+
return { text, error };
|
|
76
55
|
}
|
|
77
56
|
if (callSite.value === undefined) callSite.value = message;
|
|
78
|
-
return '[non-string message]';
|
|
57
|
+
return { text: '[non-string message]' };
|
|
79
58
|
}
|
|
80
59
|
|
|
81
60
|
const INVOCATION_KEY = 'invocation';
|
|
82
61
|
|
|
83
|
-
function isPlainObject(value) {
|
|
84
|
-
return (
|
|
85
|
-
value !== null &&
|
|
86
|
-
typeof value === 'object' &&
|
|
87
|
-
Object.getPrototypeOf(value) === Object.prototype
|
|
88
|
-
);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
62
|
// A boundary adds request detail to the scope's invocation; scope keys win.
|
|
92
|
-
function extendInvocation(scoped, detail
|
|
63
|
+
function extendInvocation(scoped, detail) {
|
|
93
64
|
const merged = { ...scoped };
|
|
94
|
-
for (const
|
|
95
|
-
if (!
|
|
96
|
-
merged[key] = value;
|
|
97
|
-
} else if (JSON.stringify(scoped[key]) !== JSON.stringify(value)) {
|
|
98
|
-
dropped.add(`${INVOCATION_KEY}.${key}`);
|
|
99
|
-
}
|
|
65
|
+
for (const key of Object.keys(detail)) {
|
|
66
|
+
if (!(key in merged)) merged[key] = detail[key];
|
|
100
67
|
}
|
|
101
68
|
return merged;
|
|
102
69
|
}
|
|
@@ -121,6 +88,60 @@ function byteLength(record) {
|
|
|
121
88
|
return Buffer.byteLength(JSON.stringify(record));
|
|
122
89
|
}
|
|
123
90
|
|
|
91
|
+
function isDroppedKey(key, dropPayloads) {
|
|
92
|
+
return (
|
|
93
|
+
OWNED_KEYS.has(key) ||
|
|
94
|
+
RESERVED_KEYS.has(key) ||
|
|
95
|
+
(dropPayloads && PAYLOAD_KEYS.has(key))
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Filters and serializes one source. `error` is serialized on its own, once
|
|
100
|
+
// (or taken from `preSerializedError`), and never walked by serializeValue.
|
|
101
|
+
function prepareSource(source, dropPayloads, preSerializedError) {
|
|
102
|
+
const accepted = {};
|
|
103
|
+
const keys = [];
|
|
104
|
+
const dropped = [];
|
|
105
|
+
let rawError;
|
|
106
|
+
for (const [key, value] of Object.entries(source)) {
|
|
107
|
+
if (value === undefined || value === null) continue;
|
|
108
|
+
if (isDroppedKey(key, dropPayloads)) {
|
|
109
|
+
dropped.push(key);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
keys.push(key);
|
|
113
|
+
if (key === 'error') rawError = value;
|
|
114
|
+
else accepted[key] = value;
|
|
115
|
+
}
|
|
116
|
+
// One pass over the object, so key rules (denied keys, digests,
|
|
117
|
+
// headers, OAuth shapes) apply to top-level fields too.
|
|
118
|
+
const safe = serializeValue(accepted);
|
|
119
|
+
const entries = [];
|
|
120
|
+
for (const key of keys) {
|
|
121
|
+
const value =
|
|
122
|
+
key === 'error'
|
|
123
|
+
? preSerializedError ?? serializeError(rawError)
|
|
124
|
+
: safe?.[key];
|
|
125
|
+
if (value !== undefined && value !== null) entries.push([key, value]);
|
|
126
|
+
}
|
|
127
|
+
return { entries, dropped };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// The scope object is frozen and shared by every record of one scope.
|
|
131
|
+
const preparedScopes = new WeakMap();
|
|
132
|
+
|
|
133
|
+
function prepareScope(scope, dropPayloads) {
|
|
134
|
+
if (!scope || typeof scope !== 'object') return { entries: [], dropped: [] };
|
|
135
|
+
let byLevel = preparedScopes.get(scope);
|
|
136
|
+
if (!byLevel) {
|
|
137
|
+
byLevel = {};
|
|
138
|
+
preparedScopes.set(scope, byLevel);
|
|
139
|
+
}
|
|
140
|
+
const slot = dropPayloads ? 'info' : 'debug';
|
|
141
|
+
if (!byLevel[slot]) byLevel[slot] = prepareSource(scope, dropPayloads);
|
|
142
|
+
return byLevel[slot];
|
|
143
|
+
}
|
|
144
|
+
|
|
124
145
|
function buildRecord({
|
|
125
146
|
level,
|
|
126
147
|
logger,
|
|
@@ -135,10 +156,11 @@ function buildRecord({
|
|
|
135
156
|
const dropPayloads = LEVELS[level] >= LEVELS.INFO;
|
|
136
157
|
// log.error('msg', err): own props of an Error (axios config, request) must not become fields.
|
|
137
158
|
const callSite = readFields(isError(fields) ? { error: fields } : fields);
|
|
159
|
+
const built = buildMessage(message, callSite);
|
|
138
160
|
const record = {
|
|
139
161
|
timestamp: now.toISOString(),
|
|
140
162
|
level,
|
|
141
|
-
message:
|
|
163
|
+
message: built.text,
|
|
142
164
|
logger: typeof logger === 'string' && logger ? logger : DEFAULT_LOGGER_NAME,
|
|
143
165
|
};
|
|
144
166
|
for (const key of RESOURCE_KEYS) {
|
|
@@ -150,52 +172,37 @@ function buildRecord({
|
|
|
150
172
|
|
|
151
173
|
const dropped = new Set();
|
|
152
174
|
const callSiteKeys = [];
|
|
153
|
-
const place = (
|
|
154
|
-
const
|
|
155
|
-
for (const [key, value] of
|
|
156
|
-
if (
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
}
|
|
167
|
-
accepted[key] = value;
|
|
168
|
-
}
|
|
169
|
-
// One pass over the object, so key rules (denied keys, digests,
|
|
170
|
-
// headers, OAuth shapes) apply to top-level fields too.
|
|
171
|
-
const safe = serializeValue(accepted);
|
|
172
|
-
for (const key of Object.keys(accepted)) {
|
|
173
|
-
const value =
|
|
174
|
-
key === 'error' ? serializeError(accepted[key]) : safe?.[key];
|
|
175
|
-
if (value === undefined || value === null) continue;
|
|
176
|
-
if (key === INVOCATION_KEY && isPlainObject(record[key])) {
|
|
177
|
-
record[key] = extendInvocation(record[key], value, dropped);
|
|
175
|
+
const place = ({ entries, dropped: droppedHere }, isCallSite) => {
|
|
176
|
+
for (const key of droppedHere) dropped.add(key);
|
|
177
|
+
for (const [key, value] of entries) {
|
|
178
|
+
if (Object.prototype.hasOwnProperty.call(record, key)) {
|
|
179
|
+
if (
|
|
180
|
+
key === INVOCATION_KEY &&
|
|
181
|
+
isPlainObject(record[key]) &&
|
|
182
|
+
isPlainObject(value)
|
|
183
|
+
) {
|
|
184
|
+
record[key] = extendInvocation(record[key], value);
|
|
185
|
+
} else {
|
|
186
|
+
dropped.add(key);
|
|
187
|
+
}
|
|
178
188
|
continue;
|
|
179
189
|
}
|
|
180
190
|
record[key] = value;
|
|
181
191
|
if (isCallSite) callSiteKeys.push(key);
|
|
182
192
|
}
|
|
183
193
|
};
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
place(readFields(bindings), false);
|
|
190
|
-
place(callSite, true);
|
|
191
|
-
|
|
192
|
-
const measure = () => {
|
|
194
|
+
place(prepareScope(scope, dropPayloads), false);
|
|
195
|
+
place(prepareSource(bindings || {}, dropPayloads), false);
|
|
196
|
+
place(prepareSource(callSite, dropPayloads, built.error), true);
|
|
197
|
+
|
|
198
|
+
const setDroppedKeys = () => {
|
|
193
199
|
delete record[DROPPED_KEYS_FIELD];
|
|
194
200
|
if (dropped.size) record[DROPPED_KEYS_FIELD] = [...dropped];
|
|
195
|
-
return byteLength(record);
|
|
196
201
|
};
|
|
202
|
+
setDroppedKeys();
|
|
203
|
+
let size = byteLength(record);
|
|
197
204
|
|
|
198
|
-
if (
|
|
205
|
+
if (size > MAX_RECORD_BYTES) {
|
|
199
206
|
const bySize = callSiteKeys
|
|
200
207
|
.filter((key) => key !== 'error')
|
|
201
208
|
.map((key) => [key, Buffer.byteLength(JSON.stringify(record[key]))])
|
|
@@ -203,27 +210,30 @@ function buildRecord({
|
|
|
203
210
|
for (const [key] of bySize) {
|
|
204
211
|
delete record[key];
|
|
205
212
|
dropped.add(key);
|
|
206
|
-
|
|
213
|
+
setDroppedKeys();
|
|
214
|
+
size = byteLength(record);
|
|
215
|
+
if (size <= MAX_RECORD_BYTES) break;
|
|
207
216
|
}
|
|
208
217
|
}
|
|
209
218
|
for (const part of ['stack', 'cause']) {
|
|
210
219
|
if (
|
|
220
|
+
size > MAX_RECORD_BYTES &&
|
|
211
221
|
record.error &&
|
|
212
222
|
typeof record.error === 'object' &&
|
|
213
|
-
record.error[part] !== undefined
|
|
214
|
-
measure() > MAX_RECORD_BYTES
|
|
223
|
+
record.error[part] !== undefined
|
|
215
224
|
) {
|
|
216
225
|
const { [part]: _removed, ...rest } = record.error;
|
|
217
226
|
record.error = rest;
|
|
227
|
+
size = byteLength(record);
|
|
218
228
|
}
|
|
219
229
|
}
|
|
220
|
-
measure();
|
|
221
230
|
|
|
222
231
|
return deepFreeze(record);
|
|
223
232
|
}
|
|
224
233
|
|
|
225
234
|
module.exports = {
|
|
226
235
|
buildRecord,
|
|
236
|
+
deepFreeze,
|
|
227
237
|
MAX_RECORD_BYTES,
|
|
228
238
|
RESERVED_KEYS,
|
|
229
239
|
PAYLOAD_KEYS,
|
package/logs/redact.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const { CONTRACT_KEYS } = require('./contract-keys');
|
|
2
|
+
|
|
1
3
|
const DENIED_SUFFIXES = [
|
|
2
4
|
'token',
|
|
3
5
|
'secret',
|
|
@@ -27,27 +29,9 @@ const deniedKeys = new Set([
|
|
|
27
29
|
|
|
28
30
|
// Record-contract fields. A credential field with one of these names must not
|
|
29
31
|
// blank the field in every record.
|
|
30
|
-
const PROTECTED_KEYS = new Set(
|
|
31
|
-
[
|
|
32
|
-
'integrationId',
|
|
33
|
-
'integrationType',
|
|
34
|
-
'userId',
|
|
35
|
-
'version',
|
|
36
|
-
'entityId',
|
|
37
|
-
'credentialId',
|
|
38
|
-
'requestId',
|
|
39
|
-
'messageId',
|
|
40
|
-
'processId',
|
|
41
|
-
'integrationEvent',
|
|
42
|
-
'eventName',
|
|
43
|
-
'handlerName',
|
|
44
|
-
'method',
|
|
45
|
-
'route',
|
|
46
|
-
'routeKey',
|
|
47
|
-
'statusCode',
|
|
48
|
-
].map((key) => key.toLowerCase())
|
|
49
|
-
);
|
|
32
|
+
const PROTECTED_KEYS = new Set(CONTRACT_KEYS.map(normalizeKey));
|
|
50
33
|
|
|
34
|
+
const DENIED_SUFFIX = new RegExp(`(?:${DENIED_SUFFIXES.join('|')})$`);
|
|
51
35
|
const PAIR_KEYS = new Set(['code', 'codeverifier']);
|
|
52
36
|
const DIGEST_KEY = /(sha(1|256)|hash|digest|checksum)$/;
|
|
53
37
|
|
|
@@ -71,7 +55,6 @@ const JSON_PAIR = /"([^"\\]{1,100})"(\s*:\s*)"((?:[^"\\]|\\.)*)"/g;
|
|
|
71
55
|
const TEXT_PAIR = /(?<![A-Za-z0-9_.-])([A-Za-z0-9_.-]{1,100})=([^\s&"'<>,;)]+)/g;
|
|
72
56
|
const HEX_RUN = /(?<![A-Za-z0-9])[0-9a-fA-F]{40,}(?![A-Za-z0-9])/g;
|
|
73
57
|
const BASE64_RUN = /(?<![A-Za-z0-9+/=_-])[A-Za-z0-9+/_-]{40,}={0,2}(?![A-Za-z0-9+/=_-])/g;
|
|
74
|
-
const BASE64_SEGMENT_RUN = /(?<![A-Za-z0-9+=_-])[A-Za-z0-9+_-]{40,}={0,2}(?![A-Za-z0-9+=_-])/g;
|
|
75
58
|
const FORM_TEXT = /^[A-Za-z0-9_.%[\]-]+=[^&\s]*(?:&[A-Za-z0-9_.%[\]-]+=[^&\s]*)+$/;
|
|
76
59
|
const PATH_SEGMENT = /^[A-Za-z0-9_-]*$/;
|
|
77
60
|
|
|
@@ -87,15 +70,23 @@ function normalizeKey(key) {
|
|
|
87
70
|
return key.toLowerCase().replace(/[-_\s]/g, '');
|
|
88
71
|
}
|
|
89
72
|
|
|
90
|
-
|
|
91
|
-
|
|
73
|
+
// The *Normalized variants take a normalizeKey() result, so a walk
|
|
74
|
+
// normalizes each key once.
|
|
75
|
+
function isDeniedNormalized(normalized) {
|
|
92
76
|
if (!normalized || PROTECTED_KEYS.has(normalized)) return false;
|
|
93
|
-
|
|
94
|
-
|
|
77
|
+
return deniedKeys.has(normalized) || DENIED_SUFFIX.test(normalized);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function isDeniedKey(key) {
|
|
81
|
+
return isDeniedNormalized(normalizeKey(key));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function isDigestNormalized(normalized) {
|
|
85
|
+
return DIGEST_KEY.test(normalized);
|
|
95
86
|
}
|
|
96
87
|
|
|
97
88
|
function isDigestKey(key) {
|
|
98
|
-
return
|
|
89
|
+
return isDigestNormalized(normalizeKey(key));
|
|
99
90
|
}
|
|
100
91
|
|
|
101
92
|
function isPairKey(key, { withState = false } = {}) {
|
|
@@ -139,7 +130,7 @@ function isRedactedValue(value) {
|
|
|
139
130
|
return value === 'REDACTED' || value.startsWith('[REDACTED');
|
|
140
131
|
}
|
|
141
132
|
|
|
142
|
-
function scrubTokens(text, { allowHex = false
|
|
133
|
+
function scrubTokens(text, { allowHex = false } = {}) {
|
|
143
134
|
let out = text.replace(PREFIXED_TOKEN, redacted);
|
|
144
135
|
out = out.replace(JWT, redacted);
|
|
145
136
|
out = out.replace(AUTH_SCHEME, (match, scheme, space, credential) => {
|
|
@@ -170,8 +161,8 @@ function scrubTokens(text, { allowHex = false, segment = false } = {}) {
|
|
|
170
161
|
: match
|
|
171
162
|
);
|
|
172
163
|
if (!allowHex) out = out.replace(HEX_RUN, redacted);
|
|
173
|
-
out = out.replace(
|
|
174
|
-
hasMixedClasses(run) &&
|
|
164
|
+
out = out.replace(BASE64_RUN, (run) =>
|
|
165
|
+
hasMixedClasses(run) && !isPathLike(run)
|
|
175
166
|
? redacted(run)
|
|
176
167
|
: run
|
|
177
168
|
);
|
|
@@ -204,7 +195,7 @@ function redactPathSegment(segment) {
|
|
|
204
195
|
if (PATH_TOKEN_SEGMENT.test(segment) && hasMixedClasses(segment)) {
|
|
205
196
|
return redacted(segment);
|
|
206
197
|
}
|
|
207
|
-
return scrubTokens(segment
|
|
198
|
+
return scrubTokens(segment);
|
|
208
199
|
}
|
|
209
200
|
|
|
210
201
|
// The raw path, not URL#pathname, which percent-encodes `{proxy+}`.
|
|
@@ -308,12 +299,13 @@ function scrubString(str, { allowHex = false } = {}) {
|
|
|
308
299
|
}
|
|
309
300
|
}
|
|
310
301
|
|
|
302
|
+
function isOAuthCallbackShapeNormalized(normalizedKeys) {
|
|
303
|
+
const keys = new Set(normalizedKeys);
|
|
304
|
+
return keys.has('code') && (keys.has('state') || keys.has('codeverifier'));
|
|
305
|
+
}
|
|
306
|
+
|
|
311
307
|
function isOAuthCallbackShape(keys) {
|
|
312
|
-
|
|
313
|
-
return (
|
|
314
|
-
normalized.has('code') &&
|
|
315
|
-
(normalized.has('state') || normalized.has('codeverifier'))
|
|
316
|
-
);
|
|
308
|
+
return isOAuthCallbackShapeNormalized(keys.map(normalizeKey));
|
|
317
309
|
}
|
|
318
310
|
|
|
319
311
|
function redactValue(value) {
|
|
@@ -324,8 +316,11 @@ function redactValue(value) {
|
|
|
324
316
|
module.exports = {
|
|
325
317
|
normalizeKey,
|
|
326
318
|
isDeniedKey,
|
|
319
|
+
isDeniedNormalized,
|
|
327
320
|
isDigestKey,
|
|
321
|
+
isDigestNormalized,
|
|
328
322
|
isOAuthCallbackShape,
|
|
323
|
+
isOAuthCallbackShapeNormalized,
|
|
329
324
|
addDeniedKeys,
|
|
330
325
|
redactUrl,
|
|
331
326
|
scrubString,
|
package/logs/serialize.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
isDeniedNormalized,
|
|
3
|
+
isDigestNormalized,
|
|
4
|
+
isOAuthCallbackShapeNormalized,
|
|
5
5
|
normalizeKey,
|
|
6
6
|
redactUrl,
|
|
7
7
|
scrubString,
|
|
@@ -27,8 +27,10 @@ function cutString(value) {
|
|
|
27
27
|
return value.slice(0, MAX_STRING - suffix.length) + suffix;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
function cleanString(value,
|
|
31
|
-
return cutString(
|
|
30
|
+
function cleanString(value, normalizedKey = '') {
|
|
31
|
+
return cutString(
|
|
32
|
+
scrubString(value, { allowHex: isDigestNormalized(normalizedKey) })
|
|
33
|
+
);
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
function isError(value) {
|
|
@@ -73,10 +75,13 @@ function headerNames(value) {
|
|
|
73
75
|
|
|
74
76
|
function walkObject(value, depth, seen) {
|
|
75
77
|
const keys = Object.keys(value);
|
|
76
|
-
const
|
|
78
|
+
const normalizedKeys = keys.map(normalizeKey);
|
|
79
|
+
const dropAll = isOAuthCallbackShapeNormalized(normalizedKeys);
|
|
77
80
|
const out = {};
|
|
78
|
-
for (
|
|
79
|
-
|
|
81
|
+
for (let i = 0; i < keys.length; i += 1) {
|
|
82
|
+
const key = keys[i];
|
|
83
|
+
const normalized = normalizedKeys[i];
|
|
84
|
+
if (dropAll || isDeniedNormalized(normalized)) {
|
|
80
85
|
out[key] = '[REDACTED]';
|
|
81
86
|
continue;
|
|
82
87
|
}
|
|
@@ -87,11 +92,11 @@ function walkObject(value, depth, seen) {
|
|
|
87
92
|
out[key] = '[Getter threw]';
|
|
88
93
|
continue;
|
|
89
94
|
}
|
|
90
|
-
if (HEADER_KEYS.has(
|
|
95
|
+
if (HEADER_KEYS.has(normalized)) {
|
|
91
96
|
out[key] = attempt(() => headerNames(child), '[Unserializable]');
|
|
92
97
|
continue;
|
|
93
98
|
}
|
|
94
|
-
const result = walk(child, depth + 1, seen,
|
|
99
|
+
const result = walk(child, depth + 1, seen, normalized);
|
|
95
100
|
if (result !== undefined) out[key] = result;
|
|
96
101
|
}
|
|
97
102
|
return out;
|
|
@@ -102,11 +107,12 @@ function walkContainer(value, depth, seen) {
|
|
|
102
107
|
const out = {};
|
|
103
108
|
for (const [k, v] of value) {
|
|
104
109
|
const key = String(k);
|
|
105
|
-
|
|
110
|
+
const normalized = normalizeKey(key);
|
|
111
|
+
if (isDeniedNormalized(normalized)) {
|
|
106
112
|
out[key] = '[REDACTED]';
|
|
107
113
|
continue;
|
|
108
114
|
}
|
|
109
|
-
const result = walk(v, depth + 1, seen,
|
|
115
|
+
const result = walk(v, depth + 1, seen, normalized);
|
|
110
116
|
if (result !== undefined) out[key] = result;
|
|
111
117
|
}
|
|
112
118
|
return out;
|
|
@@ -151,10 +157,10 @@ function walkSpecial(value) {
|
|
|
151
157
|
return undefined;
|
|
152
158
|
}
|
|
153
159
|
|
|
154
|
-
function walk(value, depth, seen,
|
|
160
|
+
function walk(value, depth, seen, normalizedKey) {
|
|
155
161
|
switch (typeof value) {
|
|
156
162
|
case 'string':
|
|
157
|
-
return cleanString(value,
|
|
163
|
+
return cleanString(value, normalizedKey);
|
|
158
164
|
case 'number':
|
|
159
165
|
return Number.isFinite(value) ? value : String(value);
|
|
160
166
|
case 'boolean':
|
|
@@ -313,6 +319,7 @@ function toSanitizedSurrogate(err) {
|
|
|
313
319
|
}
|
|
314
320
|
|
|
315
321
|
module.exports = {
|
|
322
|
+
isError,
|
|
316
323
|
serializeValue,
|
|
317
324
|
serializeError,
|
|
318
325
|
toSanitizedSurrogate,
|
package/logs/sinks.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const fs = require('node:fs');
|
|
2
|
+
const { deepFreeze } = require('./record');
|
|
2
3
|
|
|
3
4
|
const DEFAULT_MAX_RETRIES = 10;
|
|
4
5
|
const MAX_SLEEP_MS = 20;
|
|
@@ -42,42 +43,35 @@ function createStdoutSink({
|
|
|
42
43
|
}
|
|
43
44
|
let offset = 0;
|
|
44
45
|
let retries = 0;
|
|
46
|
+
// EAGAIN and a zero-byte write share one bounded retry budget.
|
|
47
|
+
const retry = () => {
|
|
48
|
+
if (retries >= maxRetries) {
|
|
49
|
+
reportFailure('EAGAIN');
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
retries += 1;
|
|
53
|
+
sleep(Math.min(retries, MAX_SLEEP_MS));
|
|
54
|
+
return true;
|
|
55
|
+
};
|
|
45
56
|
while (offset < buffer.length) {
|
|
46
57
|
let written;
|
|
47
58
|
try {
|
|
48
59
|
written = writeSync(fd, buffer, offset, buffer.length - offset);
|
|
49
60
|
} catch (error) {
|
|
50
|
-
if (error?.code
|
|
51
|
-
|
|
52
|
-
sleep(Math.min(retries, MAX_SLEEP_MS));
|
|
53
|
-
continue;
|
|
54
|
-
}
|
|
55
|
-
reportFailure(error?.code);
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
if (!(written > 0)) {
|
|
59
|
-
if (retries >= maxRetries) {
|
|
60
|
-
reportFailure('EAGAIN');
|
|
61
|
+
if (error?.code !== 'EAGAIN') {
|
|
62
|
+
reportFailure(error?.code);
|
|
61
63
|
return;
|
|
62
64
|
}
|
|
63
|
-
|
|
64
|
-
sleep(Math.min(retries, MAX_SLEEP_MS));
|
|
65
|
+
if (!retry()) return;
|
|
65
66
|
continue;
|
|
66
67
|
}
|
|
67
|
-
offset += written;
|
|
68
|
+
if (written > 0) offset += written;
|
|
69
|
+
else if (!retry()) return;
|
|
68
70
|
}
|
|
69
71
|
},
|
|
70
72
|
};
|
|
71
73
|
}
|
|
72
74
|
|
|
73
|
-
function deepFreeze(value) {
|
|
74
|
-
if (value && typeof value === 'object') {
|
|
75
|
-
for (const key of Object.keys(value)) deepFreeze(value[key]);
|
|
76
|
-
Object.freeze(value);
|
|
77
|
-
}
|
|
78
|
-
return value;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
75
|
function createMemorySink({ install = true } = {}) {
|
|
82
76
|
const records = [];
|
|
83
77
|
const sink = {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@friggframework/core",
|
|
3
3
|
"prettier": "@friggframework/prettier-config",
|
|
4
|
-
"version": "2.0.0--canary.654.
|
|
4
|
+
"version": "2.0.0--canary.654.e9530b8.0",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@aws-sdk/client-apigatewaymanagementapi": "^3.588.0",
|
|
7
7
|
"@aws-sdk/client-kms": "^3.588.0",
|
|
@@ -47,9 +47,9 @@
|
|
|
47
47
|
}
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
|
-
"@friggframework/eslint-config": "2.0.0--canary.654.
|
|
51
|
-
"@friggframework/prettier-config": "2.0.0--canary.654.
|
|
52
|
-
"@friggframework/test": "2.0.0--canary.654.
|
|
50
|
+
"@friggframework/eslint-config": "2.0.0--canary.654.e9530b8.0",
|
|
51
|
+
"@friggframework/prettier-config": "2.0.0--canary.654.e9530b8.0",
|
|
52
|
+
"@friggframework/test": "2.0.0--canary.654.e9530b8.0",
|
|
53
53
|
"@prisma/client": "^6.19.3",
|
|
54
54
|
"@types/lodash": "4.17.15",
|
|
55
55
|
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
|
@@ -89,5 +89,5 @@
|
|
|
89
89
|
"publishConfig": {
|
|
90
90
|
"access": "public"
|
|
91
91
|
},
|
|
92
|
-
"gitHead": "
|
|
92
|
+
"gitHead": "e9530b84cdbe42939e8061edc241acb55222cf34"
|
|
93
93
|
}
|