@onlineapps/infra-logger 1.0.0 → 2.0.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/index.js +151 -87
- package/package.json +12 -5
package/index.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* - serviceName for lifecycle logs
|
|
11
11
|
*
|
|
12
12
|
* NOTE: Health checks are NOT logged (too verbose). All other infrastructure
|
|
13
|
-
* operations should be logged with appropriate detail (input, output,
|
|
13
|
+
* operations should be logged with appropriate detail (input, output, fail, lifecycle).
|
|
14
14
|
*
|
|
15
15
|
* Usage:
|
|
16
16
|
* const { createLogger } = require('@onlineapps/infra-logger');
|
|
@@ -40,19 +40,23 @@
|
|
|
40
40
|
|
|
41
41
|
/**
|
|
42
42
|
* Sanitize input/output for logging (keep it concise, not full payload)
|
|
43
|
+
*
|
|
44
|
+
* The depth limit exists to stop DEEP STRUCTURES from filling the log line. It
|
|
45
|
+
* therefore applies to objects and arrays only: a scalar has no depth, and
|
|
46
|
+
* replacing one with `{_truncated: true}` deletes the very value the record was
|
|
47
|
+
* written to carry (queue names, output keys, statuses). Scalars are handled
|
|
48
|
+
* before the guard for exactly that reason — the length limit still applies to
|
|
49
|
+
* strings at any depth.
|
|
50
|
+
*
|
|
43
51
|
* @param {*} value - Value to sanitize
|
|
44
|
-
* @param {number} maxDepth - Maximum depth for nested objects
|
|
52
|
+
* @param {number} maxDepth - Maximum depth for nested objects and arrays
|
|
45
53
|
* @returns {*} Sanitized value
|
|
46
54
|
*/
|
|
47
55
|
function sanitizeValue(value, maxDepth = 2) {
|
|
48
|
-
if (maxDepth <= 0) {
|
|
49
|
-
return { _truncated: true };
|
|
50
|
-
}
|
|
51
|
-
|
|
52
56
|
if (value === null || value === undefined) {
|
|
53
57
|
return value;
|
|
54
58
|
}
|
|
55
|
-
|
|
59
|
+
|
|
56
60
|
if (typeof value === 'string') {
|
|
57
61
|
// Truncate long strings
|
|
58
62
|
if (value.length > 500) {
|
|
@@ -60,15 +64,26 @@ function sanitizeValue(value, maxDepth = 2) {
|
|
|
60
64
|
}
|
|
61
65
|
return value;
|
|
62
66
|
}
|
|
63
|
-
|
|
67
|
+
|
|
64
68
|
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
65
69
|
return value;
|
|
66
70
|
}
|
|
67
|
-
|
|
71
|
+
|
|
72
|
+
if (maxDepth <= 0) {
|
|
73
|
+
return { _truncated: true };
|
|
74
|
+
}
|
|
75
|
+
|
|
68
76
|
if (Buffer.isBuffer(value)) {
|
|
69
77
|
return { type: 'Buffer', size: value.length };
|
|
70
78
|
}
|
|
71
|
-
|
|
79
|
+
|
|
80
|
+
if (value instanceof Date) {
|
|
81
|
+
// Object.keys(date) is empty, so without this branch a timestamp reaches the
|
|
82
|
+
// record as `{}` — the value is gone and nothing says so. An invalid Date is
|
|
83
|
+
// named rather than thrown: a logger must not raise from a logging call.
|
|
84
|
+
return Number.isNaN(value.getTime()) ? 'Invalid Date' : value.toISOString();
|
|
85
|
+
}
|
|
86
|
+
|
|
72
87
|
if (Array.isArray(value)) {
|
|
73
88
|
if (value.length > 10) {
|
|
74
89
|
return { type: 'array', length: value.length, first_3: value.slice(0, 3).map(v => sanitizeValue(v, maxDepth - 1)) };
|
|
@@ -91,13 +106,131 @@ function sanitizeValue(value, maxDepth = 2) {
|
|
|
91
106
|
return String(value);
|
|
92
107
|
}
|
|
93
108
|
|
|
109
|
+
/** Every message this package throws or writes carries the same context tag. */
|
|
110
|
+
const CONTEXT = '[infra-logger]';
|
|
111
|
+
|
|
112
|
+
/** The one usage example every constructor error points at. */
|
|
113
|
+
const FIX = 'Fix: createLogger(\'api_gateway\', \'router\').';
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Reject a missing, non-string or empty logger identity at construction time.
|
|
117
|
+
*
|
|
118
|
+
* `service` and `layer` are what make a log line attributable; a record that
|
|
119
|
+
* carries `null` for either is unsearchable, and the defect surfaces far from
|
|
120
|
+
* the call that caused it (`architecture-principles.md` §4).
|
|
121
|
+
*
|
|
122
|
+
* @param {string} name - Argument name, as written in the signature.
|
|
123
|
+
* @param {*} value - Argument value as received.
|
|
124
|
+
* @throws {Error} Never returns for an invalid value.
|
|
125
|
+
*/
|
|
126
|
+
function requireIdentity(name, value) {
|
|
127
|
+
if (value === undefined || value === null) {
|
|
128
|
+
throw new Error(
|
|
129
|
+
`${CONTEXT} createLogger received no ${name} name - "${name}" must be a non-empty string. ${FIX}`
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
if (typeof value !== 'string') {
|
|
133
|
+
throw new Error(
|
|
134
|
+
`${CONTEXT} createLogger received a non-string ${name} - "${name}" must be a non-empty string, ` +
|
|
135
|
+
`got ${typeof value}. ${FIX}`
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
if (value.length === 0) {
|
|
139
|
+
throw new Error(
|
|
140
|
+
`${CONTEXT} createLogger received an empty ${name} - "${name}" must be a non-empty string. ${FIX}`
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Serialize a record, and never throw out of a logging call.
|
|
147
|
+
*
|
|
148
|
+
* `JSON.stringify` throws on a circular reference and on a BigInt. Only
|
|
149
|
+
* `input`/`output` pass through the sanitizer; every other key of `data` reaches
|
|
150
|
+
* the record untouched, so an unserializable value there would propagate a
|
|
151
|
+
* TypeError out of a line whose only job was to describe the work. That kills
|
|
152
|
+
* the operation the logger was watching.
|
|
153
|
+
*
|
|
154
|
+
* The replacement is explicit, not silent: the frame — which is built from
|
|
155
|
+
* values known to be serializable — still ships, and the record states that the
|
|
156
|
+
* payload was dropped and why. A record that quietly shrank would be worse than
|
|
157
|
+
* a crash, because nobody would ever notice.
|
|
158
|
+
*
|
|
159
|
+
* Shape taken from the module-local copy in
|
|
160
|
+
* `api/shared/mq-client-core/src/transports/rabbitmqClient.js:64` (which had the
|
|
161
|
+
* try/catch the original lacked); the frame is kept here rather than discarded,
|
|
162
|
+
* so the line stays attributable.
|
|
163
|
+
*
|
|
164
|
+
* @param {Object} logEntry - The assembled record.
|
|
165
|
+
* @returns {string} JSON text, always.
|
|
166
|
+
*/
|
|
167
|
+
function serializeEntry(logEntry) {
|
|
168
|
+
try {
|
|
169
|
+
return JSON.stringify(logEntry);
|
|
170
|
+
} catch (error) {
|
|
171
|
+
const frame = { timestamp: logEntry.timestamp };
|
|
172
|
+
if ('workflow_id' in logEntry) {
|
|
173
|
+
frame.workflow_id = String(logEntry.workflow_id);
|
|
174
|
+
}
|
|
175
|
+
frame.context = String(logEntry.context);
|
|
176
|
+
frame.service = logEntry.service;
|
|
177
|
+
frame.layer = logEntry.layer;
|
|
178
|
+
frame.action = String(logEntry.action);
|
|
179
|
+
frame._serialization_failed = true;
|
|
180
|
+
frame._serialization_error = error.message;
|
|
181
|
+
return JSON.stringify(frame);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Describe a failure for the `error` field of a `fail()` record.
|
|
187
|
+
*
|
|
188
|
+
* Replaces `error?.message || error || 'Unknown error'`, a chain that collapsed
|
|
189
|
+
* three different situations — no error, an Error with an empty message, and a
|
|
190
|
+
* non-Error value — into one indistinguishable string
|
|
191
|
+
* (`architecture-principles.md` §3). A missing error is a defect in the CALLER,
|
|
192
|
+
* so it is named in the record; a logger must not throw at log time.
|
|
193
|
+
*
|
|
194
|
+
* @param {*} error - Whatever the caller passed as `data.error`.
|
|
195
|
+
* @returns {string} The message to record.
|
|
196
|
+
*/
|
|
197
|
+
function describeError(error) {
|
|
198
|
+
if (error === undefined || error === null) {
|
|
199
|
+
return `${CONTEXT} fail() called without an error - "data.error" must be an Error instance.`;
|
|
200
|
+
}
|
|
201
|
+
if (error instanceof Error) {
|
|
202
|
+
return error.message;
|
|
203
|
+
}
|
|
204
|
+
return String(error);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Classify what the caller passed as `data.error`.
|
|
209
|
+
*
|
|
210
|
+
* @param {*} error - Whatever the caller passed as `data.error`.
|
|
211
|
+
* @returns {string} Constructor name, or 'missing' when nothing was passed.
|
|
212
|
+
*/
|
|
213
|
+
function describeErrorType(error) {
|
|
214
|
+
if (error === undefined || error === null) {
|
|
215
|
+
return 'missing';
|
|
216
|
+
}
|
|
217
|
+
if (error.constructor && typeof error.constructor.name === 'string') {
|
|
218
|
+
return error.constructor.name;
|
|
219
|
+
}
|
|
220
|
+
return typeof error;
|
|
221
|
+
}
|
|
222
|
+
|
|
94
223
|
/**
|
|
95
224
|
* Create a structured logger for an infrastructure service
|
|
96
225
|
* @param {string} service - Service name (e.g., 'api_gateway', 'api_monitoring')
|
|
97
226
|
* @param {string} layer - Layer name (e.g., 'router', 'consumer', 'dispatcher')
|
|
98
|
-
* @returns {Object} Logger with input, output,
|
|
227
|
+
* @returns {Object} Logger with input, output, fail, lifecycle methods
|
|
228
|
+
* @throws {Error} When service or layer is missing, not a string, or empty.
|
|
99
229
|
*/
|
|
100
230
|
function createLogger(service, layer) {
|
|
231
|
+
requireIdentity('service', service);
|
|
232
|
+
requireIdentity('layer', layer);
|
|
233
|
+
|
|
101
234
|
const formatLog = (contextId, action, data = {}) => {
|
|
102
235
|
const logEntry = {
|
|
103
236
|
timestamp: new Date().toISOString(),
|
|
@@ -127,7 +260,7 @@ function createLogger(service, layer) {
|
|
|
127
260
|
input: sanitizeValue(input),
|
|
128
261
|
...rest
|
|
129
262
|
});
|
|
130
|
-
console.log(prefix,
|
|
263
|
+
console.log(prefix, serializeEntry(logEntry));
|
|
131
264
|
},
|
|
132
265
|
|
|
133
266
|
/**
|
|
@@ -145,25 +278,7 @@ function createLogger(service, layer) {
|
|
|
145
278
|
output: sanitizeValue(output),
|
|
146
279
|
...rest
|
|
147
280
|
});
|
|
148
|
-
console.log(prefix,
|
|
149
|
-
},
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* Log process (transformation/processing)
|
|
153
|
-
* @param {string} contextId - Workflow ID or service name
|
|
154
|
-
* @param {string} action - Action name
|
|
155
|
-
* @param {Object} data - Data with handler, function, input, output
|
|
156
|
-
*/
|
|
157
|
-
process: (contextId, action, data = {}) => {
|
|
158
|
-
const { handler, function: fn, input, output, ...rest } = data;
|
|
159
|
-
const { prefix, logEntry } = formatLog(contextId, action, {
|
|
160
|
-
handler,
|
|
161
|
-
function: fn,
|
|
162
|
-
input: sanitizeValue(input),
|
|
163
|
-
output: sanitizeValue(output),
|
|
164
|
-
...rest
|
|
165
|
-
});
|
|
166
|
-
console.log(prefix, JSON.stringify(logEntry));
|
|
281
|
+
console.log(prefix, serializeEntry(logEntry));
|
|
167
282
|
},
|
|
168
283
|
|
|
169
284
|
/**
|
|
@@ -177,14 +292,14 @@ function createLogger(service, layer) {
|
|
|
177
292
|
const { prefix, logEntry } = formatLog(contextId, action, {
|
|
178
293
|
handler,
|
|
179
294
|
function: fn,
|
|
180
|
-
error: error
|
|
181
|
-
error_type: error
|
|
295
|
+
error: describeError(error),
|
|
296
|
+
error_type: describeErrorType(error),
|
|
182
297
|
expected,
|
|
183
298
|
received,
|
|
184
299
|
input: sanitizeValue(input),
|
|
185
300
|
...rest
|
|
186
301
|
});
|
|
187
|
-
console.error(prefix,
|
|
302
|
+
console.error(prefix, serializeEntry(logEntry));
|
|
188
303
|
},
|
|
189
304
|
|
|
190
305
|
/**
|
|
@@ -208,63 +323,12 @@ function createLogger(service, layer) {
|
|
|
208
323
|
...rest
|
|
209
324
|
};
|
|
210
325
|
const prefix = `[${serviceName}:${service}:${layer}:${action}]`;
|
|
211
|
-
console.log(prefix,
|
|
212
|
-
},
|
|
213
|
-
|
|
214
|
-
/**
|
|
215
|
-
* Generic log (backward compatibility)
|
|
216
|
-
* @param {string} contextId - Workflow ID or service name
|
|
217
|
-
* @param {string} action - Action name
|
|
218
|
-
* @param {Object} data - Additional data
|
|
219
|
-
*/
|
|
220
|
-
info: (contextId, action, data = {}) => {
|
|
221
|
-
const { prefix, logEntry } = formatLog(contextId, action, data);
|
|
222
|
-
console.log(prefix, JSON.stringify(logEntry));
|
|
223
|
-
},
|
|
224
|
-
|
|
225
|
-
warn: (contextId, action, data = {}) => {
|
|
226
|
-
const { prefix, logEntry } = formatLog(contextId, action, data);
|
|
227
|
-
console.warn(prefix, JSON.stringify(logEntry));
|
|
228
|
-
},
|
|
229
|
-
|
|
230
|
-
error: (contextId, action, data = {}) => {
|
|
231
|
-
const { prefix, logEntry } = formatLog(contextId, action, data);
|
|
232
|
-
console.error(prefix, JSON.stringify(logEntry));
|
|
326
|
+
console.log(prefix, serializeEntry(logEntry));
|
|
233
327
|
}
|
|
234
328
|
};
|
|
235
329
|
}
|
|
236
330
|
|
|
237
|
-
/**
|
|
238
|
-
* Standalone structured log function (for inline use without creating logger)
|
|
239
|
-
* @param {string} workflowId - Workflow ID
|
|
240
|
-
* @param {string} service - Service name
|
|
241
|
-
* @param {string} layer - Layer name
|
|
242
|
-
* @param {string} action - Action name
|
|
243
|
-
* @param {Object} data - Additional data
|
|
244
|
-
* @param {string} level - Log level (info, warn, error)
|
|
245
|
-
*/
|
|
246
|
-
function structuredLog(workflowId, service, layer, action, data = {}, level = 'info') {
|
|
247
|
-
const logEntry = {
|
|
248
|
-
timestamp: new Date().toISOString(),
|
|
249
|
-
workflow_id: workflowId,
|
|
250
|
-
service,
|
|
251
|
-
layer,
|
|
252
|
-
action,
|
|
253
|
-
...data
|
|
254
|
-
};
|
|
255
|
-
const prefix = `[${workflowId}:${service}:${layer}:${action}]`;
|
|
256
|
-
|
|
257
|
-
if (level === 'error') {
|
|
258
|
-
console.error(prefix, JSON.stringify(logEntry));
|
|
259
|
-
} else if (level === 'warn') {
|
|
260
|
-
console.warn(prefix, JSON.stringify(logEntry));
|
|
261
|
-
} else {
|
|
262
|
-
console.log(prefix, JSON.stringify(logEntry));
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
|
|
266
331
|
module.exports = {
|
|
267
|
-
createLogger
|
|
268
|
-
structuredLog
|
|
332
|
+
createLogger
|
|
269
333
|
};
|
|
270
334
|
|
package/package.json
CHANGED
|
@@ -1,16 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onlineapps/infra-logger",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Structured logging helper for infrastructure services",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"test": "
|
|
7
|
+
"test": "jest"
|
|
8
8
|
},
|
|
9
|
-
"keywords": [
|
|
9
|
+
"keywords": [
|
|
10
|
+
"logging",
|
|
11
|
+
"infrastructure",
|
|
12
|
+
"structured"
|
|
13
|
+
],
|
|
10
14
|
"author": "OnlineApps",
|
|
11
15
|
"license": "UNLICENSED",
|
|
12
16
|
"publishConfig": {
|
|
13
17
|
"registry": "https://registry.npmjs.org/"
|
|
14
|
-
}
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"jest": "^29.7.0"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {}
|
|
15
23
|
}
|
|
16
|
-
|