@holz/stream-backend 0.1.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.
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
var s = /* @__PURE__ */ ((t) => (t.Error = "error", t.Warn = "warn", t.Info = "info", t.Debug = "debug", t))(s || {});
|
|
2
|
+
class m {
|
|
3
|
+
constructor(e) {
|
|
4
|
+
this.stream = e.stream;
|
|
5
|
+
}
|
|
6
|
+
processLog(e) {
|
|
7
|
+
const n = new Date().toISOString(), r = g[e.level], i = this.stringifyContext(e.context), a = e.origin.length ? `[${e.origin.join(":")}] ` : "", o = `${n} ${r} ${a}`, c = this.multilineIndent(o.length, e.message), u = `${o}${c}${i ? " " + i : ""}
|
|
8
|
+
`;
|
|
9
|
+
this.stream.write(u);
|
|
10
|
+
}
|
|
11
|
+
// { id: 123, type: 'article' } -> 'id=123 type="article"'
|
|
12
|
+
stringifyContext(e) {
|
|
13
|
+
return Object.entries(e).map(([n, r]) => `${n}=${JSON.stringify(r)}`).join(" ");
|
|
14
|
+
}
|
|
15
|
+
// Some messages will ruin your output without proper indentation. Stack
|
|
16
|
+
// traces are a good example of this.
|
|
17
|
+
//
|
|
18
|
+
// Supports Unix + DOS line endings.
|
|
19
|
+
multilineIndent(e, n) {
|
|
20
|
+
return n.replace(
|
|
21
|
+
/(\r?\n)/g,
|
|
22
|
+
(r) => r + " ".repeat(e)
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
const g = {
|
|
27
|
+
[s.Debug]: "DEBUG",
|
|
28
|
+
[s.Info]: "INFO ",
|
|
29
|
+
[s.Warn]: "WARN ",
|
|
30
|
+
[s.Error]: "ERROR"
|
|
31
|
+
};
|
|
32
|
+
export {
|
|
33
|
+
m as StreamBackend,
|
|
34
|
+
m as default
|
|
35
|
+
};
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
(function(n,t){typeof exports=="object"&&typeof module<"u"?t(exports):typeof define=="function"&&define.amd?define(["exports"],t):(n=typeof globalThis<"u"?globalThis:n||self,t(n["holz-stream-backend"]={}))})(this,function(n){"use strict";var t=(r=>(r.Error="error",r.Warn="warn",r.Info="info",r.Debug="debug",r))(t||{});class s{constructor(e){this.stream=e.stream}processLog(e){const i=new Date().toISOString(),o=u[e.level],a=this.stringifyContext(e.context),d=e.origin.length?`[${e.origin.join(":")}] `:"",c=`${i} ${o} ${d}`,f=this.multilineIndent(c.length,e.message),l=`${c}${f}${a?" "+a:""}
|
|
2
|
+
`;this.stream.write(l)}stringifyContext(e){return Object.entries(e).map(([i,o])=>`${i}=${JSON.stringify(o)}`).join(" ")}multilineIndent(e,i){return i.replace(/(\r?\n)/g,o=>o+" ".repeat(e))}}const u={[t.Debug]:"DEBUG",[t.Info]:"INFO ",[t.Warn]:"WARN ",[t.Error]:"ERROR"};n.StreamBackend=s,n.default=s,Object.defineProperties(n,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}})});
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@holz/stream-backend",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Print logs to stdout or a file.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/holz-stream-backend.umd.cjs",
|
|
7
|
+
"module": "./dist/holz-stream-backend.js",
|
|
8
|
+
"types": "./src/index.ts",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "https://github.com/PsychoLlama/holz",
|
|
12
|
+
"directory": "packages/holz-stream-backend"
|
|
13
|
+
},
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"require": "./dist/holz-stream-backend.umd.cjs",
|
|
17
|
+
"import": "./dist/holz-stream-backend.js"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public"
|
|
22
|
+
},
|
|
23
|
+
"author": "Jesse Gibson",
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"sideEffects": false,
|
|
26
|
+
"files": [
|
|
27
|
+
"dist",
|
|
28
|
+
"src"
|
|
29
|
+
],
|
|
30
|
+
"keywords": [
|
|
31
|
+
"holz-backend",
|
|
32
|
+
"stream",
|
|
33
|
+
"file",
|
|
34
|
+
"stdout"
|
|
35
|
+
],
|
|
36
|
+
"scripts": {
|
|
37
|
+
"prepare": "vite build",
|
|
38
|
+
"test:unit": "vitest --color --passWithNoTests",
|
|
39
|
+
"test:types": "tsc"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@holz/core": "0.1.0",
|
|
43
|
+
"@types/node": "^18.14.0",
|
|
44
|
+
"@vitest/coverage-c8": "0.28.5",
|
|
45
|
+
"typescript": "4.9.5",
|
|
46
|
+
"vite": "^4.0.0",
|
|
47
|
+
"vitest": "^0.28.5"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { Writable } from 'node:stream';
|
|
2
|
+
import { createLogger } from '@holz/core';
|
|
3
|
+
import StreamBackend from '../stream-backend';
|
|
4
|
+
|
|
5
|
+
const CURRENT_TIME = new Date('2020-06-15T12:00:00.000Z');
|
|
6
|
+
|
|
7
|
+
describe('Stream backend', () => {
|
|
8
|
+
function 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 = new StreamBackend({ 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()).toMatchInlineSnapshot(`
|
|
44
|
+
"2020-06-15T12:00:00.000Z DEBUG shout
|
|
45
|
+
2020-06-15T12:00:00.000Z INFO normal
|
|
46
|
+
2020-06-15T12:00:00.000Z WARN hmmmm
|
|
47
|
+
2020-06-15T12:00:00.000Z ERROR oh no
|
|
48
|
+
"
|
|
49
|
+
`);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('includes the log namespace', () => {
|
|
53
|
+
const { stream, getOutput } = createStream();
|
|
54
|
+
const backend = new StreamBackend({ stream });
|
|
55
|
+
const logger = createLogger(backend)
|
|
56
|
+
.namespace('my-lib')
|
|
57
|
+
.namespace('MyClass');
|
|
58
|
+
|
|
59
|
+
logger.debug('initialized');
|
|
60
|
+
|
|
61
|
+
expect(getOutput()).toContain('[my-lib:MyClass]');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('does not print the log namespace if it is empty', () => {
|
|
65
|
+
const { stream, getOutput } = createStream();
|
|
66
|
+
const backend = new StreamBackend({ stream });
|
|
67
|
+
const logger = createLogger(backend);
|
|
68
|
+
|
|
69
|
+
logger.debug('orphan log');
|
|
70
|
+
|
|
71
|
+
expect(getOutput()).not.toContain('[]');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('includes the log context', () => {
|
|
75
|
+
const { stream, getOutput } = createStream();
|
|
76
|
+
const backend = new StreamBackend({ stream });
|
|
77
|
+
const logger = createLogger(backend);
|
|
78
|
+
|
|
79
|
+
logger.info('creating session', { sessionId: 3109, enabled: true });
|
|
80
|
+
|
|
81
|
+
expect(getOutput()).toContain('sessionId=3109');
|
|
82
|
+
expect(getOutput()).toContain('enabled=true');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('includes the timestamp for each log', () => {
|
|
86
|
+
const { stream, getOutput } = createStream();
|
|
87
|
+
const backend = new StreamBackend({ stream });
|
|
88
|
+
const logger = createLogger(backend);
|
|
89
|
+
|
|
90
|
+
logger.info('traveling through time');
|
|
91
|
+
|
|
92
|
+
expect(getOutput()).toContain(CURRENT_TIME.toISOString());
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('wraps strings in log context with quotes', () => {
|
|
96
|
+
const { stream, getOutput } = createStream();
|
|
97
|
+
const backend = new StreamBackend({ stream });
|
|
98
|
+
const logger = createLogger(backend);
|
|
99
|
+
|
|
100
|
+
logger.info('creating session', { code: 'ENOBACON' });
|
|
101
|
+
|
|
102
|
+
expect(getOutput()).toContain('code="ENOBACON"');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('joins arrays in log context', () => {
|
|
106
|
+
const { stream, getOutput } = createStream();
|
|
107
|
+
const backend = new StreamBackend({ stream });
|
|
108
|
+
const logger = createLogger(backend);
|
|
109
|
+
|
|
110
|
+
logger.info('adding tags', { tags: ['important', 'urgent'] });
|
|
111
|
+
|
|
112
|
+
expect(getOutput()).toContain('tags=["important","urgent"]');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('indents multi-line log statements', () => {
|
|
116
|
+
const { stream, getOutput } = createStream();
|
|
117
|
+
const backend = new StreamBackend({ stream });
|
|
118
|
+
const logger = createLogger(backend);
|
|
119
|
+
|
|
120
|
+
logger.info('multi-line log\r\nwith a second line\nand a third line');
|
|
121
|
+
|
|
122
|
+
expect(getOutput()).toMatchInlineSnapshot(`
|
|
123
|
+
"2020-06-15T12:00:00.000Z INFO multi-line log
|
|
124
|
+
with a second line
|
|
125
|
+
and a third line
|
|
126
|
+
"
|
|
127
|
+
`);
|
|
128
|
+
});
|
|
129
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default, default as StreamBackend } from './stream-backend';
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { Writable } from 'node:stream';
|
|
2
|
+
import { LogLevel } from '@holz/core';
|
|
3
|
+
import type { Log, LogContext, LogProcessor } from '@holz/core';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Prints logs to a writable stream in plaintext. Optimized for log files.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* new StreamBackend({
|
|
10
|
+
* stream: fs.createWriteStream('my-app.log', { flags: 'a' }),
|
|
11
|
+
* })
|
|
12
|
+
*/
|
|
13
|
+
export default class StreamBackend implements LogProcessor {
|
|
14
|
+
private stream: Writable;
|
|
15
|
+
|
|
16
|
+
constructor(options: Config) {
|
|
17
|
+
this.stream = options.stream;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
processLog(log: Log) {
|
|
21
|
+
const currentTime = new Date().toISOString();
|
|
22
|
+
const level = LOG_LEVELS[log.level];
|
|
23
|
+
const context = this.stringifyContext(log.context);
|
|
24
|
+
const namespace = log.origin.length ? `[${log.origin.join(':')}] ` : '';
|
|
25
|
+
|
|
26
|
+
const header = `${currentTime} ${level} ${namespace}`;
|
|
27
|
+
const message = this.multilineIndent(header.length, log.message);
|
|
28
|
+
const output = `${header}${message}${context ? ' ' + context : ''}\n`;
|
|
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
|
+
this.stream.write(output);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// { id: 123, type: 'article' } -> 'id=123 type="article"'
|
|
40
|
+
private stringifyContext(context: LogContext) {
|
|
41
|
+
return Object.entries(context)
|
|
42
|
+
.map(([key, value]) => `${key}=${JSON.stringify(value)}`)
|
|
43
|
+
.join(' ');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Some messages will ruin your output without proper indentation. Stack
|
|
47
|
+
// traces are a good example of this.
|
|
48
|
+
//
|
|
49
|
+
// Supports Unix + DOS line endings.
|
|
50
|
+
private multilineIndent(offset: number, message: string) {
|
|
51
|
+
return message.replace(
|
|
52
|
+
/(\r?\n)/g,
|
|
53
|
+
(newline) => newline + ' '.repeat(offset)
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const LOG_LEVELS: Record<LogLevel, string> = {
|
|
59
|
+
[LogLevel.Debug]: 'DEBUG',
|
|
60
|
+
[LogLevel.Info]: 'INFO ',
|
|
61
|
+
[LogLevel.Warn]: 'WARN ',
|
|
62
|
+
[LogLevel.Error]: 'ERROR',
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
interface Config {
|
|
66
|
+
/** Where to print logs. Normally `process.stderr`. */
|
|
67
|
+
stream: Writable;
|
|
68
|
+
}
|