@holz/json-backend 0.7.0-rc.2 → 0.8.0-rc.1

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/README.md CHANGED
@@ -12,7 +12,7 @@ import * as fs from 'node:fs';
12
12
  const logger = createLogger(
13
13
  createJsonBackend({
14
14
  stream: fs.createWriteStream('my-app.log', { flags: 'a' }),
15
- })
15
+ }),
16
16
  );
17
17
  ```
18
18
 
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const c=require("node:os");function r({stream:t}){return e=>{const n=JSON.stringify({level:e.level,time:new Date().toISOString(),msg:e.message,ctx:Object.keys(e.context).length>0?e.context:void 0});t.write(`${n}${c.EOL}`)}}exports.createJsonBackend=r;
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const n=require("node:os"),s={fatal:60,error:50,warn:40,info:30,debug:20,trace:10},c=({stream:t})=>e=>{const r=JSON.stringify({level:a[e.level],time:new Date().toISOString(),msg:e.message,ctx:Object.keys(e.context).length>0?e.context:void 0},o);t.write(`${r}${n.EOL}`)},o=(t,e)=>e instanceof Error?{...e,name:e.name,message:e.message,cause:e.cause}:e,a=Object.fromEntries(Object.entries(s).map(([t,e])=>[e,t]));exports.createJsonBackend=c;
@@ -17,6 +17,6 @@ declare interface Config {
17
17
  *
18
18
  * @see http://ndjson.org
19
19
  */
20
- export declare function createJsonBackend({ stream }: Config): LogProcessor;
20
+ export declare const createJsonBackend: ({ stream }: Config) => LogProcessor;
21
21
 
22
22
  export { }
@@ -1,15 +1,37 @@
1
- import { EOL as r } from "node:os";
2
- function i({ stream: t }) {
3
- return (e) => {
4
- const n = JSON.stringify({
5
- level: e.level,
1
+ import { EOL as n } from "node:os";
2
+ const s = {
3
+ /** A critical failure happened and the program must exit. */
4
+ fatal: 60,
5
+ /** Something failed, but we can keep going. */
6
+ error: 50,
7
+ /** Cause for concern, but we can keep going. */
8
+ warn: 40,
9
+ /** High-level progress updates. */
10
+ info: 30,
11
+ /** Verbose update about events or control flow (usually hidden). */
12
+ debug: 20,
13
+ /** Extremely detailed progress updates (usually hidden). */
14
+ trace: 10
15
+ }, a = ({ stream: t }) => (e) => {
16
+ const r = JSON.stringify(
17
+ {
18
+ level: o[e.level],
6
19
  time: (/* @__PURE__ */ new Date()).toISOString(),
7
20
  msg: e.message,
8
21
  ctx: Object.keys(e.context).length > 0 ? e.context : void 0
9
- });
10
- t.write(`${n}${r}`);
11
- };
12
- }
22
+ },
23
+ c
24
+ );
25
+ t.write(`${r}${n}`);
26
+ }, c = (t, e) => e instanceof Error ? {
27
+ ...e,
28
+ // Some custom errors have important custom properties.
29
+ name: e.name,
30
+ message: e.message,
31
+ cause: e.cause
32
+ } : e, o = Object.fromEntries(
33
+ Object.entries(s).map(([t, e]) => [e, t])
34
+ );
13
35
  export {
14
- i as createJsonBackend
36
+ a as createJsonBackend
15
37
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@holz/json-backend",
3
- "version": "0.7.0-rc.2",
3
+ "version": "0.8.0-rc.1",
4
4
  "description": "Print logs as newline-delimited JSON.",
5
5
  "type": "module",
6
6
  "repository": {
@@ -37,14 +37,14 @@
37
37
  "test:types": "tsc"
38
38
  },
39
39
  "devDependencies": {
40
- "@holz/core": "^0.7.0-rc.2",
40
+ "@holz/core": "^0.8.0-rc.1",
41
41
  "@types/node": "^22.0.0",
42
42
  "@vitest/coverage-v8": "^3.0.8",
43
- "typescript": "^5.0.0",
43
+ "typescript": "^5.8.2",
44
44
  "vite": "^6.0.0",
45
45
  "vite-plugin-dts": "^4.5.3",
46
46
  "vite-tsconfig-paths": "^5.1.4",
47
47
  "vitest": "^3.0.8"
48
48
  },
49
- "stableVersion": "0.6.0"
49
+ "stableVersion": "0.7.0"
50
50
  }
@@ -5,7 +5,7 @@ import { createJsonBackend } from '../json-backend';
5
5
  const CURRENT_TIME = new Date('2020-06-15T12:00:00.000Z');
6
6
 
7
7
  describe('JSON backend', () => {
8
- function createStream() {
8
+ const createStream = () => {
9
9
  let output = '';
10
10
  const stream = new Writable({
11
11
  write(chunk, _encoding, callback) {
@@ -18,7 +18,7 @@ describe('JSON backend', () => {
18
18
  getOutput: () => output,
19
19
  stream,
20
20
  };
21
- }
21
+ };
22
22
 
23
23
  beforeEach(() => {
24
24
  vi.useFakeTimers({
@@ -64,4 +64,54 @@ describe('JSON backend', () => {
64
64
 
65
65
  expect(getOutput()).toContain('"reqId":"abc"');
66
66
  });
67
+
68
+ it('pulls structure from errors', () => {
69
+ const { stream, getOutput } = createStream();
70
+ const backend = createJsonBackend({ stream });
71
+ const logger = createLogger(backend);
72
+
73
+ logger.error('content', {
74
+ error: new RangeError('Testing NDJSON errors'),
75
+ });
76
+
77
+ const output = getOutput();
78
+ expect(output).toContain(
79
+ '"error":{"name":"RangeError","message":"Testing NDJSON errors"}',
80
+ );
81
+ });
82
+
83
+ it('detects and includes error causes', () => {
84
+ const { stream, getOutput } = createStream();
85
+ const backend = createJsonBackend({ stream });
86
+ const logger = createLogger(backend);
87
+
88
+ logger.error('content', {
89
+ error: new Error('Testing NDJSON errors', {
90
+ cause: new Error('Cause'),
91
+ }),
92
+ });
93
+
94
+ const output = getOutput();
95
+ expect(output).toContain(
96
+ '"error":{"name":"Error","message":"Testing NDJSON errors","cause":{"name":"Error","message":"Cause"}}',
97
+ );
98
+ });
99
+
100
+ it('includes any enumerable properties on the object', () => {
101
+ class CustomError extends Error {
102
+ name = 'CustomError';
103
+ status = 418;
104
+ }
105
+
106
+ const { stream, getOutput } = createStream();
107
+ const backend = createJsonBackend({ stream });
108
+ const logger = createLogger(backend);
109
+
110
+ logger.error('content', {
111
+ error: new CustomError('Testing NDJSON errors'),
112
+ });
113
+
114
+ const output = getOutput();
115
+ expect(output).toContain('"status":418');
116
+ });
67
117
  });
@@ -1,6 +1,6 @@
1
1
  import type { Writable } from 'node:stream';
2
2
  import { EOL } from 'node:os';
3
- import type { Log, LogProcessor } from '@holz/core';
3
+ import { level, type LogLevel, type Log, type LogProcessor } from '@holz/core';
4
4
 
5
5
  /**
6
6
  * Prints structured logs to a writable stream in NDJSON form. Optimized for
@@ -13,16 +13,19 @@ import type { Log, LogProcessor } from '@holz/core';
13
13
  *
14
14
  * @see http://ndjson.org
15
15
  */
16
- export function createJsonBackend({ stream }: Config): LogProcessor {
16
+ export const createJsonBackend = ({ stream }: Config): LogProcessor => {
17
17
  return (log: Log) => {
18
18
  // Follow the order of typical log statements. Be kind to the human
19
19
  // reader.
20
- const output = JSON.stringify({
21
- level: log.level,
22
- time: new Date().toISOString(),
23
- msg: log.message,
24
- ctx: Object.keys(log.context).length > 0 ? log.context : undefined,
25
- });
20
+ const output = JSON.stringify(
21
+ {
22
+ level: labelForLevel[log.level],
23
+ time: new Date().toISOString(),
24
+ msg: log.message,
25
+ ctx: Object.keys(log.context).length > 0 ? log.context : undefined,
26
+ },
27
+ errorSerializer,
28
+ );
26
29
 
27
30
  // NOTE: If the stream applies backpressure, we will lose logs. I believe
28
31
  // this is the right tradeoff. We can't prevent the app from generating
@@ -32,7 +35,32 @@ export function createJsonBackend({ stream }: Config): LogProcessor {
32
35
  // It is unlikely that a file or tty will apply backpressure in practice.
33
36
  stream.write(`${output}${EOL}`);
34
37
  };
35
- }
38
+ };
39
+
40
+ /**
41
+ * Errors are not JSON serializable, but errors are naturally a crucial aspect
42
+ * of any logging system. This unwraps errors into a JSON representation.
43
+ */
44
+ const errorSerializer = (_key: string, value: unknown) => {
45
+ if (value instanceof Error) {
46
+ return {
47
+ ...value, // Some custom errors have important custom properties.
48
+ name: value.name,
49
+ message: value.message,
50
+ cause: value.cause,
51
+ };
52
+ }
53
+
54
+ return value;
55
+ };
56
+
57
+ /**
58
+ * Slightly heavier than logging the number, but easier to process
59
+ * programmatically which is somewhat implied by a JSON backend.
60
+ */
61
+ const labelForLevel = Object.fromEntries(
62
+ Object.entries(level).map(([key, value]) => [value, key]),
63
+ ) as Record<LogLevel, string>;
36
64
 
37
65
  interface Config {
38
66
  /** Where to print logs. */