@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.
@@ -109,7 +109,7 @@ describe('route and format logs', () => {
109
109
  key1: 'val1',
110
110
  }));
111
111
  });
112
- test('remap reserved elastic field names dynamically', () => {
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: expect.objectContaining({
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(),
@@ -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
- // Sanitize values to avoid ES mapping conflicts (e.g. Error objects → serializable shape)
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] = sanitizeForElastic(v);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mhmdhammoud/meritt-utils",
3
- "version": "1.5.7",
3
+ "version": "1.5.8",
4
4
  "description": "",
5
5
  "main": "./dist/index.js",
6
6
  "private": false,
@@ -90,7 +90,7 @@ describe('route and format logs', () => {
90
90
  )
91
91
  })
92
92
 
93
- test('remap reserved elastic field names dynamically', () => {
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: expect.objectContaining({
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
- // Sanitize values to avoid ES mapping conflicts (e.g. Error objects → serializable shape)
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] = sanitizeForElastic(v)
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)