hierarchical-area-logger 0.2.0
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/.prettierignore +4 -0
- package/.prettierrc +8 -0
- package/CONTRIBUTING.md +313 -0
- package/LICENSE.md +21 -0
- package/README.md +120 -0
- package/dist/index.cjs +145 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +35 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +117 -0
- package/dist/index.js.map +1 -0
- package/eslint.config.js +43 -0
- package/package.json +60 -0
- package/src/Logger.ts +72 -0
- package/src/index.ts +2 -0
- package/src/types.ts +38 -0
- package/src/utils.ts +64 -0
- package/test/logger.test.ts +136 -0
- package/test/utils.test.ts +453 -0
- package/tsconfig.json +19 -0
- package/tsup.config.ts +11 -0
- package/vitest.config.ts +8 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// src/Logger.ts
|
|
2
|
+
import { init } from "@paralleldrive/cuid2";
|
|
3
|
+
|
|
4
|
+
// src/utils.ts
|
|
5
|
+
var prettyStack = (stack) => {
|
|
6
|
+
if (!stack) return [];
|
|
7
|
+
let toReplace = "";
|
|
8
|
+
const regex = /file:\/\/\/(.*)(\.wrangler|node_modules)\/.*\)/gm;
|
|
9
|
+
const m = regex.exec(stack);
|
|
10
|
+
if (m && m.length > 1) {
|
|
11
|
+
toReplace = m[1];
|
|
12
|
+
}
|
|
13
|
+
return stack.split("\n").map((line) => line.trim()).filter((line) => !line.startsWith("Error: ")).map((line) => line.replace(toReplace, "")).map((line) => line.replace("file://", ""));
|
|
14
|
+
};
|
|
15
|
+
var prettyError = (err) => {
|
|
16
|
+
const stack = prettyStack(err.stack);
|
|
17
|
+
const message = err.message;
|
|
18
|
+
return { message, stack };
|
|
19
|
+
};
|
|
20
|
+
var createRootLogEntry = ({
|
|
21
|
+
path,
|
|
22
|
+
method,
|
|
23
|
+
details,
|
|
24
|
+
eventId,
|
|
25
|
+
parentEventId,
|
|
26
|
+
withParentEventId
|
|
27
|
+
}) => {
|
|
28
|
+
const url = new URL(`http://example.com${path ?? "/"}`);
|
|
29
|
+
const rootLogEntry = {
|
|
30
|
+
root: [
|
|
31
|
+
{
|
|
32
|
+
type: "info",
|
|
33
|
+
message: "Request received",
|
|
34
|
+
payload: {
|
|
35
|
+
path: `${url.pathname}${url.search}`,
|
|
36
|
+
method,
|
|
37
|
+
details,
|
|
38
|
+
eventId,
|
|
39
|
+
...parentEventId && { parentEventId }
|
|
40
|
+
},
|
|
41
|
+
timestamp: Date.now()
|
|
42
|
+
}
|
|
43
|
+
]
|
|
44
|
+
};
|
|
45
|
+
if (withParentEventId && !parentEventId) {
|
|
46
|
+
rootLogEntry.root.push({
|
|
47
|
+
type: "error",
|
|
48
|
+
message: "Parent event ID expected but not found",
|
|
49
|
+
timestamp: Date.now()
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
return rootLogEntry;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// src/Logger.ts
|
|
56
|
+
var Logger = class {
|
|
57
|
+
constructor(options) {
|
|
58
|
+
this.parentEventId = options.parentEventId;
|
|
59
|
+
this.eventId = init({ fingerprint: options.details.service })();
|
|
60
|
+
this.log = createRootLogEntry({
|
|
61
|
+
path: options.path || "/",
|
|
62
|
+
method: options.method,
|
|
63
|
+
details: options.details,
|
|
64
|
+
eventId: this.eventId,
|
|
65
|
+
parentEventId: options.parentEventId,
|
|
66
|
+
withParentEventId: options.withParentEventId
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
// Area function that returns area-specific logger methods
|
|
70
|
+
getArea(name = "dummy") {
|
|
71
|
+
if (!this.log[name]) {
|
|
72
|
+
this.log[name] = [];
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
info: (message, payload) => {
|
|
76
|
+
this.log[name].push({
|
|
77
|
+
type: "info",
|
|
78
|
+
message,
|
|
79
|
+
payload,
|
|
80
|
+
timestamp: Date.now()
|
|
81
|
+
});
|
|
82
|
+
},
|
|
83
|
+
warn: (message, payload) => {
|
|
84
|
+
this.log[name].push({
|
|
85
|
+
type: "warn",
|
|
86
|
+
message,
|
|
87
|
+
payload,
|
|
88
|
+
timestamp: Date.now()
|
|
89
|
+
});
|
|
90
|
+
},
|
|
91
|
+
error: (message, payload) => {
|
|
92
|
+
this.log[name].push({
|
|
93
|
+
type: "error",
|
|
94
|
+
message,
|
|
95
|
+
payload: payload instanceof Error ? prettyError(payload) : payload,
|
|
96
|
+
timestamp: Date.now()
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
// Public methods on main instance
|
|
102
|
+
dump() {
|
|
103
|
+
return this.log;
|
|
104
|
+
}
|
|
105
|
+
appendLogData(logData) {
|
|
106
|
+
delete logData.root;
|
|
107
|
+
this.log = { ...logData, ...this.log };
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
var createLogger = (options) => {
|
|
111
|
+
return new Logger(options);
|
|
112
|
+
};
|
|
113
|
+
export {
|
|
114
|
+
Logger,
|
|
115
|
+
createLogger
|
|
116
|
+
};
|
|
117
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/Logger.ts","../src/utils.ts"],"sourcesContent":["import { init } from '@paralleldrive/cuid2';\nimport { createRootLogEntry, prettyError } from './utils';\nimport { LogData, LoggerOptions, LogEntry } from './types';\n\nexport class Logger {\n public eventId: string;\n private log: LogData;\n public parentEventId?: string;\n\n constructor(options: LoggerOptions) {\n this.parentEventId = options.parentEventId;\n this.eventId = init({ fingerprint: options.details.service })();\n\n this.log = createRootLogEntry({\n path: options.path || '/',\n method: options.method,\n details: options.details,\n eventId: this.eventId,\n parentEventId: options.parentEventId,\n withParentEventId: options.withParentEventId,\n });\n }\n\n // Area function that returns area-specific logger methods\n public getArea(name = 'dummy') {\n if (!this.log[name]) {\n this.log[name] = [];\n }\n\n return {\n info: (message: string, payload?: LogEntry['payload']) => {\n this.log[name]!.push({\n type: 'info',\n message,\n payload,\n timestamp: Date.now(),\n });\n },\n warn: (message: string, payload?: LogEntry['payload']) => {\n this.log[name]!.push({\n type: 'warn',\n message,\n payload,\n timestamp: Date.now(),\n });\n },\n error: (message: string, payload?: LogEntry['payload'] | Error) => {\n this.log[name]!.push({\n type: 'error',\n message,\n payload: payload instanceof Error ? prettyError(payload) : payload,\n timestamp: Date.now(),\n });\n },\n };\n }\n\n // Public methods on main instance\n public dump(): LogData {\n return this.log;\n }\n\n public appendLogData(logData: LogData): void {\n delete logData.root;\n this.log = { ...logData, ...this.log };\n }\n}\n\n// Factory function for convenience\nexport const createLogger = (options: LoggerOptions): Logger => {\n return new Logger(options);\n};\n","import { CreateRootLogEntryOptions, LogData, RootPayload } from './types';\n\nexport const prettyStack = (stack?: string): string[] => {\n if (!stack) return [];\n let toReplace = '';\n\n const regex = /file:\\/\\/\\/(.*)(\\.wrangler|node_modules)\\/.*\\)/gm;\n const m = regex.exec(stack);\n\n if (m && m.length > 1) {\n toReplace = m[1];\n }\n\n return stack\n .split('\\n')\n .map((line) => line.trim())\n .filter((line) => !line.startsWith('Error: '))\n .map((line) => line.replace(toReplace, ''))\n .map((line) => line.replace('file://', ''));\n};\n\nexport const prettyError = (err: Error) => {\n const stack = prettyStack(err.stack);\n const message = err.message;\n return { message, stack };\n};\n\nexport const createRootLogEntry = ({\n path,\n method,\n details,\n eventId,\n parentEventId,\n withParentEventId,\n}: CreateRootLogEntryOptions): LogData => {\n const url = new URL(`http://example.com${path ?? '/'}`);\n\n const rootLogEntry: LogData = {\n root: [\n {\n type: 'info',\n message: 'Request received',\n payload: {\n path: `${url.pathname}${url.search}`,\n method,\n details,\n eventId,\n ...(parentEventId && { parentEventId }),\n } as RootPayload,\n timestamp: Date.now(),\n },\n ],\n };\n\n if (withParentEventId && !parentEventId) {\n rootLogEntry.root.push({\n type: 'error',\n message: 'Parent event ID expected but not found',\n timestamp: Date.now(),\n });\n }\n\n return rootLogEntry;\n};\n"],"mappings":"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","names":[]}
|
package/eslint.config.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import js from '@eslint/js';
|
|
2
|
+
import tseslint from '@typescript-eslint/eslint-plugin';
|
|
3
|
+
import tsparser from '@typescript-eslint/parser';
|
|
4
|
+
|
|
5
|
+
export default [
|
|
6
|
+
js.configs.recommended,
|
|
7
|
+
{
|
|
8
|
+
files: ['**/*.ts'],
|
|
9
|
+
languageOptions: {
|
|
10
|
+
parser: tsparser,
|
|
11
|
+
ecmaVersion: 2020,
|
|
12
|
+
sourceType: 'module',
|
|
13
|
+
globals: {
|
|
14
|
+
process: 'readonly',
|
|
15
|
+
Buffer: 'readonly',
|
|
16
|
+
__dirname: 'readonly',
|
|
17
|
+
__filename: 'readonly',
|
|
18
|
+
console: 'readonly',
|
|
19
|
+
URL: 'readonly',
|
|
20
|
+
URLSearchParams: 'readonly',
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
plugins: {
|
|
24
|
+
'@typescript-eslint': tseslint,
|
|
25
|
+
},
|
|
26
|
+
rules: {
|
|
27
|
+
...tseslint.configs.recommended.rules,
|
|
28
|
+
'@typescript-eslint/no-unused-vars': [
|
|
29
|
+
'error',
|
|
30
|
+
{ argsIgnorePattern: '^_' },
|
|
31
|
+
],
|
|
32
|
+
'@typescript-eslint/explicit-function-return-type': 'off',
|
|
33
|
+
'@typescript-eslint/no-explicit-any': 'warn',
|
|
34
|
+
'@typescript-eslint/no-empty-function': 'warn',
|
|
35
|
+
'prefer-const': 'error',
|
|
36
|
+
'no-var': 'error',
|
|
37
|
+
'no-console': 'off',
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
ignores: ['dist/', 'node_modules/', '*.js'],
|
|
42
|
+
},
|
|
43
|
+
];
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "hierarchical-area-logger",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"module": "dist/index.mjs",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "tsup",
|
|
10
|
+
"dev": "tsup --watch",
|
|
11
|
+
"test": "vitest",
|
|
12
|
+
"test:ui": "vitest --ui",
|
|
13
|
+
"coverage": "vitest --coverage",
|
|
14
|
+
"lint": "eslint .",
|
|
15
|
+
"lint:fix": "eslint . --fix",
|
|
16
|
+
"lint:check": "eslint . --max-warnings=0",
|
|
17
|
+
"format": "prettier --write .",
|
|
18
|
+
"format:check": "prettier --check ."
|
|
19
|
+
},
|
|
20
|
+
"pre-commit": [
|
|
21
|
+
"lint"
|
|
22
|
+
],
|
|
23
|
+
"author": "Roman Jankowski",
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "https://github.com/Em3ODMe/hierarchical-area-logger.git"
|
|
28
|
+
},
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=18.0.0"
|
|
31
|
+
},
|
|
32
|
+
"description": "Hierarchical Area Logger",
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@eslint/js": "^9.0.0",
|
|
35
|
+
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
|
36
|
+
"@typescript-eslint/parser": "^8.0.0",
|
|
37
|
+
"@vitest/coverage-v8": "^4.0.18",
|
|
38
|
+
"eslint": "^9.0.0",
|
|
39
|
+
"prettier": "^3.8.1",
|
|
40
|
+
"tsup": "^8.5.1",
|
|
41
|
+
"typescript": "^5.0.0",
|
|
42
|
+
"vitest": "^4.0.18"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"@paralleldrive/cuid2": "^3.0.6"
|
|
46
|
+
},
|
|
47
|
+
"exports": {
|
|
48
|
+
".": {
|
|
49
|
+
"types": "./dist/index.d.ts",
|
|
50
|
+
"import": "./dist/index.mjs",
|
|
51
|
+
"require": "./dist/index.js"
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"keywords": [
|
|
55
|
+
"logger",
|
|
56
|
+
"logging",
|
|
57
|
+
"typescript",
|
|
58
|
+
"nodejs"
|
|
59
|
+
]
|
|
60
|
+
}
|
package/src/Logger.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { init } from '@paralleldrive/cuid2';
|
|
2
|
+
import { createRootLogEntry, prettyError } from './utils';
|
|
3
|
+
import { LogData, LoggerOptions, LogEntry } from './types';
|
|
4
|
+
|
|
5
|
+
export class Logger {
|
|
6
|
+
public eventId: string;
|
|
7
|
+
private log: LogData;
|
|
8
|
+
public parentEventId?: string;
|
|
9
|
+
|
|
10
|
+
constructor(options: LoggerOptions) {
|
|
11
|
+
this.parentEventId = options.parentEventId;
|
|
12
|
+
this.eventId = init({ fingerprint: options.details.service })();
|
|
13
|
+
|
|
14
|
+
this.log = createRootLogEntry({
|
|
15
|
+
path: options.path || '/',
|
|
16
|
+
method: options.method,
|
|
17
|
+
details: options.details,
|
|
18
|
+
eventId: this.eventId,
|
|
19
|
+
parentEventId: options.parentEventId,
|
|
20
|
+
withParentEventId: options.withParentEventId,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Area function that returns area-specific logger methods
|
|
25
|
+
public getArea(name = 'dummy') {
|
|
26
|
+
if (!this.log[name]) {
|
|
27
|
+
this.log[name] = [];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
info: (message: string, payload?: LogEntry['payload']) => {
|
|
32
|
+
this.log[name]!.push({
|
|
33
|
+
type: 'info',
|
|
34
|
+
message,
|
|
35
|
+
payload,
|
|
36
|
+
timestamp: Date.now(),
|
|
37
|
+
});
|
|
38
|
+
},
|
|
39
|
+
warn: (message: string, payload?: LogEntry['payload']) => {
|
|
40
|
+
this.log[name]!.push({
|
|
41
|
+
type: 'warn',
|
|
42
|
+
message,
|
|
43
|
+
payload,
|
|
44
|
+
timestamp: Date.now(),
|
|
45
|
+
});
|
|
46
|
+
},
|
|
47
|
+
error: (message: string, payload?: LogEntry['payload'] | Error) => {
|
|
48
|
+
this.log[name]!.push({
|
|
49
|
+
type: 'error',
|
|
50
|
+
message,
|
|
51
|
+
payload: payload instanceof Error ? prettyError(payload) : payload,
|
|
52
|
+
timestamp: Date.now(),
|
|
53
|
+
});
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Public methods on main instance
|
|
59
|
+
public dump(): LogData {
|
|
60
|
+
return this.log;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
public appendLogData(logData: LogData): void {
|
|
64
|
+
delete logData.root;
|
|
65
|
+
this.log = { ...logData, ...this.log };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Factory function for convenience
|
|
70
|
+
export const createLogger = (options: LoggerOptions): Logger => {
|
|
71
|
+
return new Logger(options);
|
|
72
|
+
};
|
package/src/index.ts
ADDED
package/src/types.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export type Details = { service: string } & Record<string, unknown>;
|
|
2
|
+
export type Method =
|
|
3
|
+
| 'get'
|
|
4
|
+
| 'post'
|
|
5
|
+
| 'put'
|
|
6
|
+
| 'delete'
|
|
7
|
+
| 'patch'
|
|
8
|
+
| 'head'
|
|
9
|
+
| 'options'
|
|
10
|
+
| 'trace'
|
|
11
|
+
| 'connect';
|
|
12
|
+
|
|
13
|
+
export type LogEntry = {
|
|
14
|
+
type: 'info' | 'warn' | 'error';
|
|
15
|
+
message: string;
|
|
16
|
+
payload?: object;
|
|
17
|
+
timestamp: number;
|
|
18
|
+
};
|
|
19
|
+
export type RootPayload = {
|
|
20
|
+
path?: string;
|
|
21
|
+
method?: Method;
|
|
22
|
+
eventId: string;
|
|
23
|
+
parentEventId?: string;
|
|
24
|
+
details: Details;
|
|
25
|
+
};
|
|
26
|
+
export type CreateRootLogEntryOptions = RootPayload & {
|
|
27
|
+
path?: string;
|
|
28
|
+
withParentEventId?: boolean;
|
|
29
|
+
};
|
|
30
|
+
export type LogData = Record<string, LogEntry[]>;
|
|
31
|
+
|
|
32
|
+
export interface LoggerOptions {
|
|
33
|
+
details: Details;
|
|
34
|
+
path?: string;
|
|
35
|
+
parentEventId?: string;
|
|
36
|
+
withParentEventId?: boolean;
|
|
37
|
+
method?: Method;
|
|
38
|
+
}
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { CreateRootLogEntryOptions, LogData, RootPayload } from './types';
|
|
2
|
+
|
|
3
|
+
export const prettyStack = (stack?: string): string[] => {
|
|
4
|
+
if (!stack) return [];
|
|
5
|
+
let toReplace = '';
|
|
6
|
+
|
|
7
|
+
const regex = /file:\/\/\/(.*)(\.wrangler|node_modules)\/.*\)/gm;
|
|
8
|
+
const m = regex.exec(stack);
|
|
9
|
+
|
|
10
|
+
if (m && m.length > 1) {
|
|
11
|
+
toReplace = m[1];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return stack
|
|
15
|
+
.split('\n')
|
|
16
|
+
.map((line) => line.trim())
|
|
17
|
+
.filter((line) => !line.startsWith('Error: '))
|
|
18
|
+
.map((line) => line.replace(toReplace, ''))
|
|
19
|
+
.map((line) => line.replace('file://', ''));
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const prettyError = (err: Error) => {
|
|
23
|
+
const stack = prettyStack(err.stack);
|
|
24
|
+
const message = err.message;
|
|
25
|
+
return { message, stack };
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const createRootLogEntry = ({
|
|
29
|
+
path,
|
|
30
|
+
method,
|
|
31
|
+
details,
|
|
32
|
+
eventId,
|
|
33
|
+
parentEventId,
|
|
34
|
+
withParentEventId,
|
|
35
|
+
}: CreateRootLogEntryOptions): LogData => {
|
|
36
|
+
const url = new URL(`http://example.com${path ?? '/'}`);
|
|
37
|
+
|
|
38
|
+
const rootLogEntry: LogData = {
|
|
39
|
+
root: [
|
|
40
|
+
{
|
|
41
|
+
type: 'info',
|
|
42
|
+
message: 'Request received',
|
|
43
|
+
payload: {
|
|
44
|
+
path: `${url.pathname}${url.search}`,
|
|
45
|
+
method,
|
|
46
|
+
details,
|
|
47
|
+
eventId,
|
|
48
|
+
...(parentEventId && { parentEventId }),
|
|
49
|
+
} as RootPayload,
|
|
50
|
+
timestamp: Date.now(),
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
if (withParentEventId && !parentEventId) {
|
|
56
|
+
rootLogEntry.root.push({
|
|
57
|
+
type: 'error',
|
|
58
|
+
message: 'Parent event ID expected but not found',
|
|
59
|
+
timestamp: Date.now(),
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return rootLogEntry;
|
|
64
|
+
};
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { createLogger, Logger } from '../src/Logger';
|
|
3
|
+
import { Details } from '../src/types';
|
|
4
|
+
|
|
5
|
+
describe('Logger', () => {
|
|
6
|
+
const mockdetails: Details = { service: 'test-service' };
|
|
7
|
+
|
|
8
|
+
it('should create a logger with details', () => {
|
|
9
|
+
const logger = createLogger({ details: mockdetails });
|
|
10
|
+
expect(logger).toBeInstanceOf(Logger);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('should create a logger with custom options', () => {
|
|
14
|
+
const logger = createLogger({
|
|
15
|
+
details: mockdetails,
|
|
16
|
+
path: 'test-path',
|
|
17
|
+
parentEventId: 'parent-123',
|
|
18
|
+
});
|
|
19
|
+
expect(logger).toBeInstanceOf(Logger);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should create area logger and log messages', () => {
|
|
23
|
+
const logger = createLogger({ details: mockdetails });
|
|
24
|
+
const areaLogger = logger.getArea('test-area');
|
|
25
|
+
|
|
26
|
+
areaLogger.info('test info message');
|
|
27
|
+
areaLogger.warn('test warn message');
|
|
28
|
+
areaLogger.error('test error message');
|
|
29
|
+
|
|
30
|
+
const logs = logger.dump();
|
|
31
|
+
expect(logs['test-area']).toHaveLength(3);
|
|
32
|
+
expect(logs['test-area'][0].message).toBe('test info message');
|
|
33
|
+
expect(logs['test-area'][1].type).toBe('warn');
|
|
34
|
+
expect(logs['test-area'][2].type).toBe('error');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should generate event IDs correctly', () => {
|
|
38
|
+
const logger = createLogger({ details: mockdetails });
|
|
39
|
+
|
|
40
|
+
expect(logger.eventId).toBeDefined();
|
|
41
|
+
expect(typeof logger.eventId).toBe('string');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should handle parent event ID', () => {
|
|
45
|
+
const logger = createLogger({
|
|
46
|
+
details: mockdetails,
|
|
47
|
+
parentEventId: 'parent-123',
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
expect(logger.parentEventId).toBe('parent-123');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should append log data', () => {
|
|
54
|
+
const logger = createLogger({ details: mockdetails });
|
|
55
|
+
const logDataToAdd = {
|
|
56
|
+
area1: [
|
|
57
|
+
{
|
|
58
|
+
type: 'info' as const,
|
|
59
|
+
message: 'existing log',
|
|
60
|
+
timestamp: Date.now(),
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
logger.appendLogData(logDataToAdd);
|
|
66
|
+
const logs = logger.dump();
|
|
67
|
+
|
|
68
|
+
expect(logs.area1).toBeDefined();
|
|
69
|
+
expect(logs.area1[0].message).toBe('existing log');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should create root log entry', () => {
|
|
73
|
+
const logger = createLogger({ details: mockdetails, path: 'test' });
|
|
74
|
+
const logs = logger.dump();
|
|
75
|
+
|
|
76
|
+
expect(logs.root).toBeDefined();
|
|
77
|
+
expect(logs.root).toHaveLength(1);
|
|
78
|
+
expect(logs.root[0].message).toBe('Request received');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should handle errors in error logging', () => {
|
|
82
|
+
const logger = createLogger({ details: mockdetails });
|
|
83
|
+
const areaLogger = logger.getArea('test-area');
|
|
84
|
+
|
|
85
|
+
const testError = new Error('Test error');
|
|
86
|
+
areaLogger.error('error occurred', testError);
|
|
87
|
+
|
|
88
|
+
const logs = logger.dump();
|
|
89
|
+
expect(logs['test-area'][0].payload).toHaveProperty(
|
|
90
|
+
'message',
|
|
91
|
+
'Test error'
|
|
92
|
+
);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should use default area name when no name provided', () => {
|
|
96
|
+
const logger = createLogger({ details: mockdetails });
|
|
97
|
+
const areaLogger = logger.getArea();
|
|
98
|
+
|
|
99
|
+
areaLogger.info('test message');
|
|
100
|
+
|
|
101
|
+
const logs = logger.dump();
|
|
102
|
+
expect(logs.dummy).toBeDefined();
|
|
103
|
+
expect(logs.dummy).toHaveLength(1);
|
|
104
|
+
expect(logs.dummy[0].message).toBe('test message');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('should handle withParentEventId true without parentEventId', () => {
|
|
108
|
+
const logger = createLogger({
|
|
109
|
+
details: mockdetails,
|
|
110
|
+
withParentEventId: true,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const logs = logger.dump();
|
|
114
|
+
expect(logs.root).toHaveLength(2);
|
|
115
|
+
expect(logs.root[1].type).toBe('error');
|
|
116
|
+
expect(logs.root[1].message).toBe('Parent event ID expected but not found');
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe('createLogger', () => {
|
|
121
|
+
const mockdetails: Details = { service: 'test-service' };
|
|
122
|
+
|
|
123
|
+
it('should create a logger instance', () => {
|
|
124
|
+
const logger = createLogger({ details: mockdetails });
|
|
125
|
+
expect(logger).toBeInstanceOf(Logger);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('should create a logger with options', () => {
|
|
129
|
+
const logger = createLogger({
|
|
130
|
+
details: mockdetails,
|
|
131
|
+
path: 'test-path',
|
|
132
|
+
parentEventId: 'parent-123',
|
|
133
|
+
});
|
|
134
|
+
expect(logger).toBeInstanceOf(Logger);
|
|
135
|
+
});
|
|
136
|
+
});
|