logstash-filter-translate 3.1.0 → 3.3.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.
@@ -1,7 +1,7 @@
1
1
  Gem::Specification.new do |s|
2
2
 
3
3
  s.name = 'logstash-filter-translate'
4
- s.version = '3.1.0'
4
+ s.version = '3.3.0'
5
5
  s.licenses = ['Apache License (2.0)']
6
6
  s.summary = "Replaces field contents based on a hash or YAML file"
7
7
  s.description = "This gem is a Logstash plugin required to be installed on top of the Logstash core pipeline using $LS_HOME/bin/logstash-plugin install gemname. This gem is not a stand-alone program"
@@ -21,7 +21,14 @@ Gem::Specification.new do |s|
21
21
 
22
22
  # Gem dependencies
23
23
  s.add_runtime_dependency "logstash-core-plugin-api", ">= 1.60", "<= 2.99"
24
+ s.add_runtime_dependency 'logstash-mixin-ecs_compatibility_support', '~> 1.2'
25
+ s.add_runtime_dependency 'logstash-mixin-validator_support', '~> 1.0'
26
+ s.add_runtime_dependency 'logstash-mixin-deprecation_logger_support', '~> 1.0'
27
+ s.add_runtime_dependency 'rufus-scheduler'
24
28
 
25
29
  s.add_development_dependency 'logstash-devutils'
30
+ s.add_development_dependency 'rspec-sequencing'
31
+ s.add_development_dependency "rspec-wait"
32
+ s.add_development_dependency "benchmark-ips"
26
33
  end
27
34
 
@@ -0,0 +1,69 @@
1
+ # encoding: utf-8
2
+ require "logstash/devutils/rspec/spec_helper"
3
+
4
+ require "logstash/filters/translate"
5
+ require "benchmark/ips"
6
+
7
+ module BenchmarkingFileBuilder
8
+ def self.create_huge_csv_dictionary(directory, name, size)
9
+ tmppath = directory.join("temp_big.csv")
10
+ tmppath.open("w") do |file|
11
+ file.puts("foo,#{SecureRandom.hex(4)}")
12
+ file.puts("bar,#{SecureRandom.hex(4)}")
13
+ size.times do |i|
14
+ file.puts("#{SecureRandom.hex(12)},#{1000000 + i}")
15
+ end
16
+ file.puts("baz,quux")
17
+ end
18
+ tmppath.rename(directory.join(name))
19
+ end
20
+ end
21
+
22
+ describe LogStash::Filters::Translate do
23
+ let(:directory) { Pathname.new(Stud::Temporary.directory) }
24
+ let(:dictionary_name) { "dict-h.csv" }
25
+ let(:dictionary_path) { directory.join(dictionary_name) }
26
+ let(:dictionary_size) { 100000 }
27
+ let(:config) do
28
+ {
29
+ "field" => "[status]",
30
+ "destination" => "[translation]",
31
+ "dictionary_path" => dictionary_path.to_path,
32
+ "exact" => true,
33
+ "regex" => false,
34
+ "refresh_interval" => 0,
35
+ "override" => true,
36
+ "refresh_behaviour" => "merge"
37
+ }
38
+ end
39
+ before do
40
+ directory
41
+ BenchmarkingFileBuilder.create_huge_csv_dictionary(directory, dictionary_name, dictionary_size)
42
+ end
43
+
44
+ it 'how fast is the filter method?' do
45
+ plugin = described_class.new(config)
46
+ plugin.register
47
+ event = LogStash::Event.new("status" => "baz", "translation" => "foo")
48
+
49
+ Benchmark.ips do |x|
50
+ x.config(:time => 20, :warmup => 120)
51
+ x.report("filter(event)") { plugin.filter(event) }
52
+ end
53
+ expect(event.get("[translation]")).to eq("quux")
54
+ end
55
+
56
+ it 'how fast is the new, register then filter method?' do
57
+ event = LogStash::Event.new("status" => "baz", "translation" => "foo")
58
+
59
+ Benchmark.ips do |x|
60
+ x.config(:time => 10, :warmup => 120)
61
+ x.report("new, register, filter(event)") do
62
+ plugin = described_class.new(config)
63
+ plugin.register
64
+ plugin.filter(event)
65
+ end
66
+ end
67
+ expect(event.get("[translation]")).to eq("quux")
68
+ end
69
+ end
@@ -0,0 +1,201 @@
1
+ # encoding: utf-8
2
+ require 'rspec/wait'
3
+ require "logstash/devutils/rspec/spec_helper"
4
+ require "support/rspec_wait_handler_helper"
5
+ require "support/build_huge_dictionaries"
6
+
7
+ require "rspec_sequencing"
8
+
9
+ require "logstash/filters/translate"
10
+
11
+ describe LogStash::Filters::Translate do
12
+ let(:directory) { Pathname.new(Stud::Temporary.directory) }
13
+ describe "scheduled reloading" do
14
+ subject { described_class.new(config) }
15
+
16
+ let(:config) do
17
+ {
18
+ "source" => "[status]",
19
+ "target" => "[translation]",
20
+ "dictionary_path" => dictionary_path.to_path,
21
+ "exact" => true,
22
+ "regex" => false,
23
+ "refresh_interval" => 1,
24
+ "override" => true,
25
+ "refresh_behaviour" => refresh_behaviour
26
+ }
27
+ end
28
+
29
+ let(:event) { LogStash::Event.new("status" => "b") }
30
+
31
+ before do
32
+ directory
33
+ wait(1.0).for{Dir.exist?(directory)}.to eq(true)
34
+ dictionary_path.open("wb") do |file|
35
+ file.puts("a,1\nb,2\nc,3\n")
36
+ end
37
+ subject.register
38
+ end
39
+
40
+ after do
41
+ FileUtils.rm_rf(directory)
42
+ wait(1.0).for{Dir.exist?(directory)}.to eq(false)
43
+ end
44
+
45
+ context "replace" do
46
+ let(:dictionary_path) { directory.join("dict-r.csv") }
47
+ let(:refresh_behaviour) { "replace" }
48
+ let(:actions) do
49
+ RSpec::Sequencing
50
+ .run("translate") do
51
+ subject.filter(event)
52
+ wait(0.1).for{event.get("[translation]")}.to eq("2"), "field [translation] did not eq '2'"
53
+ end
54
+ .then_after(1,"modify file") do
55
+ dictionary_path.open("w") do |file|
56
+ file.puts("a,11\nb,12\nc,13\n")
57
+ end
58
+ end
59
+ .then_after(1.2, "wait then translate again") do
60
+ subject.filter(event)
61
+ wait(0.1).for{event.get("[translation]")}.to eq("12"), "field [translation] did not eq '12'"
62
+ end
63
+ .then("stop") do
64
+ subject.close
65
+ end
66
+ end
67
+
68
+ it "updates the event after scheduled reload" do
69
+ actions.activate_quietly
70
+ actions.assert_no_errors
71
+ end
72
+ end
73
+
74
+ context "merge" do
75
+ let(:dictionary_path) { directory.join("dict-m.csv") }
76
+ let(:refresh_behaviour) { "merge" }
77
+ let(:actions) do
78
+ RSpec::Sequencing
79
+ .run("translate") do
80
+ subject.filter(event)
81
+ wait(0.1).for{event.get("[translation]")}.to eq("2"), "field [translation] did not eq '2'"
82
+ end
83
+ .then_after(1,"modify file") do
84
+ dictionary_path.open("w") do |file|
85
+ file.puts("a,21\nb,22\nc,23\n")
86
+ end
87
+ end
88
+ .then_after(1.2, "wait then translate again") do
89
+ subject.filter(event)
90
+ wait(0.1).for{event.get("[translation]")}.to eq("22"), "field [translation] did not eq '22'"
91
+ end
92
+ .then("stop") do
93
+ subject.close
94
+ end
95
+ end
96
+
97
+ it "updates the event after scheduled reload" do
98
+ actions.activate_quietly
99
+ actions.assert_no_errors
100
+ end
101
+ end
102
+ end
103
+
104
+ describe "huge json file merge" do
105
+ let(:dictionary_path) { directory.join("dict-h.json") }
106
+ let(:dictionary_size) { 100000 }
107
+ let(:config) do
108
+ {
109
+ "source" => "[status]",
110
+ "target" => "[translation]",
111
+ "dictionary_path" => dictionary_path.to_path,
112
+ "exact" => true,
113
+ "regex" => false,
114
+ "refresh_interval" => 1,
115
+ "override" => true,
116
+ "refresh_behaviour" => "merge"
117
+ }
118
+ end
119
+ let(:event) { LogStash::Event.new("status" => "baz", "translation" => "foo") }
120
+ subject { described_class.new(config) }
121
+
122
+ before do
123
+ directory
124
+ wait(1.0).for{Dir.exist?(directory)}.to eq(true)
125
+ LogStash::Filters::Dictionary.create_huge_json_dictionary(directory, "dict-h.json", dictionary_size)
126
+ subject.register
127
+ end
128
+
129
+ let(:actions) do
130
+ RSpec::Sequencing
131
+ .run("translate") do
132
+ subject.filter(event)
133
+ wait(0.1).for{event.get("[translation]")}.not_to eq("foo"), "field [translation] should not be 'foo'"
134
+ end
135
+ .then_after(0.1,"modify file") do
136
+ LogStash::Filters::Dictionary.create_huge_json_dictionary(directory, "dict-h.json", dictionary_size)
137
+ end
138
+ .then_after(1.8, "wait then translate again") do
139
+ subject.filter(event)
140
+ wait(0.1).for{event.get("[translation]")}.not_to eq("foo"), "field [translation] should not be 'foo'"
141
+ end
142
+ .then("stop") do
143
+ subject.close
144
+ end
145
+ end
146
+
147
+ it "updates the event after scheduled reload" do
148
+ actions.activate_quietly
149
+ actions.assert_no_errors
150
+ end
151
+ end
152
+
153
+ describe "huge csv file merge" do
154
+ let(:dictionary_path) { directory.join("dict-h.csv") }
155
+ let(:dictionary_size) { 100000 }
156
+ let(:config) do
157
+ {
158
+ "source" => "[status]",
159
+ "target" => "[translation]",
160
+ "dictionary_path" => dictionary_path.to_path,
161
+ "exact" => true,
162
+ "regex" => false,
163
+ "refresh_interval" => 1,
164
+ "override" => true,
165
+ "refresh_behaviour" => "merge"
166
+ }
167
+ end
168
+ let(:event) { LogStash::Event.new("status" => "bar", "translation" => "foo") }
169
+ subject { described_class.new(config) }
170
+
171
+ before do
172
+ directory
173
+ wait(1.0).for{Dir.exist?(directory)}.to eq(true)
174
+ LogStash::Filters::Dictionary.create_huge_csv_dictionary(directory, "dict-h.csv", dictionary_size)
175
+ subject.register
176
+ end
177
+
178
+ let(:actions) do
179
+ RSpec::Sequencing
180
+ .run("translate") do
181
+ subject.filter(event)
182
+ wait(0.1).for{event.get("[translation]")}.not_to eq("foo"), "field [translation] should not be 'foo'"
183
+ end
184
+ .then_after(0.1,"modify file") do
185
+ LogStash::Filters::Dictionary.create_huge_csv_dictionary(directory, "dict-h.csv", dictionary_size)
186
+ end
187
+ .then_after(1.8, "wait then translate again") do
188
+ subject.filter(event)
189
+ wait(0.1).for{event.get("[translation]")}.not_to eq("foo"), "field [translation] should not be 'foo'"
190
+ end
191
+ .then("stop") do
192
+ subject.close
193
+ end
194
+ end
195
+
196
+ it "updates the event after scheduled reload" do
197
+ actions.activate_quietly
198
+ actions.assert_no_errors
199
+ end
200
+ end
201
+ end
@@ -1,146 +1,236 @@
1
1
  # encoding: utf-8
2
2
  require "logstash/devutils/rspec/spec_helper"
3
+ require 'logstash/plugin_mixins/ecs_compatibility_support/spec_helper'
3
4
  require "logstash/filters/translate"
4
5
 
6
+ module TranslateUtil
7
+ def self.build_fixture_path(filename)
8
+ File.join(File.dirname(__FILE__), "..", "fixtures", filename)
9
+ end
10
+ end
11
+
5
12
  describe LogStash::Filters::Translate do
6
13
 
7
14
  let(:config) { Hash.new }
8
15
  subject { described_class.new(config) }
9
16
 
17
+ let(:logger) { double('Logger').as_null_object }
18
+ let(:deprecation_logger) { double('DeprecationLogger').as_null_object }
19
+
20
+ before(:each) do
21
+ allow_any_instance_of(described_class).to receive(:logger).and_return(logger)
22
+ allow_any_instance_of(described_class).to receive(:deprecation_logger).and_return(deprecation_logger)
23
+ end
24
+
10
25
  describe "exact translation" do
11
26
 
12
27
  let(:config) do
13
28
  {
14
- "field" => "status",
15
- "destination" => "translation",
29
+ "source" => "status",
30
+ "target" => "translation",
16
31
  "dictionary" => [ "200", "OK",
17
32
  "300", "Redirect",
18
33
  "400", "Client Error",
19
34
  "500", "Server Error" ],
20
- "exact" => true,
21
- "regex" => false
35
+ "exact" => true,
36
+ "regex" => false
22
37
  }
23
38
  end
24
39
 
25
40
  let(:event) { LogStash::Event.new("status" => 200) }
26
41
 
27
- it "return the exact translation" do
42
+ it "coerces field to a string then returns the exact translation" do
28
43
  subject.register
29
44
  subject.filter(event)
30
45
  expect(event.get("translation")).to eq("OK")
31
46
  end
32
47
  end
33
48
 
34
-
35
- describe "multi translation" do
49
+ describe "translation fails when regex setting is false but keys are regex based" do
36
50
 
37
51
  let(:config) do
38
52
  {
39
- "field" => "status",
40
- "destination" => "translation",
41
- "dictionary" => [ "200", "OK",
42
- "300", "Redirect",
43
- "400", "Client Error",
44
- "500", "Server Error" ],
45
- "exact" => false,
46
- "regex" => false
53
+ "source" => "status",
54
+ "target" => "translation",
55
+ "dictionary" => [ "^2\\d\\d", "OK",
56
+ "^3\\d\\d", "Redirect",
57
+ "^4\\d\\d", "Client Error",
58
+ "^5\\d\\d", "Server Error" ],
59
+ "exact" => true,
60
+ "regex" => false
47
61
  }
48
62
  end
49
63
 
50
- let(:event) { LogStash::Event.new("status" => "200 & 500") }
64
+ let(:event) { LogStash::Event.new("status" => 200) }
51
65
 
52
- it "return the exact translation" do
66
+ it "does not return the exact translation" do
53
67
  subject.register
54
68
  subject.filter(event)
55
- expect(event.get("translation")).to eq("OK & Server Error")
69
+ expect(event.get("translation")).to be_nil
56
70
  end
57
-
58
71
  end
59
72
 
60
- describe "regex translation" do
73
+ describe "multi translation" do
74
+ context "when using an inline dictionary" do
75
+ let(:config) do
76
+ {
77
+ "source" => "status",
78
+ "target" => "translation",
79
+ "dictionary" => [ "200", "OK",
80
+ "300", "Redirect",
81
+ "400", "Client Error",
82
+ "500", "Server Error" ],
83
+ "exact" => false,
84
+ "regex" => false
85
+ }
86
+ end
61
87
 
62
- let(:config) do
63
- {
64
- "field" => "status",
65
- "destination" => "translation",
66
- "dictionary" => [ "^2[0-9][0-9]$", "OK",
67
- "^3[0-9][0-9]$", "Redirect",
68
- "^4[0-9][0-9]$", "Client Error",
69
- "^5[0-9][0-9]$", "Server Error" ],
70
- "exact" => true,
71
- "regex" => true
72
- }
88
+ let(:event) { LogStash::Event.new("status" => "200 & 500") }
89
+
90
+ it "return the exact translation" do
91
+ subject.register
92
+ subject.filter(event)
93
+ expect(event.get("translation")).to eq("OK & Server Error")
94
+ end
73
95
  end
74
96
 
75
- let(:event) { LogStash::Event.new("status" => "200") }
97
+ context "when using a file based dictionary" do
98
+ let(:dictionary_path) { TranslateUtil.build_fixture_path("regex_union_dict.csv") }
99
+ let(:config) do
100
+ {
101
+ "source" => "status",
102
+ "target" => "translation",
103
+ "dictionary_path" => dictionary_path,
104
+ "refresh_interval" => 0,
105
+ "exact" => false,
106
+ "regex" => false
107
+ }
108
+ end
76
109
 
77
- it "return the exact translation" do
78
- subject.register
79
- subject.filter(event)
80
- expect(event.get("translation")).to eq("OK")
110
+ let(:event) { LogStash::Event.new("status" => "200 & 500") }
111
+
112
+ it "return the exact regex translation" do
113
+ subject.register
114
+ subject.filter(event)
115
+ expect(event.get("translation")).to eq("OK & Server Error")
116
+ end
81
117
  end
82
118
  end
83
119
 
84
- describe "fallback value" do
85
-
86
- context "static configuration" do
120
+ describe "regex translation" do
121
+ context "when using an inline dictionary" do
87
122
  let(:config) do
88
123
  {
89
- "field" => "status",
90
- "destination" => "translation",
91
- "fallback" => "no match"
124
+ "source" => "status",
125
+ "target" => "translation",
126
+ "dictionary" => [ "^2[0-9][0-9]$", "OK",
127
+ "^3[0-9][0-9]$", "Redirect",
128
+ "^4[0-9][0-9]$", "Client Error",
129
+ "^5[0-9][0-9]$", "Server Error" ],
130
+ "exact" => true,
131
+ "regex" => true
92
132
  }
93
133
  end
94
134
 
95
135
  let(:event) { LogStash::Event.new("status" => "200") }
96
136
 
97
- it "return the exact translation" do
137
+ it "return the exact regex translation" do
98
138
  subject.register
99
139
  subject.filter(event)
100
- expect(event.get("translation")).to eq("no match")
140
+ expect(event.get("translation")).to eq("OK")
101
141
  end
102
142
  end
103
143
 
104
- context "allow sprintf" do
144
+ context "when using a file based dictionary" do
145
+ let(:dictionary_path) { TranslateUtil.build_fixture_path("regex_dict.csv") }
105
146
  let(:config) do
106
147
  {
107
- "field" => "status",
108
- "destination" => "translation",
109
- "fallback" => "%{missing_translation}"
148
+ "source" => "status",
149
+ "target" => "translation",
150
+ "dictionary_path" => dictionary_path,
151
+ "refresh_interval" => 0,
152
+ "exact" => true,
153
+ "regex" => true
110
154
  }
111
155
  end
112
156
 
113
- let(:event) { LogStash::Event.new("status" => "200", "missing_translation" => "missing no match") }
157
+ let(:event) { LogStash::Event.new("status" => "200") }
114
158
 
115
- it "return the exact translation" do
159
+ it "return the exact regex translation" do
116
160
  subject.register
117
161
  subject.filter(event)
118
- expect(event.get("translation")).to eq("missing no match")
162
+ expect(event.get("translation")).to eq("OK")
119
163
  end
120
164
  end
121
165
  end
122
166
 
167
+ describe "fallback value", :ecs_compatibility_support do
168
+ ecs_compatibility_matrix(:disabled, :v1) do
169
+ before(:each) do
170
+ allow_any_instance_of(described_class).to receive(:ecs_compatibility).and_return(ecs_compatibility)
171
+ end
172
+
173
+ context "static configuration" do
174
+ let(:config) do
175
+ {
176
+ "source" => "status",
177
+ "target" => "translation",
178
+ "fallback" => "no match"
179
+ }
180
+ end
181
+
182
+ let(:event) { LogStash::Event.new("status" => "200") }
183
+
184
+ it "return the exact translation" do
185
+ subject.register
186
+ subject.filter(event)
187
+ expect(event.get("translation")).to eq("no match")
188
+ end
189
+ end
190
+
191
+ context "allow sprintf" do
192
+ let(:config) do
193
+ {
194
+ "source" => "status",
195
+ "target" => "translation",
196
+ "fallback" => "%{missing_translation}"
197
+ }
198
+ end
199
+
200
+ let(:event) { LogStash::Event.new("status" => "200", "missing_translation" => "missing no match") }
201
+
202
+ it "return the exact translation" do
203
+ subject.register
204
+ subject.filter(event)
205
+ expect(event.get("translation")).to eq("missing no match")
206
+ end
207
+ end
208
+
209
+ end
210
+ end
211
+
123
212
  describe "loading a dictionary" do
124
213
 
125
- let(:dictionary_path) { File.join(File.dirname(__FILE__), "..", "fixtures", "dict-wrong.yml") }
214
+ let(:dictionary_path) { TranslateUtil.build_fixture_path("dict-wrong.yml") }
126
215
 
127
216
  let(:config) do
128
217
  {
129
- "field" => "status",
130
- "destination" => "translation",
218
+ "source" => "status",
219
+ "target" => "translation",
131
220
  "dictionary_path" => dictionary_path,
221
+ "refresh_interval" => -1,
132
222
  "exact" => true,
133
223
  "regex" => false
134
224
  }
135
225
  end
136
226
 
137
227
  it "raises exception when loading" do
138
- error = "(#{dictionary_path}): mapping values are not allowed here at line 1 column 45 when loading dictionary file at #{dictionary_path}"
139
- expect { subject.register }.to raise_error("#{described_class}: #{error}")
228
+ error = /mapping values are not allowed here at line 1 column 45 when loading dictionary file/
229
+ expect { subject.register }.to raise_error(error)
140
230
  end
141
231
 
142
232
  context "when using a yml file" do
143
- let(:dictionary_path) { File.join(File.dirname(__FILE__), "..", "fixtures", "dict.yml") }
233
+ let(:dictionary_path) { TranslateUtil.build_fixture_path("dict.yml") }
144
234
  let(:event) { LogStash::Event.new("status" => "a") }
145
235
 
146
236
  it "return the exact translation" do
@@ -150,8 +240,30 @@ describe LogStash::Filters::Translate do
150
240
  end
151
241
  end
152
242
 
243
+ context "when using a map tagged yml file" do
244
+ let(:dictionary_path) { TranslateUtil.build_fixture_path("tag-map-dict.yml") }
245
+ let(:event) { LogStash::Event.new("status" => "six") }
246
+
247
+ it "return the exact translation" do
248
+ subject.register
249
+ subject.filter(event)
250
+ expect(event.get("translation")).to eq("val-6-1|val-6-2")
251
+ end
252
+ end
253
+
254
+ context "when using a omap tagged yml file" do
255
+ let(:dictionary_path) { TranslateUtil.build_fixture_path("tag-omap-dict.yml") }
256
+ let(:event) { LogStash::Event.new("status" => "nine") }
257
+
258
+ it "return the exact translation" do
259
+ subject.register
260
+ subject.filter(event)
261
+ expect(event.get("translation")).to eq("val-9-1|val-9-2")
262
+ end
263
+ end
264
+
153
265
  context "when using a json file" do
154
- let(:dictionary_path) { File.join(File.dirname(__FILE__), "..", "fixtures", "dict.json") }
266
+ let(:dictionary_path) { TranslateUtil.build_fixture_path("dict.json") }
155
267
  let(:event) { LogStash::Event.new("status" => "b") }
156
268
 
157
269
  it "return the exact translation" do
@@ -162,7 +274,7 @@ describe LogStash::Filters::Translate do
162
274
  end
163
275
 
164
276
  context "when using a csv file" do
165
- let(:dictionary_path) { File.join(File.dirname(__FILE__), "..", "fixtures", "dict.csv") }
277
+ let(:dictionary_path) { TranslateUtil.build_fixture_path("dict.csv") }
166
278
  let(:event) { LogStash::Event.new("status" => "c") }
167
279
 
168
280
  it "return the exact translation" do
@@ -172,20 +284,103 @@ describe LogStash::Filters::Translate do
172
284
  end
173
285
  end
174
286
 
175
- context "when using an uknown file" do
176
- let(:dictionary_path) { File.join(File.dirname(__FILE__), "..", "fixtures", "dict.other") }
287
+ context "when using an unknown file" do
288
+ let(:dictionary_path) { TranslateUtil.build_fixture_path("dict.other") }
177
289
 
178
- it "return the exact translation" do
179
- expect { subject.register }.to raise_error(RuntimeError, /Dictionary #{dictionary_path} have a non valid format/)
290
+ it "raises error" do
291
+ expect { subject.register }.to raise_error(RuntimeError, /Dictionary #{dictionary_path} has a non valid format/)
292
+ end
293
+ end
294
+ end
295
+
296
+ describe "iterate_on functionality" do
297
+ let(:config) do
298
+ {
299
+ "iterate_on" => "foo",
300
+ "source" => iterate_on_field,
301
+ "target" => "baz",
302
+ "fallback" => "nooo",
303
+ "dictionary_path" => dictionary_path,
304
+ # "override" => true,
305
+ "refresh_interval" => 0
306
+ }
307
+ end
308
+ let(:dictionary_path) { TranslateUtil.build_fixture_path("tag-map-dict.yml") }
309
+
310
+ describe "when iterate_on is the same as field, AKA array of values" do
311
+ let(:iterate_on_field) { "foo" }
312
+ let(:event) { LogStash::Event.new("foo" => ["nine","eight", "seven"]) }
313
+ it "adds a translation to target array for each value in field array" do
314
+ subject.register
315
+ subject.filter(event)
316
+ expect(event.get("baz")).to eq(["val-9-1|val-9-2", "val-8-1|val-8-2", "val-7-1|val-7-2"])
317
+ end
318
+ end
319
+
320
+ describe "when iterate_on is the same as field, AKA array of values, coerces integer elements to strings" do
321
+ let(:iterate_on_field) { "foo" }
322
+ let(:dictionary_path) { TranslateUtil.build_fixture_path("regex_union_dict.csv") }
323
+ let(:event) { LogStash::Event.new("foo" => [200, 300, 400]) }
324
+ it "adds a translation to target array for each value in field array" do
325
+ subject.register
326
+ subject.filter(event)
327
+ expect(event.get("baz")).to eq(["OK","Redirect","Client Error"])
180
328
  end
181
329
  end
330
+
331
+ describe "when iterate_on is not the same as field, AKA array of objects" do
332
+ let(:iterate_on_field) { "bar" }
333
+ let(:event) { LogStash::Event.new("foo" => [{"bar"=>"two"},{"bar"=>"one"}, {"bar"=>"six"}]) }
334
+ it "adds a translation to each map" do
335
+ subject.register
336
+ subject.filter(event)
337
+ expect(event.get("[foo][0][baz]")).to eq("val-2-1|val-2-2")
338
+ expect(event.get("[foo][1][baz]")).to eq("val-1-1|val-1-2")
339
+ expect(event.get("[foo][2][baz]")).to eq("val-6-1|val-6-2")
340
+ end
341
+ end
342
+
343
+ describe "when iterate_on is not the same as field, AKA array of objects, coerces integer values to strings" do
344
+ let(:iterate_on_field) { "bar" }
345
+ let(:dictionary_path) { TranslateUtil.build_fixture_path("regex_union_dict.csv") }
346
+ let(:event) { LogStash::Event.new("foo" => [{"bar"=>200},{"bar"=>300}, {"bar"=>400}]) }
347
+ it "adds a translation to each map" do
348
+ subject.register
349
+ subject.filter(event)
350
+ expect(event.get("[foo][0][baz]")).to eq("OK")
351
+ expect(event.get("[foo][1][baz]")).to eq("Redirect")
352
+ expect(event.get("[foo][2][baz]")).to eq("Client Error")
353
+ end
354
+ end
355
+ end
356
+
357
+ describe "field and destination are the same (explicit override)" do
358
+ let(:dictionary_path) { TranslateUtil.build_fixture_path("tag-map-dict.yml") }
359
+ let(:config) do
360
+ {
361
+ "field" => "foo",
362
+ "destination" => "foo",
363
+ "dictionary_path" => dictionary_path,
364
+ "override" => true,
365
+ "refresh_interval" => -1,
366
+ "ecs_compatibility" => 'disabled'
367
+ }
368
+ end
369
+
370
+ let(:event) { LogStash::Event.new("foo" => "nine") }
371
+
372
+ it "overwrites existing value" do
373
+ subject.register
374
+ subject.filter(event)
375
+ expect(event.get("foo")).to eq("val-9-1|val-9-2")
376
+ end
182
377
  end
183
378
 
184
- describe "general configuration" do
185
- let(:dictionary_path) { File.join(File.dirname(__FILE__), "..", "fixtures", "dict.yml") }
379
+ context "invalid dictionary configuration" do
380
+ let(:dictionary_path) { TranslateUtil.build_fixture_path("dict.yml") }
186
381
  let(:config) do
187
382
  {
188
- "field" => "random field",
383
+ "source" => "random field",
189
384
  "dictionary" => { "a" => "b" },
190
385
  "dictionary_path" => dictionary_path,
191
386
  }
@@ -196,6 +391,77 @@ describe LogStash::Filters::Translate do
196
391
  end
197
392
  end
198
393
 
394
+ context "invalid target+destination configuration" do
395
+ let(:config) do
396
+ {
397
+ "source" => "message",
398
+ "target" => 'foo',
399
+ "destination" => 'bar',
400
+ }
401
+ end
402
+
403
+ it "raises an exception if both 'target' and 'destination' are set" do
404
+ expect { subject.register }.to raise_error(LogStash::ConfigurationError, /remove .*?destination => /)
405
+ end
406
+ end
407
+
408
+ context "invalid source+field configuration" do
409
+ let(:config) do
410
+ {
411
+ "source" => "message",
412
+ "field" => 'foo'
413
+ }
414
+ end
415
+
416
+ it "raises an exception if both 'source' and 'field' are set" do
417
+ expect { subject.register }.to raise_error(LogStash::ConfigurationError, /remove .*?field => /)
418
+ end
419
+ end
420
+
421
+ context "destination option" do
422
+ let(:config) do
423
+ {
424
+ "source" => "message", "destination" => 'bar', "ecs_compatibility" => 'v1'
425
+ }
426
+ end
427
+
428
+ it "sets the target" do
429
+ subject.register
430
+ expect( subject.target ).to eql 'bar'
431
+
432
+ expect(logger).to have_received(:debug).with(a_string_including "intercepting `destination`")
433
+ expect(deprecation_logger).to have_received(:deprecated).with(a_string_including "`destination` option is deprecated; use `target` instead.")
434
+ end
435
+ end
436
+
437
+ context "field option" do
438
+ let(:config) do
439
+ {
440
+ "field" => "message", "target" => 'bar'
441
+ }
442
+ end
443
+
444
+ it "sets the source" do
445
+ subject.register # does not raise
446
+ expect( subject.source ).to eql 'message'
447
+
448
+ expect(logger).to have_received(:debug).with(a_string_including "intercepting `field`")
449
+ expect(deprecation_logger).to have_received(:deprecated).with(a_string_including "`field` option is deprecated; use `source` instead.")
450
+ end
451
+ end
452
+
453
+ context "source option" do
454
+ let(:config) do
455
+ {
456
+ "target" => 'bar'
457
+ }
458
+ end
459
+
460
+ it "is required to be set" do
461
+ expect { subject.register }.to raise_error(LogStash::ConfigurationError, /provide .*?source => /)
462
+ end
463
+ end
464
+
199
465
  describe "refresh_behaviour" do
200
466
  let(:dictionary_content) { "a : 1\nb : 2\nc : 3" }
201
467
  let(:modified_content) { "a : 1\nb : 4" }
@@ -203,10 +469,10 @@ describe LogStash::Filters::Translate do
203
469
  let(:refresh_behaviour) { "merge" }
204
470
  let(:config) do
205
471
  {
206
- "field" => "status",
207
- "destination" => "translation",
472
+ "source" => "status",
473
+ "target" => "translation",
208
474
  "dictionary_path" => dictionary_path,
209
- "refresh_interval" => 10000, # we're controlling this manually
475
+ "refresh_interval" => -1, # we're controlling this manually
210
476
  "exact" => true,
211
477
  "regex" => false,
212
478
  "fallback" => "no match",
@@ -229,7 +495,7 @@ describe LogStash::Filters::Translate do
229
495
  it "overwrites existing entries" do
230
496
  subject.filter(before_mod)
231
497
  IO.write(dictionary_path, modified_content)
232
- subject.send(:load_dictionary)
498
+ subject.lookup.load_dictionary
233
499
  subject.filter(after_mod)
234
500
  expect(before_mod.get("translation")).to eq(2)
235
501
  expect(after_mod.get("translation")).to eq(4)
@@ -237,7 +503,7 @@ describe LogStash::Filters::Translate do
237
503
  it "keeps leftover entries" do
238
504
  subject.filter(before_del)
239
505
  IO.write(dictionary_path, modified_content)
240
- subject.send(:load_dictionary)
506
+ subject.lookup.load_dictionary
241
507
  subject.filter(after_del)
242
508
  expect(before_del.get("translation")).to eq(3)
243
509
  expect(after_del.get("translation")).to eq(3)
@@ -249,7 +515,7 @@ describe LogStash::Filters::Translate do
249
515
  it "overwrites existing entries" do
250
516
  subject.filter(before_mod)
251
517
  IO.write(dictionary_path, modified_content)
252
- subject.send(:load_dictionary)
518
+ subject.lookup.load_dictionary
253
519
  subject.filter(after_mod)
254
520
  expect(before_mod.get("translation")).to eq(2)
255
521
  expect(after_mod.get("translation")).to eq(4)
@@ -257,11 +523,135 @@ describe LogStash::Filters::Translate do
257
523
  it "removes leftover entries" do
258
524
  subject.filter(before_del)
259
525
  IO.write(dictionary_path, modified_content)
260
- subject.send(:load_dictionary)
526
+ subject.lookup.load_dictionary
261
527
  subject.filter(after_del)
262
528
  expect(before_del.get("translation")).to eq(3)
263
529
  expect(after_del.get("translation")).to eq("no match")
264
530
  end
265
531
  end
266
532
  end
533
+
534
+ describe "loading an empty dictionary" do
535
+ let(:directory) { Pathname.new(Stud::Temporary.directory) }
536
+
537
+ let(:config) do
538
+ {
539
+ "source" => "status",
540
+ "target" => "translation",
541
+ "dictionary_path" => dictionary_path.to_path,
542
+ "refresh_interval" => -1,
543
+ "fallback" => "no match",
544
+ "exact" => true,
545
+ "regex" => false
546
+ }
547
+ end
548
+
549
+ before do
550
+ dictionary_path.open("wb") do |file|
551
+ file.write("")
552
+ end
553
+ end
554
+
555
+ context "when using a yml file" do
556
+ let(:dictionary_path) { directory.join("dict-e.yml") }
557
+ let(:event) { LogStash::Event.new("status" => "a") }
558
+
559
+ it "return the exact translation" do
560
+
561
+ subject.register
562
+ subject.filter(event)
563
+ expect(event.get("translation")).to eq("no match")
564
+ end
565
+ end
566
+
567
+ context "when using a json file" do
568
+ let(:dictionary_path) { directory.join("dict-e.json") }
569
+ let(:event) { LogStash::Event.new("status" => "b") }
570
+
571
+ it "return the exact translation" do
572
+ subject.register
573
+ subject.filter(event)
574
+ expect(event.get("translation")).to eq("no match")
575
+ end
576
+ end
577
+
578
+ context "when using a csv file" do
579
+ let(:dictionary_path) { directory.join("dict-e.csv") }
580
+ let(:event) { LogStash::Event.new("status" => "c") }
581
+
582
+ it "return the exact translation" do
583
+ subject.register
584
+ subject.filter(event)
585
+ expect(event.get("translation")).to eq("no match")
586
+ end
587
+ end
588
+ end
589
+
590
+ describe "default target" do
591
+
592
+ let(:config) do
593
+ {
594
+ "source" => "message",
595
+ "dictionary" => { "foo" => "bar" }
596
+ }
597
+ end
598
+
599
+ let(:event) { LogStash::Event.new("message" => "foo") }
600
+
601
+ before { subject.register }
602
+
603
+ context "legacy mode" do
604
+
605
+ let(:config) { super().merge('ecs_compatibility' => 'disabled') }
606
+
607
+ it "uses the translation target" do
608
+ subject.filter(event)
609
+ expect(event.get("translation")).to eq("bar")
610
+ expect(event.get("message")).to eq("foo")
611
+ end
612
+
613
+ end
614
+
615
+ context "ECS mode" do
616
+
617
+ let(:config) { super().merge('ecs_compatibility' => 'v1') }
618
+
619
+ it "does in place translation" do
620
+ subject.filter(event)
621
+ expect(event.include?("translation")).to be false
622
+ expect(event.get("message")).to eq("bar")
623
+ end
624
+
625
+ end
626
+
627
+ end
628
+
629
+
630
+ describe "error handling" do
631
+
632
+ let(:config) do
633
+ {
634
+ "source" => "message",
635
+ "dictionary" => { "foo" => "bar" }
636
+ }
637
+ end
638
+
639
+ let(:event) { LogStash::Event.new("message" => "foo") }
640
+
641
+ before { subject.register }
642
+
643
+ it "handles unexpected error within filter" do
644
+ expect(subject.updater).to receive(:update).and_raise RuntimeError.new('TEST')
645
+
646
+ expect { subject.filter(event) }.to_not raise_error
647
+ end
648
+
649
+ it "propagates Java errors" do
650
+ expect(subject.updater).to receive(:update).and_raise java.lang.OutOfMemoryError.new('FAKE-OUT!')
651
+
652
+ expect { subject.filter(event) }.to raise_error(java.lang.OutOfMemoryError)
653
+ end
654
+
655
+ end
656
+
267
657
  end