logstash-filter-translate 3.4.3 → 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: 653e221a65b8b528a26f20f1b4c94ce74ecab0ba09e274bba23f0a605819672b
4
- data.tar.gz: fae94de89c0a5da2bb87aef35f1aca31fbaefb8717cc292b5f364a886f66e30b
3
+ metadata.gz: bac5f7aca89b5e4261183c450ef1fb494ea29a48dc0c32157863a5176238663a
4
+ data.tar.gz: 24b5b67a25d7cefd48d04fd8d9789583166c748e6901d38a569d61cbc68f069c
5
5
  SHA512:
6
- metadata.gz: 60feb1d75c7579f5b27ee4887f8eec27049f2b54c75b0c554923c958ae46d64261b53a5d494d39c3ac3194d03ceff6bbdff2a92e39e879a0c44d491221b34bd1
7
- data.tar.gz: 69c95f3a4313baf6f7bc40557ac73f173405607873dfd483310c8f62da415c9e46d2d24c52133cabe22011bd823f2029272023c6e903c02fc5328a926ef32e72
6
+ metadata.gz: 38ab5fecaf9a92700cae046e16dc0025b31be2adab0e2dc44609096cef043efff6afe069210010e8db3932c0710aadb29efe3fe879d5ad981345043b838b5ec6
7
+ data.tar.gz: c0d28aaaf7ab1c08c10ccec79bc25fef1e2c5e86fed295bed81ddcb1f0c84ae184917c867c37e6c9273eff681b5f34bded08b118a3936b279b99f0f1a673c22b
data/CHANGELOG.md CHANGED
@@ -1,3 +1,9 @@
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
+
4
+ ## 3.5.0
5
+ - Introduce opt-in "yaml_load_strategy => streaming" to stream parse YAML dictionaries [#106](https://github.com/logstash-plugins/logstash-filter-translate/pull/106)
6
+
1
7
  ## 3.4.3
2
8
  - Allow YamlFile's Psych::Parser and Visitor instances to be garbage collected [#104](https://github.com/logstash-plugins/logstash-filter-translate/pull/104)
3
9
 
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
 
data/docs/index.asciidoc CHANGED
@@ -109,6 +109,7 @@ This plugin supports the following configuration options plus the <<plugins-{typ
109
109
  | <<plugins-{type}s-{plugin}-refresh_behaviour>> |<<string,string>>|No
110
110
  | <<plugins-{type}s-{plugin}-target>> |<<string,string>>|No
111
111
  | <<plugins-{type}s-{plugin}-yaml_dictionary_code_point_limit>> |<<number,number>>|No
112
+ | <<plugins-{type}s-{plugin}-yaml_load_strategy>> |<<string,string>>, one of `["one_shot", "streaming"]`|No
112
113
  |=======================================================================
113
114
 
114
115
  Also see <<plugins-{type}s-{plugin}-common-options>> for a list of options supported by all
@@ -432,5 +433,21 @@ the filter will succeed. This will clobber the old value of the source field!
432
433
  The max amount of code points in the YAML file in `dictionary_path`. Please be aware that byte limit depends on the encoding.
433
434
  This setting is effective for YAML file only. YAML over the limit throws exception.
434
435
 
436
+ [id="plugins-{type}s-{plugin}-yaml_load_strategy"]
437
+ ===== `yaml_load_strategy`
438
+
439
+ * Value can be any of: `one_shot`, `streaming`
440
+ * Default value is `one_shot`
441
+
442
+ How to load and parse the YAML file. This setting defaults to `one_shot`, which loads the entire
443
+ YAML file into the parser in one go, emitting the final dictionary from the fully parsed YAML document.
444
+
445
+ Setting to `streaming` will instead instruct the parser to emit one "YAML element" at a time, constructing the dictionary
446
+ during parsing. This mode drastically reduces the amount of memory required to load or refresh the dictionary and it is also faster.
447
+
448
+ Due to underlying implementation differences this mode only supports basic types such as Arrays, Objects, Strings, numbers and booleans, and does not support tags.
449
+
450
+ If you have a lot of translate filters with large YAML documents consider changing this setting to `streaming` instead.
451
+
435
452
  [id="plugins-{type}s-{plugin}-common-options"]
436
453
  include::{include_path}/{type}.asciidoc[]
@@ -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
@@ -0,0 +1,111 @@
1
+ module LogStash module Filters module Dictionary
2
+ class StreamingYamlDictParser
3
+ def snakeYamlEngineV2
4
+ Java::org.snakeyaml.engine.v2
5
+ end
6
+
7
+ def snakeYamlEngineV2Events
8
+ snakeYamlEngineV2.events
9
+ end
10
+
11
+ def initialize(filename, yaml_code_point_limit)
12
+ settings = snakeYamlEngineV2.api.LoadSettings.builder
13
+ .set_code_point_limit(yaml_code_point_limit)
14
+ .build
15
+
16
+ stream = Java::java.io.FileInputStream.new(filename)
17
+ reader = Java::java.io.InputStreamReader.new(stream, Java::java.nio.charset.StandardCharsets::UTF_8)
18
+ stream_reader = snakeYamlEngineV2.scanner.StreamReader.new(reader, settings)
19
+
20
+ @parser = snakeYamlEngineV2.parser.ParserImpl.new(stream_reader, settings)
21
+
22
+ skip_until(snakeYamlEngineV2Events.MappingStartEvent)
23
+ end
24
+
25
+
26
+ def each_pair
27
+ while peek_event && !peek_event.is_a?(snakeYamlEngineV2Events.MappingEndEvent)
28
+ key = parse_node
29
+ value = parse_node
30
+ yield(key, value)
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def next_event
37
+ @parser.next
38
+ ensure
39
+ nil
40
+ end
41
+
42
+ def peek_event
43
+ @parser.peek_event
44
+ end
45
+
46
+ def skip_until(event_class)
47
+ while @parser.has_next
48
+ evt = @parser.next
49
+ return if event_class === evt
50
+ end
51
+ end
52
+
53
+ def parse_node
54
+ event = next_event
55
+
56
+ case event
57
+ when snakeYamlEngineV2Events.ScalarEvent
58
+ parse_scalar(event)
59
+ when snakeYamlEngineV2Events.MappingStartEvent
60
+ parse_mapping
61
+ when snakeYamlEngineV2Events.SequenceStartEvent
62
+ parse_sequence
63
+ else
64
+ raise "Unexpected event: #{event.class}"
65
+ end
66
+ end
67
+
68
+ def parse_mapping
69
+ hash = {}
70
+ while peek_event && !peek_event.is_a?(snakeYamlEngineV2Events.MappingEndEvent)
71
+ key = parse_node
72
+ value = parse_node
73
+ hash[key] = value
74
+ end
75
+ next_event
76
+ hash
77
+ end
78
+
79
+ def parse_sequence
80
+ array = []
81
+ while peek_event && !peek_event.is_a?(snakeYamlEngineV2Events.SequenceEndEvent)
82
+ array << parse_node
83
+ end
84
+ next_event
85
+ array
86
+ end
87
+
88
+ def parse_scalar(scalar)
89
+ value = scalar.value
90
+ # return quoted scalars as they are
91
+ # e.g. don't convert "true" to true
92
+ return value unless scalar.is_plain
93
+
94
+ # otherwise let's do some checking and conversions
95
+ case value
96
+ when 'null', '', '~' then nil
97
+ when 'true' then true
98
+ when 'false' then false
99
+ else
100
+ # Try to convert to integer or float
101
+ if value.match?(/\A-?\d+\z/)
102
+ value.to_i
103
+ elsif value.match?(/\A-?\d+\.\d+\z/)
104
+ value.to_f
105
+ else
106
+ value
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end end end
@@ -1,6 +1,7 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  require_relative "yaml_visitor"
4
+ require_relative "streaming_yaml_parser"
4
5
 
5
6
  module LogStash module Filters module Dictionary
6
7
  class YamlFile < File
@@ -9,18 +10,27 @@ module LogStash module Filters module Dictionary
9
10
 
10
11
  def initialize_for_file_type(**file_type_args)
11
12
  @yaml_code_point_limit = file_type_args[:yaml_code_point_limit]
13
+ @yaml_load_strategy = file_type_args[:yaml_load_strategy]
12
14
  end
13
15
 
14
- def read_file_into_dictionary
15
- visitor = YamlVisitor.create
16
- parser = Psych::Parser.new(Psych::TreeBuilder.new)
17
- parser.code_point_limit = @yaml_code_point_limit
18
- # low level YAML read that tries to create as
19
- # few intermediate objects as possible
20
- # this overwrites the value at key
21
- yaml_string = IO.read(@dictionary_path, :mode => 'r:bom|utf-8')
22
- parser.parse(yaml_string, @dictionary_path)
23
- visitor.accept_with_dictionary(@dictionary, parser.handler.root)
16
+ def read_dictionary
17
+ return enum_for(:read_dictionary) unless block_given?
18
+
19
+ if @yaml_load_strategy == "one_shot"
20
+ visitor = YamlVisitor.create
21
+ parser = Psych::Parser.new(Psych::TreeBuilder.new)
22
+ parser.code_point_limit = @yaml_code_point_limit
23
+ # low level YAML read that tries to create as
24
+ # few intermediate objects as possible
25
+ yaml_string = IO.read(@dictionary_path, :mode => 'r:bom|utf-8')
26
+ parser.parse(yaml_string, @dictionary_path)
27
+ temp_dictionary = {}
28
+ visitor.accept_with_dictionary(temp_dictionary, parser.handler.root)
29
+ temp_dictionary.each_pair { |key, value| yield(key, value) }
30
+ else # stream parse it
31
+ parser = StreamingYamlDictParser.new(@dictionary_path, @yaml_code_point_limit)
32
+ parser.each_pair {|key, value| yield(key, value) }
33
+ end
24
34
  end
25
35
  end
26
36
  end end end
@@ -108,6 +108,10 @@ class Translate < LogStash::Filters::Base
108
108
  # The default value is 128MB for code points of size 1 byte
109
109
  config :yaml_dictionary_code_point_limit, :validate => :number
110
110
 
111
+ # either load the entire yaml into memory before generating the in-memory dictionary
112
+ # alternatively "streaming" will gradually build the dictionary with little memory overhead
113
+ config :yaml_load_strategy, :validate => ["streaming", "one_shot"], :default => "one_shot"
114
+
111
115
  # When using a dictionary file, this setting will indicate how frequently
112
116
  # (in seconds) logstash will check the dictionary file for updates.
113
117
  config :refresh_interval, :validate => :number, :default => 300
@@ -195,7 +199,7 @@ class Translate < LogStash::Filters::Base
195
199
  if @yaml_dictionary_code_point_limit <= 0
196
200
  raise LogStash::ConfigurationError, "Please set a positive number in `yaml_dictionary_code_point_limit => #{@yaml_dictionary_code_point_limit}`."
197
201
  else
198
- @lookup = Dictionary::File.create(@dictionary_path, @refresh_interval, @refresh_behaviour, @exact, @regex, yaml_code_point_limit: @yaml_dictionary_code_point_limit)
202
+ @lookup = Dictionary::File.create(@dictionary_path, @refresh_interval, @refresh_behaviour, @exact, @regex, yaml_code_point_limit: @yaml_dictionary_code_point_limit, yaml_load_strategy: @yaml_load_strategy)
199
203
  end
200
204
  elsif @yaml_dictionary_code_point_limit != nil
201
205
  raise LogStash::ConfigurationError, "Please remove `yaml_dictionary_code_point_limit` for dictionary file in JSON or CSV format"
@@ -245,11 +249,11 @@ class Translate < LogStash::Filters::Base
245
249
  @updater = ArrayOfMapsValueUpdate.new(@iterate_on, @source, @target, @fallback, @lookup)
246
250
  end
247
251
 
248
- @logger.debug? && @logger.debug("#{self.class.name}: Dictionary - ", :dictionary => @lookup.dictionary)
252
+ logger.debug? && logger.debug("#{self.class.name}: Dictionary - ", :dictionary => @lookup.dictionary)
249
253
  if @exact
250
- @logger.debug? && @logger.debug("#{self.class.name}: Dictionary translation method - Exact")
254
+ logger.debug? && logger.debug("#{self.class.name}: Dictionary translation method - Exact")
251
255
  else
252
- @logger.debug? && @logger.debug("#{self.class.name}: Dictionary translation method - Fuzzy")
256
+ logger.debug? && logger.debug("#{self.class.name}: Dictionary translation method - Fuzzy")
253
257
  end
254
258
 
255
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.4.3'
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
 
@@ -238,6 +238,21 @@ describe LogStash::Filters::Translate do
238
238
  subject.filter(event)
239
239
  expect(event.get("translation")).to eq(1)
240
240
  end
241
+
242
+ describe "yaml_load_strategy" do
243
+ let(:one_shot_parse_filter) { subject }
244
+ let(:streaming_parse_filter) { described_class.new(config.merge("yaml_load_strategy" => 'streaming')) }
245
+
246
+ before(:each) do
247
+ subject.register
248
+ streaming_parse_filter.register
249
+ end
250
+ let(:one_shot_dictionary) { one_shot_parse_filter.lookup.dictionary }
251
+ let(:streaming_dictionary) { streaming_parse_filter.lookup.dictionary }
252
+ it "produces an equivalent dictionary for both strategies" do
253
+ expect(one_shot_dictionary).to eq(streaming_dictionary)
254
+ end
255
+ end
241
256
  end
242
257
 
243
258
  describe "when using a yml dictionary with code point limit" do
@@ -246,14 +261,16 @@ describe LogStash::Filters::Translate do
246
261
  "source" => "status",
247
262
  "target" => "translation",
248
263
  "dictionary_path" => dictionary_path,
249
- "yaml_dictionary_code_point_limit" => dictionary_size # the file is 18 bytes
264
+ "yaml_dictionary_code_point_limit" => codepoint_limit
250
265
  }
251
266
  end
252
267
  let(:dictionary_path) { TranslateUtil.build_fixture_path("dict.yml") }
268
+ let(:dictionary_size) { IO.read(dictionary_path).size }
253
269
  let(:event) { LogStash::Event.new("status" => "a") }
270
+ let(:codepoint_limit) { dictionary_size }
254
271
 
255
- context "dictionary is over limit" do
256
- let(:dictionary_size) { 17 }
272
+ context "codepoint limit under dictionary size" do
273
+ let(:codepoint_limit) { dictionary_size / 2 }
257
274
 
258
275
  it "raises exception" do
259
276
  expect { subject.register }.to raise_error(/The incoming YAML document exceeds/)
@@ -261,8 +278,6 @@ describe LogStash::Filters::Translate do
261
278
  end
262
279
 
263
280
  context "dictionary is within limit" do
264
- let(:dictionary_size) { 18 }
265
-
266
281
  it "returns the exact translation" do
267
282
  subject.register
268
283
  subject.filter(event)
@@ -1,3 +1,4 @@
1
1
  a : 1
2
2
  b : 2
3
3
  c : 3
4
+ d : { "e": [1, "hello", true, "false", "1", "1.1"] }
@@ -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,16 +1,16 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: logstash-filter-translate
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.4.3
4
+ version: 3.5.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Elastic
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2025-07-24 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
13
+ name: logstash-core-plugin-api
14
14
  requirement: !ruby/object:Gem::Requirement
15
15
  requirements:
16
16
  - - ">="
@@ -19,7 +19,6 @@ dependencies:
19
19
  - - "<="
20
20
  - !ruby/object:Gem::Version
21
21
  version: '2.99'
22
- name: logstash-core-plugin-api
23
22
  type: :runtime
24
23
  prerelease: false
25
24
  version_requirements: !ruby/object:Gem::Requirement
@@ -31,12 +30,12 @@ dependencies:
31
30
  - !ruby/object:Gem::Version
32
31
  version: '2.99'
33
32
  - !ruby/object:Gem::Dependency
33
+ name: logstash-mixin-ecs_compatibility_support
34
34
  requirement: !ruby/object:Gem::Requirement
35
35
  requirements:
36
36
  - - "~>"
37
37
  - !ruby/object:Gem::Version
38
38
  version: '1.2'
39
- name: logstash-mixin-ecs_compatibility_support
40
39
  type: :runtime
41
40
  prerelease: false
42
41
  version_requirements: !ruby/object:Gem::Requirement
@@ -45,12 +44,12 @@ dependencies:
45
44
  - !ruby/object:Gem::Version
46
45
  version: '1.2'
47
46
  - !ruby/object:Gem::Dependency
47
+ name: logstash-mixin-validator_support
48
48
  requirement: !ruby/object:Gem::Requirement
49
49
  requirements:
50
50
  - - "~>"
51
51
  - !ruby/object:Gem::Version
52
52
  version: '1.0'
53
- name: logstash-mixin-validator_support
54
53
  type: :runtime
55
54
  prerelease: false
56
55
  version_requirements: !ruby/object:Gem::Requirement
@@ -59,12 +58,12 @@ dependencies:
59
58
  - !ruby/object:Gem::Version
60
59
  version: '1.0'
61
60
  - !ruby/object:Gem::Dependency
61
+ name: logstash-mixin-deprecation_logger_support
62
62
  requirement: !ruby/object:Gem::Requirement
63
63
  requirements:
64
64
  - - "~>"
65
65
  - !ruby/object:Gem::Version
66
66
  version: '1.0'
67
- name: logstash-mixin-deprecation_logger_support
68
67
  type: :runtime
69
68
  prerelease: false
70
69
  version_requirements: !ruby/object:Gem::Requirement
@@ -73,12 +72,12 @@ dependencies:
73
72
  - !ruby/object:Gem::Version
74
73
  version: '1.0'
75
74
  - !ruby/object:Gem::Dependency
75
+ name: logstash-mixin-scheduler
76
76
  requirement: !ruby/object:Gem::Requirement
77
77
  requirements:
78
78
  - - "~>"
79
79
  - !ruby/object:Gem::Version
80
80
  version: '1.0'
81
- name: logstash-mixin-scheduler
82
81
  type: :runtime
83
82
  prerelease: false
84
83
  version_requirements: !ruby/object:Gem::Requirement
@@ -87,12 +86,12 @@ dependencies:
87
86
  - !ruby/object:Gem::Version
88
87
  version: '1.0'
89
88
  - !ruby/object:Gem::Dependency
89
+ name: psych
90
90
  requirement: !ruby/object:Gem::Requirement
91
91
  requirements:
92
92
  - - ">="
93
93
  - !ruby/object:Gem::Version
94
94
  version: 5.1.0
95
- name: psych
96
95
  type: :runtime
97
96
  prerelease: false
98
97
  version_requirements: !ruby/object:Gem::Requirement
@@ -101,12 +100,12 @@ dependencies:
101
100
  - !ruby/object:Gem::Version
102
101
  version: 5.1.0
103
102
  - !ruby/object:Gem::Dependency
103
+ name: logstash-devutils
104
104
  requirement: !ruby/object:Gem::Requirement
105
105
  requirements:
106
106
  - - ">="
107
107
  - !ruby/object:Gem::Version
108
108
  version: '0'
109
- name: logstash-devutils
110
109
  type: :development
111
110
  prerelease: false
112
111
  version_requirements: !ruby/object:Gem::Requirement
@@ -115,12 +114,12 @@ dependencies:
115
114
  - !ruby/object:Gem::Version
116
115
  version: '0'
117
116
  - !ruby/object:Gem::Dependency
117
+ name: rspec-sequencing
118
118
  requirement: !ruby/object:Gem::Requirement
119
119
  requirements:
120
120
  - - ">="
121
121
  - !ruby/object:Gem::Version
122
122
  version: '0'
123
- name: rspec-sequencing
124
123
  type: :development
125
124
  prerelease: false
126
125
  version_requirements: !ruby/object:Gem::Requirement
@@ -129,12 +128,12 @@ dependencies:
129
128
  - !ruby/object:Gem::Version
130
129
  version: '0'
131
130
  - !ruby/object:Gem::Dependency
131
+ name: rspec-wait
132
132
  requirement: !ruby/object:Gem::Requirement
133
133
  requirements:
134
134
  - - ">="
135
135
  - !ruby/object:Gem::Version
136
136
  version: '0'
137
- name: rspec-wait
138
137
  type: :development
139
138
  prerelease: false
140
139
  version_requirements: !ruby/object:Gem::Requirement
@@ -143,12 +142,12 @@ dependencies:
143
142
  - !ruby/object:Gem::Version
144
143
  version: '0'
145
144
  - !ruby/object:Gem::Dependency
145
+ name: benchmark-ips
146
146
  requirement: !ruby/object:Gem::Requirement
147
147
  requirements:
148
148
  - - ">="
149
149
  - !ruby/object:Gem::Version
150
150
  version: '0'
151
- name: benchmark-ips
152
151
  type: :development
153
152
  prerelease: false
154
153
  version_requirements: !ruby/object:Gem::Requirement
@@ -177,6 +176,7 @@ files:
177
176
  - lib/logstash/filters/dictionary/file.rb
178
177
  - lib/logstash/filters/dictionary/json_file.rb
179
178
  - lib/logstash/filters/dictionary/memory.rb
179
+ - lib/logstash/filters/dictionary/streaming_yaml_parser.rb
180
180
  - lib/logstash/filters/dictionary/yaml_file.rb
181
181
  - lib/logstash/filters/dictionary/yaml_visitor.rb
182
182
  - lib/logstash/filters/fetch_strategy/file.rb
@@ -199,13 +199,13 @@ 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)
205
206
  metadata:
206
207
  logstash_plugin: 'true'
207
208
  logstash_group: filter
208
- post_install_message:
209
209
  rdoc_options: []
210
210
  require_paths:
211
211
  - lib
@@ -220,8 +220,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
220
220
  - !ruby/object:Gem::Version
221
221
  version: '0'
222
222
  requirements: []
223
- rubygems_version: 3.3.26
224
- signing_key:
223
+ rubygems_version: 3.7.2
225
224
  specification_version: 4
226
225
  summary: Replaces field contents based on a hash or YAML file
227
226
  test_files: