@mhmdhammoud/meritt-utils 1.5.8 → 1.5.9
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/dist/__tests__/logger.test.js +8 -7
- package/dist/lib/logger.js +8 -38
- package/package.json +1 -1
- package/src/__tests__/logger.test.ts +8 -7
- package/src/lib/logger.ts +8 -37
|
@@ -94,7 +94,7 @@ describe('route and format logs', () => {
|
|
|
94
94
|
key0: 'val0',
|
|
95
95
|
key1: 'val1',
|
|
96
96
|
};
|
|
97
|
-
test('log info with structured context (single object
|
|
97
|
+
test('log info with structured context (single object in context field)', () => {
|
|
98
98
|
//@ts-ignore
|
|
99
99
|
jest.spyOn(pino_1.pino, 'destination').mockReturnValue(PINO_DESTINATION);
|
|
100
100
|
//@ts-ignore
|
|
@@ -105,11 +105,10 @@ describe('route and format logs', () => {
|
|
|
105
105
|
component: LOGGER_NAME,
|
|
106
106
|
code: LOG_EVENT.code,
|
|
107
107
|
msg: LOG_EVENT.msg,
|
|
108
|
-
key0: 'val0',
|
|
109
|
-
key1: 'val1',
|
|
108
|
+
context: { key0: 'val0', key1: 'val1' },
|
|
110
109
|
}));
|
|
111
110
|
});
|
|
112
|
-
test('remap reserved elastic field names
|
|
111
|
+
test('remap reserved elastic field names in context', () => {
|
|
113
112
|
//@ts-ignore
|
|
114
113
|
jest.spyOn(pino_1.pino, 'destination').mockReturnValue(PINO_DESTINATION);
|
|
115
114
|
//@ts-ignore
|
|
@@ -122,10 +121,12 @@ describe('route and format logs', () => {
|
|
|
122
121
|
_index: 'bad-index',
|
|
123
122
|
},
|
|
124
123
|
});
|
|
125
|
-
//
|
|
124
|
+
// Context holds sanitized structure; reserved names remapped recursively
|
|
126
125
|
expect(PINO.info).toHaveBeenCalledWith(expect.objectContaining({
|
|
127
|
-
|
|
128
|
-
|
|
126
|
+
context: expect.objectContaining({
|
|
127
|
+
mongo_id: 'abc123',
|
|
128
|
+
nested: { mongo_id: 'nested-1', es_index: 'bad-index' },
|
|
129
|
+
}),
|
|
129
130
|
}));
|
|
130
131
|
expect(PINO.info).not.toHaveBeenCalledWith(expect.objectContaining({
|
|
131
132
|
_id: expect.anything(),
|
package/dist/lib/logger.js
CHANGED
|
@@ -77,37 +77,6 @@ const isPlainObject = (v) => v !== null &&
|
|
|
77
77
|
!Array.isArray(v) &&
|
|
78
78
|
!(v instanceof Error) &&
|
|
79
79
|
!(v instanceof Date);
|
|
80
|
-
/**
|
|
81
|
-
* Reduces object values to scalars for top-level ES fields.
|
|
82
|
-
* ES text/keyword fields reject nested objects; use _id or truncated JSON.
|
|
83
|
-
*/
|
|
84
|
-
const toScalarForTopLevel = (value) => {
|
|
85
|
-
if (value === null || value === undefined)
|
|
86
|
-
return null;
|
|
87
|
-
if (typeof value === 'string' ||
|
|
88
|
-
typeof value === 'number' ||
|
|
89
|
-
typeof value === 'boolean')
|
|
90
|
-
return value;
|
|
91
|
-
if (value instanceof Date)
|
|
92
|
-
return value.toISOString();
|
|
93
|
-
if (isObjectIdLike(value))
|
|
94
|
-
return value.toHexString();
|
|
95
|
-
if (value instanceof Error)
|
|
96
|
-
return value.message;
|
|
97
|
-
if (isPlainObject(value) && '_id' in value) {
|
|
98
|
-
const id = value._id;
|
|
99
|
-
if (id != null) {
|
|
100
|
-
if (typeof id === 'string')
|
|
101
|
-
return id;
|
|
102
|
-
if (isObjectIdLike(id))
|
|
103
|
-
return id.toHexString();
|
|
104
|
-
if (typeof id === 'object' && id !== null && 'toString' in id)
|
|
105
|
-
return String(id.toString());
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
const str = typeof value === 'object' ? JSON.stringify(value) : String(value);
|
|
109
|
-
return str.length > 200 ? `${str.slice(0, 200)}...` : str;
|
|
110
|
-
};
|
|
111
80
|
/**
|
|
112
81
|
* Recursively sanitize log values for Elasticsearch safety.
|
|
113
82
|
* - Remaps reserved key names (e.g. `_id` -> `mongo_id`)
|
|
@@ -412,17 +381,15 @@ class Logger {
|
|
|
412
381
|
if (trace) {
|
|
413
382
|
ecs.trace_id = trace.traceId;
|
|
414
383
|
}
|
|
415
|
-
// Structured context:
|
|
416
|
-
//
|
|
384
|
+
// Structured context: put in single 'context' field to avoid ES mapping conflicts.
|
|
385
|
+
// Flattening to top-level caused document_parsing_exception (object vs scalar type mismatches).
|
|
386
|
+
// Nesting in context keeps structure consistent and avoids per-field mapping conflicts.
|
|
387
|
+
let context;
|
|
417
388
|
let detail;
|
|
418
389
|
if (args.length === 1 &&
|
|
419
390
|
isPlainObject(args[0]) &&
|
|
420
391
|
Object.keys(args[0]).length > 0) {
|
|
421
|
-
|
|
422
|
-
for (const [k, v] of Object.entries(obj)) {
|
|
423
|
-
const key = toSafeElasticFieldName(k);
|
|
424
|
-
ecs[key] = toScalarForTopLevel(v);
|
|
425
|
-
}
|
|
392
|
+
context = sanitizeForElastic(args[0]);
|
|
426
393
|
}
|
|
427
394
|
else {
|
|
428
395
|
detail = isLocal ? args : JSON.stringify(sanitizeForElastic(args));
|
|
@@ -434,6 +401,9 @@ class Logger {
|
|
|
434
401
|
code: event.code,
|
|
435
402
|
msg: event.msg,
|
|
436
403
|
};
|
|
404
|
+
if (context !== undefined) {
|
|
405
|
+
base.context = context;
|
|
406
|
+
}
|
|
437
407
|
if (detail !== undefined) {
|
|
438
408
|
base.detail = detail;
|
|
439
409
|
}
|
package/package.json
CHANGED
|
@@ -70,7 +70,7 @@ describe('route and format logs', () => {
|
|
|
70
70
|
key0: 'val0',
|
|
71
71
|
key1: 'val1',
|
|
72
72
|
}
|
|
73
|
-
test('log info with structured context (single object
|
|
73
|
+
test('log info with structured context (single object in context field)', () => {
|
|
74
74
|
//@ts-ignore
|
|
75
75
|
jest.spyOn(pino, 'destination').mockReturnValue(PINO_DESTINATION)
|
|
76
76
|
//@ts-ignore
|
|
@@ -84,13 +84,12 @@ describe('route and format logs', () => {
|
|
|
84
84
|
component: LOGGER_NAME,
|
|
85
85
|
code: LOG_EVENT.code,
|
|
86
86
|
msg: LOG_EVENT.msg,
|
|
87
|
-
key0: 'val0',
|
|
88
|
-
key1: 'val1',
|
|
87
|
+
context: { key0: 'val0', key1: 'val1' },
|
|
89
88
|
})
|
|
90
89
|
)
|
|
91
90
|
})
|
|
92
91
|
|
|
93
|
-
test('remap reserved elastic field names
|
|
92
|
+
test('remap reserved elastic field names in context', () => {
|
|
94
93
|
//@ts-ignore
|
|
95
94
|
jest.spyOn(pino, 'destination').mockReturnValue(PINO_DESTINATION)
|
|
96
95
|
//@ts-ignore
|
|
@@ -105,11 +104,13 @@ describe('route and format logs', () => {
|
|
|
105
104
|
},
|
|
106
105
|
})
|
|
107
106
|
|
|
108
|
-
//
|
|
107
|
+
// Context holds sanitized structure; reserved names remapped recursively
|
|
109
108
|
expect(PINO.info).toHaveBeenCalledWith(
|
|
110
109
|
expect.objectContaining({
|
|
111
|
-
|
|
112
|
-
|
|
110
|
+
context: expect.objectContaining({
|
|
111
|
+
mongo_id: 'abc123',
|
|
112
|
+
nested: { mongo_id: 'nested-1', es_index: 'bad-index' },
|
|
113
|
+
}),
|
|
113
114
|
})
|
|
114
115
|
)
|
|
115
116
|
expect(PINO.info).not.toHaveBeenCalledWith(
|
package/src/lib/logger.ts
CHANGED
|
@@ -52,36 +52,6 @@ const isPlainObject = (v: unknown): v is Record<string, unknown> =>
|
|
|
52
52
|
!(v instanceof Error) &&
|
|
53
53
|
!(v instanceof Date)
|
|
54
54
|
|
|
55
|
-
/**
|
|
56
|
-
* Reduces object values to scalars for top-level ES fields.
|
|
57
|
-
* ES text/keyword fields reject nested objects; use _id or truncated JSON.
|
|
58
|
-
*/
|
|
59
|
-
const toScalarForTopLevel = (
|
|
60
|
-
value: unknown
|
|
61
|
-
): string | number | boolean | null => {
|
|
62
|
-
if (value === null || value === undefined) return null
|
|
63
|
-
if (
|
|
64
|
-
typeof value === 'string' ||
|
|
65
|
-
typeof value === 'number' ||
|
|
66
|
-
typeof value === 'boolean'
|
|
67
|
-
)
|
|
68
|
-
return value
|
|
69
|
-
if (value instanceof Date) return value.toISOString()
|
|
70
|
-
if (isObjectIdLike(value)) return value.toHexString()
|
|
71
|
-
if (value instanceof Error) return value.message
|
|
72
|
-
if (isPlainObject(value) && '_id' in value) {
|
|
73
|
-
const id = (value as { _id?: unknown })._id
|
|
74
|
-
if (id != null) {
|
|
75
|
-
if (typeof id === 'string') return id
|
|
76
|
-
if (isObjectIdLike(id)) return id.toHexString()
|
|
77
|
-
if (typeof id === 'object' && id !== null && 'toString' in id)
|
|
78
|
-
return String((id as { toString: () => string }).toString())
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
const str = typeof value === 'object' ? JSON.stringify(value) : String(value)
|
|
82
|
-
return str.length > 200 ? `${str.slice(0, 200)}...` : str
|
|
83
|
-
}
|
|
84
|
-
|
|
85
55
|
/**
|
|
86
56
|
* Recursively sanitize log values for Elasticsearch safety.
|
|
87
57
|
* - Remaps reserved key names (e.g. `_id` -> `mongo_id`)
|
|
@@ -491,19 +461,17 @@ class Logger {
|
|
|
491
461
|
ecs.trace_id = trace.traceId
|
|
492
462
|
}
|
|
493
463
|
|
|
494
|
-
// Structured context:
|
|
495
|
-
//
|
|
464
|
+
// Structured context: put in single 'context' field to avoid ES mapping conflicts.
|
|
465
|
+
// Flattening to top-level caused document_parsing_exception (object vs scalar type mismatches).
|
|
466
|
+
// Nesting in context keeps structure consistent and avoids per-field mapping conflicts.
|
|
467
|
+
let context: Record<string, unknown> | undefined
|
|
496
468
|
let detail: unknown
|
|
497
469
|
if (
|
|
498
470
|
args.length === 1 &&
|
|
499
471
|
isPlainObject(args[0]) &&
|
|
500
472
|
Object.keys(args[0]).length > 0
|
|
501
473
|
) {
|
|
502
|
-
|
|
503
|
-
for (const [k, v] of Object.entries(obj)) {
|
|
504
|
-
const key = toSafeElasticFieldName(k)
|
|
505
|
-
ecs[key] = toScalarForTopLevel(v)
|
|
506
|
-
}
|
|
474
|
+
context = sanitizeForElastic(args[0]) as Record<string, unknown>
|
|
507
475
|
} else {
|
|
508
476
|
detail = isLocal ? args : JSON.stringify(sanitizeForElastic(args))
|
|
509
477
|
}
|
|
@@ -515,6 +483,9 @@ class Logger {
|
|
|
515
483
|
code: event.code,
|
|
516
484
|
msg: event.msg,
|
|
517
485
|
}
|
|
486
|
+
if (context !== undefined) {
|
|
487
|
+
base.context = context
|
|
488
|
+
}
|
|
518
489
|
if (detail !== undefined) {
|
|
519
490
|
base.detail = detail
|
|
520
491
|
}
|