@sockethub/logger 1.0.0-alpha.11
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/LICENSE +165 -0
- package/README.md +150 -0
- package/dist/index.js +9963 -0
- package/dist/index.js.map +122 -0
- package/package.json +51 -0
- package/src/index.test.ts +245 -0
- package/src/index.ts +263 -0
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sockethub/logger",
|
|
3
|
+
"description": "Winston-based logger for Sockethub packages",
|
|
4
|
+
"version": "1.0.0-alpha.11",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"private": false,
|
|
7
|
+
"author": "Nick Jennings <nick@silverbucket.net>",
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"publishConfig": {
|
|
10
|
+
"access": "public"
|
|
11
|
+
},
|
|
12
|
+
"main": "dist/index.js",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"bun": "./src/index.ts",
|
|
16
|
+
"import": "./dist/index.js",
|
|
17
|
+
"default": "./dist/index.js"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"src/",
|
|
22
|
+
"dist/"
|
|
23
|
+
],
|
|
24
|
+
"engines": {
|
|
25
|
+
"bun": ">=1.2"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"sockethub",
|
|
29
|
+
"logging",
|
|
30
|
+
"winston"
|
|
31
|
+
],
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/sockethub/sockethub.git",
|
|
35
|
+
"directory": "packages/logger"
|
|
36
|
+
},
|
|
37
|
+
"homepage": "https://github.com/sockethub/sockethub/tree/master/packages/logger",
|
|
38
|
+
"scripts": {
|
|
39
|
+
"build": "bun build src/index.ts --outdir dist --target node --format esm --sourcemap=external",
|
|
40
|
+
"clean": "rm -rf dist",
|
|
41
|
+
"clean:deps": "rm -rf node_modules",
|
|
42
|
+
"test": "bun test src/"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"winston": "^3.19.0"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@types/bun": "latest"
|
|
49
|
+
},
|
|
50
|
+
"gitHead": "c243fa9e76c688ce5ffcf524400b1dd27dcce615"
|
|
51
|
+
}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
type Logger,
|
|
4
|
+
type LoggerOptions,
|
|
5
|
+
createLogger,
|
|
6
|
+
getLoggerContext,
|
|
7
|
+
getLoggerNamespace,
|
|
8
|
+
initLogger,
|
|
9
|
+
resetLoggerContext,
|
|
10
|
+
resetLoggerForTesting,
|
|
11
|
+
setLoggerContext,
|
|
12
|
+
} from "./index.js";
|
|
13
|
+
|
|
14
|
+
describe("Logger Package", () => {
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
resetLoggerForTesting();
|
|
17
|
+
delete process.env.LOG_LEVEL;
|
|
18
|
+
delete process.env.LOG_FILE_LEVEL;
|
|
19
|
+
delete process.env.LOG_FILE;
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
resetLoggerForTesting();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe("createLogger", () => {
|
|
27
|
+
it("creates a logger with namespace", () => {
|
|
28
|
+
const log = createLogger("test:namespace");
|
|
29
|
+
expect(log).toBeDefined();
|
|
30
|
+
expect(log.info).toBeFunction();
|
|
31
|
+
expect(log.warn).toBeFunction();
|
|
32
|
+
expect(log.error).toBeFunction();
|
|
33
|
+
expect(log.debug).toBeFunction();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("uses default level info when no config provided", () => {
|
|
37
|
+
const log = createLogger("test:namespace");
|
|
38
|
+
expect(log).toBeDefined();
|
|
39
|
+
// Winston loggers have transports array
|
|
40
|
+
expect(log.transports).toBeArray();
|
|
41
|
+
expect(log.transports.length).toBeGreaterThan(0);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("respects explicit options over defaults", () => {
|
|
45
|
+
const log = createLogger("test:namespace", { level: "error" });
|
|
46
|
+
expect(log).toBeDefined();
|
|
47
|
+
const consoleTransport = log.transports[0];
|
|
48
|
+
expect(consoleTransport.level).toBe("error");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("respects environment variables when no global config", () => {
|
|
52
|
+
process.env.LOG_LEVEL = "warn";
|
|
53
|
+
const log = createLogger("test:namespace");
|
|
54
|
+
const consoleTransport = log.transports[0];
|
|
55
|
+
expect(consoleTransport.level).toBe("warn");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("creates file transport when file path provided", () => {
|
|
59
|
+
const log = createLogger("test:namespace", {
|
|
60
|
+
file: "/tmp/test.log",
|
|
61
|
+
});
|
|
62
|
+
expect(log.transports.length).toBe(2); // console + file
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("does not create file transport when file is empty string", () => {
|
|
66
|
+
const log = createLogger("test:namespace", { file: "" });
|
|
67
|
+
expect(log.transports.length).toBe(1); // console only
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe("getLoggerNamespace", () => {
|
|
72
|
+
it("returns namespace from logger instance", () => {
|
|
73
|
+
const log = createLogger("test:namespace");
|
|
74
|
+
const namespace = getLoggerNamespace(log);
|
|
75
|
+
expect(namespace).toBe("test:namespace");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("returns namespace with context when set", () => {
|
|
79
|
+
setLoggerContext("myapp");
|
|
80
|
+
const log = createLogger("test:namespace");
|
|
81
|
+
const namespace = getLoggerNamespace(log);
|
|
82
|
+
expect(namespace).toBe("myapp:test:namespace");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("returns empty string for logger without namespace", () => {
|
|
86
|
+
const log = createLogger("");
|
|
87
|
+
const namespace = getLoggerNamespace(log);
|
|
88
|
+
expect(namespace).toBe("");
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe("initLogger", () => {
|
|
93
|
+
it("sets global config for subsequent createLogger calls", () => {
|
|
94
|
+
initLogger({
|
|
95
|
+
level: "debug",
|
|
96
|
+
fileLevel: "info",
|
|
97
|
+
file: "/tmp/global.log",
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const log = createLogger("test:namespace");
|
|
101
|
+
expect(log.transports.length).toBe(2); // console + file
|
|
102
|
+
const consoleTransport = log.transports[0];
|
|
103
|
+
expect(consoleTransport.level).toBe("debug");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("allows explicit options to override global config", () => {
|
|
107
|
+
initLogger({ level: "info" });
|
|
108
|
+
|
|
109
|
+
const log = createLogger("test:namespace", { level: "error" });
|
|
110
|
+
const consoleTransport = log.transports[0];
|
|
111
|
+
expect(consoleTransport.level).toBe("error");
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("global config takes precedence over ENV vars", () => {
|
|
115
|
+
process.env.LOG_LEVEL = "warn";
|
|
116
|
+
initLogger({ level: "debug" });
|
|
117
|
+
|
|
118
|
+
const log = createLogger("test:namespace");
|
|
119
|
+
const consoleTransport = log.transports[0];
|
|
120
|
+
expect(consoleTransport.level).toBe("debug");
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe("configuration priority", () => {
|
|
125
|
+
it("follows priority: explicit > global > env > default", () => {
|
|
126
|
+
// Set up all sources
|
|
127
|
+
process.env.LOG_LEVEL = "warn";
|
|
128
|
+
initLogger({ level: "info" });
|
|
129
|
+
|
|
130
|
+
// Explicit wins
|
|
131
|
+
const log1 = createLogger("test:1", { level: "error" });
|
|
132
|
+
expect(log1.transports[0].level).toBe("error");
|
|
133
|
+
|
|
134
|
+
// Global wins over env
|
|
135
|
+
const log2 = createLogger("test:2");
|
|
136
|
+
expect(log2.transports[0].level).toBe("info");
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("falls back to env when no global config", () => {
|
|
140
|
+
process.env.LOG_LEVEL = "warn";
|
|
141
|
+
const log = createLogger("test:namespace");
|
|
142
|
+
expect(log.transports[0].level).toBe("warn");
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("falls back to default when no config at all", () => {
|
|
146
|
+
const log = createLogger("test:namespace");
|
|
147
|
+
expect(log.transports[0].level).toBe("info");
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe("edge cases", () => {
|
|
152
|
+
it("handles createLogger before initLogger", () => {
|
|
153
|
+
// This happens during early bootstrap
|
|
154
|
+
const earlyLog = createLogger("bootstrap", { level: "info" });
|
|
155
|
+
expect(earlyLog).toBeDefined();
|
|
156
|
+
|
|
157
|
+
// Later, initLogger is called
|
|
158
|
+
initLogger({ level: "debug" });
|
|
159
|
+
|
|
160
|
+
// New loggers get global config
|
|
161
|
+
const laterLog = createLogger("later");
|
|
162
|
+
expect(laterLog.transports[0].level).toBe("debug");
|
|
163
|
+
|
|
164
|
+
// But early logger keeps its config
|
|
165
|
+
expect(earlyLog.transports[0].level).toBe("info");
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("handles file: undefined vs file: empty string", () => {
|
|
169
|
+
initLogger({ file: "/tmp/test.log" });
|
|
170
|
+
|
|
171
|
+
// Explicit empty string disables file
|
|
172
|
+
const log1 = createLogger("test:1", { file: "" });
|
|
173
|
+
expect(log1.transports.length).toBe(1);
|
|
174
|
+
|
|
175
|
+
// No explicit option uses global
|
|
176
|
+
const log2 = createLogger("test:2");
|
|
177
|
+
expect(log2.transports.length).toBe(2);
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
describe("logger context", () => {
|
|
182
|
+
it("createLogger without context uses namespace as-is", () => {
|
|
183
|
+
const log = createLogger("test:namespace");
|
|
184
|
+
expect(log.defaultMeta.namespace).toBe("test:namespace");
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("createLogger with context prepends context to namespace", () => {
|
|
188
|
+
setLoggerContext("myapp");
|
|
189
|
+
const log = createLogger("test:namespace");
|
|
190
|
+
expect(log.defaultMeta.namespace).toBe("myapp:test:namespace");
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("getLoggerContext returns empty string by default", () => {
|
|
194
|
+
expect(getLoggerContext()).toBe("");
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("getLoggerContext returns the set context", () => {
|
|
198
|
+
setLoggerContext("myapp:process");
|
|
199
|
+
expect(getLoggerContext()).toBe("myapp:process");
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it("resetLoggerContext clears the context", () => {
|
|
203
|
+
setLoggerContext("myapp");
|
|
204
|
+
expect(getLoggerContext()).toBe("myapp");
|
|
205
|
+
|
|
206
|
+
resetLoggerContext();
|
|
207
|
+
expect(getLoggerContext()).toBe("");
|
|
208
|
+
|
|
209
|
+
const log = createLogger("test");
|
|
210
|
+
expect(log.defaultMeta.namespace).toBe("test");
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("context works with nested namespaces", () => {
|
|
214
|
+
setLoggerContext("sockethub:platform:irc:abc123");
|
|
215
|
+
const log = createLogger("data-layer:worker");
|
|
216
|
+
expect(log.defaultMeta.namespace).toBe(
|
|
217
|
+
"sockethub:platform:irc:abc123:data-layer:worker",
|
|
218
|
+
);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("context is process-wide, affects all subsequent loggers", () => {
|
|
222
|
+
setLoggerContext("process-context");
|
|
223
|
+
|
|
224
|
+
const log1 = createLogger("module1");
|
|
225
|
+
const log2 = createLogger("module2");
|
|
226
|
+
const log3 = createLogger("module3");
|
|
227
|
+
|
|
228
|
+
expect(log1.defaultMeta.namespace).toBe("process-context:module1");
|
|
229
|
+
expect(log2.defaultMeta.namespace).toBe("process-context:module2");
|
|
230
|
+
expect(log3.defaultMeta.namespace).toBe("process-context:module3");
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it("resetLoggerForTesting also resets context", () => {
|
|
234
|
+
setLoggerContext("myapp");
|
|
235
|
+
initLogger({ level: "debug" });
|
|
236
|
+
|
|
237
|
+
resetLoggerForTesting();
|
|
238
|
+
|
|
239
|
+
expect(getLoggerContext()).toBe("");
|
|
240
|
+
const log = createLogger("test");
|
|
241
|
+
expect(log.defaultMeta.namespace).toBe("test");
|
|
242
|
+
expect(log.transports[0].level).toBe("info"); // back to default
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import winston from "winston";
|
|
2
|
+
|
|
3
|
+
export type Logger = winston.Logger;
|
|
4
|
+
|
|
5
|
+
export interface LoggerOptions {
|
|
6
|
+
level?: string;
|
|
7
|
+
fileLevel?: string;
|
|
8
|
+
file?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Global configuration state (set once at bootstrap via initLogger)
|
|
12
|
+
let globalConfig: LoggerOptions | null = null;
|
|
13
|
+
let hasLoggedInit = false;
|
|
14
|
+
|
|
15
|
+
// Process-wide logger context (set once at process startup)
|
|
16
|
+
let loggerContext = "";
|
|
17
|
+
let loggerNamespaceStore = new WeakMap<Logger, string>();
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Initialize the logger system with global configuration.
|
|
21
|
+
*
|
|
22
|
+
* Called once at server bootstrap with config settings. All subsequent
|
|
23
|
+
* createLogger() calls will use these settings as defaults unless explicitly
|
|
24
|
+
* overridden.
|
|
25
|
+
*
|
|
26
|
+
* @param options - Global logger configuration
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```typescript
|
|
30
|
+
* // In server bootstrap
|
|
31
|
+
* initLogger({
|
|
32
|
+
* level: config.get("logging:level"),
|
|
33
|
+
* fileLevel: config.get("logging:fileLevel"),
|
|
34
|
+
* file: config.get("logging:file"),
|
|
35
|
+
* });
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export function initLogger(options: LoggerOptions): void {
|
|
39
|
+
globalConfig = options;
|
|
40
|
+
|
|
41
|
+
if (!hasLoggedInit) {
|
|
42
|
+
hasLoggedInit = true;
|
|
43
|
+
const initLog = createLogger("sockethub:logger");
|
|
44
|
+
initLog.debug("logger system initialized");
|
|
45
|
+
if (options.file) {
|
|
46
|
+
initLog.info(`log file path: ${options.file}`);
|
|
47
|
+
initLog.info(`file log level: ${options.fileLevel || "debug"}`);
|
|
48
|
+
}
|
|
49
|
+
initLog.info(`console log level: ${options.level || "info"}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Set the logger context for this process.
|
|
55
|
+
*
|
|
56
|
+
* All subsequent createLogger() calls will prepend this context to their namespace.
|
|
57
|
+
* This is typically set once at process startup to identify the process (e.g., "sockethub"
|
|
58
|
+
* for the main server, or "sockethub:platform:irc:abc123" for a platform child process).
|
|
59
|
+
*
|
|
60
|
+
* @param context - The context string to prepend to all logger namespaces in this process
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* ```typescript
|
|
64
|
+
* // In main server process
|
|
65
|
+
* setLoggerContext('sockethub');
|
|
66
|
+
* const log = createLogger('server:listener');
|
|
67
|
+
* // Output namespace: "sockethub:server:listener"
|
|
68
|
+
*
|
|
69
|
+
* // In platform child process
|
|
70
|
+
* setLoggerContext('sockethub:platform:irc:abc123');
|
|
71
|
+
* const log = createLogger('main');
|
|
72
|
+
* // Output namespace: "sockethub:platform:irc:abc123:main"
|
|
73
|
+
* ```
|
|
74
|
+
*/
|
|
75
|
+
export function setLoggerContext(context: string): void {
|
|
76
|
+
loggerContext = context;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Get the current logger context for this process.
|
|
81
|
+
*
|
|
82
|
+
* @returns The current logger context string, or empty string if not set
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* ```typescript
|
|
86
|
+
* const context = getLoggerContext();
|
|
87
|
+
* if (context.includes(':platform:')) {
|
|
88
|
+
* // We're in a platform child process
|
|
89
|
+
* }
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
92
|
+
export function getLoggerContext(): string {
|
|
93
|
+
return loggerContext;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Reset the logger context.
|
|
98
|
+
*
|
|
99
|
+
* Primarily used for testing to reset state between test cases.
|
|
100
|
+
*
|
|
101
|
+
* @example
|
|
102
|
+
* ```typescript
|
|
103
|
+
* afterEach(() => {
|
|
104
|
+
* resetLoggerContext();
|
|
105
|
+
* });
|
|
106
|
+
* ```
|
|
107
|
+
*/
|
|
108
|
+
export function resetLoggerContext(): void {
|
|
109
|
+
loggerContext = "";
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Creates a Winston logger instance with console and optional file transports.
|
|
114
|
+
*
|
|
115
|
+
* Configuration priority (highest to lowest):
|
|
116
|
+
* 1. Explicit options parameter
|
|
117
|
+
* 2. Global config (set via initLogger)
|
|
118
|
+
* 3. Environment variables (LOG_LEVEL, LOG_FILE_LEVEL, LOG_FILE)
|
|
119
|
+
* 4. Defaults (info for console, debug for file)
|
|
120
|
+
*
|
|
121
|
+
* Log levels: error, warn, info, debug
|
|
122
|
+
*
|
|
123
|
+
* NODE_ENV=production disables console timestamps (for systemd)
|
|
124
|
+
*
|
|
125
|
+
* @param namespace - Logger namespace (e.g., "sockethub:data-layer:queue:...")
|
|
126
|
+
* @param options - Optional logger configuration overrides
|
|
127
|
+
* @returns Winston logger instance
|
|
128
|
+
*
|
|
129
|
+
* @example
|
|
130
|
+
* ```typescript
|
|
131
|
+
* // Uses global config if initLogger() was called
|
|
132
|
+
* const log = createLogger("sockethub:data-layer:queue:irc-123");
|
|
133
|
+
*
|
|
134
|
+
* // Override for specific use case (e.g., early bootstrap logging)
|
|
135
|
+
* const earlyLog = createLogger("sockethub:bootstrap", { level: "info" });
|
|
136
|
+
* ```
|
|
137
|
+
*/
|
|
138
|
+
export function createLogger(
|
|
139
|
+
namespace: string,
|
|
140
|
+
options: LoggerOptions = {},
|
|
141
|
+
): Logger {
|
|
142
|
+
// Prepend logger context if set
|
|
143
|
+
const fullNamespace = loggerContext
|
|
144
|
+
? `${loggerContext}:${namespace}`
|
|
145
|
+
: namespace;
|
|
146
|
+
|
|
147
|
+
// Priority: explicit options > global config > ENV > defaults
|
|
148
|
+
const cfg: LoggerOptions = {
|
|
149
|
+
level:
|
|
150
|
+
options.level ??
|
|
151
|
+
globalConfig?.level ??
|
|
152
|
+
process.env.LOG_LEVEL ??
|
|
153
|
+
"info",
|
|
154
|
+
fileLevel:
|
|
155
|
+
options.fileLevel ??
|
|
156
|
+
globalConfig?.fileLevel ??
|
|
157
|
+
process.env.LOG_FILE_LEVEL ??
|
|
158
|
+
"debug",
|
|
159
|
+
file:
|
|
160
|
+
options.file !== undefined
|
|
161
|
+
? options.file
|
|
162
|
+
: globalConfig?.file !== undefined
|
|
163
|
+
? globalConfig.file
|
|
164
|
+
: (process.env.LOG_FILE ?? ""),
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const isProduction = process.env.NODE_ENV === "production";
|
|
168
|
+
|
|
169
|
+
const consoleFormat = isProduction
|
|
170
|
+
? winston.format.combine(
|
|
171
|
+
winston.format.colorize(),
|
|
172
|
+
winston.format.printf(
|
|
173
|
+
({ level, message, namespace, ...meta }) => {
|
|
174
|
+
const ns = namespace ? `${namespace} ` : "";
|
|
175
|
+
const metaStr =
|
|
176
|
+
Object.keys(meta).length > 0
|
|
177
|
+
? ` ${JSON.stringify(meta)}`
|
|
178
|
+
: "";
|
|
179
|
+
return `${level}: ${ns}${message}${metaStr}`;
|
|
180
|
+
},
|
|
181
|
+
),
|
|
182
|
+
)
|
|
183
|
+
: winston.format.combine(
|
|
184
|
+
winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
|
|
185
|
+
winston.format.colorize(),
|
|
186
|
+
winston.format.printf(
|
|
187
|
+
({ level, message, timestamp, namespace, ...meta }) => {
|
|
188
|
+
const ns = namespace ? `${namespace} ` : "";
|
|
189
|
+
const metaStr =
|
|
190
|
+
Object.keys(meta).length > 0
|
|
191
|
+
? ` ${JSON.stringify(meta)}`
|
|
192
|
+
: "";
|
|
193
|
+
return `${timestamp} ${level}: ${ns}${message}${metaStr}`;
|
|
194
|
+
},
|
|
195
|
+
),
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
const transports: winston.transport[] = [
|
|
199
|
+
new winston.transports.Console({
|
|
200
|
+
level: cfg.level,
|
|
201
|
+
format: consoleFormat,
|
|
202
|
+
}),
|
|
203
|
+
];
|
|
204
|
+
|
|
205
|
+
if (cfg.file) {
|
|
206
|
+
transports.push(
|
|
207
|
+
new winston.transports.File({
|
|
208
|
+
level: cfg.fileLevel,
|
|
209
|
+
filename: cfg.file,
|
|
210
|
+
format: winston.format.combine(
|
|
211
|
+
winston.format.timestamp(),
|
|
212
|
+
winston.format.json(),
|
|
213
|
+
),
|
|
214
|
+
}),
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const logger = winston.createLogger({
|
|
219
|
+
level: "debug", // Set to lowest level, let transports filter
|
|
220
|
+
defaultMeta: { namespace: fullNamespace },
|
|
221
|
+
transports,
|
|
222
|
+
});
|
|
223
|
+
loggerNamespaceStore.set(logger, fullNamespace);
|
|
224
|
+
return logger;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Get the namespace from a logger instance.
|
|
229
|
+
*
|
|
230
|
+
* Extracts the full namespace (including context) that was set when the logger was created.
|
|
231
|
+
* Useful for reusing the namespace in other systems like Redis connection names.
|
|
232
|
+
*
|
|
233
|
+
* @param logger - Logger instance created by createLogger()
|
|
234
|
+
* @returns The full namespace string, or empty string if not found
|
|
235
|
+
*
|
|
236
|
+
* @example
|
|
237
|
+
* ```typescript
|
|
238
|
+
* const log = createLogger('data-layer:queue');
|
|
239
|
+
* const namespace = getLoggerNamespace(log);
|
|
240
|
+
* // Returns: "sockethub:data-layer:queue" (if context is "sockethub")
|
|
241
|
+
*
|
|
242
|
+
* // Use for Redis connection name
|
|
243
|
+
* redisConfig.connectionName = namespace;
|
|
244
|
+
* ```
|
|
245
|
+
*/
|
|
246
|
+
export function getLoggerNamespace(logger: Logger): string {
|
|
247
|
+
return (
|
|
248
|
+
loggerNamespaceStore.get(logger) ??
|
|
249
|
+
(logger.defaultMeta as { namespace?: string })?.namespace ??
|
|
250
|
+
""
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Reset logger state. Used primarily for testing.
|
|
256
|
+
* @internal
|
|
257
|
+
*/
|
|
258
|
+
export function resetLoggerForTesting(): void {
|
|
259
|
+
globalConfig = null;
|
|
260
|
+
hasLoggedInit = false;
|
|
261
|
+
loggerContext = "";
|
|
262
|
+
loggerNamespaceStore = new WeakMap<Logger, string>();
|
|
263
|
+
}
|