@holz/stream-backend 0.4.0 → 0.6.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/README.md ADDED
@@ -0,0 +1,46 @@
1
+ # `@holz/stream-backend`
2
+
3
+ A Holz plugin that streams logs in plaintext. It is optimized for log files.
4
+
5
+ ## Usage
6
+
7
+ To use `@holz/stream-backend`, you first need to import it and create a backend with `createStreamBackend()`:
8
+
9
+ ```typescript
10
+ import { createStreamBackend } from '@holz/stream-backend';
11
+ import { createLogger } from '@holz/core';
12
+ import * as fs from 'fs';
13
+
14
+ createLogger(
15
+ createStreamBackend({
16
+ stream: fs.createWriteStream('server.log', { flags: 'a' }),
17
+ })
18
+ );
19
+ ```
20
+
21
+ The `stream` option specifies the NodeJS writable stream that the logs should be written to. You can use any writable stream, such as a file or `process.stderr`.
22
+
23
+ > **Note**
24
+ > If your system supports JSON, consider [`@holz/json-backend`](https://github.com/PsychoLlama/holz/tree/main/packages/holz-json-backend) as a structured alternative.
25
+
26
+ ## Example Output
27
+
28
+ When you write logs using `@holz/stream-backend`, they are formatted as plaintext and written to the stream. Each log entry is separated by a line break.
29
+
30
+ ```
31
+ 2023-03-04T21:58:11.620Z INFO [app:devices] Requesting media devices
32
+ 2023-03-04T21:58:14.288Z INFO [app:devices] Received media stream kind=["audio"]
33
+ 2023-03-04T21:58:14.288Z WARN [app:devices] User does not have a camera
34
+ 2023-03-04T21:58:14.288Z INFO [app:rtc] Creating WebRTC session signalingMode="polite"
35
+ 2023-03-04T21:58:14.288Z INFO [app:signaling] Opening signaling channel
36
+ 2023-03-04T21:58:14.346Z DEBUG [app:signaling] Signaling channel is open
37
+ 2023-03-04T21:58:14.347Z INFO [app:rtc] Sending session description type="offer"
38
+ 2023-03-04T21:58:14.491Z INFO [app:rtc] Receiving session description type="answer"
39
+ 2023-03-04T21:58:14.494Z DEBUG [app:rtc] Sending ICE candidate type="UDP" ip="10.0.0.10" port="33150"
40
+ 2023-03-04T21:58:14.494Z INFO [app:rtc] Attaching local media tracks kind=["audio"]
41
+ 2023-03-04T21:58:14.619Z DEBUG [app:rtc] Receiving ICE candidate type="UDP" ip="10.0.0.11" port="36877"
42
+ 2023-03-04T21:58:14.630Z INFO [app:rtc] Testing remote candidates...
43
+ 2023-03-04T21:58:14.684Z INFO [app:rtc] Connection successful.
44
+ ```
45
+
46
+ Note that the logs include a timestamp, log level, namespace, message, and any additional context that was provided.
@@ -1 +1 @@
1
- "use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const u=require("node:os"),r=require("@holz/core");class i{constructor(e){this.stream=e.stream}processLog(e){const t=new Date().toISOString(),n=g[e.level],s=this.stringifyContext(e.context),c=e.origin.length?`[${e.origin.join(":")}] `:"",o=`${t} ${n} ${c}`,a=this.multilineIndent(o.length,e.message),l=`${o}${a}${s?" "+s:""}${u.EOL}`;this.stream.write(l)}stringifyContext(e){return Object.entries(e).map(([t,n])=>`${t}=${JSON.stringify(n)}`).join(" ")}multilineIndent(e,t){return t.replace(/(\r?\n)/g,n=>n+" ".repeat(e))}}const g={[r.LogLevel.Debug]:"DEBUG",[r.LogLevel.Info]:"INFO ",[r.LogLevel.Warn]:"WARN ",[r.LogLevel.Error]:"ERROR"};exports.StreamBackend=i;exports.default=i;
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const g=require("node:os"),r=require("@holz/core");function l({stream:t}){return e=>{const n=new Date().toISOString(),i=m[e.level],o=$(e.context),s=e.origin.length?`[${e.origin.join(":")}] `:"",c=`${n} ${i} ${s}`,a=L(c.length,e.message),u=`${c}${a}${o?" "+o:""}`;t.write(`${u}${g.EOL}`)}}function L(t,e){return e.replace(/(\r?\n)/g,n=>n+" ".repeat(t))}function $(t){return Object.entries(t).map(([e,n])=>`${e}=${JSON.stringify(n)}`).join(" ")}const m={[r.LogLevel.Debug]:"DEBUG",[r.LogLevel.Info]:"INFO ",[r.LogLevel.Warn]:"WARN ",[r.LogLevel.Error]:"ERROR"};exports.createStreamBackend=l;
@@ -1,27 +1,16 @@
1
- import { EOL as m } from "node:os";
1
+ import { EOL as u } from "node:os";
2
2
  import { LogLevel as r } from "@holz/core";
3
- class p {
4
- constructor(t) {
5
- this.stream = t.stream;
6
- }
7
- processLog(t) {
8
- const e = new Date().toISOString(), n = g[t.level], s = this.stringifyContext(t.context), o = t.origin.length ? `[${t.origin.join(":")}] ` : "", i = `${e} ${n} ${o}`, c = this.multilineIndent(i.length, t.message), a = `${i}${c}${s ? " " + s : ""}${m}`;
9
- this.stream.write(a);
10
- }
11
- // { id: 123, type: 'article' } -> 'id=123 type="article"'
12
- stringifyContext(t) {
13
- return Object.entries(t).map(([e, n]) => `${e}=${JSON.stringify(n)}`).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(t, e) {
20
- return e.replace(
21
- /(\r?\n)/g,
22
- (n) => n + " ".repeat(t)
23
- );
24
- }
3
+ function l({ stream: t }) {
4
+ return (e) => {
5
+ const n = new Date().toISOString(), c = g[e.level], o = f(e.context), s = e.origin.length ? `[${e.origin.join(":")}] ` : "", i = `${n} ${c} ${s}`, a = $(i.length, e.message), m = `${i}${a}${o ? " " + o : ""}`;
6
+ t.write(`${m}${u}`);
7
+ };
8
+ }
9
+ function $(t, e) {
10
+ return e.replace(/(\r?\n)/g, (n) => n + " ".repeat(t));
11
+ }
12
+ function f(t) {
13
+ return Object.entries(t).map(([e, n]) => `${e}=${JSON.stringify(n)}`).join(" ");
25
14
  }
26
15
  const g = {
27
16
  [r.Debug]: "DEBUG",
@@ -30,6 +19,5 @@ const g = {
30
19
  [r.Error]: "ERROR"
31
20
  };
32
21
  export {
33
- p as StreamBackend,
34
- p as default
22
+ l as createStreamBackend
35
23
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@holz/stream-backend",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "Print logs to stdout or a file.",
5
5
  "type": "module",
6
6
  "main": "./dist/holz-stream-backend.cjs",
@@ -39,13 +39,13 @@
39
39
  "test:types": "tsc"
40
40
  },
41
41
  "peerDependencies": {
42
- "@holz/core": "0.2.0"
42
+ "@holz/core": "^0.6.0"
43
43
  },
44
44
  "devDependencies": {
45
- "@holz/core": "0.2.0",
45
+ "@holz/core": "^0.6.0",
46
46
  "@types/node": "^18.14.0",
47
- "@vitest/coverage-c8": "0.28.5",
48
- "typescript": "4.9.5",
47
+ "@vitest/coverage-c8": "^0.28.5",
48
+ "typescript": "^4.9.5",
49
49
  "vite": "^4.0.0",
50
50
  "vitest": "^0.28.5"
51
51
  }
@@ -1,6 +1,6 @@
1
1
  import { Writable } from 'node:stream';
2
2
  import { createLogger } from '@holz/core';
3
- import StreamBackend from '../stream-backend';
3
+ import { createStreamBackend } from '../stream-backend';
4
4
 
5
5
  const CURRENT_TIME = new Date('2020-06-15T12:00:00.000Z');
6
6
 
@@ -32,7 +32,7 @@ describe('Stream backend', () => {
32
32
 
33
33
  it('prints the logs to the writable stream', () => {
34
34
  const { stream, getOutput } = createStream();
35
- const backend = new StreamBackend({ stream });
35
+ const backend = createStreamBackend({ stream });
36
36
 
37
37
  const logger = createLogger(backend);
38
38
  logger.debug('shout');
@@ -51,7 +51,7 @@ describe('Stream backend', () => {
51
51
 
52
52
  it('includes the log namespace', () => {
53
53
  const { stream, getOutput } = createStream();
54
- const backend = new StreamBackend({ stream });
54
+ const backend = createStreamBackend({ stream });
55
55
  const logger = createLogger(backend)
56
56
  .namespace('my-lib')
57
57
  .namespace('MyClass');
@@ -63,7 +63,7 @@ describe('Stream backend', () => {
63
63
 
64
64
  it('does not print the log namespace if it is empty', () => {
65
65
  const { stream, getOutput } = createStream();
66
- const backend = new StreamBackend({ stream });
66
+ const backend = createStreamBackend({ stream });
67
67
  const logger = createLogger(backend);
68
68
 
69
69
  logger.debug('orphan log');
@@ -73,7 +73,7 @@ describe('Stream backend', () => {
73
73
 
74
74
  it('includes the log context', () => {
75
75
  const { stream, getOutput } = createStream();
76
- const backend = new StreamBackend({ stream });
76
+ const backend = createStreamBackend({ stream });
77
77
  const logger = createLogger(backend);
78
78
 
79
79
  logger.info('creating session', { sessionId: 3109, enabled: true });
@@ -84,7 +84,7 @@ describe('Stream backend', () => {
84
84
 
85
85
  it('includes the timestamp for each log', () => {
86
86
  const { stream, getOutput } = createStream();
87
- const backend = new StreamBackend({ stream });
87
+ const backend = createStreamBackend({ stream });
88
88
  const logger = createLogger(backend);
89
89
 
90
90
  logger.info('traveling through time');
@@ -94,7 +94,7 @@ describe('Stream backend', () => {
94
94
 
95
95
  it('wraps strings in log context with quotes', () => {
96
96
  const { stream, getOutput } = createStream();
97
- const backend = new StreamBackend({ stream });
97
+ const backend = createStreamBackend({ stream });
98
98
  const logger = createLogger(backend);
99
99
 
100
100
  logger.info('creating session', { code: 'ENOBACON' });
@@ -104,7 +104,7 @@ describe('Stream backend', () => {
104
104
 
105
105
  it('joins arrays in log context', () => {
106
106
  const { stream, getOutput } = createStream();
107
- const backend = new StreamBackend({ stream });
107
+ const backend = createStreamBackend({ stream });
108
108
  const logger = createLogger(backend);
109
109
 
110
110
  logger.info('adding tags', { tags: ['important', 'urgent'] });
@@ -114,7 +114,7 @@ describe('Stream backend', () => {
114
114
 
115
115
  it('indents multi-line log statements', () => {
116
116
  const { stream, getOutput } = createStream();
117
- const backend = new StreamBackend({ stream });
117
+ const backend = createStreamBackend({ stream });
118
118
  const logger = createLogger(backend);
119
119
 
120
120
  logger.info('multi-line log\r\nwith a second line\nand a third line');
package/src/index.ts CHANGED
@@ -1 +1 @@
1
- export { default, default as StreamBackend } from './stream-backend';
1
+ export { createStreamBackend } from './stream-backend';
@@ -7,26 +7,20 @@ import type { Log, LogContext, LogProcessor } from '@holz/core';
7
7
  * Prints logs to a writable stream in plaintext. Optimized for log files.
8
8
  *
9
9
  * @example
10
- * new StreamBackend({
10
+ * createStreamBackend({
11
11
  * stream: fs.createWriteStream('my-app.log', { flags: 'a' }),
12
12
  * })
13
13
  */
14
- export default class StreamBackend implements LogProcessor {
15
- private stream: Writable;
16
-
17
- constructor(options: Config) {
18
- this.stream = options.stream;
19
- }
20
-
21
- processLog(log: Log) {
14
+ export function createStreamBackend({ stream }: Config): LogProcessor {
15
+ return (log: Log) => {
22
16
  const currentTime = new Date().toISOString();
23
17
  const level = LOG_LEVELS[log.level];
24
- const context = this.stringifyContext(log.context);
18
+ const context = stringifyContext(log.context);
25
19
  const namespace = log.origin.length ? `[${log.origin.join(':')}] ` : '';
26
20
 
27
21
  const header = `${currentTime} ${level} ${namespace}`;
28
- const message = this.multilineIndent(header.length, log.message);
29
- const output = `${header}${message}${context ? ' ' + context : ''}${EOL}`;
22
+ const message = multilineIndent(header.length, log.message);
23
+ const output = `${header}${message}${context ? ' ' + context : ''}`;
30
24
 
31
25
  // NOTE: If the stream applies backpressure, we will lose logs. I believe
32
26
  // this is the right tradeoff. We can't prevent the app from generating
@@ -34,26 +28,25 @@ export default class StreamBackend implements LogProcessor {
34
28
  // crashing the process.
35
29
  //
36
30
  // It is unlikely that a file or tty will apply backpressure in practice.
37
- this.stream.write(output);
38
- }
31
+ stream.write(`${output}${EOL}`);
32
+ };
33
+ }
39
34
 
40
- // { id: 123, type: 'article' } -> 'id=123 type="article"'
41
- private stringifyContext(context: LogContext) {
42
- return Object.entries(context)
43
- .map(([key, value]) => `${key}=${JSON.stringify(value)}`)
44
- .join(' ');
45
- }
35
+ /**
36
+ * Some messages will ruin your output without proper indentation. Stack
37
+ * traces are a good example of this.
38
+ *
39
+ * Supports Unix + DOS line endings.
40
+ */
41
+ function multilineIndent(offset: number, message: string) {
42
+ return message.replace(/(\r?\n)/g, (newline) => newline + ' '.repeat(offset));
43
+ }
46
44
 
47
- // Some messages will ruin your output without proper indentation. Stack
48
- // traces are a good example of this.
49
- //
50
- // Supports Unix + DOS line endings.
51
- private multilineIndent(offset: number, message: string) {
52
- return message.replace(
53
- /(\r?\n)/g,
54
- (newline) => newline + ' '.repeat(offset)
55
- );
56
- }
45
+ // { id: 123, type: 'article' } -> 'id=123 type="article"'
46
+ function stringifyContext(context: LogContext) {
47
+ return Object.entries(context)
48
+ .map(([key, value]) => `${key}=${JSON.stringify(value)}`)
49
+ .join(' ');
57
50
  }
58
51
 
59
52
  const LOG_LEVELS: Record<LogLevel, string> = {