@friggframework/core 2.0.0-next.111 → 2.0.0-next.113
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.json +18 -1
- package/CLAUDE.md +101 -13
- 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/documentdb-encryption-service.js +6 -2
- package/database/documentdb-utils.js +2 -1
- package/database/encryption/README.md +25 -1
- package/database/encryption/encryption-schema-registry.js +3 -0
- package/database/encryption/integration-mapping-encryption.js +97 -0
- package/database/encryption/logger.js +5 -2
- package/database/encryption/prisma-encryption-extension.js +20 -9
- package/errors/fetch-error.js +48 -65
- 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/repositories/integration-mapping-query-pipeline.js +152 -0
- package/integrations/repositories/integration-mapping-query.js +193 -0
- package/integrations/repositories/integration-mapping-repository-documentdb.js +53 -0
- package/integrations/repositories/integration-mapping-repository-interface.js +48 -0
- package/integrations/repositories/integration-mapping-repository-mongo.js +72 -0
- package/integrations/repositories/integration-mapping-repository-postgres.js +118 -0
- 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
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const util = require('node:util');
|
|
2
|
+
const { getLogger } = require('./logger');
|
|
3
|
+
const { getContext } = require('./context');
|
|
4
|
+
const {
|
|
5
|
+
summarizeLambdaEvent,
|
|
6
|
+
toRequestInvocation,
|
|
7
|
+
} = require('./summarize-event');
|
|
8
|
+
|
|
9
|
+
// Deprecated. Kept for one major version; use getLogger or this.logger.
|
|
10
|
+
const LEGACY_LOGGER_NAME = 'frigg.legacy';
|
|
11
|
+
const FORMAT_DIRECTIVE = /%[sdifjoOc%]/;
|
|
12
|
+
|
|
13
|
+
const legacy = () => getLogger(LEGACY_LOGGER_NAME);
|
|
14
|
+
|
|
15
|
+
function debug(...messages) {
|
|
16
|
+
if (!messages.length) return;
|
|
17
|
+
const log = legacy();
|
|
18
|
+
if (!log.isLevelEnabled('DEBUG')) return;
|
|
19
|
+
const index = messages.findIndex((m) => typeof m === 'string');
|
|
20
|
+
if (index === 0 && FORMAT_DIRECTIVE.test(messages[0])) {
|
|
21
|
+
log.debug(util.format(...messages));
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const message = index === -1 ? '' : messages[index];
|
|
25
|
+
const args = messages.filter((_, i) => i !== index);
|
|
26
|
+
log.debug(message, args.length ? { args } : undefined);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function initDebugLog(...initMessages) {
|
|
30
|
+
if (getContext()?.log) return;
|
|
31
|
+
const log = legacy();
|
|
32
|
+
if (!log.isLevelEnabled('DEBUG')) return;
|
|
33
|
+
const event = initMessages.find((m) => m && typeof m === 'object');
|
|
34
|
+
log.debug('Debug log initialized', {
|
|
35
|
+
invocation: toRequestInvocation(summarizeLambdaEvent(event)),
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function flushDebugLog(error) {
|
|
40
|
+
legacy().error('Unhandled error', {
|
|
41
|
+
eventName: 'frigg.legacy.error',
|
|
42
|
+
error: error || new Error('flushDebugLog called with empty error'),
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = { debug, initDebugLog, flushDebugLog };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const { addDeniedKeys } = require('./redact');
|
|
2
|
+
const { getLogger } = require('./logger');
|
|
3
|
+
|
|
4
|
+
const warnedKeys = new Set();
|
|
5
|
+
|
|
6
|
+
// addDeniedKeys plus one WARN per record-contract key it had to skip, so a
|
|
7
|
+
// credential field named like a record field is visible wherever it is set.
|
|
8
|
+
function registerDeniedKeys(keys) {
|
|
9
|
+
const ignored = addDeniedKeys(keys);
|
|
10
|
+
for (const key of ignored) {
|
|
11
|
+
if (warnedKeys.has(key)) continue;
|
|
12
|
+
warnedKeys.add(key);
|
|
13
|
+
getLogger('frigg.logger').warn(
|
|
14
|
+
'Credential field name is a record field; it stays visible in logs',
|
|
15
|
+
{ eventName: 'frigg.logger.denied_key_ignored', key }
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
return ignored;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
module.exports = { registerDeniedKeys };
|
package/logs/index.js
CHANGED
|
@@ -1,7 +1,18 @@
|
|
|
1
|
-
const {
|
|
1
|
+
const { getLogger } = require('./logger');
|
|
2
|
+
const { createMemorySink } = require('./sinks');
|
|
3
|
+
const { resetLoggerForTests } = require('./logger-runtime');
|
|
4
|
+
const { serializeError, toSanitizedSurrogate } = require('./serialize');
|
|
5
|
+
const { redactValue } = require('./redact');
|
|
6
|
+
const { debug, initDebugLog, flushDebugLog } = require('./debug-shims');
|
|
2
7
|
|
|
3
8
|
module.exports = {
|
|
9
|
+
getLogger,
|
|
10
|
+
createMemorySink,
|
|
11
|
+
resetLoggerForTests,
|
|
12
|
+
serializeError,
|
|
13
|
+
redactValue,
|
|
14
|
+
toSanitizedSurrogate,
|
|
4
15
|
debug,
|
|
5
16
|
initDebugLog,
|
|
6
|
-
flushDebugLog
|
|
7
|
-
}
|
|
17
|
+
flushDebugLog,
|
|
18
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const { createMemorySink } = require('./sinks');
|
|
2
|
+
const { resetLoggerForTests, takeViolationsForTests } = require('./logger-runtime');
|
|
3
|
+
const { toContainNoSecretWindow } = require('./__fixtures__/matchers');
|
|
4
|
+
|
|
5
|
+
expect.extend({ toContainNoSecretWindow });
|
|
6
|
+
|
|
7
|
+
const resetToMemory = () =>
|
|
8
|
+
resetLoggerForTests({
|
|
9
|
+
level: 'TRACE',
|
|
10
|
+
sinks: [createMemorySink({ install: false })],
|
|
11
|
+
trackViolations: true,
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
// Also at load: records written at require time must not reach fd 1.
|
|
15
|
+
resetToMemory();
|
|
16
|
+
|
|
17
|
+
beforeEach(resetToMemory);
|
|
18
|
+
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
const violations = takeViolationsForTests();
|
|
21
|
+
if (violations.length) {
|
|
22
|
+
const list = violations.map((v) => `${v.level} ${v.logger}: ${v.message}`).join('\n');
|
|
23
|
+
throw new Error(`frigg.* records at WARN or above need an eventName:\n${list}`);
|
|
24
|
+
}
|
|
25
|
+
});
|
package/logs/levels.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
const LEVELS = Object.freeze({
|
|
2
|
+
TRACE: 10,
|
|
3
|
+
DEBUG: 20,
|
|
4
|
+
INFO: 30,
|
|
5
|
+
WARN: 40,
|
|
6
|
+
ERROR: 50,
|
|
7
|
+
FATAL: 60,
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
const LEVEL_NAMES = Object.freeze(Object.keys(LEVELS));
|
|
11
|
+
const DEFAULT_LEVEL = 'INFO';
|
|
12
|
+
const MAX_ECHO_LENGTH = 32;
|
|
13
|
+
|
|
14
|
+
function parseLevel(value) {
|
|
15
|
+
if (typeof value !== 'string') return null;
|
|
16
|
+
const name = value.trim().toUpperCase();
|
|
17
|
+
return Object.prototype.hasOwnProperty.call(LEVELS, name) ? name : null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function isSet(value) {
|
|
21
|
+
return typeof value === 'string' && value.trim() !== '';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function isLocalRun(env) {
|
|
25
|
+
if (env.STAGE === 'local') return true;
|
|
26
|
+
const offline = String(env.IS_OFFLINE ?? '').trim().toLowerCase();
|
|
27
|
+
return offline === 'true' || offline === '1';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function lessVerbose(a, b) {
|
|
31
|
+
return LEVELS[a] >= LEVELS[b] ? a : b;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function resolveLevel(env = {}) {
|
|
35
|
+
const warnings = [];
|
|
36
|
+
let level;
|
|
37
|
+
|
|
38
|
+
if (isSet(env.FRIGG_LOG_LEVEL)) {
|
|
39
|
+
level = parseLevel(env.FRIGG_LOG_LEVEL);
|
|
40
|
+
if (!level) {
|
|
41
|
+
level = DEFAULT_LEVEL;
|
|
42
|
+
warnings.push({
|
|
43
|
+
eventName: 'frigg.logger.invalid_level',
|
|
44
|
+
message: 'Unknown FRIGG_LOG_LEVEL, using INFO',
|
|
45
|
+
fields: {
|
|
46
|
+
received: env.FRIGG_LOG_LEVEL.trim().slice(0, MAX_ECHO_LENGTH),
|
|
47
|
+
allowed: LEVEL_NAMES,
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
} else if (env.DEBUG_VERBOSE === '1' || isLocalRun(env)) {
|
|
52
|
+
level = 'DEBUG';
|
|
53
|
+
} else {
|
|
54
|
+
level = DEFAULT_LEVEL;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const awsLevel = parseLevel(env.AWS_LAMBDA_LOG_LEVEL);
|
|
58
|
+
if (awsLevel && awsLevel !== level) {
|
|
59
|
+
const effective = lessVerbose(level, awsLevel);
|
|
60
|
+
warnings.push({
|
|
61
|
+
eventName: 'frigg.logger.level_conflict',
|
|
62
|
+
message: 'FRIGG_LOG_LEVEL and AWS_LAMBDA_LOG_LEVEL differ, using the less verbose level',
|
|
63
|
+
fields: { friggLevel: level, awsLevel, effectiveLevel: effective },
|
|
64
|
+
});
|
|
65
|
+
level = effective;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return { level, warnings };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
module.exports = { LEVELS, LEVEL_NAMES, DEFAULT_LEVEL, parseLevel, resolveLevel };
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
const { LEVELS, parseLevel, resolveLevel } = require('./levels');
|
|
2
|
+
|
|
3
|
+
// On globalThis so jest.resetModules and nested core copies share one state.
|
|
4
|
+
const STATE_KEY = Symbol.for('@friggframework/core.logs');
|
|
5
|
+
|
|
6
|
+
function state() {
|
|
7
|
+
if (!globalThis[STATE_KEY]) {
|
|
8
|
+
globalThis[STATE_KEY] = {
|
|
9
|
+
config: null,
|
|
10
|
+
levelOverride: null,
|
|
11
|
+
sinks: null,
|
|
12
|
+
spanContextProvider: null,
|
|
13
|
+
warned: new Set(),
|
|
14
|
+
inLog: false,
|
|
15
|
+
trackViolations: false,
|
|
16
|
+
violations: [],
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
return globalThis[STATE_KEY];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function readEnv(name) {
|
|
23
|
+
const value = process.env[name];
|
|
24
|
+
return typeof value === 'string' && value !== '' ? value : undefined;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function getConfig() {
|
|
28
|
+
const s = state();
|
|
29
|
+
if (!s.config) {
|
|
30
|
+
const resolved = s.levelOverride
|
|
31
|
+
? { level: s.levelOverride, warnings: [] }
|
|
32
|
+
: resolveLevel(process.env);
|
|
33
|
+
s.config = {
|
|
34
|
+
level: resolved.level,
|
|
35
|
+
threshold: LEVELS[resolved.level],
|
|
36
|
+
appName: readEnv('FRIGG_STACK'),
|
|
37
|
+
stage: readEnv('STAGE'),
|
|
38
|
+
pendingWarnings: resolved.warnings,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
return s.config;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function takeConfigWarnings() {
|
|
45
|
+
const s = state();
|
|
46
|
+
const config = getConfig();
|
|
47
|
+
const pending = config.pendingWarnings.splice(0);
|
|
48
|
+
return pending.filter((warning) => {
|
|
49
|
+
if (s.warned.has(warning.eventName)) return false;
|
|
50
|
+
s.warned.add(warning.eventName);
|
|
51
|
+
return true;
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function getSinks() {
|
|
56
|
+
const s = state();
|
|
57
|
+
if (!s.sinks) {
|
|
58
|
+
s.sinks = [require('./sinks').createStdoutSink()];
|
|
59
|
+
}
|
|
60
|
+
return s.sinks;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function setSinks(sinks) {
|
|
64
|
+
state().sinks = Array.isArray(sinks) ? [...sinks] : null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function hasFlushableSinks() {
|
|
68
|
+
return getSinks().some((sink) => typeof sink.flush === 'function');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function flushSinks({ signal } = {}) {
|
|
72
|
+
await Promise.allSettled(
|
|
73
|
+
getSinks()
|
|
74
|
+
.filter((sink) => typeof sink.flush === 'function')
|
|
75
|
+
.map(async (sink) => sink.flush({ signal }))
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function setSpanContextProvider(provider) {
|
|
80
|
+
state().spanContextProvider = typeof provider === 'function' ? provider : null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function getSpanContextProvider() {
|
|
84
|
+
return state().spanContextProvider;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function getSpanContext() {
|
|
88
|
+
const provider = state().spanContextProvider;
|
|
89
|
+
if (!provider) return null;
|
|
90
|
+
try {
|
|
91
|
+
const ctx = provider();
|
|
92
|
+
if (ctx && typeof ctx.traceId === 'string' && typeof ctx.spanId === 'string') {
|
|
93
|
+
return ctx;
|
|
94
|
+
}
|
|
95
|
+
} catch {
|
|
96
|
+
// A broken provider must not break logging.
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Off in production, so a warm container never grows this list.
|
|
102
|
+
function recordViolation(violation) {
|
|
103
|
+
const s = state();
|
|
104
|
+
if (s.trackViolations) s.violations.push(violation);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function takeViolationsForTests() {
|
|
108
|
+
return state().violations.splice(0);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function resetLoggerForTests({ level, sinks, trackViolations } = {}) {
|
|
112
|
+
const s = state();
|
|
113
|
+
s.config = null;
|
|
114
|
+
s.levelOverride = parseLevel(level);
|
|
115
|
+
setSinks(sinks);
|
|
116
|
+
s.spanContextProvider = null;
|
|
117
|
+
s.warned.clear();
|
|
118
|
+
s.violations.length = 0;
|
|
119
|
+
s.inLog = false;
|
|
120
|
+
if (typeof trackViolations === 'boolean') s.trackViolations = trackViolations;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
module.exports = {
|
|
124
|
+
state,
|
|
125
|
+
getConfig,
|
|
126
|
+
takeConfigWarnings,
|
|
127
|
+
getSinks,
|
|
128
|
+
setSinks,
|
|
129
|
+
hasFlushableSinks,
|
|
130
|
+
flushSinks,
|
|
131
|
+
setSpanContextProvider,
|
|
132
|
+
getSpanContextProvider,
|
|
133
|
+
getSpanContext,
|
|
134
|
+
recordViolation,
|
|
135
|
+
takeViolationsForTests,
|
|
136
|
+
resetLoggerForTests,
|
|
137
|
+
};
|
package/logs/logger.js
CHANGED
|
@@ -1,65 +1,157 @@
|
|
|
1
|
-
const
|
|
1
|
+
const { LEVELS, parseLevel } = require('./levels');
|
|
2
|
+
const { getLoggerScope } = require('./context');
|
|
3
|
+
const runtime = require('./logger-runtime');
|
|
4
|
+
const { buildRecord, DEFAULT_LOGGER_NAME } = require('./record');
|
|
2
5
|
|
|
3
|
-
|
|
4
|
-
/* eslint-disable no-console */
|
|
6
|
+
const LOGGER_OWN_NAME = 'frigg.logger';
|
|
5
7
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
function copyBindings(bindings, into = {}) {
|
|
9
|
+
if (!bindings || typeof bindings !== 'object') return into;
|
|
10
|
+
for (const key of Object.keys(bindings)) {
|
|
11
|
+
try {
|
|
12
|
+
into[key] = bindings[key];
|
|
13
|
+
} catch {
|
|
14
|
+
// A throwing getter drops only that binding.
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return into;
|
|
18
|
+
}
|
|
13
19
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
20
|
+
// One plain object per record. Static sources were copied at child() time.
|
|
21
|
+
function evaluateBindings(sources) {
|
|
22
|
+
const merged = {};
|
|
23
|
+
for (const source of sources) {
|
|
24
|
+
if (typeof source !== 'function') {
|
|
25
|
+
Object.assign(merged, source);
|
|
26
|
+
continue;
|
|
18
27
|
}
|
|
28
|
+
let value;
|
|
29
|
+
try {
|
|
30
|
+
value = source();
|
|
31
|
+
} catch {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
copyBindings(value, merged);
|
|
19
35
|
}
|
|
36
|
+
return merged;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function isFriggViolation(record) {
|
|
40
|
+
return (
|
|
41
|
+
record.logger.startsWith('frigg.') &&
|
|
42
|
+
LEVELS[record.level] >= LEVELS.WARN &&
|
|
43
|
+
!record.eventName
|
|
44
|
+
);
|
|
20
45
|
}
|
|
21
46
|
|
|
22
|
-
|
|
23
|
-
|
|
47
|
+
class Logger {
|
|
48
|
+
constructor(name, bindingSources = []) {
|
|
49
|
+
this.name = name;
|
|
50
|
+
this._bindingSources = bindingSources;
|
|
51
|
+
}
|
|
24
52
|
|
|
25
|
-
|
|
26
|
-
|
|
53
|
+
trace(message, fields) {
|
|
54
|
+
this._log('TRACE', message, fields);
|
|
55
|
+
}
|
|
27
56
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
57
|
+
debug(message, fields) {
|
|
58
|
+
this._log('DEBUG', message, fields);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
info(message, fields) {
|
|
62
|
+
this._log('INFO', message, fields);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
warn(message, fields) {
|
|
66
|
+
this._log('WARN', message, fields);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
error(message, fields) {
|
|
70
|
+
this._log('ERROR', message, fields);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
fatal(message, fields) {
|
|
74
|
+
this._log('FATAL', message, fields);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
child(bindings) {
|
|
78
|
+
const source = typeof bindings === 'function' ? bindings : copyBindings(bindings);
|
|
79
|
+
return new Logger(this.name, [...this._bindingSources, source]);
|
|
80
|
+
}
|
|
31
81
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
82
|
+
isLevelEnabled(level) {
|
|
83
|
+
try {
|
|
84
|
+
const name = parseLevel(level);
|
|
85
|
+
return name !== null && LEVELS[name] >= runtime.getConfig().threshold;
|
|
86
|
+
} catch {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
37
89
|
}
|
|
38
90
|
|
|
39
|
-
|
|
91
|
+
_log(level, message, fields) {
|
|
92
|
+
const state = runtime.state();
|
|
93
|
+
if (state.inLog) return;
|
|
94
|
+
try {
|
|
95
|
+
const config = runtime.getConfig();
|
|
96
|
+
emitConfigWarnings();
|
|
97
|
+
if (LEVELS[level] < config.threshold) return;
|
|
98
|
+
// Set before fields, bindings and sinks run: any of them may log.
|
|
99
|
+
state.inLog = true;
|
|
40
100
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
101
|
+
const record = buildRecord({
|
|
102
|
+
level,
|
|
103
|
+
logger: this.name,
|
|
104
|
+
message,
|
|
105
|
+
fields,
|
|
106
|
+
bindings: evaluateBindings(this._bindingSources),
|
|
107
|
+
scope: getLoggerScope(),
|
|
108
|
+
spanContext: runtime.getSpanContext(),
|
|
109
|
+
resource: { appName: config.appName, stage: config.stage },
|
|
110
|
+
});
|
|
111
|
+
if (isFriggViolation(record)) {
|
|
112
|
+
runtime.recordViolation({
|
|
113
|
+
logger: record.logger,
|
|
114
|
+
level: record.level,
|
|
115
|
+
message: record.message,
|
|
116
|
+
});
|
|
46
117
|
}
|
|
118
|
+
writeToSinks(record);
|
|
119
|
+
} catch {
|
|
120
|
+
// The logger never throws.
|
|
121
|
+
} finally {
|
|
122
|
+
state.inLog = false;
|
|
47
123
|
}
|
|
48
124
|
}
|
|
125
|
+
}
|
|
49
126
|
|
|
50
|
-
|
|
51
|
-
|
|
127
|
+
function writeToSinks(record) {
|
|
128
|
+
for (const sink of runtime.getSinks()) {
|
|
129
|
+
try {
|
|
130
|
+
sink.write(record);
|
|
131
|
+
} catch {
|
|
132
|
+
// One failing sink must not stop the others.
|
|
133
|
+
}
|
|
52
134
|
}
|
|
135
|
+
}
|
|
53
136
|
|
|
54
|
-
|
|
137
|
+
function emitConfigWarnings() {
|
|
138
|
+
const warnings = runtime.takeConfigWarnings();
|
|
139
|
+
if (!warnings.length) return;
|
|
140
|
+
const logger = getLogger(LOGGER_OWN_NAME);
|
|
141
|
+
for (const warning of warnings) {
|
|
142
|
+
logger.warn(warning.message, {
|
|
143
|
+
...warning.fields,
|
|
144
|
+
eventName: warning.eventName,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
55
148
|
|
|
56
|
-
|
|
149
|
+
const loggers = new Map();
|
|
57
150
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
151
|
+
function getLogger(name) {
|
|
152
|
+
const key = typeof name === 'string' && name ? name : DEFAULT_LOGGER_NAME;
|
|
153
|
+
if (!loggers.has(key)) loggers.set(key, new Logger(key));
|
|
154
|
+
return loggers.get(key);
|
|
63
155
|
}
|
|
64
156
|
|
|
65
|
-
module.exports = {
|
|
157
|
+
module.exports = { Logger, getLogger };
|