@holz/json-backend 0.8.0 → 0.8.2-rc.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/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 Jesse Gibson
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@holz/json-backend",
|
|
3
|
-
"version": "0.8.0",
|
|
3
|
+
"version": "0.8.2-rc.0",
|
|
4
4
|
"description": "Print logs as newline-delimited JSON.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -22,8 +22,7 @@
|
|
|
22
22
|
"license": "MIT",
|
|
23
23
|
"sideEffects": false,
|
|
24
24
|
"files": [
|
|
25
|
-
"dist"
|
|
26
|
-
"src"
|
|
25
|
+
"dist"
|
|
27
26
|
],
|
|
28
27
|
"keywords": [
|
|
29
28
|
"holz-backend",
|
|
@@ -37,7 +36,7 @@
|
|
|
37
36
|
"test:types": "tsc"
|
|
38
37
|
},
|
|
39
38
|
"devDependencies": {
|
|
40
|
-
"@holz/core": "^0.8.0",
|
|
39
|
+
"@holz/core": "^0.8.2-rc.0",
|
|
41
40
|
"@types/node": "^22.0.0",
|
|
42
41
|
"@vitest/coverage-v8": "^3.0.8",
|
|
43
42
|
"typescript": "^5.8.2",
|
|
@@ -45,5 +44,6 @@
|
|
|
45
44
|
"vite-plugin-dts": "^4.5.3",
|
|
46
45
|
"vite-tsconfig-paths": "^5.1.4",
|
|
47
46
|
"vitest": "^3.0.8"
|
|
48
|
-
}
|
|
49
|
-
|
|
47
|
+
},
|
|
48
|
+
"gitHead": "59a13334498d9c3f0952235ed6cb59e493558824"
|
|
49
|
+
}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
-
|
|
3
|
-
exports[`JSON backend > prints the logs to the writable stream 1`] = `
|
|
4
|
-
"{"level":"debug","time":"2020-06-15T12:00:00.000Z","msg":"shout"}
|
|
5
|
-
{"level":"info","time":"2020-06-15T12:00:00.000Z","msg":"normal"}
|
|
6
|
-
{"level":"warn","time":"2020-06-15T12:00:00.000Z","msg":"hmmmm"}
|
|
7
|
-
{"level":"error","time":"2020-06-15T12:00:00.000Z","msg":"oh no"}
|
|
8
|
-
"
|
|
9
|
-
`;
|
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
import { Writable } from 'node:stream';
|
|
2
|
-
import { createLogger } from '@holz/core';
|
|
3
|
-
import { createJsonBackend } from '../json-backend';
|
|
4
|
-
|
|
5
|
-
const CURRENT_TIME = new Date('2020-06-15T12:00:00.000Z');
|
|
6
|
-
|
|
7
|
-
describe('JSON backend', () => {
|
|
8
|
-
const createStream = () => {
|
|
9
|
-
let output = '';
|
|
10
|
-
const stream = new Writable({
|
|
11
|
-
write(chunk, _encoding, callback) {
|
|
12
|
-
output += String(chunk);
|
|
13
|
-
callback();
|
|
14
|
-
},
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
return {
|
|
18
|
-
getOutput: () => output,
|
|
19
|
-
stream,
|
|
20
|
-
};
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
beforeEach(() => {
|
|
24
|
-
vi.useFakeTimers({
|
|
25
|
-
now: CURRENT_TIME,
|
|
26
|
-
});
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
afterEach(() => {
|
|
30
|
-
vi.useRealTimers();
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it('prints the logs to the writable stream', () => {
|
|
34
|
-
const { stream, getOutput } = createStream();
|
|
35
|
-
const backend = createJsonBackend({ stream });
|
|
36
|
-
|
|
37
|
-
const logger = createLogger(backend);
|
|
38
|
-
logger.debug('shout');
|
|
39
|
-
logger.info('normal');
|
|
40
|
-
logger.warn('hmmmm');
|
|
41
|
-
logger.error('oh no');
|
|
42
|
-
|
|
43
|
-
expect(getOutput()).toMatchSnapshot();
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it('escapes newlines and carriage returns', () => {
|
|
47
|
-
const { stream, getOutput } = createStream();
|
|
48
|
-
const backend = createJsonBackend({ stream });
|
|
49
|
-
const logger = createLogger(backend);
|
|
50
|
-
|
|
51
|
-
logger.debug('sneaky log\nwith newlines\rand carriage returns\r\n');
|
|
52
|
-
|
|
53
|
-
expect(getOutput()).toContain(
|
|
54
|
-
'sneaky log\\nwith newlines\\rand carriage returns\\r\\n',
|
|
55
|
-
);
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it('includes log context', () => {
|
|
59
|
-
const { stream, getOutput } = createStream();
|
|
60
|
-
const backend = createJsonBackend({ stream });
|
|
61
|
-
const logger = createLogger(backend);
|
|
62
|
-
|
|
63
|
-
logger.debug('hello', { reqId: 'abc' });
|
|
64
|
-
|
|
65
|
-
expect(getOutput()).toContain('"reqId":"abc"');
|
|
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
|
-
});
|
|
117
|
-
});
|
package/src/index.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { createJsonBackend } from './json-backend';
|
package/src/json-backend.ts
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import type { Writable } from 'node:stream';
|
|
2
|
-
import { EOL } from 'node:os';
|
|
3
|
-
import { level, type LogLevel, type Log, type LogProcessor } from '@holz/core';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Prints structured logs to a writable stream in NDJSON form. Optimized for
|
|
7
|
-
* log files.
|
|
8
|
-
*
|
|
9
|
-
* @example
|
|
10
|
-
* createJsonBackend({
|
|
11
|
-
* stream: fs.createWriteStream('my-app.log', { flags: 'a' }),
|
|
12
|
-
* })
|
|
13
|
-
*
|
|
14
|
-
* @see http://ndjson.org
|
|
15
|
-
*/
|
|
16
|
-
export const createJsonBackend = ({ stream }: Config): LogProcessor => {
|
|
17
|
-
return (log: Log) => {
|
|
18
|
-
// Follow the order of typical log statements. Be kind to the human
|
|
19
|
-
// reader.
|
|
20
|
-
const output = JSON.stringify(
|
|
21
|
-
{
|
|
22
|
-
level: labelForLevel[log.level],
|
|
23
|
-
time: new Date(log.timestamp).toISOString(),
|
|
24
|
-
msg: log.message,
|
|
25
|
-
ctx: Object.keys(log.context).length > 0 ? log.context : undefined,
|
|
26
|
-
},
|
|
27
|
-
errorSerializer,
|
|
28
|
-
);
|
|
29
|
-
|
|
30
|
-
// NOTE: If the stream applies backpressure, we will lose logs. I believe
|
|
31
|
-
// this is the right tradeoff. We can't prevent the app from generating
|
|
32
|
-
// more logs, and if we buffered it would risk running out of memory and
|
|
33
|
-
// crashing the process.
|
|
34
|
-
//
|
|
35
|
-
// It is unlikely that a file or tty will apply backpressure in practice.
|
|
36
|
-
stream.write(`${output}${EOL}`);
|
|
37
|
-
};
|
|
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>;
|
|
64
|
-
|
|
65
|
-
interface Config {
|
|
66
|
-
/** Where to print logs. */
|
|
67
|
-
stream: Writable;
|
|
68
|
-
}
|