@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.
Files changed (2) hide show
  1. package/index.js +151 -87
  2. 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, process, fail).
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, process, fail, lifecycle methods
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, JSON.stringify(logEntry));
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, JSON.stringify(logEntry));
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?.message || error || 'Unknown error',
181
- error_type: error?.constructor?.name || typeof 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, JSON.stringify(logEntry));
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, JSON.stringify(logEntry));
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": "1.0.0",
3
+ "version": "2.0.0",
4
4
  "description": "Structured logging helper for infrastructure services",
5
5
  "main": "index.js",
6
6
  "scripts": {
7
- "test": "echo \"No tests yet\""
7
+ "test": "jest"
8
8
  },
9
- "keywords": ["logging", "infrastructure", "structured"],
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
-