logtunnel 1.1.0 → 1.2.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 CHANGED
@@ -218,6 +218,20 @@ Convert `kubectl` table output to logfmt for easier downstream filtering:
218
218
  curl -s https://cdn.codetunnel.net/lt/table.log | lt -p table -o logfmt
219
219
  ```
220
220
 
221
+ ### Pick columns (`-P`)
222
+
223
+ Pick specific columns to be displayed after parsing (works with any parser):
224
+
225
+ List kubernetes pods and show only the specified columns:
226
+ ```bash
227
+ cat ./examples/table.log | lt -p table -P NAME,STATUS,AGE -o table
228
+ ```
229
+
230
+ Find logs containing "error", parse as JSON and keep just the specified columns:
231
+ ```bash
232
+ curl -s https://cdn.codetunnel.net/lt/text.log | lt -f error -p json -P ts,level,message -o json
233
+ ```
234
+
221
235
  ### Custom regex parsing (`-p '(?<name>...)'`)
222
236
 
223
237
  Use a regex with named groups to “extract fields” from unstructured text:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "logtunnel",
3
- "version": "1.1.0",
3
+ "version": "1.2.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": {
package/src/definition.js CHANGED
@@ -49,6 +49,11 @@ const definition = {
49
49
  alias: 'kubectl',
50
50
  type: 'boolean',
51
51
  },
52
+ P: {
53
+ description: 'Pick only these columns from parsed objects (comma-separated). Requires a parser like -p json.',
54
+ alias: 'pick',
55
+ type: 'string',
56
+ },
52
57
  };
53
58
 
54
59
  const $ = '$ '.gray;
@@ -76,6 +81,8 @@ const examples = [
76
81
  $ + 'curl -s https://cdn.codetunnel.net/lt/table.log | lt -H gateway',
77
82
  'Kubernetes tables: -k is shorthand for -p table -o table:'.dim,
78
83
  $ + 'curl -s https://cdn.codetunnel.net/lt/table.log | lt -k payment',
84
+ 'Kubernetes tables: pick columns (NAME, STATUS, AGE):'.dim,
85
+ $ + 'cat ./examples/table.log | lt -p table -P NAME,STATUS,AGE -o table',
79
86
  'Kubernetes tables: show pods that are not fully ready:'.dim,
80
87
  $ + 'curl -s https://cdn.codetunnel.net/lt/table.log | lt -k -F \'lt (itemAt (split READY "/") 0) (itemAt (split READY "/") 1)\'',
81
88
  'Custom regex parsing (extract fields) and output as logfmt:'.dim,
package/src/main.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  const semver = require('semver');
3
- if(!semver.satisfies(process.version, '>=12')) {
4
- console.error(`Your NodeJS version (${process.version}) is too old for logtunnel :(\nUse at least NodeJS 12`);
3
+ if(!semver.satisfies(process.version, '>=16')) {
4
+ console.error(`Your NodeJS version (${process.version}) is too old for logtunnel :(\nUse at least NodeJS 16`);
5
5
  process.exit(1);
6
6
  }
7
7
 
package/src/pipeline.js CHANGED
@@ -3,6 +3,7 @@
3
3
  const { FieldFilter } = require('./transformers/filters/field');
4
4
  const { FindFilter } = require('./transformers/filters/find');
5
5
  const { IgnoreFilter } = require('./transformers/filters/ignore');
6
+ const { PickFilter } = require('./transformers/filters/pick');
6
7
  const { outputFactory } = require('./transformers/outputs/factory');
7
8
  const { parseFactory } = require('./transformers/parsers/factory');
8
9
 
@@ -20,17 +21,16 @@ class LogPipeline {
20
21
  }
21
22
 
22
23
  onLogLine(line) {
23
- try {
24
- debug('got line: ' + line)
25
- const isFirstLine = this._updateFirstLine(line);
26
- this._logLine(line, isFirstLine);
27
- } catch (e) {
28
- // Covering this would kill the process
29
- /* $lab:coverage:off$ */
30
- console.error('Error:', e.message);
31
- process.exit(1);
32
- /* $lab:coverage:on$ */
33
- }
24
+ debug('got line: ' + line)
25
+ const isFirstLine = this._updateFirstLine(line);
26
+ this._logLine(line, isFirstLine)
27
+ .catch(error => {
28
+ // Covering this would kill the process
29
+ /* $lab:coverage:off$ */
30
+ console.error('Error:', error.message);
31
+ process.exit(1);
32
+ /* $lab:coverage:on$ */
33
+ });
34
34
  }
35
35
 
36
36
  async _logLine(line, isFirstLine) {
@@ -99,6 +99,8 @@ class LogPipeline {
99
99
  parseFactory(this.args.parser),
100
100
  // ...and apply field filters
101
101
  ...this.args.field.map(f => new FieldFilter(f)),
102
+ // ...optionally pick specific columns
103
+ this.args.pick ? new PickFilter(this.args.pick) : null,
102
104
  // And finally format the output
103
105
  this.outputTransformer,
104
106
  ];
@@ -0,0 +1,22 @@
1
+ class PickFilter {
2
+ constructor(columns) {
3
+ this.columns = columns
4
+ .split(',')
5
+ .map(column => column.trim())
6
+ .filter(Boolean);
7
+ }
8
+
9
+ async run(line) {
10
+ if (typeof line !== 'object') {
11
+ throw new Error("To use -P/--pick, you need to specify a parser like '-p json'");
12
+ }
13
+
14
+ return Object.fromEntries(
15
+ this.columns
16
+ .filter(column => line[column] !== undefined)
17
+ .map(column => [column, line[column]])
18
+ );
19
+ }
20
+ }
21
+
22
+ module.exports = { PickFilter };
@@ -4,6 +4,7 @@ const Code = require('@hapi/code');
4
4
  const { FindFilter } = require('../src/transformers/filters/find');
5
5
  const { IgnoreFilter } = require('../src/transformers/filters/ignore');
6
6
  const { FieldFilter } = require('../src/transformers/filters/field');
7
+ const { PickFilter } = require('../src/transformers/filters/pick');
7
8
 
8
9
  const { describe, it } = exports.lab = Lab.script();
9
10
  const { expect } = Code;
@@ -83,4 +84,35 @@ describe('filters', () => {
83
84
  expect(await transformer.run({ foo: 'ABADA' })).to.be.false();
84
85
  });
85
86
  });
87
+
88
+ describe('--pick', () => {
89
+ it('should error when strings weren\'t parsed into object', async () => {
90
+ const transformer = new PickFilter('foo');
91
+ expect(transformer.run('a')).to.reject();
92
+ });
93
+
94
+ it('should pick only selected field (1 field)', async () => {
95
+ const transformer = new PickFilter('foo');
96
+ const result = await transformer.run({ foo: 'bar', baz: 'qux', extra: 'nope' });
97
+ expect(result).to.equal({ foo: 'bar' });
98
+ });
99
+
100
+ it('should pick only selected fields (2 fields)', async () => {
101
+ const transformer = new PickFilter('foo,baz');
102
+ const result = await transformer.run({ foo: 'bar', baz: 'qux', extra: 'nope' });
103
+ expect(result).to.equal({ foo: 'bar', baz: 'qux' });
104
+ });
105
+
106
+ it('should not break with undefined fields', async () => {
107
+ const transformer = new PickFilter('foo,bar');
108
+ const result = await transformer.run({ foo: 'bar', baz: 'qux', extra: 'nope' });
109
+ expect(result).to.equal({ foo: 'bar' });
110
+ });
111
+
112
+ it('should include falsy values', async () => {
113
+ const transformer = new PickFilter('foo,baz');
114
+ const result = await transformer.run({ foo: 0, baz: null, extra: 'nope' });
115
+ expect(result).to.equal({ foo: 0, baz: null });
116
+ });
117
+ });
86
118
  });
@@ -1,6 +1,6 @@
1
1
  const Lab = require('@hapi/lab');
2
2
  const Code = require('@hapi/code');
3
- const { runPipeline, _, f, i, F, p, o, H, k } = require('./utils');
3
+ const { runPipeline, _, f, i, F, P, p, o, H, k } = require('./utils');
4
4
 
5
5
  const { describe, it } = exports.lab = Lab.script();
6
6
  const { expect } = Code;
@@ -146,6 +146,22 @@ describe('pipeline', () => {
146
146
  expect(actual).to.equal(expected);
147
147
  });
148
148
 
149
+ it('should pick columns before formatting', async () => {
150
+ const args = [
151
+ p('logfmt'),
152
+ o('json'),
153
+ P('foo,baz'),
154
+ ];
155
+ const actual = await runPipeline([
156
+ 'foo=bar baz=qux extra=nope',
157
+ ], args);
158
+ const expected = [
159
+ '{"foo":"bar","baz":"qux"}',
160
+ ];
161
+
162
+ expect(actual).to.equal(expected);
163
+ });
164
+
149
165
  it('should log headers when -H is provided', async () => {
150
166
  const args = [
151
167
  p('table'),
package/test/utils.js CHANGED
@@ -64,9 +64,10 @@ const _ = str => ({ _: str });
64
64
  const f = str => ({ filter: [str] });
65
65
  const i = str => ({ ignore: [str] });
66
66
  const F = str => ({ field: [str] });
67
+ const P = str => ({ pick: str });
67
68
  const p = str => ({ parser: str });
68
69
  const o = str => ({ output: str });
69
70
  const H = () => ({ headers: true });
70
71
  const k = () => ({ kubectl: true });
71
72
 
72
- module.exports = { pod, slowPod, runPipeline, _, f, i, F, p, o, H, k };
73
+ module.exports = { pod, slowPod, runPipeline, _, f, i, F, P, p, o, H, k };