@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.
@@ -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 flattened)', () => {
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 and reduce objects to scalars', () => {
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
- // Top-level fields use scalars only (avoids ES document_parsing_exception)
124
+ // Context holds sanitized structure; reserved names remapped recursively
126
125
  expect(PINO.info).toHaveBeenCalledWith(expect.objectContaining({
127
- mongo_id: 'abc123',
128
- nested: 'nested-1',
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(),
@@ -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: flatten single plain object as top-level fields
416
- // Use scalars only for top-level ES fields; text/keyword mappings reject nested objects
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
- const obj = args[0];
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mhmdhammoud/meritt-utils",
3
- "version": "1.5.8",
3
+ "version": "1.5.9",
4
4
  "description": "",
5
5
  "main": "./dist/index.js",
6
6
  "private": false,
@@ -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 flattened)', () => {
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 and reduce objects to scalars', () => {
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
- // Top-level fields use scalars only (avoids ES document_parsing_exception)
107
+ // Context holds sanitized structure; reserved names remapped recursively
109
108
  expect(PINO.info).toHaveBeenCalledWith(
110
109
  expect.objectContaining({
111
- mongo_id: 'abc123',
112
- nested: 'nested-1',
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: flatten single plain object as top-level fields
495
- // Use scalars only for top-level ES fields; text/keyword mappings reject nested objects
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
- const obj = args[0]
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
  }