@mhmdhammoud/meritt-utils 1.5.7 → 1.5.8
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 +3 -5
- package/dist/lib/logger.js +34 -3
- package/package.json +1 -1
- package/src/__tests__/logger.test.ts +3 -5
- package/src/lib/logger.ts +33 -3
|
@@ -109,7 +109,7 @@ describe('route and format logs', () => {
|
|
|
109
109
|
key1: 'val1',
|
|
110
110
|
}));
|
|
111
111
|
});
|
|
112
|
-
test('remap reserved elastic field names
|
|
112
|
+
test('remap reserved elastic field names and reduce objects to scalars', () => {
|
|
113
113
|
//@ts-ignore
|
|
114
114
|
jest.spyOn(pino_1.pino, 'destination').mockReturnValue(PINO_DESTINATION);
|
|
115
115
|
//@ts-ignore
|
|
@@ -122,12 +122,10 @@ describe('route and format logs', () => {
|
|
|
122
122
|
_index: 'bad-index',
|
|
123
123
|
},
|
|
124
124
|
});
|
|
125
|
+
// Top-level fields use scalars only (avoids ES document_parsing_exception)
|
|
125
126
|
expect(PINO.info).toHaveBeenCalledWith(expect.objectContaining({
|
|
126
127
|
mongo_id: 'abc123',
|
|
127
|
-
nested:
|
|
128
|
-
mongo_id: 'nested-1',
|
|
129
|
-
es_index: 'bad-index',
|
|
130
|
-
}),
|
|
128
|
+
nested: 'nested-1',
|
|
131
129
|
}));
|
|
132
130
|
expect(PINO.info).not.toHaveBeenCalledWith(expect.objectContaining({
|
|
133
131
|
_id: expect.anything(),
|
package/dist/lib/logger.js
CHANGED
|
@@ -77,6 +77,37 @@ 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
|
+
};
|
|
80
111
|
/**
|
|
81
112
|
* Recursively sanitize log values for Elasticsearch safety.
|
|
82
113
|
* - Remaps reserved key names (e.g. `_id` -> `mongo_id`)
|
|
@@ -382,7 +413,7 @@ class Logger {
|
|
|
382
413
|
ecs.trace_id = trace.traceId;
|
|
383
414
|
}
|
|
384
415
|
// Structured context: flatten single plain object as top-level fields
|
|
385
|
-
//
|
|
416
|
+
// Use scalars only for top-level ES fields; text/keyword mappings reject nested objects
|
|
386
417
|
let detail;
|
|
387
418
|
if (args.length === 1 &&
|
|
388
419
|
isPlainObject(args[0]) &&
|
|
@@ -390,11 +421,11 @@ class Logger {
|
|
|
390
421
|
const obj = args[0];
|
|
391
422
|
for (const [k, v] of Object.entries(obj)) {
|
|
392
423
|
const key = toSafeElasticFieldName(k);
|
|
393
|
-
ecs[key] =
|
|
424
|
+
ecs[key] = toScalarForTopLevel(v);
|
|
394
425
|
}
|
|
395
426
|
}
|
|
396
427
|
else {
|
|
397
|
-
detail = isLocal ? args : JSON.stringify(args);
|
|
428
|
+
detail = isLocal ? args : JSON.stringify(sanitizeForElastic(args));
|
|
398
429
|
}
|
|
399
430
|
// Legacy fields for backward compatibility (component, code, msg)
|
|
400
431
|
const base = {
|
package/package.json
CHANGED
|
@@ -90,7 +90,7 @@ describe('route and format logs', () => {
|
|
|
90
90
|
)
|
|
91
91
|
})
|
|
92
92
|
|
|
93
|
-
test('remap reserved elastic field names
|
|
93
|
+
test('remap reserved elastic field names and reduce objects to scalars', () => {
|
|
94
94
|
//@ts-ignore
|
|
95
95
|
jest.spyOn(pino, 'destination').mockReturnValue(PINO_DESTINATION)
|
|
96
96
|
//@ts-ignore
|
|
@@ -105,13 +105,11 @@ describe('route and format logs', () => {
|
|
|
105
105
|
},
|
|
106
106
|
})
|
|
107
107
|
|
|
108
|
+
// Top-level fields use scalars only (avoids ES document_parsing_exception)
|
|
108
109
|
expect(PINO.info).toHaveBeenCalledWith(
|
|
109
110
|
expect.objectContaining({
|
|
110
111
|
mongo_id: 'abc123',
|
|
111
|
-
nested:
|
|
112
|
-
mongo_id: 'nested-1',
|
|
113
|
-
es_index: 'bad-index',
|
|
114
|
-
}),
|
|
112
|
+
nested: 'nested-1',
|
|
115
113
|
})
|
|
116
114
|
)
|
|
117
115
|
expect(PINO.info).not.toHaveBeenCalledWith(
|
package/src/lib/logger.ts
CHANGED
|
@@ -52,6 +52,36 @@ 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
|
+
|
|
55
85
|
/**
|
|
56
86
|
* Recursively sanitize log values for Elasticsearch safety.
|
|
57
87
|
* - Remaps reserved key names (e.g. `_id` -> `mongo_id`)
|
|
@@ -462,7 +492,7 @@ class Logger {
|
|
|
462
492
|
}
|
|
463
493
|
|
|
464
494
|
// Structured context: flatten single plain object as top-level fields
|
|
465
|
-
//
|
|
495
|
+
// Use scalars only for top-level ES fields; text/keyword mappings reject nested objects
|
|
466
496
|
let detail: unknown
|
|
467
497
|
if (
|
|
468
498
|
args.length === 1 &&
|
|
@@ -472,10 +502,10 @@ class Logger {
|
|
|
472
502
|
const obj = args[0]
|
|
473
503
|
for (const [k, v] of Object.entries(obj)) {
|
|
474
504
|
const key = toSafeElasticFieldName(k)
|
|
475
|
-
ecs[key] =
|
|
505
|
+
ecs[key] = toScalarForTopLevel(v)
|
|
476
506
|
}
|
|
477
507
|
} else {
|
|
478
|
-
detail = isLocal ? args : JSON.stringify(args)
|
|
508
|
+
detail = isLocal ? args : JSON.stringify(sanitizeForElastic(args))
|
|
479
509
|
}
|
|
480
510
|
|
|
481
511
|
// Legacy fields for backward compatibility (component, code, msg)
|