@mhmdhammoud/meritt-utils 1.5.3 → 1.5.5
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 +7 -5
- package/dist/lib/index.d.ts +1 -0
- package/dist/lib/index.js +5 -1
- package/dist/lib/logger.d.ts +17 -0
- package/dist/lib/logger.js +95 -8
- package/dist/lib/trace-store.d.ts +27 -0
- package/dist/lib/trace-store.js +34 -0
- package/package.json +2 -2
- package/src/__tests__/logger.test.ts +10 -6
- package/src/lib/index.ts +6 -0
- package/src/lib/logger.ts +114 -12
- package/src/lib/trace-store.ts +46 -0
|
@@ -94,17 +94,19 @@ describe('route and format logs', () => {
|
|
|
94
94
|
key0: 'val0',
|
|
95
95
|
key1: 'val1',
|
|
96
96
|
};
|
|
97
|
-
test('log info with
|
|
97
|
+
test('log info with structured context (single object flattened)', () => {
|
|
98
98
|
//@ts-ignore
|
|
99
99
|
jest.spyOn(pino_1.pino, 'destination').mockReturnValue(PINO_DESTINATION);
|
|
100
100
|
//@ts-ignore
|
|
101
101
|
pino_1.pino.mockReturnValue(PINO);
|
|
102
102
|
const logger = new logger_1.default(LOGGER_NAME);
|
|
103
103
|
logger.info(LOG_EVENT, DETAILS);
|
|
104
|
-
expect(PINO.info).toHaveBeenCalledWith({
|
|
104
|
+
expect(PINO.info).toHaveBeenCalledWith(expect.objectContaining({
|
|
105
105
|
component: LOGGER_NAME,
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
106
|
+
code: LOG_EVENT.code,
|
|
107
|
+
msg: LOG_EVENT.msg,
|
|
108
|
+
key0: 'val0',
|
|
109
|
+
key1: 'val1',
|
|
110
|
+
}));
|
|
109
111
|
});
|
|
110
112
|
});
|
package/dist/lib/index.d.ts
CHANGED
|
@@ -4,3 +4,4 @@ export { default as Pdf } from './formatter';
|
|
|
4
4
|
export { default as Colorful } from './colorful';
|
|
5
5
|
export { default as ImageFull } from './imagefull';
|
|
6
6
|
export { default as Logger } from './logger';
|
|
7
|
+
export { runWithTrace, runWithTraceSync, getTraceContext, type TraceContext, } from './trace-store';
|
package/dist/lib/index.js
CHANGED
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.Logger = exports.ImageFull = exports.Colorful = exports.Pdf = exports.Formatter = exports.Crypto = void 0;
|
|
6
|
+
exports.getTraceContext = exports.runWithTraceSync = exports.runWithTrace = exports.Logger = exports.ImageFull = exports.Colorful = exports.Pdf = exports.Formatter = exports.Crypto = void 0;
|
|
7
7
|
var cypto_1 = require("./cypto");
|
|
8
8
|
Object.defineProperty(exports, "Crypto", { enumerable: true, get: function () { return __importDefault(cypto_1).default; } });
|
|
9
9
|
var formatter_1 = require("./formatter");
|
|
@@ -16,3 +16,7 @@ var imagefull_1 = require("./imagefull");
|
|
|
16
16
|
Object.defineProperty(exports, "ImageFull", { enumerable: true, get: function () { return __importDefault(imagefull_1).default; } });
|
|
17
17
|
var logger_1 = require("./logger");
|
|
18
18
|
Object.defineProperty(exports, "Logger", { enumerable: true, get: function () { return __importDefault(logger_1).default; } });
|
|
19
|
+
var trace_store_1 = require("./trace-store");
|
|
20
|
+
Object.defineProperty(exports, "runWithTrace", { enumerable: true, get: function () { return trace_store_1.runWithTrace; } });
|
|
21
|
+
Object.defineProperty(exports, "runWithTraceSync", { enumerable: true, get: function () { return trace_store_1.runWithTraceSync; } });
|
|
22
|
+
Object.defineProperty(exports, "getTraceContext", { enumerable: true, get: function () { return trace_store_1.getTraceContext; } });
|
package/dist/lib/logger.d.ts
CHANGED
|
@@ -23,6 +23,13 @@ declare class Logger {
|
|
|
23
23
|
* @param elasticConfig - Optional Elasticsearch configuration.
|
|
24
24
|
*/
|
|
25
25
|
constructor(name: string, elasticConfig?: ElasticConfig);
|
|
26
|
+
/**
|
|
27
|
+
* Build ECS-aligned and structured log payload.
|
|
28
|
+
* - ECS: log.level, log.logger, event.code, service.name, service.environment, message
|
|
29
|
+
* - Structured: Single plain object flattened as top-level snake_case fields (Kibana filterable)
|
|
30
|
+
* - Trace: trace.id when running inside runWithTrace
|
|
31
|
+
*/
|
|
32
|
+
private buildPayload;
|
|
26
33
|
private log;
|
|
27
34
|
/**
|
|
28
35
|
* Logs an error message.
|
|
@@ -54,5 +61,15 @@ declare class Logger {
|
|
|
54
61
|
* @param args - Additional arguments to include in the log.
|
|
55
62
|
*/
|
|
56
63
|
trace(logEvent: LogEvent, ...args: unknown[]): void;
|
|
64
|
+
/**
|
|
65
|
+
* Runs an async operation and logs its duration.
|
|
66
|
+
* Adds event.duration (ms) for Kibana performance dashboards and alerts.
|
|
67
|
+
*
|
|
68
|
+
* @param logEvent - The event to log on completion
|
|
69
|
+
* @param fn - Async function to execute
|
|
70
|
+
* @param context - Optional context object (flattened as top-level fields)
|
|
71
|
+
* @returns Result of fn
|
|
72
|
+
*/
|
|
73
|
+
withDuration<T>(logEvent: LogEvent, fn: () => Promise<T>, context?: Record<string, unknown>): Promise<T>;
|
|
57
74
|
}
|
|
58
75
|
export default Logger;
|
package/dist/lib/logger.js
CHANGED
|
@@ -36,8 +36,18 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.isValidLogLevel = isValidLogLevel;
|
|
37
37
|
const pino_1 = require("pino");
|
|
38
38
|
const dotenv = __importStar(require("dotenv"));
|
|
39
|
+
const os_1 = require("os");
|
|
39
40
|
const elastic_transport_1 = require("./elastic-transport");
|
|
41
|
+
const trace_store_1 = require("./trace-store");
|
|
40
42
|
dotenv.config();
|
|
43
|
+
/** Convert camelCase to snake_case for Kibana/ECS-friendly field names */
|
|
44
|
+
const toSnakeCase = (str) => str.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`);
|
|
45
|
+
/** Check if value is a plain object (not Error, Date, Array, null) */
|
|
46
|
+
const isPlainObject = (v) => v !== null &&
|
|
47
|
+
typeof v === 'object' &&
|
|
48
|
+
!Array.isArray(v) &&
|
|
49
|
+
!(v instanceof Error) &&
|
|
50
|
+
!(v instanceof Date);
|
|
41
51
|
/**
|
|
42
52
|
* Pino logger backend - singleton
|
|
43
53
|
*/
|
|
@@ -240,19 +250,64 @@ class Logger {
|
|
|
240
250
|
this._name = name;
|
|
241
251
|
this._logger = getLogger(elasticConfig);
|
|
242
252
|
}
|
|
243
|
-
|
|
253
|
+
/**
|
|
254
|
+
* Build ECS-aligned and structured log payload.
|
|
255
|
+
* - ECS: log.level, log.logger, event.code, service.name, service.environment, message
|
|
256
|
+
* - Structured: Single plain object flattened as top-level snake_case fields (Kibana filterable)
|
|
257
|
+
* - Trace: trace.id when running inside runWithTrace
|
|
258
|
+
*/
|
|
259
|
+
buildPayload(logLevel, logEvent, args) {
|
|
260
|
+
var _a, _b;
|
|
261
|
+
const isLocal = process.env.NODE_ENV === 'local' || process.env.NODE_ENV === 'test';
|
|
262
|
+
// ECS-aligned fields for Kibana Discover, Lens, alerts
|
|
263
|
+
const ecs = {
|
|
264
|
+
'log.level': logLevel,
|
|
265
|
+
'log.logger': this._name,
|
|
266
|
+
'event.code': logEvent.code,
|
|
267
|
+
message: logEvent.msg,
|
|
268
|
+
service: {
|
|
269
|
+
name: (_a = process.env.SERVER_NICKNAME) !== null && _a !== void 0 ? _a : 'unknown',
|
|
270
|
+
environment: (_b = process.env.NODE_ENV) !== null && _b !== void 0 ? _b : 'development',
|
|
271
|
+
},
|
|
272
|
+
host: {
|
|
273
|
+
name: (0, os_1.hostname)(),
|
|
274
|
+
},
|
|
275
|
+
};
|
|
276
|
+
// Trace context for request-scoped correlation
|
|
277
|
+
const trace = (0, trace_store_1.getTraceContext)();
|
|
278
|
+
if (trace) {
|
|
279
|
+
;
|
|
280
|
+
ecs.trace = { id: trace.traceId };
|
|
281
|
+
}
|
|
282
|
+
// Structured context: flatten single plain object as top-level fields
|
|
244
283
|
let detail;
|
|
245
|
-
if (
|
|
246
|
-
|
|
284
|
+
if (args.length === 1 &&
|
|
285
|
+
isPlainObject(args[0]) &&
|
|
286
|
+
Object.keys(args[0]).length > 0) {
|
|
287
|
+
const obj = args[0];
|
|
288
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
289
|
+
const key = toSnakeCase(k);
|
|
290
|
+
ecs[key] = v;
|
|
291
|
+
}
|
|
247
292
|
}
|
|
248
293
|
else {
|
|
249
|
-
detail = JSON.stringify(args);
|
|
294
|
+
detail = isLocal ? args : JSON.stringify(args);
|
|
250
295
|
}
|
|
251
|
-
|
|
296
|
+
// Legacy fields for backward compatibility (component, code, msg)
|
|
297
|
+
const base = {
|
|
298
|
+
...ecs,
|
|
252
299
|
component: this._name,
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
}
|
|
300
|
+
code: logEvent.code,
|
|
301
|
+
msg: logEvent.msg,
|
|
302
|
+
};
|
|
303
|
+
if (detail !== undefined) {
|
|
304
|
+
base.detail = detail;
|
|
305
|
+
}
|
|
306
|
+
return base;
|
|
307
|
+
}
|
|
308
|
+
log(logLevel, logEvent, ...args) {
|
|
309
|
+
const payload = this.buildPayload(logLevel, logEvent, args);
|
|
310
|
+
this._logger[logLevel](payload);
|
|
256
311
|
}
|
|
257
312
|
/**
|
|
258
313
|
* Logs an error message.
|
|
@@ -294,5 +349,37 @@ class Logger {
|
|
|
294
349
|
trace(logEvent, ...args) {
|
|
295
350
|
this.log('trace', logEvent, ...args);
|
|
296
351
|
}
|
|
352
|
+
/**
|
|
353
|
+
* Runs an async operation and logs its duration.
|
|
354
|
+
* Adds event.duration (ms) for Kibana performance dashboards and alerts.
|
|
355
|
+
*
|
|
356
|
+
* @param logEvent - The event to log on completion
|
|
357
|
+
* @param fn - Async function to execute
|
|
358
|
+
* @param context - Optional context object (flattened as top-level fields)
|
|
359
|
+
* @returns Result of fn
|
|
360
|
+
*/
|
|
361
|
+
async withDuration(logEvent, fn, context) {
|
|
362
|
+
const start = Date.now();
|
|
363
|
+
try {
|
|
364
|
+
const result = await fn();
|
|
365
|
+
const durationMs = Date.now() - start;
|
|
366
|
+
const payload = this.buildPayload('info', logEvent, [
|
|
367
|
+
{ ...context, duration_ms: durationMs, success: true },
|
|
368
|
+
]);
|
|
369
|
+
this._logger.info(payload);
|
|
370
|
+
return result;
|
|
371
|
+
}
|
|
372
|
+
catch (error) {
|
|
373
|
+
const durationMs = Date.now() - start;
|
|
374
|
+
const errObj = error instanceof Error
|
|
375
|
+
? { error_message: error.message, error_type: error.constructor.name }
|
|
376
|
+
: {};
|
|
377
|
+
const payload = this.buildPayload('error', logEvent, [
|
|
378
|
+
{ ...context, ...errObj, duration_ms: durationMs, success: false },
|
|
379
|
+
]);
|
|
380
|
+
this._logger.error(payload);
|
|
381
|
+
throw error;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
297
384
|
}
|
|
298
385
|
exports.default = Logger;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Trace context for request-scoped correlation in Kibana.
|
|
3
|
+
* Enables filtering all logs from a single request/job by trace.id.
|
|
4
|
+
*/
|
|
5
|
+
export interface TraceContext {
|
|
6
|
+
/** Unique ID for the entire request/job flow */
|
|
7
|
+
traceId: string;
|
|
8
|
+
/** Optional span ID for sub-operations */
|
|
9
|
+
spanId?: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Run an async function with a new trace context.
|
|
13
|
+
* All Logger calls within the callback will automatically include trace.id.
|
|
14
|
+
*
|
|
15
|
+
* @param fn - Async function to run within the trace context
|
|
16
|
+
* @param traceId - Optional trace ID (defaults to random UUID)
|
|
17
|
+
* @returns Result of fn
|
|
18
|
+
*/
|
|
19
|
+
export declare const runWithTrace: <T>(fn: () => Promise<T>, traceId?: string) => Promise<T>;
|
|
20
|
+
/**
|
|
21
|
+
* Run a sync function with a new trace context.
|
|
22
|
+
*/
|
|
23
|
+
export declare const runWithTraceSync: <T>(fn: () => T, traceId?: string) => T;
|
|
24
|
+
/**
|
|
25
|
+
* Get the current trace context (if running inside runWithTrace).
|
|
26
|
+
*/
|
|
27
|
+
export declare const getTraceContext: () => TraceContext | undefined;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getTraceContext = exports.runWithTraceSync = exports.runWithTrace = void 0;
|
|
4
|
+
const async_hooks_1 = require("async_hooks");
|
|
5
|
+
const crypto_1 = require("crypto");
|
|
6
|
+
const traceStorage = new async_hooks_1.AsyncLocalStorage();
|
|
7
|
+
/**
|
|
8
|
+
* Run an async function with a new trace context.
|
|
9
|
+
* All Logger calls within the callback will automatically include trace.id.
|
|
10
|
+
*
|
|
11
|
+
* @param fn - Async function to run within the trace context
|
|
12
|
+
* @param traceId - Optional trace ID (defaults to random UUID)
|
|
13
|
+
* @returns Result of fn
|
|
14
|
+
*/
|
|
15
|
+
const runWithTrace = async (fn, traceId) => {
|
|
16
|
+
const id = traceId !== null && traceId !== void 0 ? traceId : (0, crypto_1.randomUUID)();
|
|
17
|
+
return traceStorage.run({ traceId: id }, () => fn());
|
|
18
|
+
};
|
|
19
|
+
exports.runWithTrace = runWithTrace;
|
|
20
|
+
/**
|
|
21
|
+
* Run a sync function with a new trace context.
|
|
22
|
+
*/
|
|
23
|
+
const runWithTraceSync = (fn, traceId) => {
|
|
24
|
+
const id = traceId !== null && traceId !== void 0 ? traceId : (0, crypto_1.randomUUID)();
|
|
25
|
+
return traceStorage.run({ traceId: id }, () => fn());
|
|
26
|
+
};
|
|
27
|
+
exports.runWithTraceSync = runWithTraceSync;
|
|
28
|
+
/**
|
|
29
|
+
* Get the current trace context (if running inside runWithTrace).
|
|
30
|
+
*/
|
|
31
|
+
const getTraceContext = () => {
|
|
32
|
+
return traceStorage.getStore();
|
|
33
|
+
};
|
|
34
|
+
exports.getTraceContext = getTraceContext;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mhmdhammoud/meritt-utils",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.5",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"private": false,
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"author": "Mhmdhammoud",
|
|
45
45
|
"license": "ISC",
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@elastic/elasticsearch": "^
|
|
47
|
+
"@elastic/elasticsearch": "^8.17.0",
|
|
48
48
|
"axios": "^1.4.0",
|
|
49
49
|
"dotenv": "^16.4.1",
|
|
50
50
|
"imagesloaded": "^5.0.0",
|
|
@@ -70,7 +70,7 @@ describe('route and format logs', () => {
|
|
|
70
70
|
key0: 'val0',
|
|
71
71
|
key1: 'val1',
|
|
72
72
|
}
|
|
73
|
-
test('log info with
|
|
73
|
+
test('log info with structured context (single object flattened)', () => {
|
|
74
74
|
//@ts-ignore
|
|
75
75
|
jest.spyOn(pino, 'destination').mockReturnValue(PINO_DESTINATION)
|
|
76
76
|
//@ts-ignore
|
|
@@ -79,10 +79,14 @@ describe('route and format logs', () => {
|
|
|
79
79
|
|
|
80
80
|
logger.info(LOG_EVENT, DETAILS)
|
|
81
81
|
|
|
82
|
-
expect(PINO.info).toHaveBeenCalledWith(
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
82
|
+
expect(PINO.info).toHaveBeenCalledWith(
|
|
83
|
+
expect.objectContaining({
|
|
84
|
+
component: LOGGER_NAME,
|
|
85
|
+
code: LOG_EVENT.code,
|
|
86
|
+
msg: LOG_EVENT.msg,
|
|
87
|
+
key0: 'val0',
|
|
88
|
+
key1: 'val1',
|
|
89
|
+
})
|
|
90
|
+
)
|
|
87
91
|
})
|
|
88
92
|
})
|
package/src/lib/index.ts
CHANGED
|
@@ -4,3 +4,9 @@ export { default as Pdf } from './formatter'
|
|
|
4
4
|
export { default as Colorful } from './colorful'
|
|
5
5
|
export { default as ImageFull } from './imagefull'
|
|
6
6
|
export { default as Logger } from './logger'
|
|
7
|
+
export {
|
|
8
|
+
runWithTrace,
|
|
9
|
+
runWithTraceSync,
|
|
10
|
+
getTraceContext,
|
|
11
|
+
type TraceContext,
|
|
12
|
+
} from './trace-store'
|
package/src/lib/logger.ts
CHANGED
|
@@ -1,10 +1,24 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {Logger as PinoLogger, pino, stdTimeFunctions} from 'pino'
|
|
2
2
|
import * as dotenv from 'dotenv'
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import {hostname} from 'os'
|
|
4
|
+
import {createElasticTransport} from './elastic-transport'
|
|
5
|
+
import {getTraceContext} from './trace-store'
|
|
6
|
+
import {LOG_LEVEL, LogEvent, ElasticConfig} from '../types'
|
|
5
7
|
|
|
6
8
|
dotenv.config()
|
|
7
9
|
|
|
10
|
+
/** Convert camelCase to snake_case for Kibana/ECS-friendly field names */
|
|
11
|
+
const toSnakeCase = (str: string): string =>
|
|
12
|
+
str.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`)
|
|
13
|
+
|
|
14
|
+
/** Check if value is a plain object (not Error, Date, Array, null) */
|
|
15
|
+
const isPlainObject = (v: unknown): v is Record<string, unknown> =>
|
|
16
|
+
v !== null &&
|
|
17
|
+
typeof v === 'object' &&
|
|
18
|
+
!Array.isArray(v) &&
|
|
19
|
+
!(v instanceof Error) &&
|
|
20
|
+
!(v instanceof Date)
|
|
21
|
+
|
|
8
22
|
/**
|
|
9
23
|
* Pino logger backend - singleton
|
|
10
24
|
*/
|
|
@@ -288,18 +302,69 @@ class Logger {
|
|
|
288
302
|
this._logger = getLogger(elasticConfig)
|
|
289
303
|
}
|
|
290
304
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
305
|
+
/**
|
|
306
|
+
* Build ECS-aligned and structured log payload.
|
|
307
|
+
* - ECS: log.level, log.logger, event.code, service.name, service.environment, message
|
|
308
|
+
* - Structured: Single plain object flattened as top-level snake_case fields (Kibana filterable)
|
|
309
|
+
* - Trace: trace.id when running inside runWithTrace
|
|
310
|
+
*/
|
|
311
|
+
private buildPayload(logLevel: LOG_LEVEL, logEvent: LogEvent, args: unknown[]) {
|
|
312
|
+
const isLocal =
|
|
313
|
+
process.env.NODE_ENV === 'local' || process.env.NODE_ENV === 'test'
|
|
314
|
+
|
|
315
|
+
// ECS-aligned fields for Kibana Discover, Lens, alerts
|
|
316
|
+
const ecs: Record<string, unknown> = {
|
|
317
|
+
'log.level': logLevel,
|
|
318
|
+
'log.logger': this._name,
|
|
319
|
+
'event.code': logEvent.code,
|
|
320
|
+
message: logEvent.msg,
|
|
321
|
+
service: {
|
|
322
|
+
name: process.env.SERVER_NICKNAME ?? 'unknown',
|
|
323
|
+
environment: process.env.NODE_ENV ?? 'development',
|
|
324
|
+
},
|
|
325
|
+
host: {
|
|
326
|
+
name: hostname(),
|
|
327
|
+
},
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Trace context for request-scoped correlation
|
|
331
|
+
const trace = getTraceContext()
|
|
332
|
+
if (trace) {
|
|
333
|
+
;(ecs as Record<string, unknown>).trace = {id: trace.traceId}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Structured context: flatten single plain object as top-level fields
|
|
337
|
+
let detail: unknown
|
|
338
|
+
if (
|
|
339
|
+
args.length === 1 &&
|
|
340
|
+
isPlainObject(args[0]) &&
|
|
341
|
+
Object.keys(args[0]).length > 0
|
|
342
|
+
) {
|
|
343
|
+
const obj = args[0] as Record<string, unknown>
|
|
344
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
345
|
+
const key = toSnakeCase(k)
|
|
346
|
+
;(ecs as Record<string, unknown>)[key] = v
|
|
347
|
+
}
|
|
295
348
|
} else {
|
|
296
|
-
detail = JSON.stringify(args)
|
|
349
|
+
detail = isLocal ? args : JSON.stringify(args)
|
|
297
350
|
}
|
|
298
|
-
|
|
351
|
+
|
|
352
|
+
// Legacy fields for backward compatibility (component, code, msg)
|
|
353
|
+
const base: Record<string, unknown> = {
|
|
354
|
+
...ecs,
|
|
299
355
|
component: this._name,
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
}
|
|
356
|
+
code: logEvent.code,
|
|
357
|
+
msg: logEvent.msg,
|
|
358
|
+
}
|
|
359
|
+
if (detail !== undefined) {
|
|
360
|
+
base.detail = detail
|
|
361
|
+
}
|
|
362
|
+
return base
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
private log(logLevel: LOG_LEVEL, logEvent: LogEvent, ...args: unknown[]) {
|
|
366
|
+
const payload = this.buildPayload(logLevel, logEvent, args)
|
|
367
|
+
this._logger[logLevel](payload)
|
|
303
368
|
}
|
|
304
369
|
|
|
305
370
|
/**
|
|
@@ -346,6 +411,43 @@ class Logger {
|
|
|
346
411
|
trace(logEvent: LogEvent, ...args: unknown[]) {
|
|
347
412
|
this.log('trace', logEvent, ...args)
|
|
348
413
|
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Runs an async operation and logs its duration.
|
|
417
|
+
* Adds event.duration (ms) for Kibana performance dashboards and alerts.
|
|
418
|
+
*
|
|
419
|
+
* @param logEvent - The event to log on completion
|
|
420
|
+
* @param fn - Async function to execute
|
|
421
|
+
* @param context - Optional context object (flattened as top-level fields)
|
|
422
|
+
* @returns Result of fn
|
|
423
|
+
*/
|
|
424
|
+
async withDuration<T>(
|
|
425
|
+
logEvent: LogEvent,
|
|
426
|
+
fn: () => Promise<T>,
|
|
427
|
+
context?: Record<string, unknown>
|
|
428
|
+
): Promise<T> {
|
|
429
|
+
const start = Date.now()
|
|
430
|
+
try {
|
|
431
|
+
const result = await fn()
|
|
432
|
+
const durationMs = Date.now() - start
|
|
433
|
+
const payload = this.buildPayload('info', logEvent, [
|
|
434
|
+
{...context, duration_ms: durationMs, success: true},
|
|
435
|
+
])
|
|
436
|
+
this._logger.info(payload)
|
|
437
|
+
return result
|
|
438
|
+
} catch (error) {
|
|
439
|
+
const durationMs = Date.now() - start
|
|
440
|
+
const errObj =
|
|
441
|
+
error instanceof Error
|
|
442
|
+
? {error_message: error.message, error_type: error.constructor.name}
|
|
443
|
+
: {}
|
|
444
|
+
const payload = this.buildPayload('error', logEvent, [
|
|
445
|
+
{...context, ...errObj, duration_ms: durationMs, success: false},
|
|
446
|
+
])
|
|
447
|
+
this._logger.error(payload)
|
|
448
|
+
throw error
|
|
449
|
+
}
|
|
450
|
+
}
|
|
349
451
|
}
|
|
350
452
|
|
|
351
453
|
export default Logger
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import {AsyncLocalStorage} from 'async_hooks'
|
|
2
|
+
import {randomUUID} from 'crypto'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Trace context for request-scoped correlation in Kibana.
|
|
6
|
+
* Enables filtering all logs from a single request/job by trace.id.
|
|
7
|
+
*/
|
|
8
|
+
export interface TraceContext {
|
|
9
|
+
/** Unique ID for the entire request/job flow */
|
|
10
|
+
traceId: string
|
|
11
|
+
/** Optional span ID for sub-operations */
|
|
12
|
+
spanId?: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const traceStorage = new AsyncLocalStorage<TraceContext>()
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Run an async function with a new trace context.
|
|
19
|
+
* All Logger calls within the callback will automatically include trace.id.
|
|
20
|
+
*
|
|
21
|
+
* @param fn - Async function to run within the trace context
|
|
22
|
+
* @param traceId - Optional trace ID (defaults to random UUID)
|
|
23
|
+
* @returns Result of fn
|
|
24
|
+
*/
|
|
25
|
+
export const runWithTrace = async <T>(
|
|
26
|
+
fn: () => Promise<T>,
|
|
27
|
+
traceId?: string
|
|
28
|
+
): Promise<T> => {
|
|
29
|
+
const id = traceId ?? randomUUID()
|
|
30
|
+
return traceStorage.run({traceId: id}, () => fn())
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Run a sync function with a new trace context.
|
|
35
|
+
*/
|
|
36
|
+
export const runWithTraceSync = <T>(fn: () => T, traceId?: string): T => {
|
|
37
|
+
const id = traceId ?? randomUUID()
|
|
38
|
+
return traceStorage.run({traceId: id}, () => fn())
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Get the current trace context (if running inside runWithTrace).
|
|
43
|
+
*/
|
|
44
|
+
export const getTraceContext = (): TraceContext | undefined => {
|
|
45
|
+
return traceStorage.getStore()
|
|
46
|
+
}
|