@mhmdhammoud/meritt-utils 1.5.6 → 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 +22 -0
- package/dist/lib/logger.js +141 -10
- package/package.json +1 -1
- package/src/__tests__/logger.test.ts +29 -0
- package/src/lib/logger.ts +159 -11
|
@@ -109,4 +109,26 @@ describe('route and format logs', () => {
|
|
|
109
109
|
key1: 'val1',
|
|
110
110
|
}));
|
|
111
111
|
});
|
|
112
|
+
test('remap reserved elastic field names and reduce objects to scalars', () => {
|
|
113
|
+
//@ts-ignore
|
|
114
|
+
jest.spyOn(pino_1.pino, 'destination').mockReturnValue(PINO_DESTINATION);
|
|
115
|
+
//@ts-ignore
|
|
116
|
+
pino_1.pino.mockReturnValue(PINO);
|
|
117
|
+
const logger = new logger_1.default(LOGGER_NAME);
|
|
118
|
+
logger.info(LOG_EVENT, {
|
|
119
|
+
_id: 'abc123',
|
|
120
|
+
nested: {
|
|
121
|
+
_id: 'nested-1',
|
|
122
|
+
_index: 'bad-index',
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
// Top-level fields use scalars only (avoids ES document_parsing_exception)
|
|
126
|
+
expect(PINO.info).toHaveBeenCalledWith(expect.objectContaining({
|
|
127
|
+
mongo_id: 'abc123',
|
|
128
|
+
nested: 'nested-1',
|
|
129
|
+
}));
|
|
130
|
+
expect(PINO.info).not.toHaveBeenCalledWith(expect.objectContaining({
|
|
131
|
+
_id: expect.anything(),
|
|
132
|
+
}));
|
|
133
|
+
});
|
|
112
134
|
});
|
package/dist/lib/logger.js
CHANGED
|
@@ -42,12 +42,138 @@ const trace_store_1 = require("./trace-store");
|
|
|
42
42
|
dotenv.config();
|
|
43
43
|
/** Convert camelCase to snake_case for Kibana/ECS-friendly field names */
|
|
44
44
|
const toSnakeCase = (str) => str.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`);
|
|
45
|
+
/**
|
|
46
|
+
* Elasticsearch reserves metadata fields (e.g. `_id`) and rejects documents
|
|
47
|
+
* containing them in payload body. Remap them to safe application fields.
|
|
48
|
+
*/
|
|
49
|
+
const RESERVED_ES_FIELD_ALIASES = {
|
|
50
|
+
_id: 'mongo_id',
|
|
51
|
+
_index: 'es_index',
|
|
52
|
+
_type: 'es_type',
|
|
53
|
+
_score: 'es_score',
|
|
54
|
+
_source: 'es_source',
|
|
55
|
+
_routing: 'es_routing',
|
|
56
|
+
_seq_no: 'es_seq_no',
|
|
57
|
+
_primary_term: 'es_primary_term',
|
|
58
|
+
_version: 'es_version',
|
|
59
|
+
};
|
|
60
|
+
/** Convert arbitrary field names into ES-safe flattened keys */
|
|
61
|
+
const toSafeElasticFieldName = (key) => {
|
|
62
|
+
const normalized = toSnakeCase(key);
|
|
63
|
+
const mapped = RESERVED_ES_FIELD_ALIASES[normalized];
|
|
64
|
+
if (mapped) {
|
|
65
|
+
return mapped;
|
|
66
|
+
}
|
|
67
|
+
// Any leading underscore can conflict with ES internals, remap defensively.
|
|
68
|
+
return normalized.startsWith('_') ? `meta${normalized}` : normalized;
|
|
69
|
+
};
|
|
70
|
+
const isObjectIdLike = (v) => v !== null &&
|
|
71
|
+
typeof v === 'object' &&
|
|
72
|
+
'toHexString' in v &&
|
|
73
|
+
typeof v.toHexString === 'function';
|
|
45
74
|
/** Check if value is a plain object (not Error, Date, Array, null) */
|
|
46
75
|
const isPlainObject = (v) => v !== null &&
|
|
47
76
|
typeof v === 'object' &&
|
|
48
77
|
!Array.isArray(v) &&
|
|
49
78
|
!(v instanceof Error) &&
|
|
50
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
|
+
/**
|
|
112
|
+
* Recursively sanitize log values for Elasticsearch safety.
|
|
113
|
+
* - Remaps reserved key names (e.g. `_id` -> `mongo_id`)
|
|
114
|
+
* - Converts Error to a stable serializable shape
|
|
115
|
+
* - Converts ObjectId-like objects to hex strings
|
|
116
|
+
* - Prevents circular structure failures
|
|
117
|
+
*/
|
|
118
|
+
const sanitizeForElastic = (value, seen = new WeakSet()) => {
|
|
119
|
+
if (value === null ||
|
|
120
|
+
typeof value === 'string' ||
|
|
121
|
+
typeof value === 'number' ||
|
|
122
|
+
typeof value === 'boolean') {
|
|
123
|
+
return value;
|
|
124
|
+
}
|
|
125
|
+
if (value instanceof Date) {
|
|
126
|
+
return value.toISOString();
|
|
127
|
+
}
|
|
128
|
+
if (value instanceof Error) {
|
|
129
|
+
return { message: value.message, type: value.constructor.name };
|
|
130
|
+
}
|
|
131
|
+
if (isObjectIdLike(value)) {
|
|
132
|
+
return value.toHexString();
|
|
133
|
+
}
|
|
134
|
+
if (Array.isArray(value)) {
|
|
135
|
+
return value.map((item) => sanitizeForElastic(item, seen));
|
|
136
|
+
}
|
|
137
|
+
if (isPlainObject(value)) {
|
|
138
|
+
if (seen.has(value)) {
|
|
139
|
+
return '[Circular]';
|
|
140
|
+
}
|
|
141
|
+
seen.add(value);
|
|
142
|
+
const sanitizedObject = {};
|
|
143
|
+
for (const [k, v] of Object.entries(value)) {
|
|
144
|
+
sanitizedObject[toSafeElasticFieldName(k)] = sanitizeForElastic(v, seen);
|
|
145
|
+
}
|
|
146
|
+
return sanitizedObject;
|
|
147
|
+
}
|
|
148
|
+
// Fallback for class instances and non-plain objects.
|
|
149
|
+
return String(value);
|
|
150
|
+
};
|
|
151
|
+
/**
|
|
152
|
+
* Captures the call site from the stack when log event is missing.
|
|
153
|
+
* Returns file:line (e.g. product.service.ts:2266) for Kibana/Elasticsearch filtering.
|
|
154
|
+
*/
|
|
155
|
+
const getCallSiteForMissingLog = () => {
|
|
156
|
+
var _a;
|
|
157
|
+
try {
|
|
158
|
+
const stack = (_a = new Error().stack) !== null && _a !== void 0 ? _a : '';
|
|
159
|
+
const lines = stack.split('\n');
|
|
160
|
+
// First frame outside Logger / node_modules / meritt-utils
|
|
161
|
+
const appFrame = lines.find((line) => !line.includes('node_modules') &&
|
|
162
|
+
!line.includes('meritt-utils') &&
|
|
163
|
+
!line.includes('Logger.'));
|
|
164
|
+
if (!appFrame)
|
|
165
|
+
return undefined;
|
|
166
|
+
// Extract file:line e.g. "product.service.ts:2266"
|
|
167
|
+
const match = appFrame.match(/([^/\\]+\.(?:ts|js|tsx|jsx)):(\d+)/);
|
|
168
|
+
if (match) {
|
|
169
|
+
return `${match[1]}:${match[2]}`;
|
|
170
|
+
}
|
|
171
|
+
return appFrame.trim().slice(0, 100);
|
|
172
|
+
}
|
|
173
|
+
catch (_b) {
|
|
174
|
+
return undefined;
|
|
175
|
+
}
|
|
176
|
+
};
|
|
51
177
|
/**
|
|
52
178
|
* Pino logger backend - singleton
|
|
53
179
|
*/
|
|
@@ -267,9 +393,10 @@ class Logger {
|
|
|
267
393
|
var _a, _b;
|
|
268
394
|
const isLocal = process.env.NODE_ENV === 'local' || process.env.NODE_ENV === 'test';
|
|
269
395
|
// Defensive: missing Logs constant (undefined) crashes on logEvent.code
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
|
|
396
|
+
const useFallback = !logEvent || typeof logEvent !== 'object' || !('code' in logEvent);
|
|
397
|
+
const event = useFallback
|
|
398
|
+
? { code: 'UNKNOWN', msg: 'Missing or invalid log event constant' }
|
|
399
|
+
: logEvent;
|
|
273
400
|
// ECS-aligned fields for Kibana (flat names to avoid mapping conflicts with existing indices)
|
|
274
401
|
const ecs = {
|
|
275
402
|
log_level: logLevel,
|
|
@@ -286,22 +413,19 @@ class Logger {
|
|
|
286
413
|
ecs.trace_id = trace.traceId;
|
|
287
414
|
}
|
|
288
415
|
// Structured context: flatten single plain object as top-level fields
|
|
289
|
-
//
|
|
416
|
+
// Use scalars only for top-level ES fields; text/keyword mappings reject nested objects
|
|
290
417
|
let detail;
|
|
291
418
|
if (args.length === 1 &&
|
|
292
419
|
isPlainObject(args[0]) &&
|
|
293
420
|
Object.keys(args[0]).length > 0) {
|
|
294
421
|
const obj = args[0];
|
|
295
422
|
for (const [k, v] of Object.entries(obj)) {
|
|
296
|
-
const key =
|
|
297
|
-
ecs[key] =
|
|
298
|
-
v instanceof Error
|
|
299
|
-
? { message: v.message, type: v.constructor.name }
|
|
300
|
-
: v;
|
|
423
|
+
const key = toSafeElasticFieldName(k);
|
|
424
|
+
ecs[key] = toScalarForTopLevel(v);
|
|
301
425
|
}
|
|
302
426
|
}
|
|
303
427
|
else {
|
|
304
|
-
detail = isLocal ? args : JSON.stringify(args);
|
|
428
|
+
detail = isLocal ? args : JSON.stringify(sanitizeForElastic(args));
|
|
305
429
|
}
|
|
306
430
|
// Legacy fields for backward compatibility (component, code, msg)
|
|
307
431
|
const base = {
|
|
@@ -313,6 +437,13 @@ class Logger {
|
|
|
313
437
|
if (detail !== undefined) {
|
|
314
438
|
base.detail = detail;
|
|
315
439
|
}
|
|
440
|
+
// When fallback used: add call site for Kibana/Elasticsearch querying
|
|
441
|
+
if (useFallback) {
|
|
442
|
+
const callSite = getCallSiteForMissingLog();
|
|
443
|
+
if (callSite) {
|
|
444
|
+
base.missing_log_call_site = callSite;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
316
447
|
return base;
|
|
317
448
|
}
|
|
318
449
|
log(logLevel, logEvent, ...args) {
|
package/package.json
CHANGED
|
@@ -89,4 +89,33 @@ describe('route and format logs', () => {
|
|
|
89
89
|
})
|
|
90
90
|
)
|
|
91
91
|
})
|
|
92
|
+
|
|
93
|
+
test('remap reserved elastic field names and reduce objects to scalars', () => {
|
|
94
|
+
//@ts-ignore
|
|
95
|
+
jest.spyOn(pino, 'destination').mockReturnValue(PINO_DESTINATION)
|
|
96
|
+
//@ts-ignore
|
|
97
|
+
pino.mockReturnValue(PINO)
|
|
98
|
+
const logger = new Logger(LOGGER_NAME)
|
|
99
|
+
|
|
100
|
+
logger.info(LOG_EVENT, {
|
|
101
|
+
_id: 'abc123',
|
|
102
|
+
nested: {
|
|
103
|
+
_id: 'nested-1',
|
|
104
|
+
_index: 'bad-index',
|
|
105
|
+
},
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
// Top-level fields use scalars only (avoids ES document_parsing_exception)
|
|
109
|
+
expect(PINO.info).toHaveBeenCalledWith(
|
|
110
|
+
expect.objectContaining({
|
|
111
|
+
mongo_id: 'abc123',
|
|
112
|
+
nested: 'nested-1',
|
|
113
|
+
})
|
|
114
|
+
)
|
|
115
|
+
expect(PINO.info).not.toHaveBeenCalledWith(
|
|
116
|
+
expect.objectContaining({
|
|
117
|
+
_id: expect.anything(),
|
|
118
|
+
})
|
|
119
|
+
)
|
|
120
|
+
})
|
|
92
121
|
})
|
package/src/lib/logger.ts
CHANGED
|
@@ -11,6 +11,39 @@ dotenv.config()
|
|
|
11
11
|
const toSnakeCase = (str: string): string =>
|
|
12
12
|
str.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`)
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Elasticsearch reserves metadata fields (e.g. `_id`) and rejects documents
|
|
16
|
+
* containing them in payload body. Remap them to safe application fields.
|
|
17
|
+
*/
|
|
18
|
+
const RESERVED_ES_FIELD_ALIASES: Record<string, string> = {
|
|
19
|
+
_id: 'mongo_id',
|
|
20
|
+
_index: 'es_index',
|
|
21
|
+
_type: 'es_type',
|
|
22
|
+
_score: 'es_score',
|
|
23
|
+
_source: 'es_source',
|
|
24
|
+
_routing: 'es_routing',
|
|
25
|
+
_seq_no: 'es_seq_no',
|
|
26
|
+
_primary_term: 'es_primary_term',
|
|
27
|
+
_version: 'es_version',
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Convert arbitrary field names into ES-safe flattened keys */
|
|
31
|
+
const toSafeElasticFieldName = (key: string): string => {
|
|
32
|
+
const normalized = toSnakeCase(key)
|
|
33
|
+
const mapped = RESERVED_ES_FIELD_ALIASES[normalized]
|
|
34
|
+
if (mapped) {
|
|
35
|
+
return mapped
|
|
36
|
+
}
|
|
37
|
+
// Any leading underscore can conflict with ES internals, remap defensively.
|
|
38
|
+
return normalized.startsWith('_') ? `meta${normalized}` : normalized
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const isObjectIdLike = (v: unknown): v is { toHexString: () => string } =>
|
|
42
|
+
v !== null &&
|
|
43
|
+
typeof v === 'object' &&
|
|
44
|
+
'toHexString' in v &&
|
|
45
|
+
typeof (v as { toHexString?: unknown }).toHexString === 'function'
|
|
46
|
+
|
|
14
47
|
/** Check if value is a plain object (not Error, Date, Array, null) */
|
|
15
48
|
const isPlainObject = (v: unknown): v is Record<string, unknown> =>
|
|
16
49
|
v !== null &&
|
|
@@ -19,6 +52,116 @@ const isPlainObject = (v: unknown): v is Record<string, unknown> =>
|
|
|
19
52
|
!(v instanceof Error) &&
|
|
20
53
|
!(v instanceof Date)
|
|
21
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
|
+
/**
|
|
86
|
+
* Recursively sanitize log values for Elasticsearch safety.
|
|
87
|
+
* - Remaps reserved key names (e.g. `_id` -> `mongo_id`)
|
|
88
|
+
* - Converts Error to a stable serializable shape
|
|
89
|
+
* - Converts ObjectId-like objects to hex strings
|
|
90
|
+
* - Prevents circular structure failures
|
|
91
|
+
*/
|
|
92
|
+
const sanitizeForElastic = (
|
|
93
|
+
value: unknown,
|
|
94
|
+
seen: WeakSet<object> = new WeakSet<object>()
|
|
95
|
+
): unknown => {
|
|
96
|
+
if (
|
|
97
|
+
value === null ||
|
|
98
|
+
typeof value === 'string' ||
|
|
99
|
+
typeof value === 'number' ||
|
|
100
|
+
typeof value === 'boolean'
|
|
101
|
+
) {
|
|
102
|
+
return value
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (value instanceof Date) {
|
|
106
|
+
return value.toISOString()
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (value instanceof Error) {
|
|
110
|
+
return { message: value.message, type: value.constructor.name }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (isObjectIdLike(value)) {
|
|
114
|
+
return value.toHexString()
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (Array.isArray(value)) {
|
|
118
|
+
return value.map((item) => sanitizeForElastic(item, seen))
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (isPlainObject(value)) {
|
|
122
|
+
if (seen.has(value)) {
|
|
123
|
+
return '[Circular]'
|
|
124
|
+
}
|
|
125
|
+
seen.add(value)
|
|
126
|
+
|
|
127
|
+
const sanitizedObject: Record<string, unknown> = {}
|
|
128
|
+
for (const [k, v] of Object.entries(value)) {
|
|
129
|
+
sanitizedObject[toSafeElasticFieldName(k)] = sanitizeForElastic(v, seen)
|
|
130
|
+
}
|
|
131
|
+
return sanitizedObject
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Fallback for class instances and non-plain objects.
|
|
135
|
+
return String(value)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Captures the call site from the stack when log event is missing.
|
|
140
|
+
* Returns file:line (e.g. product.service.ts:2266) for Kibana/Elasticsearch filtering.
|
|
141
|
+
*/
|
|
142
|
+
const getCallSiteForMissingLog = (): string | undefined => {
|
|
143
|
+
try {
|
|
144
|
+
const stack = new Error().stack ?? ''
|
|
145
|
+
const lines = stack.split('\n')
|
|
146
|
+
// First frame outside Logger / node_modules / meritt-utils
|
|
147
|
+
const appFrame = lines.find(
|
|
148
|
+
(line) =>
|
|
149
|
+
!line.includes('node_modules') &&
|
|
150
|
+
!line.includes('meritt-utils') &&
|
|
151
|
+
!line.includes('Logger.')
|
|
152
|
+
)
|
|
153
|
+
if (!appFrame) return undefined
|
|
154
|
+
// Extract file:line e.g. "product.service.ts:2266"
|
|
155
|
+
const match = appFrame.match(/([^/\\]+\.(?:ts|js|tsx|jsx)):(\d+)/)
|
|
156
|
+
if (match) {
|
|
157
|
+
return `${match[1]}:${match[2]}`
|
|
158
|
+
}
|
|
159
|
+
return appFrame.trim().slice(0, 100)
|
|
160
|
+
} catch {
|
|
161
|
+
return undefined
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
22
165
|
/**
|
|
23
166
|
* Pino logger backend - singleton
|
|
24
167
|
*/
|
|
@@ -325,10 +468,11 @@ class Logger {
|
|
|
325
468
|
process.env.NODE_ENV === 'local' || process.env.NODE_ENV === 'test'
|
|
326
469
|
|
|
327
470
|
// Defensive: missing Logs constant (undefined) crashes on logEvent.code
|
|
328
|
-
const
|
|
329
|
-
logEvent
|
|
330
|
-
|
|
331
|
-
|
|
471
|
+
const useFallback =
|
|
472
|
+
!logEvent || typeof logEvent !== 'object' || !('code' in logEvent)
|
|
473
|
+
const event = useFallback
|
|
474
|
+
? { code: 'UNKNOWN', msg: 'Missing or invalid log event constant' }
|
|
475
|
+
: logEvent
|
|
332
476
|
|
|
333
477
|
// ECS-aligned fields for Kibana (flat names to avoid mapping conflicts with existing indices)
|
|
334
478
|
const ecs: Record<string, unknown> = {
|
|
@@ -348,7 +492,7 @@ class Logger {
|
|
|
348
492
|
}
|
|
349
493
|
|
|
350
494
|
// Structured context: flatten single plain object as top-level fields
|
|
351
|
-
//
|
|
495
|
+
// Use scalars only for top-level ES fields; text/keyword mappings reject nested objects
|
|
352
496
|
let detail: unknown
|
|
353
497
|
if (
|
|
354
498
|
args.length === 1 &&
|
|
@@ -357,14 +501,11 @@ class Logger {
|
|
|
357
501
|
) {
|
|
358
502
|
const obj = args[0]
|
|
359
503
|
for (const [k, v] of Object.entries(obj)) {
|
|
360
|
-
const key =
|
|
361
|
-
ecs[key] =
|
|
362
|
-
v instanceof Error
|
|
363
|
-
? { message: v.message, type: v.constructor.name }
|
|
364
|
-
: v
|
|
504
|
+
const key = toSafeElasticFieldName(k)
|
|
505
|
+
ecs[key] = toScalarForTopLevel(v)
|
|
365
506
|
}
|
|
366
507
|
} else {
|
|
367
|
-
detail = isLocal ? args : JSON.stringify(args)
|
|
508
|
+
detail = isLocal ? args : JSON.stringify(sanitizeForElastic(args))
|
|
368
509
|
}
|
|
369
510
|
|
|
370
511
|
// Legacy fields for backward compatibility (component, code, msg)
|
|
@@ -377,6 +518,13 @@ class Logger {
|
|
|
377
518
|
if (detail !== undefined) {
|
|
378
519
|
base.detail = detail
|
|
379
520
|
}
|
|
521
|
+
// When fallback used: add call site for Kibana/Elasticsearch querying
|
|
522
|
+
if (useFallback) {
|
|
523
|
+
const callSite = getCallSiteForMissingLog()
|
|
524
|
+
if (callSite) {
|
|
525
|
+
base.missing_log_call_site = callSite
|
|
526
|
+
}
|
|
527
|
+
}
|
|
380
528
|
return base
|
|
381
529
|
}
|
|
382
530
|
|