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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +27 -0
- data/LICENSE +199 -10
- data/README.md +1 -1
- data/docs/index.asciidoc +257 -56
- data/lib/logstash/filters/array_of_maps_value_update.rb +44 -0
- data/lib/logstash/filters/array_of_values_update.rb +47 -0
- data/lib/logstash/filters/dictionary/csv_file.rb +25 -0
- data/lib/logstash/filters/dictionary/file.rb +143 -0
- data/lib/logstash/filters/dictionary/json_file.rb +87 -0
- data/lib/logstash/filters/dictionary/memory.rb +32 -0
- data/lib/logstash/filters/dictionary/yaml_file.rb +24 -0
- data/lib/logstash/filters/dictionary/yaml_visitor.rb +42 -0
- data/lib/logstash/filters/fetch_strategy/file.rb +81 -0
- data/lib/logstash/filters/fetch_strategy/memory.rb +52 -0
- data/lib/logstash/filters/single_value_update.rb +49 -0
- data/lib/logstash/filters/translate.rb +104 -158
- data/logstash-filter-translate.gemspec +8 -1
- data/spec/filters/benchmark_rspec.rb +69 -0
- data/spec/filters/scheduling_spec.rb +201 -0
- data/spec/filters/translate_spec.rb +463 -73
- data/spec/filters/yaml_visitor_spec.rb +16 -0
- data/spec/fixtures/regex_dict.csv +4 -0
- data/spec/fixtures/regex_union_dict.csv +4 -0
- data/spec/fixtures/tag-map-dict.yml +21 -0
- data/spec/fixtures/tag-omap-dict.yml +21 -0
- data/spec/support/build_huge_dictionaries.rb +33 -0
- data/spec/support/rspec_wait_handler_helper.rb +38 -0
- metadata +129 -2
@@ -1,7 +1,7 @@
|
|
1
1
|
Gem::Specification.new do |s|
|
2
2
|
|
3
3
|
s.name = 'logstash-filter-translate'
|
4
|
-
s.version = '3.
|
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
|
-
"
|
15
|
-
"
|
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
|
-
|
21
|
-
|
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 "
|
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
|
-
"
|
40
|
-
"
|
41
|
-
"dictionary" => [ "
|
42
|
-
"
|
43
|
-
"
|
44
|
-
"
|
45
|
-
|
46
|
-
|
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" =>
|
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
|
69
|
+
expect(event.get("translation")).to be_nil
|
56
70
|
end
|
57
|
-
|
58
71
|
end
|
59
72
|
|
60
|
-
describe "
|
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
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
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
|
-
|
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
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
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 "
|
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
|
-
"
|
90
|
-
"
|
91
|
-
"
|
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("
|
140
|
+
expect(event.get("translation")).to eq("OK")
|
101
141
|
end
|
102
142
|
end
|
103
143
|
|
104
|
-
context "
|
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
|
-
"
|
108
|
-
"
|
109
|
-
"
|
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"
|
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("
|
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) {
|
214
|
+
let(:dictionary_path) { TranslateUtil.build_fixture_path("dict-wrong.yml") }
|
126
215
|
|
127
216
|
let(:config) do
|
128
217
|
{
|
129
|
-
"
|
130
|
-
"
|
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 =
|
139
|
-
expect { subject.register }.to raise_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) {
|
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) {
|
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) {
|
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
|
176
|
-
let(:dictionary_path) {
|
287
|
+
context "when using an unknown file" do
|
288
|
+
let(:dictionary_path) { TranslateUtil.build_fixture_path("dict.other") }
|
177
289
|
|
178
|
-
it "
|
179
|
-
expect { subject.register }.to raise_error(RuntimeError, /Dictionary #{dictionary_path}
|
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
|
-
|
185
|
-
let(:dictionary_path) {
|
379
|
+
context "invalid dictionary configuration" do
|
380
|
+
let(:dictionary_path) { TranslateUtil.build_fixture_path("dict.yml") }
|
186
381
|
let(:config) do
|
187
382
|
{
|
188
|
-
"
|
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
|
-
"
|
207
|
-
"
|
472
|
+
"source" => "status",
|
473
|
+
"target" => "translation",
|
208
474
|
"dictionary_path" => dictionary_path,
|
209
|
-
"refresh_interval" =>
|
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.
|
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.
|
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.
|
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.
|
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
|