logtunnel 0.3.0 → 1.0.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.
Files changed (42) hide show
  1. package/README.md +61 -0
  2. package/package.json +3 -3
  3. package/src/definition.js +3 -1
  4. package/src/log-source.js +26 -0
  5. package/src/main.js +5 -17
  6. package/src/pipeline.js +51 -8
  7. package/src/transformers/filters/field.js +18 -0
  8. package/src/transformers/filters/find.js +11 -0
  9. package/src/transformers/filters/ignore.js +11 -0
  10. package/src/transformers/outputs/bigodon.js +17 -0
  11. package/src/transformers/outputs/factory.js +21 -0
  12. package/src/transformers/outputs/inspect.js +22 -0
  13. package/src/transformers/outputs/json.js +11 -0
  14. package/src/transformers/outputs/logfmt.js +13 -0
  15. package/src/transformers/outputs/original.js +7 -0
  16. package/src/transformers/outputs/table.js +67 -0
  17. package/src/transformers/parsers/factory.js +16 -0
  18. package/src/transformers/parsers/json.js +16 -0
  19. package/src/transformers/parsers/logfmt.js +13 -0
  20. package/src/transformers/parsers/regex.js +11 -0
  21. package/src/transformers/parsers/table.js +30 -0
  22. package/test/filters.spec.js +86 -0
  23. package/test/log-source.spec.js +71 -0
  24. package/test/output.spec.js +96 -0
  25. package/test/parse.spec.js +72 -0
  26. package/test/pipeline.spec.js +199 -0
  27. package/test/utils.js +71 -0
  28. package/src/stdin.js +0 -17
  29. package/src/transformers/field.js +0 -13
  30. package/src/transformers/filter.js +0 -4
  31. package/src/transformers/ignore.js +0 -4
  32. package/src/transformers/output-json.js +0 -7
  33. package/src/transformers/output-logfmt.js +0 -9
  34. package/src/transformers/output-mustache.js +0 -10
  35. package/src/transformers/output-original.js +0 -6
  36. package/src/transformers/output-unset.js +0 -12
  37. package/src/transformers/output.js +0 -15
  38. package/src/transformers/parse-json.js +0 -7
  39. package/src/transformers/parse-logfmt.js +0 -9
  40. package/src/transformers/parse-regex.js +0 -11
  41. package/src/transformers/parse-table.js +0 -24
  42. package/src/transformers/parse.js +0 -14
@@ -0,0 +1,71 @@
1
+ const Lab = require('@hapi/lab');
2
+ const Code = require('@hapi/code');
3
+ const Hoek = require('@hapi/hoek');
4
+ const { logSource } = require('../src/log-source');
5
+ const { pod, slowPod } = require('./utils');
6
+
7
+ const { describe, it } = exports.lab = Lab.script();
8
+ const { expect } = Code;
9
+
10
+ describe('log-source', () => {
11
+
12
+ it('should split into lines with LF', async () => {
13
+ const source = pod('hello\nworld\n');
14
+ const output = logSource(source);
15
+ const lines = [];
16
+
17
+ output.on('log-line', l => lines.push(l));
18
+ await output.once('end');
19
+
20
+ expect(lines).to.equal(['hello', 'world']);
21
+ });
22
+
23
+ it('should split into lines with CRLF', async () => {
24
+ const source = pod('hello\r\nworld\r\n');
25
+ const output = logSource(source);
26
+ const lines = [];
27
+
28
+ output.on('log-line', l => lines.push(l));
29
+ await output.once('end');
30
+
31
+ expect(lines).to.equal(['hello', 'world']);
32
+ });
33
+
34
+ it('should join incomplete lines', async () => {
35
+ const source = pod('hello\nwor', 'ld\n:)\n');
36
+ const output = logSource(source);
37
+ const lines = [];
38
+
39
+ output.on('log-line', l => lines.push(l));
40
+ await output.once('end');
41
+
42
+ expect(lines).to.equal(['hello', 'world', ':)']);
43
+ });
44
+
45
+ it('should flush incomplete lines on end', async () => {
46
+ const source = pod('hello\nworld');
47
+ const output = logSource(source);
48
+ const lines = [];
49
+
50
+ output.on('log-line', l => lines.push(l));
51
+ await output.once('end');
52
+
53
+ expect(lines).to.equal(['hello', 'world']);
54
+ });
55
+
56
+ it('should not wait for end or line completion to send lines', async () => {
57
+ const source = slowPod('hello\nwo', 'rld\n:)');
58
+ const output = logSource(source);
59
+ const lines = [];
60
+ output.on('log-line', l => lines.push(l));
61
+
62
+ await Hoek.wait(50);
63
+ expect(lines).to.equal(['hello']);
64
+
65
+ await Hoek.wait(100);
66
+ expect(lines).to.equal(['hello', 'world']);
67
+
68
+ await Hoek.wait(100);
69
+ expect(lines).to.equal(['hello', 'world', ':)']);
70
+ });
71
+ });
@@ -0,0 +1,96 @@
1
+ const Lab = require('@hapi/lab');
2
+ const Code = require('@hapi/code');
3
+
4
+ const { outputFactory } = require('../src/transformers/outputs/factory');
5
+
6
+ const { describe, it } = exports.lab = Lab.script();
7
+ const { expect } = Code;
8
+
9
+ describe('outputs', () => {
10
+ it('should should not modify unparsed when unset', () => {
11
+ const format = outputFactory();
12
+ expect(format.run('foo')).to.be.true();
13
+ });
14
+
15
+ it('should inspect parsed lines single-line when unset', () => {
16
+ const format = outputFactory();
17
+ const line = format.run({ foo: 'bar' });
18
+ expect(line).to.be.a.string();
19
+ expect(line).to.match(/\{.*foo.*:.*bar.*\}/);
20
+ expect(line.split('\n').length).to.equal(1);
21
+ });
22
+
23
+ it('should inspect parsed lines multi-line', () => {
24
+ const format = outputFactory('inspect');
25
+ const line = format.run({ foo: 'bar' });
26
+ expect(line).to.be.a.string();
27
+ expect(line.split('\n').join(' ')).to.match(/\{.*foo.*:.*bar.*\}/);
28
+ expect(line.split('\n').length).to.equal(3);
29
+ });
30
+
31
+ it('should format as json', () => {
32
+ const format = outputFactory('json');
33
+ const line = format.run({ foo: 'bar' });
34
+ expect(line).to.equal('{"foo":"bar"}');
35
+ });
36
+
37
+ it('should format as logfmt', () => {
38
+ const format = outputFactory('logfmt');
39
+ const line = format.run({ foo: 'bar' });
40
+ expect(line).to.equal('foo=bar');
41
+ });
42
+
43
+ it('should format as original', () => {
44
+ const format = outputFactory('original');
45
+ const line = format.run({ foo: 'bar' }, 'fubá');
46
+ expect(line).to.equal('fubá');
47
+ });
48
+
49
+ it('should format as table', () => {
50
+ const format = outputFactory('table');
51
+ format.run({ foo: 'bar', baz: 'qux' });
52
+ format.run({ foo: 'longer' });
53
+ const lines = format.flush();
54
+
55
+ expect(lines).to.equal([
56
+ 'foo baz',
57
+ 'bar qux',
58
+ 'longer',
59
+ ]);
60
+ });
61
+
62
+ it('should return empty table output when no rows were provided', () => {
63
+ const format = outputFactory('table');
64
+ expect(format.flush()).to.equal([]);
65
+ });
66
+
67
+ it('should stringify object and null table cells', () => {
68
+ const format = outputFactory('table');
69
+ format.run({ foo: null, bar: { baz: 1 } });
70
+ const lines = format.flush();
71
+
72
+ expect(lines).to.equal([
73
+ 'foo bar',
74
+ ' {"baz":1}',
75
+ ]);
76
+ });
77
+
78
+ it('should format with bigodon', async () => {
79
+ const format = outputFactory('[{{foo}}]');
80
+ const line = await format.run({ foo: 'bar' });
81
+ expect(line).to.equal('[bar]');
82
+ });
83
+
84
+ it('should allow bigodon helpers', async () => {
85
+ const format = outputFactory('[{{upper foo }}]');
86
+ const line = await format.run({ foo: 'bar' });
87
+ expect(line).to.equal('[BAR]');
88
+ });
89
+
90
+ it('should refuse to format unparsed lines', async () => {
91
+ expect(() => outputFactory('json').run('foo')).to.throw();
92
+ expect(() => outputFactory('logfmt').run('foo')).to.throw();
93
+ expect(() => outputFactory('table').run('foo')).to.throw();
94
+ await expect(outputFactory('{{foo}}').run('foo')).to.reject();
95
+ });
96
+ });
@@ -0,0 +1,72 @@
1
+ const Lab = require('@hapi/lab');
2
+ const Code = require('@hapi/code');
3
+
4
+ const { parseFactory } = require('../src/transformers/parsers/factory');
5
+
6
+ const { describe, it } = exports.lab = Lab.script();
7
+ const { expect } = Code;
8
+
9
+ describe('parsers', () => {
10
+ it('should return null when no format is specified', () => {
11
+ expect(parseFactory(void 0)).to.be.null();
12
+ });
13
+
14
+ it('should parse json', () => {
15
+ const parser = parseFactory('json');
16
+ expect(parser.run('{"foo": "bar"}')).to.equal({ foo: 'bar' });
17
+ });
18
+
19
+ it('should ignore invalid json lines when parsing json', () => {
20
+ const parser = parseFactory('json');
21
+ expect(parser.run('Oy')).to.be.false();
22
+ expect(parser.run('"Oy"')).to.be.false();
23
+ });
24
+
25
+ it('should parse logfmt', () => {
26
+ const parser = parseFactory('logfmt');
27
+ expect(parser.run('foo=bar')).to.equal({ foo: 'bar' });
28
+ });
29
+
30
+ it('should ignore invalid logfmt lines when parsing logfmt', () => {
31
+ const parser = parseFactory('logfmt');
32
+ expect(parser.run(void 0)).to.be.false();
33
+ });
34
+
35
+ it('should parse regex', () => {
36
+ const parser = parseFactory('\\[(?<severity>\\S+)\\s*(?<delay>\\d+)ms\\] (?<message>.*)');
37
+ expect(parser.run('[info 20ms] lorem ipsum dolor')).to.equal({
38
+ severity: 'info',
39
+ delay: '20',
40
+ message: 'lorem ipsum dolor',
41
+ });
42
+
43
+ expect(parser.run('foo')).to.be.false();
44
+ });
45
+
46
+ describe('tables', () => {
47
+ const tableRows = [
48
+ 'NAME TYPE',
49
+ 'foo bar',
50
+ 'baz qux',
51
+ ];
52
+
53
+ it('should ignore headers', () => {
54
+ const pipeline = { firstLine: null };
55
+ const parser = parseFactory('table');
56
+ const line = tableRows[0];
57
+
58
+ expect(parser.run(line, line, pipeline)).to.be.false();
59
+ });
60
+
61
+ it('should parse subsequent rows', () => {
62
+ const pipeline = { firstLine: tableRows[0] };
63
+ const parser = parseFactory('table');
64
+ let line;
65
+
66
+ line = tableRows[1];
67
+ expect(parser.run(line, line, pipeline)).to.equal({ NAME: 'foo', TYPE: 'bar' });
68
+ line = tableRows[2];
69
+ expect(parser.run(line, line, pipeline)).to.equal({ NAME: 'baz', TYPE: 'qux' });
70
+ });
71
+ });
72
+ });
@@ -0,0 +1,199 @@
1
+ const Lab = require('@hapi/lab');
2
+ const Code = require('@hapi/code');
3
+ const { runPipeline, _, f, i, F, p, o, H } = require('./utils');
4
+
5
+ const { describe, it } = exports.lab = Lab.script();
6
+ const { expect } = Code;
7
+
8
+ describe('pipeline', () => {
9
+ it('should remove rejected lines and keep accepted ones', async () => {
10
+ const args = [
11
+ f('potato')
12
+ ];
13
+ const actual = await runPipeline([
14
+ 'yada yada yada',
15
+ 'lorem ipsum POTATO dolor',
16
+ 'ablueblue POTATO',
17
+ 'foo bar',
18
+ ], args);
19
+ const expected = [
20
+ 'lorem ipsum POTATO dolor',
21
+ 'ablueblue POTATO',
22
+ ];
23
+
24
+ expect(actual).to.equal(expected);
25
+ });
26
+
27
+ it('should apply multiple filters', async () => {
28
+ const args = [
29
+ f('foo'),
30
+ f('bar'),
31
+ i('nope'),
32
+ ];
33
+ const actual = await runPipeline([
34
+ '1. foo',
35
+ '2. foo bar nope',
36
+ '3. foo bar',
37
+ '4. bar',
38
+ ], args);
39
+ const expected = [
40
+ '3. foo bar',
41
+ ];
42
+
43
+ expect(actual).to.equal(expected);
44
+ });
45
+
46
+ it('should apply default argument as filter', async () => {
47
+ const args = [
48
+ _('foo'),
49
+ i('bar'),
50
+ ];
51
+ const actual = await runPipeline([
52
+ '1. foo',
53
+ '2. foo bar',
54
+ '3. baz',
55
+ ], args);
56
+ const expected = [
57
+ '1. foo',
58
+ ];
59
+
60
+ expect(actual).to.equal(expected);
61
+ });
62
+
63
+ it('should parse and format correctly', async () => {
64
+ const args = [
65
+ p('logfmt'),
66
+ o('json'),
67
+ ];
68
+ const actual = await runPipeline([
69
+ 'foo=bar baz=qux',
70
+ ], args);
71
+ const expected = [
72
+ '{"foo":"bar","baz":"qux"}',
73
+ ];
74
+
75
+ expect(actual).to.equal(expected);
76
+ });
77
+
78
+ it('should buffer and format tables when requested', async () => {
79
+ const args = [
80
+ p('logfmt'),
81
+ o('table'),
82
+ ];
83
+ const actual = await runPipeline([
84
+ 'foo=bar baz=qux',
85
+ 'foo=longer',
86
+ ], args);
87
+ const expected = [
88
+ 'foo baz',
89
+ 'bar qux',
90
+ 'longer',
91
+ ];
92
+
93
+ expect(actual).to.equal(expected);
94
+ });
95
+
96
+ it('should filter by the original line even when parsers and outputs are provided', async () => {
97
+ const args = [
98
+ p('logfmt'),
99
+ o('json'),
100
+ i('o=n'),
101
+ ];
102
+ const actual = await runPipeline([
103
+ 'foo=bar baz=qux',
104
+ 'foo=nope baz=qux',
105
+ ], args);
106
+ const expected = [
107
+ '{"foo":"bar","baz":"qux"}',
108
+ ];
109
+
110
+ expect(actual).to.equal(expected);
111
+ });
112
+
113
+ it('should apply field filters after parse', async () => {
114
+ const args = [
115
+ p('logfmt'),
116
+ o('json'),
117
+ F('startsWith foo "b"'),
118
+ ];
119
+ const actual = await runPipeline([
120
+ 'foo=bar baz=qux',
121
+ 'foo=nope baz=qux',
122
+ ], args);
123
+ const expected = [
124
+ '{"foo":"bar","baz":"qux"}',
125
+ ];
126
+
127
+ expect(actual).to.equal(expected);
128
+ });
129
+
130
+ it('should apply multiple field filters', async () => {
131
+ const args = [
132
+ p('logfmt'),
133
+ o('json'),
134
+ F('startsWith foo "b"'),
135
+ F('endsWith foo "r"'),
136
+ ];
137
+ const actual = await runPipeline([
138
+ 'foo=baz baz=qux',
139
+ 'foo=bar baz=qux',
140
+ 'foo=tar baz=qux',
141
+ ], args);
142
+ const expected = [
143
+ '{"foo":"bar","baz":"qux"}',
144
+ ];
145
+
146
+ expect(actual).to.equal(expected);
147
+ });
148
+
149
+ it('should log headers when -H is provided', async () => {
150
+ const args = [
151
+ p('table'),
152
+ o('original'),
153
+ F('eq NAME "foo"'),
154
+ H(),
155
+ ];
156
+ const actual = await runPipeline([
157
+ 'NAME',
158
+ 'foo',
159
+ 'bar',
160
+ ], args);
161
+ const expected = [
162
+ 'NAME',
163
+ 'foo',
164
+ ];
165
+
166
+ expect(actual).to.equal(expected);
167
+ });
168
+
169
+ it('should log headers only once when both -H and -o table is provided', async () => {
170
+ const args = [
171
+ p('table'),
172
+ o('table'),
173
+ F('eq NAME "foo"'),
174
+ H(),
175
+ ];
176
+ const actual = await runPipeline([
177
+ 'NAME',
178
+ 'foo',
179
+ 'bar',
180
+ ], args);
181
+ const expected = [
182
+ 'NAME',
183
+ 'foo',
184
+ ];
185
+
186
+ expect(actual).to.equal(expected);
187
+ });
188
+
189
+ it('should not specify null prototype of regex parser', async () => {
190
+ const args = [
191
+ p('(?<num>\\d*)'),
192
+ ];
193
+ const [actual] = await runPipeline([
194
+ '12',
195
+ ], args);
196
+
197
+ expect(actual).not.to.include('null prototype');
198
+ });
199
+ });
package/test/utils.js ADDED
@@ -0,0 +1,71 @@
1
+ const Podium = require('@hapi/podium');
2
+ const Hoek = require('@hapi/hoek');
3
+ const { LogPipeline } = require('../src/pipeline');
4
+
5
+ function pod(...strs) {
6
+ const emitter = new Podium(['data', 'end']);
7
+
8
+ (async () => {
9
+ await Hoek.wait(0);
10
+ for(const str of strs) {
11
+ emitter.emit('data', str);
12
+ }
13
+ emitter.emit('end');
14
+ })();
15
+
16
+ return emitter;
17
+ }
18
+
19
+ function slowPod(...strs) {
20
+ const emitter = new Podium(['data', 'end']);
21
+
22
+ (async () => {
23
+ await Hoek.wait(0);
24
+ for(const str of strs) {
25
+ emitter.emit('data', str);
26
+ await Hoek.wait(100);
27
+ }
28
+ emitter.emit('end');
29
+ })();
30
+
31
+ return emitter;
32
+ }
33
+
34
+ async function runPipeline(inputLines, args) {
35
+ const lines = [];
36
+
37
+ const stdout = { write: (line) => lines.push(line) };
38
+ const pipeline = new LogPipeline(mergeArgs(args), stdout);
39
+
40
+ inputLines.forEach(line => pipeline.onLogLine(line));
41
+
42
+ // Waiting for nextTick promises to resolve
43
+ await Hoek.wait(0);
44
+
45
+ pipeline.onEnd();
46
+
47
+ // Removing \n from the end of each line
48
+ return lines.map(line => line.slice(0, -1));
49
+ }
50
+
51
+ function mergeArgs(args) {
52
+ let argsObj = {
53
+ filter: [],
54
+ ignore: [],
55
+ field: [],
56
+ };
57
+
58
+ args.forEach(arg => argsObj = Hoek.merge(argsObj, arg));
59
+
60
+ return argsObj;
61
+ }
62
+
63
+ const _ = str => ({ _: str });
64
+ const f = str => ({ filter: [str] });
65
+ const i = str => ({ ignore: [str] });
66
+ const F = str => ({ field: [str] });
67
+ const p = str => ({ parser: str });
68
+ const o = str => ({ output: str });
69
+ const H = () => ({ headers: true });
70
+
71
+ module.exports = { pod, slowPod, runPipeline, _, f, i, F, p, o, H };
package/src/stdin.js DELETED
@@ -1,17 +0,0 @@
1
- const Podium = require("@hapi/podium");
2
-
3
- const stdin = new Podium('log-line');
4
- let incompleteLine = '';
5
-
6
- process.stdin.on('data', data => {
7
- const lines = data.toString()
8
- .split(/[\r\n|\n]/);
9
-
10
- lines[0] = incompleteLine + lines[0];
11
- incompleteLine = lines.pop(); // Either an incomplete line or an empty string due to the last \n
12
-
13
- lines.filter(line => line.length > 0)
14
- .forEach(line => stdin.emit('log-line', line));
15
- });
16
-
17
- module.exports.stdin = stdin;
@@ -1,13 +0,0 @@
1
- const vm = require('vm');
2
-
3
- module.exports = filter => {
4
- return line => {
5
- if(typeof line !== 'object') {
6
- throw new Error("To use a field filter, you need to specify a parser like '-p json' ");
7
- }
8
-
9
- vm.createContext(line);
10
- const result = vm.runInContext(filter, line);
11
- return Boolean(result);
12
- };
13
- };
@@ -1,4 +0,0 @@
1
- module.exports = regexStr => line => {
2
- const regex = new RegExp(regexStr, 'i');
3
- return regex.test(line);
4
- };
@@ -1,4 +0,0 @@
1
- module.exports = regexStr => line => {
2
- const regex = new RegExp(regexStr, 'i');
3
- return !regex.test(line);
4
- };
@@ -1,7 +0,0 @@
1
- module.exports = () => line => {
2
- if(typeof line !== 'object') {
3
- throw new Error("To use an output transformer, you need to specify a parser like '-p json'");
4
- }
5
-
6
- return JSON.stringify(line);
7
- };
@@ -1,9 +0,0 @@
1
- const logfmt = require('logfmt');
2
-
3
- module.exports = () => line => {
4
- if(typeof line !== 'object') {
5
- throw new Error("To use an output transformer, you need to specify a parser like '-p json'");
6
- }
7
-
8
- return logfmt.stringify(line);
9
- };
@@ -1,10 +0,0 @@
1
- const Mustache = require('mustache');
2
- Mustache.escape = text => text;
3
-
4
- module.exports = format => line => {
5
- if(typeof line !== 'object') {
6
- throw new Error("To use an output transformer, you need to specify a parser like '-p json' ");
7
- }
8
-
9
- return Mustache.render(format, line);
10
- };
@@ -1,6 +0,0 @@
1
- const Mustache = require('mustache');
2
- Mustache.escape = text => text;
3
-
4
- module.exports = () => (_line, original) => {
5
- return original;
6
- };
@@ -1,12 +0,0 @@
1
- const util = require('util');
2
-
3
- module.exports = () => line => {
4
- if(typeof line === 'string') {
5
- return true;
6
- }
7
-
8
- return util.inspect({ ...line }, {
9
- colors: true,
10
- depth: null,
11
- });
12
- };
@@ -1,15 +0,0 @@
1
- const json = require('./output-json');
2
- const logfmt = require('./output-logfmt');
3
- const mustache = require('./output-mustache');
4
- const original = require('./output-original');
5
- const unset = require('./output-unset');
6
-
7
- module.exports = format => {
8
- switch(format?.toLowerCase()) {
9
- case void 0: return unset();
10
- case 'json': return json();
11
- case 'logfmt': return logfmt();
12
- case 'original': return original();
13
- default: return mustache(format);
14
- }
15
- };
@@ -1,7 +0,0 @@
1
- module.exports = () => line => {
2
- try {
3
- return JSON.parse(line);
4
- } catch {
5
- return false;
6
- }
7
- };
@@ -1,9 +0,0 @@
1
- const logfmt = require('logfmt');
2
-
3
- module.exports = () => line => {
4
- try {
5
- return logfmt.parse(line);
6
- } catch {
7
- return false;
8
- }
9
- };
@@ -1,11 +0,0 @@
1
- module.exports = regexStr => {
2
- const regex = new RegExp(regexStr, 'i');
3
-
4
- return line => {
5
- try {
6
- return regex.exec(line)?.groups ?? false;
7
- } catch {
8
- return false;
9
- }
10
- };
11
- };
@@ -1,24 +0,0 @@
1
- let headers;
2
-
3
- function splitColumns(line) {
4
- return line.replace(/\s+/g, ' ').split(' ');
5
- }
6
-
7
- module.exports = () => (line, _original, pipeline) => {
8
- if(!pipeline.firstLine) {
9
- // Ignore first line, it's the headers
10
- return false;
11
- }
12
- if(!headers) {
13
- headers = splitColumns(pipeline.firstLine);
14
- }
15
-
16
- const columns = splitColumns(line);
17
- const obj = {};
18
-
19
- headers.forEach((header, i) => {
20
- obj[header] = columns[i];
21
- });
22
-
23
- return obj;
24
- };