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.
- package/README.md +61 -0
- package/package.json +3 -3
- package/src/definition.js +3 -1
- package/src/log-source.js +26 -0
- package/src/main.js +5 -17
- package/src/pipeline.js +51 -8
- package/src/transformers/filters/field.js +18 -0
- package/src/transformers/filters/find.js +11 -0
- package/src/transformers/filters/ignore.js +11 -0
- package/src/transformers/outputs/bigodon.js +17 -0
- package/src/transformers/outputs/factory.js +21 -0
- package/src/transformers/outputs/inspect.js +22 -0
- package/src/transformers/outputs/json.js +11 -0
- package/src/transformers/outputs/logfmt.js +13 -0
- package/src/transformers/outputs/original.js +7 -0
- package/src/transformers/outputs/table.js +67 -0
- package/src/transformers/parsers/factory.js +16 -0
- package/src/transformers/parsers/json.js +16 -0
- package/src/transformers/parsers/logfmt.js +13 -0
- package/src/transformers/parsers/regex.js +11 -0
- package/src/transformers/parsers/table.js +30 -0
- package/test/filters.spec.js +86 -0
- package/test/log-source.spec.js +71 -0
- package/test/output.spec.js +96 -0
- package/test/parse.spec.js +72 -0
- package/test/pipeline.spec.js +199 -0
- package/test/utils.js +71 -0
- package/src/stdin.js +0 -17
- package/src/transformers/field.js +0 -13
- package/src/transformers/filter.js +0 -4
- package/src/transformers/ignore.js +0 -4
- package/src/transformers/output-json.js +0 -7
- package/src/transformers/output-logfmt.js +0 -9
- package/src/transformers/output-mustache.js +0 -10
- package/src/transformers/output-original.js +0 -6
- package/src/transformers/output-unset.js +0 -12
- package/src/transformers/output.js +0 -15
- package/src/transformers/parse-json.js +0 -7
- package/src/transformers/parse-logfmt.js +0 -9
- package/src/transformers/parse-regex.js +0 -11
- package/src/transformers/parse-table.js +0 -24
- package/src/transformers/parse.js +0 -14
package/README.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Logtunnel
|
|
2
|
+
|
|
3
|
+
CLI tool that allows you to format, filter and search your log output
|
|
4
|
+
|
|
5
|
+
## Requirements
|
|
6
|
+
|
|
7
|
+
- [NodeJS >= 12](https://nodejs.org/en/download/)
|
|
8
|
+
- [NPM](https://docs.npmjs.com/cli/v7/configuring-npm/install)
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
``sudo npm i -g logtunnel``
|
|
13
|
+
|
|
14
|
+
## Usage
|
|
15
|
+
|
|
16
|
+
Find logs that contain "alice":
|
|
17
|
+
``curl -s https://cdn.codetunnel.net/lt/text.log | lt alice``
|
|
18
|
+
|
|
19
|
+
Find logs that contain "alice" and "purchase":
|
|
20
|
+
``curl -s https://cdn.codetunnel.net/lt/text.log | lt -f alice -f purchase``
|
|
21
|
+
|
|
22
|
+
Find logs that contain "alice" and ignore the ones that contain "info":
|
|
23
|
+
``curl -s https://cdn.codetunnel.net/lt/text.log | lt -f alice -i info``
|
|
24
|
+
|
|
25
|
+
Parse logs as JSON and output them with that template:
|
|
26
|
+
``curl -s https://cdn.codetunnel.net/lt/json.log | lt -p json -o '[{{lvl}}] {{log}}'``
|
|
27
|
+
|
|
28
|
+
Parse logs as JSON, apply template and find the ones containing "alice":
|
|
29
|
+
``curl -s https://cdn.codetunnel.net/lt/json.log | lt -p json -o '[{{lvl}}] {{log}}' -f alice``
|
|
30
|
+
|
|
31
|
+
Parse logs as JSON, apply template and show the ones with "delay > 200":
|
|
32
|
+
``curl -s https://cdn.codetunnel.net/lt/json.log | lt -p json -o '[{{lvl}} in {{delay}}ms] {{log}}' -F 'delay > 200'``
|
|
33
|
+
|
|
34
|
+
Parse logs as JSON, apply template and show the ones with "log" containing "Alice":
|
|
35
|
+
``curl -s https://cdn.codetunnel.net/lt/json.log | lt -p json -o '[{{lvl}}] {{log}}' -F 'log.toLowerCase().includes("alice")'``
|
|
36
|
+
|
|
37
|
+
Parse logs as logfmt, show the ones with "delay > 200" and show their original line (as if no parsing happened):
|
|
38
|
+
``curl -s https://cdn.codetunnel.net/lt/logfmt.log | lt -p logfmt -o original -F 'delay > 200'``
|
|
39
|
+
|
|
40
|
+
Parse logs as JSON and output them as a table:
|
|
41
|
+
``curl -s https://cdn.codetunnel.net/lt/json.log | lt -p json -o table``
|
|
42
|
+
|
|
43
|
+
Parse logs with regex, and output in logfmt:
|
|
44
|
+
``curl -s https://cdn.codetunnel.net/lt/text.log | lt -p '\[(?<lvl>\S*) in\s*(?<delay>\d*)ms\] (?<log>.*)' -o logfmt``
|
|
45
|
+
|
|
46
|
+
Parse logs with regex, and show the ones with "delay > 200":
|
|
47
|
+
``curl -s https://cdn.codetunnel.net/lt/text.log | lt -p '(?<delay>\d+)ms' -o original -F 'delay > 200'``
|
|
48
|
+
|
|
49
|
+
Parse table and show rows containing "cilium":
|
|
50
|
+
``curl -s https://cdn.codetunnel.net/lt/table.log | lt -p table -o original -f cilium``
|
|
51
|
+
|
|
52
|
+
Parse table, show rows containing "cilium" and the first headers row:
|
|
53
|
+
``curl -s https://cdn.codetunnel.net/lt/table.log | lt -p table -o original -f cilium -H``
|
|
54
|
+
|
|
55
|
+
Parse table, show rows with RESTARTS > 0:
|
|
56
|
+
``curl -s https://cdn.codetunnel.net/lt/table.log | lt -p table -o original -F 'RESTARTS > 0' -H``
|
|
57
|
+
|
|
58
|
+
Show rows that are not ready:
|
|
59
|
+
``curl -s https://cdn.codetunnel.net/lt/table.log | lt -p '(?<up>\d)/(?<total>\d)' -o original -F 'up < total' -H``
|
|
60
|
+
|
|
61
|
+
For more information ``lt --help``
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "logtunnel",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "CLI tool that allows you to format, filter and search your log output",
|
|
5
5
|
"main": "src/main.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"lt": "src/main.js"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
|
-
"test": "lab -v -
|
|
10
|
+
"test": "lab -v -t 100 -I require -a @hapi/code"
|
|
11
11
|
},
|
|
12
12
|
"repository": {
|
|
13
13
|
"type": "git",
|
|
@@ -34,10 +34,10 @@
|
|
|
34
34
|
"@hapi/bossy": "^5.1.0",
|
|
35
35
|
"@hapi/hoek": "^9.2.0",
|
|
36
36
|
"@hapi/podium": "^4.1.3",
|
|
37
|
+
"bigodon": "^2.3.0",
|
|
37
38
|
"colors": "^1.4.0",
|
|
38
39
|
"debug": "^4.3.2",
|
|
39
40
|
"logfmt": "^1.3.2",
|
|
40
|
-
"mustache": "^4.2.0",
|
|
41
41
|
"semver": "^7.3.5"
|
|
42
42
|
}
|
|
43
43
|
}
|
package/src/definition.js
CHANGED
|
@@ -29,7 +29,7 @@ const definition = {
|
|
|
29
29
|
type: 'string',
|
|
30
30
|
},
|
|
31
31
|
o: {
|
|
32
|
-
description: 'Formats the output using this template. Allowed: json, logfmt, original or a
|
|
32
|
+
description: 'Formats the output using this template. Allowed: json, logfmt, inspect, original, table or a bigodon template.',
|
|
33
33
|
alias: 'output',
|
|
34
34
|
type: 'string',
|
|
35
35
|
},
|
|
@@ -65,6 +65,8 @@ const examples = [
|
|
|
65
65
|
$ + 'curl -s https://cdn.codetunnel.net/lt/json.log | lt -p json -o \'[{{lvl}}] {{log}}\' -F \'log.toLowerCase().includes("alice")\'',
|
|
66
66
|
'Parse logs as logfmt, show the ones with "delay > 200" and show their original line (as if no parsing happened)'.dim,
|
|
67
67
|
$ + 'curl -s https://cdn.codetunnel.net/lt/logfmt.log | lt -p logfmt -o original -F \'delay > 200\'',
|
|
68
|
+
'Parse logs as JSON and output them as a table'.dim,
|
|
69
|
+
$ + 'curl -s https://cdn.codetunnel.net/lt/json.log | lt -p json -o table',
|
|
68
70
|
'Parse logs with regex, and output in logfmt'.dim,
|
|
69
71
|
$ + 'curl -s https://cdn.codetunnel.net/lt/text.log | lt -p \'\\[(?<lvl>\\S*) in\\s*(?<delay>\\d*)ms\\] (?<log>.*)\' -o logfmt',
|
|
70
72
|
'Parse logs with regex, and show the ones with "delay > 200"'.dim,
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const Podium = require("@hapi/podium");
|
|
2
|
+
|
|
3
|
+
module.exports.logSource = emitter => {
|
|
4
|
+
const source = new Podium(['log-line', 'end']);
|
|
5
|
+
let incompleteLine = '';
|
|
6
|
+
|
|
7
|
+
emitter.on('data', data => {
|
|
8
|
+
const lines = data.toString()
|
|
9
|
+
.split(/[\r\n|\n]/);
|
|
10
|
+
|
|
11
|
+
lines[0] = incompleteLine + lines[0];
|
|
12
|
+
incompleteLine = lines.pop(); // Either an incomplete line or an empty string due to the last \n
|
|
13
|
+
|
|
14
|
+
lines.filter(line => line.length > 0)
|
|
15
|
+
.forEach(line => source.emit('log-line', line));
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
emitter.on('end', () => {
|
|
19
|
+
if(incompleteLine.length > 0) {
|
|
20
|
+
source.emit('log-line', incompleteLine);
|
|
21
|
+
}
|
|
22
|
+
source.emit('end');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
return source;
|
|
26
|
+
};
|
package/src/main.js
CHANGED
|
@@ -9,15 +9,9 @@ require('colors');
|
|
|
9
9
|
const Bossy = require('@hapi/bossy');
|
|
10
10
|
const pkg = require('../package.json');
|
|
11
11
|
const { definition, usage } = require('./definition');
|
|
12
|
-
const {
|
|
12
|
+
const { logSource } = require('./log-source');
|
|
13
13
|
const { LogPipeline } = require('./pipeline');
|
|
14
14
|
|
|
15
|
-
const filter = require('./transformers/filter');
|
|
16
|
-
const ignore = require('./transformers/ignore');
|
|
17
|
-
const parse = require('./transformers/parse');
|
|
18
|
-
const field = require('./transformers/field');
|
|
19
|
-
const output = require('./transformers/output');
|
|
20
|
-
|
|
21
15
|
const debug = require('debug')('logtunnel:main');
|
|
22
16
|
|
|
23
17
|
function run() {
|
|
@@ -36,17 +30,11 @@ function run() {
|
|
|
36
30
|
|
|
37
31
|
try {
|
|
38
32
|
debug('building pipeline');
|
|
39
|
-
const pipeline = new LogPipeline(
|
|
40
|
-
args._ ? filter(args._) : null,
|
|
41
|
-
...args.filter.map(filter),
|
|
42
|
-
...args.ignore.map(ignore),
|
|
43
|
-
parse(args.parser),
|
|
44
|
-
...args.field.map(field),
|
|
45
|
-
output(args.output),
|
|
46
|
-
], args);
|
|
47
|
-
|
|
33
|
+
const pipeline = new LogPipeline(args, process.stdout);
|
|
48
34
|
debug('registering stdin');
|
|
49
|
-
|
|
35
|
+
logSource(process.stdin)
|
|
36
|
+
.on('log-line', l => pipeline.onLogLine(l))
|
|
37
|
+
.on('end', () => pipeline.onEnd());
|
|
50
38
|
} catch(e) {
|
|
51
39
|
console.error('Error:', e.message);
|
|
52
40
|
process.exit(1);
|
package/src/pipeline.js
CHANGED
|
@@ -1,10 +1,22 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { FieldFilter } = require('./transformers/filters/field');
|
|
4
|
+
const { FindFilter } = require('./transformers/filters/find');
|
|
5
|
+
const { IgnoreFilter } = require('./transformers/filters/ignore');
|
|
6
|
+
const { outputFactory } = require('./transformers/outputs/factory');
|
|
7
|
+
const { parseFactory } = require('./transformers/parsers/factory');
|
|
8
|
+
|
|
1
9
|
const debug = require('debug')('logtunnel:pipeline');
|
|
2
10
|
|
|
3
11
|
class LogPipeline {
|
|
4
|
-
constructor(
|
|
5
|
-
this.transformers = transformers.filter(t => t !== null);
|
|
12
|
+
constructor(args, stdout) {
|
|
6
13
|
this.firstLine = null;
|
|
7
14
|
this.args = args;
|
|
15
|
+
this.stdout = stdout;
|
|
16
|
+
this.outputTransformer = outputFactory(this.args.output);
|
|
17
|
+
this.isOutputBuffered = Boolean(this.outputTransformer.flush);
|
|
18
|
+
this.transformers = this._buildTransformers()
|
|
19
|
+
.filter(t => t !== null);
|
|
8
20
|
}
|
|
9
21
|
|
|
10
22
|
onLogLine(line) {
|
|
@@ -13,16 +25,19 @@ class LogPipeline {
|
|
|
13
25
|
this._logLine(line);
|
|
14
26
|
this._updateFirstLine(line);
|
|
15
27
|
} catch (e) {
|
|
28
|
+
// Covering this would kill the process
|
|
29
|
+
/* $lab:coverage:off$ */
|
|
16
30
|
console.error('Error:', e.message);
|
|
17
31
|
process.exit(1);
|
|
32
|
+
/* $lab:coverage:on$ */
|
|
18
33
|
}
|
|
19
34
|
}
|
|
20
35
|
|
|
21
|
-
_logLine(line) {
|
|
36
|
+
async _logLine(line) {
|
|
22
37
|
let output = line;
|
|
23
38
|
|
|
24
39
|
for (let transformer of this.transformers) {
|
|
25
|
-
const result = transformer(output, line, this);
|
|
40
|
+
const result = await transformer.run(output, line, this);
|
|
26
41
|
|
|
27
42
|
// Transformer accepted the line
|
|
28
43
|
if(result === true) {
|
|
@@ -38,10 +53,14 @@ class LogPipeline {
|
|
|
38
53
|
|
|
39
54
|
// Transformer modified the line
|
|
40
55
|
output = result;
|
|
41
|
-
debug('line transformed: ' + JSON.stringify(output));
|
|
56
|
+
debug('line transformed: ' + JSON.stringify(output));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (this.isOutputBuffered) {
|
|
60
|
+
return;
|
|
42
61
|
}
|
|
43
62
|
|
|
44
|
-
|
|
63
|
+
this.stdout.write(output + '\n');
|
|
45
64
|
}
|
|
46
65
|
|
|
47
66
|
_updateFirstLine(line) {
|
|
@@ -50,9 +69,33 @@ class LogPipeline {
|
|
|
50
69
|
}
|
|
51
70
|
|
|
52
71
|
this.firstLine = line;
|
|
53
|
-
if(this.args.headers) {
|
|
54
|
-
|
|
72
|
+
if(this.args.headers && !this.isOutputBuffered) {
|
|
73
|
+
this.stdout.write(this.firstLine + '\n');
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
onEnd() {
|
|
78
|
+
if (!this.isOutputBuffered) {
|
|
79
|
+
return;
|
|
55
80
|
}
|
|
81
|
+
|
|
82
|
+
const lines = this.outputTransformer.flush();
|
|
83
|
+
lines.forEach(line => this.stdout.write(line + '\n'));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
_buildTransformers() {
|
|
87
|
+
return [
|
|
88
|
+
// First of all, filter which lines to accept
|
|
89
|
+
this.args._ ? new FindFilter(this.args._) : null,
|
|
90
|
+
...this.args.filter.map(f => new FindFilter(f)),
|
|
91
|
+
...this.args.ignore.map(i => new IgnoreFilter(i)),
|
|
92
|
+
// Parse them...
|
|
93
|
+
parseFactory(this.args.parser),
|
|
94
|
+
// ...and apply field filters
|
|
95
|
+
...this.args.field.map(f => new FieldFilter(f)),
|
|
96
|
+
// And finally format the output
|
|
97
|
+
this.outputTransformer,
|
|
98
|
+
];
|
|
56
99
|
}
|
|
57
100
|
}
|
|
58
101
|
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const { compileExpression } = require('bigodon');
|
|
2
|
+
|
|
3
|
+
class FieldFilter {
|
|
4
|
+
constructor(expression) {
|
|
5
|
+
this.expression = compileExpression(expression);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
async run(line) {
|
|
9
|
+
if(typeof line !== 'object') {
|
|
10
|
+
throw new Error("To use a field filter, you need to specify a parser like '-p json' ");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const result = await this.expression(line);
|
|
14
|
+
return Boolean(result);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
module.exports = { FieldFilter };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const { compile } = require('bigodon');
|
|
2
|
+
|
|
3
|
+
class BigodonOutput {
|
|
4
|
+
constructor(template) {
|
|
5
|
+
this.template = compile(template);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
async run(line) {
|
|
9
|
+
if (typeof line !== 'object') {
|
|
10
|
+
throw new Error("To use an output transformer, you need to specify a parser like '-p json' ");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return this.template(line);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
module.exports = { BigodonOutput };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const { JsonOutput } = require('./json');
|
|
2
|
+
const { LogfmtOutput } = require('./logfmt');
|
|
3
|
+
const { BigodonOutput } = require('./bigodon');
|
|
4
|
+
const { OriginalOutput } = require('./original');
|
|
5
|
+
const { InspectOutput } = require('./inspect');
|
|
6
|
+
const { TableOutput } = require('./table');
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
function outputFactory(format) {
|
|
10
|
+
switch(format?.toLowerCase()) {
|
|
11
|
+
case undefined: return new InspectOutput(false);
|
|
12
|
+
case 'json': return new JsonOutput();
|
|
13
|
+
case 'table': return new TableOutput();
|
|
14
|
+
case 'logfmt': return new LogfmtOutput();
|
|
15
|
+
case 'inspect': return new InspectOutput(true);
|
|
16
|
+
case 'original': return new OriginalOutput();
|
|
17
|
+
default: return new BigodonOutput(format);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
module.exports = { outputFactory };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const util = require('util');
|
|
2
|
+
|
|
3
|
+
class InspectOutput {
|
|
4
|
+
constructor(breakLines = false) {
|
|
5
|
+
this.options = {
|
|
6
|
+
colors: true,
|
|
7
|
+
depth: null,
|
|
8
|
+
breakLength: breakLines ? 80 : Infinity,
|
|
9
|
+
compact: !breakLines,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
run(line) {
|
|
14
|
+
if (typeof line === 'string') {
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return util.inspect({ ...line }, this.options);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
module.exports = { InspectOutput };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const logfmt = require('logfmt');
|
|
2
|
+
|
|
3
|
+
class LogfmtOutput {
|
|
4
|
+
run(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 logfmt.stringify(line);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
module.exports = { LogfmtOutput };
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
class TableOutput {
|
|
2
|
+
constructor() {
|
|
3
|
+
this.rows = [];
|
|
4
|
+
this.columns = [];
|
|
5
|
+
this.columnWidths = new Map();
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
run(line) {
|
|
9
|
+
if (typeof line !== 'object') {
|
|
10
|
+
throw new Error("To use an output transformer, you need to specify a parser like '-p json'");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
this.rows.push(line);
|
|
14
|
+
|
|
15
|
+
Object.keys(line).forEach(column => {
|
|
16
|
+
if (!this.columnWidths.has(column)) {
|
|
17
|
+
this.columns.push(column);
|
|
18
|
+
this.columnWidths.set(column, column.length);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const cell = this._toCell(line[column]);
|
|
22
|
+
const width = this.columnWidths.get(column);
|
|
23
|
+
if (cell.length > width) {
|
|
24
|
+
this.columnWidths.set(column, cell.length);
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
flush() {
|
|
31
|
+
if (this.rows.length === 0) {
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const columns = this.columns;
|
|
36
|
+
const widths = columns.map(column => this.columnWidths.get(column));
|
|
37
|
+
|
|
38
|
+
const lines = [];
|
|
39
|
+
lines.push(this._formatRow(columns, widths));
|
|
40
|
+
this.rows.forEach(row => {
|
|
41
|
+
const cells = columns.map(column => this._toCell(row[column]));
|
|
42
|
+
lines.push(this._formatRow(cells, widths));
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
return lines;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
_toCell(value) {
|
|
49
|
+
if (value === null || value === undefined) {
|
|
50
|
+
return '';
|
|
51
|
+
}
|
|
52
|
+
if (typeof value === 'object') {
|
|
53
|
+
return JSON.stringify(value);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return String(value);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
_formatRow(cells, widths) {
|
|
60
|
+
return cells
|
|
61
|
+
.map((cell, index) => cell.padEnd(widths[index]))
|
|
62
|
+
.join(' ')
|
|
63
|
+
.trimEnd();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = { TableOutput };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const { JsonParse } = require("./json");
|
|
2
|
+
const { LogfmtParse } = require("./logfmt");
|
|
3
|
+
const { TableParse } = require("./table");
|
|
4
|
+
const { RegexParse } = require("./regex");
|
|
5
|
+
|
|
6
|
+
function parseFactory(format) {
|
|
7
|
+
switch(format?.toLowerCase()) {
|
|
8
|
+
case void 0: return null;
|
|
9
|
+
case 'json': return new JsonParse();
|
|
10
|
+
case 'logfmt': return new LogfmtParse();
|
|
11
|
+
case 'table': return new TableParse();
|
|
12
|
+
default: return new RegexParse(format);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
module.exports = { parseFactory };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
class TableParse {
|
|
2
|
+
constructor() {
|
|
3
|
+
this.headers = null;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
run(line, _original, pipeline) {
|
|
7
|
+
if (!pipeline.firstLine) {
|
|
8
|
+
// Ignore first line, it's the headers
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
if (!this.headers) {
|
|
12
|
+
this.headers = this._splitColumns(pipeline.firstLine);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const columns = this._splitColumns(line);
|
|
16
|
+
const obj = {};
|
|
17
|
+
|
|
18
|
+
this.headers.forEach((header, i) => {
|
|
19
|
+
obj[header] = columns[i];
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
return obj;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
_splitColumns(line) {
|
|
26
|
+
return line.replace(/\s+/g, ' ').split(' ');
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
module.exports = { TableParse };
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
const Lab = require('@hapi/lab');
|
|
2
|
+
const Code = require('@hapi/code');
|
|
3
|
+
|
|
4
|
+
const { FindFilter } = require('../src/transformers/filters/find');
|
|
5
|
+
const { IgnoreFilter } = require('../src/transformers/filters/ignore');
|
|
6
|
+
const { FieldFilter } = require('../src/transformers/filters/field');
|
|
7
|
+
|
|
8
|
+
const { describe, it } = exports.lab = Lab.script();
|
|
9
|
+
const { expect } = Code;
|
|
10
|
+
|
|
11
|
+
describe('filters', () => {
|
|
12
|
+
describe('--filter', () => {
|
|
13
|
+
it('should accept matching strings', () => {
|
|
14
|
+
const transformer = new FindFilter('psu');
|
|
15
|
+
const result = transformer.run('lorem ipsum dolor sit amet');
|
|
16
|
+
expect(result).to.be.true();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('should reject non-matching strings', () => {
|
|
20
|
+
const transformer = new FindFilter('aaa');
|
|
21
|
+
const result = transformer.run('lorem ipsum dolor sit amet');
|
|
22
|
+
expect(result).to.be.false();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('should be case insensitive', () => {
|
|
26
|
+
const transformer = new FindFilter('PsU');
|
|
27
|
+
const result = transformer.run('lorem ipSUm dolor sit amet');
|
|
28
|
+
expect(result).to.be.true();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should support regex', () => {
|
|
32
|
+
const transformer = new FindFilter('i.*m');
|
|
33
|
+
const result = transformer.run('lorem ipsum dolor sit amet');
|
|
34
|
+
expect(result).to.be.true();
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
describe('--ignore', () => {
|
|
38
|
+
it('should reject matching strings', () => {
|
|
39
|
+
const transformer = new IgnoreFilter('psu');
|
|
40
|
+
const result = transformer.run('lorem ipsum dolor sit amet');
|
|
41
|
+
expect(result).to.be.false();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should accept non-matching strings', () => {
|
|
45
|
+
const transformer = new IgnoreFilter('aaa');
|
|
46
|
+
const result = transformer.run('lorem ipsum dolor sit amet');
|
|
47
|
+
expect(result).to.be.true();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should be case insensitive', () => {
|
|
51
|
+
const transformer = new IgnoreFilter('PsU');
|
|
52
|
+
const result = transformer.run('lorem ipSUm dolor sit amet');
|
|
53
|
+
expect(result).to.be.false();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should support regex', () => {
|
|
57
|
+
const transformer = new IgnoreFilter('i.*m');
|
|
58
|
+
const result = transformer.run('lorem ipsum dolor sit amet');
|
|
59
|
+
expect(result).to.be.false();
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
describe('--field', () => {
|
|
63
|
+
it('should error when strings wasn\'t parsed into object', async () => {
|
|
64
|
+
const transformer = new FieldFilter('a');
|
|
65
|
+
expect(transformer.run('a')).to.reject();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should compare equality', async () => {
|
|
69
|
+
const transformer = new FieldFilter('eq foo "bar"');
|
|
70
|
+
expect(await transformer.run({ foo: 'bar' })).to.be.true();
|
|
71
|
+
expect(await transformer.run({ foo: 'baz' })).to.be.false();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should compare inequality with strings', async () => {
|
|
75
|
+
const transformer = new FieldFilter('gte foo 3');
|
|
76
|
+
expect(await transformer.run({ foo: '3' })).to.be.true();
|
|
77
|
+
expect(await transformer.run({ foo: '2' })).to.be.false();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should accept javascript', async () => {
|
|
81
|
+
const transformer = new FieldFilter('includes (lower foo) "bar"');
|
|
82
|
+
expect(await transformer.run({ foo: 'ABARA' })).to.be.true();
|
|
83
|
+
expect(await transformer.run({ foo: 'ABADA' })).to.be.false();
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
});
|