logstash-filter-translate 3.5.0 → 3.5.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a657937387054b277b294032c5acb3ea34bc8255e3c0e344894ee1160d7d1fa5
4
- data.tar.gz: 61c5fdbd1fb8e702109eb494caeffe3100a912e2fb4de98491848969dd7265f1
3
+ metadata.gz: bac5f7aca89b5e4261183c450ef1fb494ea29a48dc0c32157863a5176238663a
4
+ data.tar.gz: 24b5b67a25d7cefd48d04fd8d9789583166c748e6901d38a569d61cbc68f069c
5
5
  SHA512:
6
- metadata.gz: 595cbd7c55aa076f4ce4818654cd02fe0c181527266cc5f808a1bf85c8ad7c6864845f12e7401779aecde04b2017e50282af5b9a137d2284077a70a21055c183
7
- data.tar.gz: dc77c15b65f70981a77271c174c2fcc64266d8f1cd59925e31d334c8293c0a683597ee130f40213ced66b3ffd7c3ca00b359b937685f499eb4f27118ad28f512
6
+ metadata.gz: 38ab5fecaf9a92700cae046e16dc0025b31be2adab0e2dc44609096cef043efff6afe069210010e8db3932c0710aadb29efe3fe879d5ad981345043b838b5ec6
7
+ data.tar.gz: c0d28aaaf7ab1c08c10ccec79bc25fef1e2c5e86fed295bed81ddcb1f0c84ae184917c867c37e6c9273eff681b5f34bded08b118a3936b279b99f0f1a673c22b
data/CHANGELOG.md CHANGED
@@ -1,3 +1,6 @@
1
+ ## 3.5.1
2
+ - Fixes an issue where failing to load a dictionary could cause the plugin to continue to run with a missing or partially-updated dictionary; this issue was especially noticeable when configured with `refresh_behaviour => replace`, which clears the dictionary before loading the replacement [#112](https://github.com/logstash-plugins/logstash-filter-translate/issues/112).
3
+
1
4
  ## 3.5.0
2
5
  - Introduce opt-in "yaml_load_strategy => streaming" to stream parse YAML dictionaries [#106](https://github.com/logstash-plugins/logstash-filter-translate/pull/106)
3
6
 
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Logstash Plugin
2
2
 
3
- [![Travis Build Status](https://travis-ci.com/logstash-plugins/logstash-filter-translate.svg)](https://travis-ci.com/logstash-plugins/logstash-filter-translate)
3
+ [![Unit Tests](https://github.com/logstash-plugins/logstash-filter-translate/actions/workflows/unit-tests.yml/badge.svg?branch=main)](https://github.com/logstash-plugins/logstash-filter-translate/actions/workflows/unit-tests.yml)
4
4
 
5
5
  This is a plugin for [Logstash](https://github.com/elastic/logstash).
6
6
 
@@ -6,9 +6,11 @@ module LogStash module Filters module Dictionary
6
6
 
7
7
  protected
8
8
 
9
- def read_file_into_dictionary
9
+ def read_dictionary
10
+ return enum_for(:read_dictionary) unless block_given?
11
+
10
12
  ::CSV.open(@dictionary_path, 'r:bom|utf-8') do |csv|
11
- csv.each { |k,v| @dictionary[k] = v }
13
+ csv.each { |k,v| yield(k,v) }
12
14
  end
13
15
  end
14
16
  end
@@ -72,27 +72,50 @@ module LogStash module Filters module Dictionary
72
72
  # sub class specific initializer
73
73
  end
74
74
 
75
- def read_file_into_dictionary
76
- # defined in csv_file, yaml_file and json_file
75
+ ##
76
+ # read the dictionary from file into a new hash
77
+ # @yieldparam [String] key
78
+ # @yieldparam [Object] value
79
+ def read_dictionary
80
+ # defined in concrete implementation
81
+ fail NotImplementedError, "#{self.class} does not implement `read_dictionary`"
77
82
  end
78
83
 
79
84
  private
80
85
 
86
+ ##
87
+ # merges the parsed contents of the dictionary file into the current dictionary.
88
+ # This is an all-or-nothing operation.
89
+ # @return [void]
81
90
  def merge_dictionary
82
91
  @write_lock.lock
83
92
  begin
84
- read_file_into_dictionary
93
+ changes = {}
94
+ read_dictionary do |key, value|
95
+ changes[key] = value unless value == @dictionary[key]
96
+ end
97
+ @dictionary.update(changes)
85
98
  @fetch_strategy.dictionary_updated
86
99
  ensure
87
100
  @write_lock.unlock
88
101
  end
89
102
  end
90
103
 
104
+ ##
105
+ # replaces the current dictionary with the parsed contents of the dictionary file,
106
+ # using the existing value objects where possible to limit memory growth.
107
+ # This is an all-or-nothing operation.
108
+ # @return [void]
91
109
  def replace_dictionary
92
110
  @write_lock.lock
93
111
  begin
94
- @dictionary.clear
95
- read_file_into_dictionary
112
+ replacement_dictionary = {}
113
+ read_dictionary do |key, value|
114
+ existing_value = @dictionary[key]
115
+ # if the value is equivalent to the existing value, use the existing value instead
116
+ replacement_dictionary[key] = (existing_value == value) ? existing_value : value
117
+ end
118
+ @dictionary.replace(replacement_dictionary)
96
119
  @fetch_strategy.dictionary_updated
97
120
  ensure
98
121
  @write_lock.unlock
@@ -4,11 +4,18 @@ require "json"
4
4
  module LogStash module Filters module Dictionary
5
5
  class JsonFile < File
6
6
 
7
+ EMPTY = Hash.new.freeze
8
+ private_constant :EMPTY
9
+
7
10
  protected
8
11
 
9
- def read_file_into_dictionary
12
+ def read_dictionary
13
+ return enum_for(:read_dictionary) unless block_given?
14
+
10
15
  content = IO.read(@dictionary_path, :mode => 'r:bom|utf-8')
11
- @dictionary.update(LogStash::Json.load(content)) unless content.nil? || content.empty?
16
+ return EMPTY if content.nil? || content.empty?
17
+
18
+ LogStash::Json.load(content).each_pair { |key, value| yield(key, value) }
12
19
  end
13
20
  end
14
21
  end end end
@@ -13,20 +13,23 @@ module LogStash module Filters module Dictionary
13
13
  @yaml_load_strategy = file_type_args[:yaml_load_strategy]
14
14
  end
15
15
 
16
- def read_file_into_dictionary
16
+ def read_dictionary
17
+ return enum_for(:read_dictionary) unless block_given?
18
+
17
19
  if @yaml_load_strategy == "one_shot"
18
20
  visitor = YamlVisitor.create
19
21
  parser = Psych::Parser.new(Psych::TreeBuilder.new)
20
22
  parser.code_point_limit = @yaml_code_point_limit
21
23
  # low level YAML read that tries to create as
22
24
  # few intermediate objects as possible
23
- # this overwrites the value at key
24
25
  yaml_string = IO.read(@dictionary_path, :mode => 'r:bom|utf-8')
25
26
  parser.parse(yaml_string, @dictionary_path)
26
- visitor.accept_with_dictionary(@dictionary, parser.handler.root)
27
+ temp_dictionary = {}
28
+ visitor.accept_with_dictionary(temp_dictionary, parser.handler.root)
29
+ temp_dictionary.each_pair { |key, value| yield(key, value) }
27
30
  else # stream parse it
28
31
  parser = StreamingYamlDictParser.new(@dictionary_path, @yaml_code_point_limit)
29
- parser.each_pair {|key, value| @dictionary[key] = value }
32
+ parser.each_pair {|key, value| yield(key, value) }
30
33
  end
31
34
  end
32
35
  end
@@ -249,11 +249,11 @@ class Translate < LogStash::Filters::Base
249
249
  @updater = ArrayOfMapsValueUpdate.new(@iterate_on, @source, @target, @fallback, @lookup)
250
250
  end
251
251
 
252
- @logger.debug? && @logger.debug("#{self.class.name}: Dictionary - ", :dictionary => @lookup.dictionary)
252
+ logger.debug? && logger.debug("#{self.class.name}: Dictionary - ", :dictionary => @lookup.dictionary)
253
253
  if @exact
254
- @logger.debug? && @logger.debug("#{self.class.name}: Dictionary translation method - Exact")
254
+ logger.debug? && logger.debug("#{self.class.name}: Dictionary translation method - Exact")
255
255
  else
256
- @logger.debug? && @logger.debug("#{self.class.name}: Dictionary translation method - Fuzzy")
256
+ logger.debug? && logger.debug("#{self.class.name}: Dictionary translation method - Fuzzy")
257
257
  end
258
258
 
259
259
  if @lookup.respond_to?(:reload_dictionary) && @refresh_interval > 0 # a scheduler interval of zero makes no sense
@@ -1,7 +1,7 @@
1
1
  Gem::Specification.new do |s|
2
2
 
3
3
  s.name = 'logstash-filter-translate'
4
- s.version = '3.5.0'
4
+ s.version = ::File.read('version').split("\n").first
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"
@@ -11,7 +11,7 @@ Gem::Specification.new do |s|
11
11
  s.require_paths = ["lib"]
12
12
 
13
13
  # Files
14
- s.files = Dir["lib/**/*","spec/**/*","*.gemspec","*.md","CONTRIBUTORS","Gemfile","LICENSE","NOTICE.TXT", "vendor/jar-dependencies/**/*.jar", "vendor/jar-dependencies/**/*.rb", "VERSION", "docs/**/*"]
14
+ s.files = Dir["lib/**/*","spec/**/*","*.gemspec","*.md","CONTRIBUTORS","Gemfile","LICENSE","NOTICE.TXT", "vendor/jar-dependencies/**/*.jar", "vendor/jar-dependencies/**/*.rb", "VERSION", "version", "docs/**/*"]
15
15
 
16
16
  # Tests
17
17
  s.test_files = s.files.grep(%r{^(test|spec|features)/})
@@ -1,4 +1,5 @@
1
1
  # encoding: utf-8
2
+ # frozen_string_literal: true
2
3
  require 'rspec/wait'
3
4
  require "logstash/devutils/rspec/spec_helper"
4
5
  require "support/rspec_wait_handler_helper"
@@ -35,6 +36,7 @@ describe LogStash::Filters::Translate do
35
36
  file.puts("a,1\nb,2\nc,3\n")
36
37
  end
37
38
  subject.register
39
+ allow(subject.lookup).to receive(:logger).and_return(double("LookupLogger").as_null_object)
38
40
  end
39
41
 
40
42
  after do
@@ -71,6 +73,95 @@ describe LogStash::Filters::Translate do
71
73
  actions.activate_quietly
72
74
  actions.assert_no_errors
73
75
  end
76
+
77
+ it 'uses the replacement dictionary after reload' do
78
+ actions = RSpec::Sequencing
79
+ .run("translate") do
80
+ event_a = LogStash::Event.new("status" => "a" )
81
+ event_b = LogStash::Event.new("status" => "b" )
82
+ event_c = LogStash::Event.new("status" => "c" )
83
+ event_d = LogStash::Event.new("status" => "d" )
84
+
85
+ subject.multi_filter([event_a, event_b, event_c, event_d])
86
+
87
+ expect(event_a.get("translation")).to eq('1')
88
+ expect(event_b.get("translation")).to eq('2')
89
+ expect(event_c.get("translation")).to eq('3')
90
+ expect(event_d.get("translation")).to be_nil
91
+ end
92
+ .then("modify file with new CSV") do
93
+ dictionary_path.open("w") do |file|
94
+ file.puts("a,11\nb,12\nd,14\n") # changes a+b, removes c, adds d
95
+ end
96
+ end
97
+ .then_after(2, "translate again, ensuring we use the replacement dictionary") do
98
+
99
+ event_a = LogStash::Event.new("status" => "a" )
100
+ event_b = LogStash::Event.new("status" => "b" )
101
+ event_c = LogStash::Event.new("status" => "c" )
102
+ event_d = LogStash::Event.new("status" => "d" )
103
+
104
+ subject.multi_filter([event_a, event_b, event_c, event_d])
105
+
106
+ expect(event_a.get("translation")).to eq('11')
107
+ expect(event_b.get("translation")).to eq('12')
108
+ expect(event_c.get("translation")).to be_nil # not present in updated dict
109
+ expect(event_d.get("translation")).to eq('14')
110
+ end
111
+ .then("stop") do
112
+ subject.close
113
+ end
114
+
115
+ actions.activate_quietly
116
+ actions.assert_no_errors
117
+ end
118
+
119
+ context "when replacement file is corrupt" do
120
+
121
+ it "logs a warning with the parse error but keeps processing with existing definitions" do
122
+ actions = RSpec::Sequencing
123
+ .run("translate") do
124
+ event_a = LogStash::Event.new("status" => "a" )
125
+ event_b = LogStash::Event.new("status" => "b" )
126
+ event_c = LogStash::Event.new("status" => "c" )
127
+ event_d = LogStash::Event.new("status" => "d" )
128
+
129
+ subject.multi_filter([event_a, event_b, event_c, event_d])
130
+
131
+ expect(event_a.get("translation")).to eq('1')
132
+ expect(event_b.get("translation")).to eq('2')
133
+ expect(event_c.get("translation")).to eq('3')
134
+ expect(event_d.get("translation")).to be_nil
135
+ end
136
+ .then("modify file with invalid CSV") do
137
+ dictionary_path.open("w") do |file|
138
+ file.puts("a,11\nb,12\n\"\xFF".dup.force_encoding("UTF-8")) # intentional broken utf-8
139
+ end
140
+ end
141
+ .then_after(2, "wait for dictionary reload attempt and ensure logs were emitted") do
142
+ wait_for { subject.lookup.logger }.to have_received(:warn).with(/continuing with old dictionary/, anything)
143
+ end
144
+ .then("translate again, ensuring we still use the old dictionary") do
145
+
146
+ event_a = LogStash::Event.new("status" => "a" )
147
+ event_b = LogStash::Event.new("status" => "b" )
148
+ event_c = LogStash::Event.new("status" => "c" )
149
+ event_d = LogStash::Event.new("status" => "d" )
150
+
151
+ subject.multi_filter([event_a, event_b, event_c, event_d])
152
+
153
+ expect(event_a.get("translation")).to eq('1')
154
+ expect(event_b.get("translation")).to eq('2')
155
+ expect(event_c.get("translation")).to eq('3')
156
+ expect(event_d.get("translation")).to be_nil
157
+ end
158
+ .then("stop") do
159
+ subject.close
160
+ end
161
+ actions.activate_quietly
162
+ actions.assert_no_errors
163
+ end
164
+ end
74
165
  end
75
166
 
76
167
  context "merge" do
@@ -102,6 +193,92 @@ describe LogStash::Filters::Translate do
102
193
  actions.activate_quietly
103
194
  actions.assert_no_errors
104
195
  end
196
+
197
+ it 'uses the merged dictionary after reload' do
198
+ actions = RSpec::Sequencing
199
+ .run("translate") do
200
+ event_a = LogStash::Event.new("status" => "a" )
201
+ event_b = LogStash::Event.new("status" => "b" )
202
+ event_c = LogStash::Event.new("status" => "c" )
203
+ event_d = LogStash::Event.new("status" => "d" )
204
+
205
+ subject.multi_filter([event_a, event_b, event_c, event_d])
206
+
207
+ expect(event_a.get("translation")).to eq('1')
208
+ expect(event_b.get("translation")).to eq('2')
209
+ expect(event_c.get("translation")).to eq('3')
210
+ expect(event_d.get("translation")).to be_nil
211
+ end
212
+ .then("modify file with new CSV") do
213
+ dictionary_path.open("w") do |file|
214
+ file.puts("a,11\nb,12\nd,14\n") # changes a+b, removes c, adds d
215
+ end
216
+ end
217
+ .then_after(2, "translate again, ensuring we use the merged dictionary") do
218
+ event_a = LogStash::Event.new("status" => "a" )
219
+ event_b = LogStash::Event.new("status" => "b" )
220
+ event_c = LogStash::Event.new("status" => "c" )
221
+ event_d = LogStash::Event.new("status" => "d" )
222
+
223
+ subject.multi_filter([event_a, event_b, event_c, event_d])
224
+
225
+ expect(event_a.get("translation")).to eq('11')
226
+ expect(event_b.get("translation")).to eq('12')
227
+ expect(event_c.get("translation")).to eq('3') # deleted in update, uses old value after merge
228
+ expect(event_d.get("translation")).to eq('14')
229
+ end
230
+ .then("stop") do
231
+ subject.close
232
+ end
233
+ actions.activate_quietly
234
+ actions.assert_no_errors
235
+ end
236
+
237
+ context "when replacement file is corrupt" do
238
+
239
+ it "logs a warning with the parse error but keeps processing with existing definitions" do
240
+ actions = RSpec::Sequencing
241
+ .run("translate") do
242
+ event_a = LogStash::Event.new("status" => "a" )
243
+ event_b = LogStash::Event.new("status" => "b" )
244
+ event_c = LogStash::Event.new("status" => "c" )
245
+ event_d = LogStash::Event.new("status" => "d" )
246
+
247
+ subject.multi_filter([event_a, event_b, event_c, event_d])
248
+
249
+ expect(event_a.get("translation")).to eq('1')
250
+ expect(event_b.get("translation")).to eq('2')
251
+ expect(event_c.get("translation")).to eq('3')
252
+ expect(event_d.get("translation")).to be_nil
253
+ end
254
+ .then("modify file with invalid CSV") do
255
+ dictionary_path.open("w") do |file|
256
+ file.puts("a,11\nb,12\n\"\xFF".dup.force_encoding("UTF-8")) # intentional broken utf-8
257
+ end
258
+ end
259
+ .then_after(2, "wait for dictionary reload attempt and ensure logs were emitted") do
260
+ wait_for { subject.lookup.logger }.to have_received(:warn).with(/continuing with old dictionary/, anything)
261
+ end
262
+ .then("translate again, ensuring we still use the old dictionary") do
263
+ event_a = LogStash::Event.new("status" => "a" )
264
+ event_b = LogStash::Event.new("status" => "b" )
265
+ event_c = LogStash::Event.new("status" => "c" )
266
+ event_d = LogStash::Event.new("status" => "d" )
267
+
268
+ subject.multi_filter([event_a, event_b, event_c, event_d])
269
+
270
+ expect(event_a.get("translation")).to eq('1')
271
+ expect(event_b.get("translation")).to eq('2')
272
+ expect(event_c.get("translation")).to eq('3')
273
+ expect(event_d.get("translation")).to be_nil
274
+ end
275
+ .then("stop") do
276
+ subject.close
277
+ end
278
+ actions.activate_quietly
279
+ actions.assert_no_errors
280
+ end
281
+ end
105
282
  end
106
283
  end
107
284
 
@@ -127,6 +304,7 @@ describe LogStash::Filters::Translate do
127
304
  directory
128
305
  wait(1.0).for{Dir.exist?(directory)}.to eq(true)
129
306
  LogStash::Filters::Dictionary.create_huge_json_dictionary(directory, "dict-h.json", dictionary_size)
307
+ allow(subject).to receive(:logger).and_return(double("Logger").as_null_object)
130
308
  subject.register
131
309
  end
132
310
 
@@ -176,6 +354,7 @@ describe LogStash::Filters::Translate do
176
354
  directory
177
355
  wait(1.0).for{Dir.exist?(directory)}.to eq(true)
178
356
  LogStash::Filters::Dictionary.create_huge_csv_dictionary(directory, "dict-h.csv", dictionary_size)
357
+ allow(subject).to receive(:logger).and_return(double("Logger").as_null_object)
179
358
  subject.register
180
359
  end
181
360
 
@@ -1,4 +1,5 @@
1
1
  # encoding: utf-8
2
+ # frozen_string_literal: true
2
3
 
3
4
  require "securerandom"
4
5
 
@@ -20,10 +21,10 @@ module LogStash module Filters module Dictionary
20
21
  tmppath = directory.join("temp_big.json")
21
22
  tmppath.open("w") do |file|
22
23
  file.puts("{")
23
- file.puts(' "foo":"'.concat(SecureRandom.hex(4)).concat('",'))
24
- file.puts(' "bar":"'.concat(SecureRandom.hex(4)).concat('",'))
24
+ file.puts(%Q( "foo":"#{SecureRandom.hex(4)}",))
25
+ file.puts(%Q( "bar":"#{SecureRandom.hex(4)}",))
25
26
  size.times do |i|
26
- file.puts(' "'.concat(SecureRandom.hex(12)).concat('":"').concat("#{1000000 + i}").concat('",'))
27
+ file.puts(%Q( "#{SecureRandom.hex(12)}":"#{1000000 + i}",))
27
28
  end
28
29
  file.puts(' "baz":"quux"')
29
30
  file.puts("}")
data/version ADDED
@@ -0,0 +1 @@
1
+ 3.5.1
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: logstash-filter-translate
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.5.0
4
+ version: 3.5.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Elastic
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-08-04 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: logstash-core-plugin-api
@@ -199,6 +199,7 @@ files:
199
199
  - spec/fixtures/tag-omap-dict.yml
200
200
  - spec/support/build_huge_dictionaries.rb
201
201
  - spec/support/rspec_wait_handler_helper.rb
202
+ - version
202
203
  homepage: http://www.elastic.co/guide/en/logstash/current/index.html
203
204
  licenses:
204
205
  - Apache License (2.0)
@@ -219,7 +220,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
219
220
  - !ruby/object:Gem::Version
220
221
  version: '0'
221
222
  requirements: []
222
- rubygems_version: 3.6.3
223
+ rubygems_version: 3.7.2
223
224
  specification_version: 4
224
225
  summary: Replaces field contents based on a hash or YAML file
225
226
  test_files: