@sockethub/logger 1.0.0-alpha.12 → 1.0.0-alpha.13

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.
@@ -0,0 +1,129 @@
1
+ import winston from "winston";
2
+ export type Logger = winston.Logger;
3
+ export interface LoggerOptions {
4
+ level?: string;
5
+ fileLevel?: string;
6
+ file?: string;
7
+ }
8
+ /**
9
+ * Initialize the logger system with global configuration.
10
+ *
11
+ * Called once at server bootstrap with config settings. All subsequent
12
+ * createLogger() calls will use these settings as defaults unless explicitly
13
+ * overridden.
14
+ *
15
+ * @param options - Global logger configuration
16
+ *
17
+ * @example
18
+ * ```typescript
19
+ * // In server bootstrap
20
+ * initLogger({
21
+ * level: config.get("logging:level"),
22
+ * fileLevel: config.get("logging:fileLevel"),
23
+ * file: config.get("logging:file"),
24
+ * });
25
+ * ```
26
+ */
27
+ export declare function initLogger(options: LoggerOptions): void;
28
+ /**
29
+ * Set the logger context for this process.
30
+ *
31
+ * All subsequent createLogger() calls will prepend this context to their namespace.
32
+ * This is typically set once at process startup to identify the process (e.g., "sockethub"
33
+ * for the main server, or "sockethub:platform:irc:abc123" for a platform child process).
34
+ *
35
+ * @param context - The context string to prepend to all logger namespaces in this process
36
+ *
37
+ * @example
38
+ * ```typescript
39
+ * // In main server process
40
+ * setLoggerContext('sockethub');
41
+ * const log = createLogger('server:listener');
42
+ * // Output namespace: "sockethub:server:listener"
43
+ *
44
+ * // In platform child process
45
+ * setLoggerContext('sockethub:platform:irc:abc123');
46
+ * const log = createLogger('main');
47
+ * // Output namespace: "sockethub:platform:irc:abc123:main"
48
+ * ```
49
+ */
50
+ export declare function setLoggerContext(context: string): void;
51
+ /**
52
+ * Get the current logger context for this process.
53
+ *
54
+ * @returns The current logger context string, or empty string if not set
55
+ *
56
+ * @example
57
+ * ```typescript
58
+ * const context = getLoggerContext();
59
+ * if (context.includes(':platform:')) {
60
+ * // We're in a platform child process
61
+ * }
62
+ * ```
63
+ */
64
+ export declare function getLoggerContext(): string;
65
+ /**
66
+ * Reset the logger context.
67
+ *
68
+ * Primarily used for testing to reset state between test cases.
69
+ *
70
+ * @example
71
+ * ```typescript
72
+ * afterEach(() => {
73
+ * resetLoggerContext();
74
+ * });
75
+ * ```
76
+ */
77
+ export declare function resetLoggerContext(): void;
78
+ /**
79
+ * Creates a Winston logger instance with console and optional file transports.
80
+ *
81
+ * Configuration priority (highest to lowest):
82
+ * 1. Explicit options parameter
83
+ * 2. Global config (set via initLogger)
84
+ * 3. Environment variables (LOG_LEVEL, LOG_FILE_LEVEL, LOG_FILE)
85
+ * 4. Defaults (info for console, debug for file)
86
+ *
87
+ * Log levels: error, warn, info, debug
88
+ *
89
+ * NODE_ENV=production disables console timestamps (for systemd)
90
+ *
91
+ * @param namespace - Logger namespace (e.g., "sockethub:data-layer:queue:...")
92
+ * @param options - Optional logger configuration overrides
93
+ * @returns Winston logger instance
94
+ *
95
+ * @example
96
+ * ```typescript
97
+ * // Uses global config if initLogger() was called
98
+ * const log = createLogger("sockethub:data-layer:queue:irc-123");
99
+ *
100
+ * // Override for specific use case (e.g., early bootstrap logging)
101
+ * const earlyLog = createLogger("sockethub:bootstrap", { level: "info" });
102
+ * ```
103
+ */
104
+ export declare function createLogger(namespace: string, options?: LoggerOptions): Logger;
105
+ /**
106
+ * Get the namespace from a logger instance.
107
+ *
108
+ * Extracts the full namespace (including context) that was set when the logger was created.
109
+ * Useful for reusing the namespace in other systems like Redis connection names.
110
+ *
111
+ * @param logger - Logger instance created by createLogger()
112
+ * @returns The full namespace string, or empty string if not found
113
+ *
114
+ * @example
115
+ * ```typescript
116
+ * const log = createLogger('data-layer:queue');
117
+ * const namespace = getLoggerNamespace(log);
118
+ * // Returns: "sockethub:data-layer:queue" (if context is "sockethub")
119
+ *
120
+ * // Use for Redis connection name
121
+ * redisConfig.connectionName = namespace;
122
+ * ```
123
+ */
124
+ export declare function getLoggerNamespace(logger: Logger): string;
125
+ /**
126
+ * Reset logger state. Used primarily for testing.
127
+ * @internal
128
+ */
129
+ export declare function resetLoggerForTesting(): void;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@sockethub/logger",
3
3
  "description": "Winston-based logger for Sockethub packages",
4
- "version": "1.0.0-alpha.12",
4
+ "version": "1.0.0-alpha.13",
5
5
  "type": "module",
6
6
  "private": false,
7
7
  "author": "Nick Jennings <nick@silverbucket.net>",
@@ -12,17 +12,16 @@
12
12
  "main": "dist/index.js",
13
13
  "exports": {
14
14
  ".": {
15
- "bun": "./src/index.ts",
15
+ "types": "./dist/index.d.ts",
16
16
  "import": "./dist/index.js",
17
17
  "default": "./dist/index.js"
18
18
  }
19
19
  },
20
20
  "files": [
21
- "src/",
22
21
  "dist/"
23
22
  ],
24
23
  "engines": {
25
- "bun": ">=1.2"
24
+ "node": ">=20"
26
25
  },
27
26
  "keywords": [
28
27
  "sockethub",
@@ -47,5 +46,6 @@
47
46
  "devDependencies": {
48
47
  "@types/bun": "latest"
49
48
  },
50
- "gitHead": "f039dab3c3f67cbbf204476fc397532e973f82a8"
49
+ "gitHead": "864733e5b34449ef39542eceb4ff11aa308081bc",
50
+ "types": "./dist/index.d.ts"
51
51
  }
package/src/index.test.ts DELETED
@@ -1,324 +0,0 @@
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
- it("serializes circular metadata with [Circular] marker in log output", () => {
181
- const previousNodeEnv = process.env.NODE_ENV;
182
- process.env.NODE_ENV = "production";
183
-
184
- try {
185
- const log = createLogger("test:namespace");
186
- const circular: { self?: unknown } = {};
187
- circular.self = circular;
188
-
189
- const consoleTransport = log.transports[0] as {
190
- format: {
191
- transform: (
192
- info: Record<string, unknown>,
193
- opts?: unknown,
194
- ) => Record<PropertyKey, unknown>;
195
- options?: unknown;
196
- };
197
- };
198
-
199
- const transformed = consoleTransport.format.transform(
200
- {
201
- level: "info",
202
- [Symbol.for("level")]: "info",
203
- message: "circular metadata test",
204
- namespace: "test:namespace",
205
- circular,
206
- },
207
- consoleTransport.format.options,
208
- );
209
- const output = String(
210
- transformed[Symbol.for("message")] ?? "",
211
- ).replace(/\u001b\[[0-9;]*m/g, "");
212
-
213
- expect(output).toContain("circular metadata test");
214
- expect(output).toContain("\"[Circular]\"");
215
- } finally {
216
- process.env.NODE_ENV = previousNodeEnv;
217
- }
218
- });
219
-
220
- it("serializes Error metadata with name/message/stack in log output", () => {
221
- const previousNodeEnv = process.env.NODE_ENV;
222
- process.env.NODE_ENV = "production";
223
-
224
- try {
225
- const log = createLogger("test:namespace");
226
- const consoleTransport = log.transports[0] as {
227
- format: {
228
- transform: (
229
- info: Record<string, unknown>,
230
- opts?: unknown,
231
- ) => Record<PropertyKey, unknown>;
232
- options?: unknown;
233
- };
234
- };
235
-
236
- const transformed = consoleTransport.format.transform(
237
- {
238
- level: "error",
239
- [Symbol.for("level")]: "error",
240
- message: "error metadata test",
241
- namespace: "test:namespace",
242
- err: new Error("boom"),
243
- },
244
- consoleTransport.format.options,
245
- );
246
- const output = String(
247
- transformed[Symbol.for("message")] ?? "",
248
- ).replace(/\u001b\[[0-9;]*m/g, "");
249
-
250
- expect(output).toContain("error metadata test");
251
- expect(output).toContain("\"name\":\"Error\"");
252
- expect(output).toContain("\"message\":\"boom\"");
253
- expect(output).toContain("\"stack\"");
254
- } finally {
255
- process.env.NODE_ENV = previousNodeEnv;
256
- }
257
- });
258
- });
259
-
260
- describe("logger context", () => {
261
- it("createLogger without context uses namespace as-is", () => {
262
- const log = createLogger("test:namespace");
263
- expect(log.defaultMeta.namespace).toBe("test:namespace");
264
- });
265
-
266
- it("createLogger with context prepends context to namespace", () => {
267
- setLoggerContext("myapp");
268
- const log = createLogger("test:namespace");
269
- expect(log.defaultMeta.namespace).toBe("myapp:test:namespace");
270
- });
271
-
272
- it("getLoggerContext returns empty string by default", () => {
273
- expect(getLoggerContext()).toBe("");
274
- });
275
-
276
- it("getLoggerContext returns the set context", () => {
277
- setLoggerContext("myapp:process");
278
- expect(getLoggerContext()).toBe("myapp:process");
279
- });
280
-
281
- it("resetLoggerContext clears the context", () => {
282
- setLoggerContext("myapp");
283
- expect(getLoggerContext()).toBe("myapp");
284
-
285
- resetLoggerContext();
286
- expect(getLoggerContext()).toBe("");
287
-
288
- const log = createLogger("test");
289
- expect(log.defaultMeta.namespace).toBe("test");
290
- });
291
-
292
- it("context works with nested namespaces", () => {
293
- setLoggerContext("sockethub:platform:irc:abc123");
294
- const log = createLogger("data-layer:worker");
295
- expect(log.defaultMeta.namespace).toBe(
296
- "sockethub:platform:irc:abc123:data-layer:worker",
297
- );
298
- });
299
-
300
- it("context is process-wide, affects all subsequent loggers", () => {
301
- setLoggerContext("process-context");
302
-
303
- const log1 = createLogger("module1");
304
- const log2 = createLogger("module2");
305
- const log3 = createLogger("module3");
306
-
307
- expect(log1.defaultMeta.namespace).toBe("process-context:module1");
308
- expect(log2.defaultMeta.namespace).toBe("process-context:module2");
309
- expect(log3.defaultMeta.namespace).toBe("process-context:module3");
310
- });
311
-
312
- it("resetLoggerForTesting also resets context", () => {
313
- setLoggerContext("myapp");
314
- initLogger({ level: "debug" });
315
-
316
- resetLoggerForTesting();
317
-
318
- expect(getLoggerContext()).toBe("");
319
- const log = createLogger("test");
320
- expect(log.defaultMeta.namespace).toBe("test");
321
- expect(log.transports[0].level).toBe("info"); // back to default
322
- });
323
- });
324
- });
package/src/index.ts DELETED
@@ -1,300 +0,0 @@
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
- // Keep log formatting resilient when metadata contains errors, bigints, or cycles.
20
- function safeStringify(value: unknown): string {
21
- try {
22
- const parents: object[] = [];
23
- return JSON.stringify(value, function (_key, innerValue) {
24
- if (innerValue instanceof Error) {
25
- return {
26
- name: innerValue.name,
27
- message: innerValue.message,
28
- stack: innerValue.stack,
29
- };
30
- }
31
- if (typeof innerValue === "bigint") {
32
- return innerValue.toString();
33
- }
34
- if (typeof innerValue === "object" && innerValue !== null) {
35
- // Only treat values on the current traversal path as circular.
36
- // Shared references in sibling branches should serialize normally.
37
- const parent = this as unknown;
38
- while (
39
- parents.length > 0 &&
40
- parents[parents.length - 1] !== parent
41
- ) {
42
- parents.pop();
43
- }
44
- if (parents.includes(innerValue)) {
45
- return "[Circular]";
46
- }
47
- parents.push(innerValue);
48
- }
49
- return innerValue;
50
- });
51
- } catch {
52
- return '"[Unserializable]"';
53
- }
54
- }
55
-
56
- /**
57
- * Initialize the logger system with global configuration.
58
- *
59
- * Called once at server bootstrap with config settings. All subsequent
60
- * createLogger() calls will use these settings as defaults unless explicitly
61
- * overridden.
62
- *
63
- * @param options - Global logger configuration
64
- *
65
- * @example
66
- * ```typescript
67
- * // In server bootstrap
68
- * initLogger({
69
- * level: config.get("logging:level"),
70
- * fileLevel: config.get("logging:fileLevel"),
71
- * file: config.get("logging:file"),
72
- * });
73
- * ```
74
- */
75
- export function initLogger(options: LoggerOptions): void {
76
- globalConfig = options;
77
-
78
- if (!hasLoggedInit) {
79
- hasLoggedInit = true;
80
- const initLog = createLogger("sockethub:logger");
81
- initLog.debug("logger system initialized");
82
- if (options.file) {
83
- initLog.info(`log file path: ${options.file}`);
84
- initLog.info(`file log level: ${options.fileLevel || "debug"}`);
85
- }
86
- initLog.info(`console log level: ${options.level || "info"}`);
87
- }
88
- }
89
-
90
- /**
91
- * Set the logger context for this process.
92
- *
93
- * All subsequent createLogger() calls will prepend this context to their namespace.
94
- * This is typically set once at process startup to identify the process (e.g., "sockethub"
95
- * for the main server, or "sockethub:platform:irc:abc123" for a platform child process).
96
- *
97
- * @param context - The context string to prepend to all logger namespaces in this process
98
- *
99
- * @example
100
- * ```typescript
101
- * // In main server process
102
- * setLoggerContext('sockethub');
103
- * const log = createLogger('server:listener');
104
- * // Output namespace: "sockethub:server:listener"
105
- *
106
- * // In platform child process
107
- * setLoggerContext('sockethub:platform:irc:abc123');
108
- * const log = createLogger('main');
109
- * // Output namespace: "sockethub:platform:irc:abc123:main"
110
- * ```
111
- */
112
- export function setLoggerContext(context: string): void {
113
- loggerContext = context;
114
- }
115
-
116
- /**
117
- * Get the current logger context for this process.
118
- *
119
- * @returns The current logger context string, or empty string if not set
120
- *
121
- * @example
122
- * ```typescript
123
- * const context = getLoggerContext();
124
- * if (context.includes(':platform:')) {
125
- * // We're in a platform child process
126
- * }
127
- * ```
128
- */
129
- export function getLoggerContext(): string {
130
- return loggerContext;
131
- }
132
-
133
- /**
134
- * Reset the logger context.
135
- *
136
- * Primarily used for testing to reset state between test cases.
137
- *
138
- * @example
139
- * ```typescript
140
- * afterEach(() => {
141
- * resetLoggerContext();
142
- * });
143
- * ```
144
- */
145
- export function resetLoggerContext(): void {
146
- loggerContext = "";
147
- }
148
-
149
- /**
150
- * Creates a Winston logger instance with console and optional file transports.
151
- *
152
- * Configuration priority (highest to lowest):
153
- * 1. Explicit options parameter
154
- * 2. Global config (set via initLogger)
155
- * 3. Environment variables (LOG_LEVEL, LOG_FILE_LEVEL, LOG_FILE)
156
- * 4. Defaults (info for console, debug for file)
157
- *
158
- * Log levels: error, warn, info, debug
159
- *
160
- * NODE_ENV=production disables console timestamps (for systemd)
161
- *
162
- * @param namespace - Logger namespace (e.g., "sockethub:data-layer:queue:...")
163
- * @param options - Optional logger configuration overrides
164
- * @returns Winston logger instance
165
- *
166
- * @example
167
- * ```typescript
168
- * // Uses global config if initLogger() was called
169
- * const log = createLogger("sockethub:data-layer:queue:irc-123");
170
- *
171
- * // Override for specific use case (e.g., early bootstrap logging)
172
- * const earlyLog = createLogger("sockethub:bootstrap", { level: "info" });
173
- * ```
174
- */
175
- export function createLogger(
176
- namespace: string,
177
- options: LoggerOptions = {},
178
- ): Logger {
179
- // Prepend logger context if set
180
- const fullNamespace = loggerContext
181
- ? `${loggerContext}:${namespace}`
182
- : namespace;
183
-
184
- // Priority: explicit options > global config > ENV > defaults
185
- const cfg: LoggerOptions = {
186
- level:
187
- options.level ??
188
- globalConfig?.level ??
189
- process.env.LOG_LEVEL ??
190
- "info",
191
- fileLevel:
192
- options.fileLevel ??
193
- globalConfig?.fileLevel ??
194
- process.env.LOG_FILE_LEVEL ??
195
- "debug",
196
- file:
197
- options.file !== undefined
198
- ? options.file
199
- : globalConfig?.file !== undefined
200
- ? globalConfig.file
201
- : (process.env.LOG_FILE ?? ""),
202
- };
203
-
204
- const isProduction = process.env.NODE_ENV === "production";
205
-
206
- const consoleFormat = isProduction
207
- ? winston.format.combine(
208
- winston.format.colorize(),
209
- winston.format.printf(
210
- ({ level, message, namespace, ...meta }) => {
211
- const ns = namespace ? `${namespace} ` : "";
212
- const metaStr =
213
- Object.keys(meta).length > 0
214
- ? ` ${safeStringify(meta)}`
215
- : "";
216
- return `${level}: ${ns}${message}${metaStr}`;
217
- },
218
- ),
219
- )
220
- : winston.format.combine(
221
- winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
222
- winston.format.colorize(),
223
- winston.format.printf(
224
- ({ level, message, timestamp, namespace, ...meta }) => {
225
- const ns = namespace ? `${namespace} ` : "";
226
- const metaStr =
227
- Object.keys(meta).length > 0
228
- ? ` ${safeStringify(meta)}`
229
- : "";
230
- return `${timestamp} ${level}: ${ns}${message}${metaStr}`;
231
- },
232
- ),
233
- );
234
-
235
- const transports: winston.transport[] = [
236
- new winston.transports.Console({
237
- level: cfg.level,
238
- format: consoleFormat,
239
- }),
240
- ];
241
-
242
- if (cfg.file) {
243
- transports.push(
244
- new winston.transports.File({
245
- level: cfg.fileLevel,
246
- filename: cfg.file,
247
- format: winston.format.combine(
248
- winston.format.timestamp(),
249
- winston.format.json(),
250
- ),
251
- }),
252
- );
253
- }
254
-
255
- const logger = winston.createLogger({
256
- level: "debug", // Set to lowest level, let transports filter
257
- defaultMeta: { namespace: fullNamespace },
258
- transports,
259
- });
260
- loggerNamespaceStore.set(logger, fullNamespace);
261
- return logger;
262
- }
263
-
264
- /**
265
- * Get the namespace from a logger instance.
266
- *
267
- * Extracts the full namespace (including context) that was set when the logger was created.
268
- * Useful for reusing the namespace in other systems like Redis connection names.
269
- *
270
- * @param logger - Logger instance created by createLogger()
271
- * @returns The full namespace string, or empty string if not found
272
- *
273
- * @example
274
- * ```typescript
275
- * const log = createLogger('data-layer:queue');
276
- * const namespace = getLoggerNamespace(log);
277
- * // Returns: "sockethub:data-layer:queue" (if context is "sockethub")
278
- *
279
- * // Use for Redis connection name
280
- * redisConfig.connectionName = namespace;
281
- * ```
282
- */
283
- export function getLoggerNamespace(logger: Logger): string {
284
- return (
285
- loggerNamespaceStore.get(logger) ??
286
- (logger.defaultMeta as { namespace?: string })?.namespace ??
287
- ""
288
- );
289
- }
290
-
291
- /**
292
- * Reset logger state. Used primarily for testing.
293
- * @internal
294
- */
295
- export function resetLoggerForTesting(): void {
296
- globalConfig = null;
297
- hasLoggedInit = false;
298
- loggerContext = "";
299
- loggerNamespaceStore = new WeakMap<Logger, string>();
300
- }