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

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/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.11",
4
+ "version": "1.0.0-alpha.12",
5
5
  "type": "module",
6
6
  "private": false,
7
7
  "author": "Nick Jennings <nick@silverbucket.net>",
@@ -47,5 +47,5 @@
47
47
  "devDependencies": {
48
48
  "@types/bun": "latest"
49
49
  },
50
- "gitHead": "c243fa9e76c688ce5ffcf524400b1dd27dcce615"
50
+ "gitHead": "f039dab3c3f67cbbf204476fc397532e973f82a8"
51
51
  }
package/src/index.test.ts CHANGED
@@ -176,6 +176,85 @@ describe("Logger Package", () => {
176
176
  const log2 = createLogger("test:2");
177
177
  expect(log2.transports.length).toBe(2);
178
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
+ });
179
258
  });
180
259
 
181
260
  describe("logger context", () => {
package/src/index.ts CHANGED
@@ -16,6 +16,43 @@ let hasLoggedInit = false;
16
16
  let loggerContext = "";
17
17
  let loggerNamespaceStore = new WeakMap<Logger, string>();
18
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
+
19
56
  /**
20
57
  * Initialize the logger system with global configuration.
21
58
  *
@@ -174,7 +211,7 @@ export function createLogger(
174
211
  const ns = namespace ? `${namespace} ` : "";
175
212
  const metaStr =
176
213
  Object.keys(meta).length > 0
177
- ? ` ${JSON.stringify(meta)}`
214
+ ? ` ${safeStringify(meta)}`
178
215
  : "";
179
216
  return `${level}: ${ns}${message}${metaStr}`;
180
217
  },
@@ -188,7 +225,7 @@ export function createLogger(
188
225
  const ns = namespace ? `${namespace} ` : "";
189
226
  const metaStr =
190
227
  Object.keys(meta).length > 0
191
- ? ` ${JSON.stringify(meta)}`
228
+ ? ` ${safeStringify(meta)}`
192
229
  : "";
193
230
  return `${timestamp} ${level}: ${ns}${message}${metaStr}`;
194
231
  },