fluentd 1.16.4-x64-mingw32 → 1.17.0-x64-mingw32

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.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/.github/DISCUSSION_TEMPLATE/q-a-japanese.yml +50 -0
  3. data/.github/DISCUSSION_TEMPLATE/q-a.yml +47 -0
  4. data/.github/workflows/test-ruby-head.yml +31 -0
  5. data/.github/workflows/test.yml +3 -3
  6. data/CHANGELOG.md +50 -0
  7. data/README.md +1 -1
  8. data/Rakefile +1 -1
  9. data/fluentd.gemspec +9 -1
  10. data/lib/fluent/command/binlog_reader.rb +1 -1
  11. data/lib/fluent/config/configure_proxy.rb +2 -2
  12. data/lib/fluent/config/types.rb +1 -1
  13. data/lib/fluent/configurable.rb +2 -2
  14. data/lib/fluent/counter/mutex_hash.rb +1 -1
  15. data/lib/fluent/fluent_log_event_router.rb +0 -2
  16. data/lib/fluent/plugin/buf_file.rb +1 -1
  17. data/lib/fluent/plugin/buffer/file_chunk.rb +1 -1
  18. data/lib/fluent/plugin/buffer/file_single_chunk.rb +2 -3
  19. data/lib/fluent/plugin/buffer.rb +75 -68
  20. data/lib/fluent/plugin/filter_parser.rb +26 -8
  21. data/lib/fluent/plugin/in_http.rb +18 -53
  22. data/lib/fluent/plugin/in_tail.rb +34 -2
  23. data/lib/fluent/plugin/out_http.rb +125 -13
  24. data/lib/fluent/plugin/owned_by_mixin.rb +0 -1
  25. data/lib/fluent/plugin/parser_json.rb +22 -5
  26. data/lib/fluent/plugin/parser_msgpack.rb +24 -3
  27. data/lib/fluent/plugin_helper/metrics.rb +2 -2
  28. data/lib/fluent/registry.rb +6 -6
  29. data/lib/fluent/test/output_test.rb +1 -1
  30. data/lib/fluent/unique_id.rb +1 -1
  31. data/lib/fluent/version.rb +1 -1
  32. data/test/log/test_console_adapter.rb +10 -3
  33. data/test/plugin/data/log_numeric/01.log +0 -0
  34. data/test/plugin/data/log_numeric/02.log +0 -0
  35. data/test/plugin/data/log_numeric/12.log +0 -0
  36. data/test/plugin/data/log_numeric/14.log +0 -0
  37. data/test/plugin/test_buffer.rb +59 -0
  38. data/test/plugin/test_in_http.rb +23 -1
  39. data/test/plugin/test_in_tail.rb +141 -0
  40. data/test/plugin/test_out_http.rb +128 -0
  41. data/test/plugin/test_owned_by.rb +0 -1
  42. data/test/plugin/test_parser_json.rb +106 -0
  43. data/test/plugin/test_parser_msgpack.rb +127 -0
  44. data/test/plugin/test_storage.rb +0 -1
  45. data/test/plugin_helper/test_child_process.rb +4 -4
  46. metadata +101 -4
@@ -203,54 +203,24 @@ module Fluent::Plugin
203
203
  begin
204
204
  path = path_info[1..-1] # remove /
205
205
  tag = path.split('/').join('.')
206
- record_time, record = parse_params(params)
207
206
 
208
- # Skip nil record
209
- if record.nil?
210
- log.debug { "incoming event is invalid: path=#{path_info} params=#{params.to_json}" }
211
- if @respond_with_empty_img
212
- return RESPONSE_IMG
213
- else
214
- if @use_204_response
215
- return RESPONSE_204
216
- else
217
- return RESPONSE_200
218
- end
207
+ mes = Fluent::MultiEventStream.new
208
+ parse_params(params) do |record_time, record|
209
+ if record.nil?
210
+ log.debug { "incoming event is invalid: path=#{path_info} params=#{params.to_json}" }
211
+ next
219
212
  end
220
- end
221
213
 
222
- mes = nil
223
- # Support batched requests
224
- if record.is_a?(Array)
225
- mes = Fluent::MultiEventStream.new
226
- record.each do |single_record|
227
- add_params_to_record(single_record, params)
228
-
229
- if param_time = params['time']
230
- param_time = param_time.to_f
231
- single_time = param_time.zero? ? Fluent::EventTime.now : @float_time_parser.parse(param_time)
232
- elsif @custom_parser
233
- single_time = @custom_parser.parse_time(single_record)
234
- single_time, single_record = @custom_parser.convert_values(single_time, single_record)
235
- else
236
- single_time = convert_time_field(single_record)
237
- end
238
-
239
- mes.add(single_time, single_record)
240
- end
241
- else
242
214
  add_params_to_record(record, params)
243
215
 
244
216
  time = if param_time = params['time']
245
217
  param_time = param_time.to_f
246
218
  param_time.zero? ? Fluent::EventTime.now : @float_time_parser.parse(param_time)
247
219
  else
248
- if record_time.nil?
249
- convert_time_field(record)
250
- else
251
- record_time
252
- end
220
+ record_time.nil? ? convert_time_field(record) : record_time
253
221
  end
222
+
223
+ mes.add(time, record)
254
224
  end
255
225
  rescue => e
256
226
  if @dump_error_log
@@ -261,11 +231,7 @@ module Fluent::Plugin
261
231
 
262
232
  # TODO server error
263
233
  begin
264
- if mes
265
- router.emit_stream(tag, mes)
266
- else
267
- router.emit(tag, time, record)
268
- end
234
+ router.emit_stream(tag, mes) unless mes.empty?
269
235
  rescue => e
270
236
  if @dump_error_log
271
237
  log.error "failed to emit data", error: e
@@ -308,20 +274,18 @@ module Fluent::Plugin
308
274
  def parse_params_default(params)
309
275
  if msgpack = params['msgpack']
310
276
  @parser_msgpack.parse(msgpack) do |_time, record|
311
- return nil, record
277
+ yield nil, record
312
278
  end
313
279
  elsif js = params['json']
314
280
  @parser_json.parse(js) do |_time, record|
315
- return nil, record
281
+ yield nil, record
316
282
  end
317
283
  elsif ndjson = params['ndjson']
318
- events = []
319
284
  ndjson.split(/\r?\n/).each do |js|
320
285
  @parser_json.parse(js) do |_time, record|
321
- events.push(record)
286
+ yield nil, record
322
287
  end
323
288
  end
324
- return nil, events
325
289
  else
326
290
  raise "'json', 'ndjson' or 'msgpack' parameter is required"
327
291
  end
@@ -329,10 +293,9 @@ module Fluent::Plugin
329
293
 
330
294
  def parse_params_with_parser(params)
331
295
  if content = params[EVENT_RECORD_PARAMETER]
332
- @custom_parser.parse(content) { |time, record|
333
- raise "Received event is not #{@format_name}: #{content}" if record.nil?
334
- return time, record
335
- }
296
+ @custom_parser.parse(content) do |time, record|
297
+ yield time, record
298
+ end
336
299
  else
337
300
  raise "'#{EVENT_RECORD_PARAMETER}' parameter is required"
338
301
  end
@@ -573,6 +536,8 @@ module Fluent::Plugin
573
536
  params.update WEBrick::HTTPUtils.parse_form_data(@body, boundary)
574
537
  elsif /^application\/json/.match?(@content_type)
575
538
  params['json'] = @body
539
+ elsif /^application\/csp-report/.match?(@content_type)
540
+ params['json'] = @body
576
541
  elsif /^application\/msgpack/.match?(@content_type)
577
542
  params['msgpack'] = @body
578
543
  elsif /^application\/x-ndjson/.match?(@content_type)
@@ -580,7 +545,7 @@ module Fluent::Plugin
580
545
  end
581
546
  path_info = uri.path
582
547
 
583
- if (@add_query_params)
548
+ if (@add_query_params)
584
549
 
585
550
  query_params = WEBrick::HTTPUtils.parse_query(uri.query)
586
551
 
@@ -65,6 +65,8 @@ module Fluent::Plugin
65
65
  config_param :path, :string
66
66
  desc 'path delimiter used for spliting path config'
67
67
  config_param :path_delimiter, :string, default: ','
68
+ desc 'Choose using glob patterns. Adding capabilities to handle [] and ?, and {}.'
69
+ config_param :glob_policy, :enum, list: [:backward_compatible, :extended, :always], default: :backward_compatible
68
70
  desc 'The tag of the event.'
69
71
  config_param :tag, :string
70
72
  desc 'The paths to exclude the files from watcher list.'
@@ -141,6 +143,14 @@ module Fluent::Plugin
141
143
  raise Fluent::ConfigError, "either of enable_watch_timer or enable_stat_watcher must be true"
142
144
  end
143
145
 
146
+ if @glob_policy == :always && @path_delimiter == ','
147
+ raise Fluent::ConfigError, "cannot use glob_policy as always with the default path_delimitor: `,\""
148
+ end
149
+
150
+ if @glob_policy == :extended && /\{.*,.*\}/.match(@path) && extended_glob_pattern(@path)
151
+ raise Fluent::ConfigError, "cannot include curly braces with glob patterns in `#{@path}\". Use glob_policy always instead."
152
+ end
153
+
144
154
  if RESERVED_CHARS.include?(@path_delimiter)
145
155
  rc = RESERVED_CHARS.join(', ')
146
156
  raise Fluent::ConfigError, "#{rc} are reserved words: #{@path_delimiter}"
@@ -288,6 +298,28 @@ module Fluent::Plugin
288
298
  @capability.have_capability?(:effective, :dac_override)
289
299
  end
290
300
 
301
+ def extended_glob_pattern(path)
302
+ path.include?('*') || path.include?('?') || /\[.*\]/.match(path)
303
+ end
304
+
305
+ # Curly braces is not supported with default path_delimiter
306
+ # because the default delimiter of path is ",".
307
+ # This should be collided for wildcard pattern for curly braces and
308
+ # be handled as an error on #configure.
309
+ def use_glob?(path)
310
+ if @glob_policy == :always
311
+ # For future extensions, we decided to use `always' term to handle
312
+ # regular expressions as much as possible.
313
+ # This is because not using `true' as a returning value
314
+ # when choosing :always here.
315
+ extended_glob_pattern(path) || /\{.*,.*\}/.match(path)
316
+ elsif @glob_policy == :extended
317
+ extended_glob_pattern(path)
318
+ elsif @glob_policy == :backward_compatible
319
+ path.include?('*')
320
+ end
321
+ end
322
+
291
323
  def expand_paths
292
324
  date = Fluent::EventTime.now
293
325
  paths = []
@@ -297,7 +329,7 @@ module Fluent::Plugin
297
329
  else
298
330
  date.to_time.strftime(path)
299
331
  end
300
- if path.include?('*')
332
+ if use_glob?(path)
301
333
  paths += Dir.glob(path).select { |p|
302
334
  begin
303
335
  is_file = !File.directory?(p)
@@ -332,7 +364,7 @@ module Fluent::Plugin
332
364
  else
333
365
  date.to_time.strftime(path)
334
366
  end
335
- path.include?('*') ? Dir.glob(path) : path
367
+ use_glob?(path) ? Dir.glob(path) : path
336
368
  }.flatten.uniq
337
369
  # filter out non existing files, so in case pattern is without '*' we don't do unnecessary work
338
370
  hash = {}
@@ -37,6 +37,8 @@ module Fluent::Plugin
37
37
 
38
38
  class RetryableResponse < StandardError; end
39
39
 
40
+ ConnectionCache = Struct.new(:uri, :conn)
41
+
40
42
  helpers :formatter
41
43
 
42
44
  desc 'The endpoint for HTTP request, e.g. http://example.com/api'
@@ -60,6 +62,8 @@ module Fluent::Plugin
60
62
  config_param :read_timeout, :integer, default: nil
61
63
  desc 'The TLS timeout in seconds'
62
64
  config_param :ssl_timeout, :integer, default: nil
65
+ desc 'Try to reuse connections'
66
+ config_param :reuse_connections, :bool, default: false
63
67
 
64
68
  desc 'The CA certificate path for TLS'
65
69
  config_param :tls_ca_cert_path, :string, default: nil
@@ -87,11 +91,29 @@ module Fluent::Plugin
87
91
 
88
92
  config_section :auth, required: false, multi: false do
89
93
  desc 'The method for HTTP authentication'
90
- config_param :method, :enum, list: [:basic], default: :basic
94
+ config_param :method, :enum, list: [:basic, :aws_sigv4], default: :basic
91
95
  desc 'The username for basic authentication'
92
96
  config_param :username, :string, default: nil
93
97
  desc 'The password for basic authentication'
94
98
  config_param :password, :string, default: nil, secret: true
99
+ desc 'The AWS service to authenticate against'
100
+ config_param :aws_service, :string, default: nil
101
+ desc 'The AWS region to use when authenticating'
102
+ config_param :aws_region, :string, default: nil
103
+ desc 'The AWS role ARN to assume when authenticating'
104
+ config_param :aws_role_arn, :string, default: nil
105
+ end
106
+
107
+ def connection_cache_id_thread_key
108
+ "#{plugin_id}_connection_cache_id"
109
+ end
110
+
111
+ def connection_cache_id_for_thread
112
+ Thread.current[connection_cache_id_thread_key]
113
+ end
114
+
115
+ def connection_cache_id_for_thread=(id)
116
+ Thread.current[connection_cache_id_thread_key] = id
95
117
  end
96
118
 
97
119
  def initialize
@@ -100,11 +122,23 @@ module Fluent::Plugin
100
122
  @uri = nil
101
123
  @proxy_uri = nil
102
124
  @formatter = nil
125
+
126
+ @connection_cache = []
127
+ @connection_cache_id_mutex = Mutex.new
128
+ @connection_cache_next_id = 0
129
+ end
130
+
131
+ def close
132
+ super
133
+
134
+ @connection_cache.each {|entry| entry.conn.finish if entry.conn&.started? }
103
135
  end
104
136
 
105
137
  def configure(conf)
106
138
  super
107
139
 
140
+ @connection_cache = Array.new(actual_flush_thread_count, ConnectionCache.new("", nil)) if @reuse_connections
141
+
108
142
  if @retryable_response_codes.nil?
109
143
  log.warn('Status code 503 is going to be removed from default `retryable_response_codes` from fluentd v2. Please add it by yourself if you wish')
110
144
  @retryable_response_codes = [503]
@@ -121,6 +155,36 @@ module Fluent::Plugin
121
155
  end
122
156
  define_singleton_method(:format, method(:format_json_array))
123
157
  end
158
+
159
+ if @auth and @auth.method == :aws_sigv4
160
+ begin
161
+ require 'aws-sigv4'
162
+ require 'aws-sdk-core'
163
+ rescue LoadError
164
+ raise Fluent::ConfigError, "The aws-sdk-core and aws-sigv4 gems are required for aws_sigv4 auth. Run: gem install aws-sdk-core -v '~> 3.191'"
165
+ end
166
+
167
+ raise Fluent::ConfigError, "aws_service is required for aws_sigv4 auth" unless @auth.aws_service != nil
168
+ raise Fluent::ConfigError, "aws_region is required for aws_sigv4 auth" unless @auth.aws_region != nil
169
+
170
+ if @auth.aws_role_arn == nil
171
+ aws_credentials = Aws::CredentialProviderChain.new.resolve
172
+ else
173
+ aws_credentials = Aws::AssumeRoleCredentials.new(
174
+ client: Aws::STS::Client.new(
175
+ region: @auth.aws_region
176
+ ),
177
+ role_arn: @auth.aws_role_arn,
178
+ role_session_name: "fluentd"
179
+ )
180
+ end
181
+
182
+ @aws_signer = Aws::Sigv4::Signer.new(
183
+ service: @auth.aws_service,
184
+ region: @auth.aws_region,
185
+ credentials_provider: aws_credentials
186
+ )
187
+ end
124
188
  end
125
189
 
126
190
  def multi_workers_ready?
@@ -215,7 +279,7 @@ module Fluent::Plugin
215
279
  URI.parse(endpoint)
216
280
  end
217
281
 
218
- def set_headers(req, chunk)
282
+ def set_headers(req, uri, chunk)
219
283
  if @headers
220
284
  @headers.each do |k, v|
221
285
  req[k] = v
@@ -229,6 +293,28 @@ module Fluent::Plugin
229
293
  req['Content-Type'] = @content_type
230
294
  end
231
295
 
296
+ def set_auth(req, uri)
297
+ return unless @auth
298
+
299
+ if @auth.method == :basic
300
+ req.basic_auth(@auth.username, @auth.password)
301
+ elsif @auth.method == :aws_sigv4
302
+ signature = @aws_signer.sign_request(
303
+ http_method: req.method,
304
+ url: uri.request_uri,
305
+ headers: {
306
+ 'Content-Type' => @content_type,
307
+ 'Host' => uri.host
308
+ },
309
+ body: req.body
310
+ )
311
+ req.add_field('x-amz-date', signature.headers['x-amz-date'])
312
+ req.add_field('x-amz-security-token', signature.headers['x-amz-security-token'])
313
+ req.add_field('x-amz-content-sha256', signature.headers['x-amz-content-sha256'])
314
+ req.add_field('authorization', signature.headers['authorization'])
315
+ end
316
+ end
317
+
232
318
  def create_request(chunk, uri)
233
319
  req = case @http_method
234
320
  when :post
@@ -236,23 +322,49 @@ module Fluent::Plugin
236
322
  when :put
237
323
  Net::HTTP::Put.new(uri.request_uri)
238
324
  end
239
- if @auth
240
- req.basic_auth(@auth.username, @auth.password)
241
- end
242
- set_headers(req, chunk)
325
+ set_headers(req, uri, chunk)
243
326
  req.body = @json_array ? "[#{chunk.read.chop}]" : chunk.read
327
+
328
+ # At least one authentication method requires the body and other headers, so the order of this call matters
329
+ set_auth(req, uri)
244
330
  req
245
331
  end
246
332
 
333
+ def make_request_cached(uri, req)
334
+ id = self.connection_cache_id_for_thread
335
+ if id.nil?
336
+ @connection_cache_id_mutex.synchronize {
337
+ id = @connection_cache_next_id
338
+ @connection_cache_next_id += 1
339
+ }
340
+ self.connection_cache_id_for_thread = id
341
+ end
342
+ uri_str = uri.to_s
343
+ if @connection_cache[id].uri != uri_str
344
+ @connection_cache[id].conn.finish if @connection_cache[id].conn&.started?
345
+ http = if @proxy_uri
346
+ Net::HTTP.start(uri.host, uri.port, @proxy_uri.host, @proxy_uri.port, @proxy_uri.user, @proxy_uri.password, @http_opt)
347
+ else
348
+ Net::HTTP.start(uri.host, uri.port, @http_opt)
349
+ end
350
+ @connection_cache[id] = ConnectionCache.new(uri_str, http)
351
+ end
352
+ @connection_cache[id].conn.request(req)
353
+ end
354
+
355
+ def make_request(uri, req, &block)
356
+ if @proxy_uri
357
+ Net::HTTP.start(uri.host, uri.port, @proxy_uri.host, @proxy_uri.port, @proxy_uri.user, @proxy_uri.password, @http_opt, &block)
358
+ else
359
+ Net::HTTP.start(uri.host, uri.port, @http_opt, &block)
360
+ end
361
+ end
362
+
247
363
  def send_request(uri, req)
248
- res = if @proxy_uri
249
- Net::HTTP.start(uri.host, uri.port, @proxy_uri.host, @proxy_uri.port, @proxy_uri.user, @proxy_uri.password, @http_opt) { |http|
250
- http.request(req)
251
- }
364
+ res = if @reuse_connections
365
+ make_request_cached(uri, req)
252
366
  else
253
- Net::HTTP.start(uri.host, uri.port, @http_opt) { |http|
254
- http.request(req)
255
- }
367
+ make_request(uri, req) { |http| http.request(req) }
256
368
  end
257
369
 
258
370
  if res.is_a?(Net::HTTPSuccess)
@@ -21,7 +21,6 @@ module Fluent
21
21
  @_owner = plugin
22
22
 
23
23
  @_plugin_id = plugin.plugin_id
24
- @_plugin_id_configured = plugin.plugin_id_configured?
25
24
 
26
25
  @log = plugin.log
27
26
  end
@@ -70,16 +70,33 @@ module Fluent
70
70
  end
71
71
 
72
72
  def parse(text)
73
- record = @load_proc.call(text)
74
- time = parse_time(record)
75
- if @execute_convert_values
76
- time, record = convert_values(time, record)
73
+ parsed_json = @load_proc.call(text)
74
+
75
+ if parsed_json.is_a?(Hash)
76
+ time, record = parse_one_record(parsed_json)
77
+ yield time, record
78
+ elsif parsed_json.is_a?(Array)
79
+ parsed_json.each do |record|
80
+ unless record.is_a?(Hash)
81
+ yield nil, nil
82
+ next
83
+ end
84
+ time, parsed_record = parse_one_record(record)
85
+ yield time, parsed_record
86
+ end
87
+ else
88
+ yield nil, nil
77
89
  end
78
- yield time, record
90
+
79
91
  rescue @error_class, EncodingError # EncodingError is for oj 3.x or later
80
92
  yield nil, nil
81
93
  end
82
94
 
95
+ def parse_one_record(record)
96
+ time = parse_time(record)
97
+ convert_values(time, record)
98
+ end
99
+
83
100
  def parser_type
84
101
  :text
85
102
  end
@@ -31,9 +31,9 @@ module Fluent
31
31
  :binary
32
32
  end
33
33
 
34
- def parse(data)
34
+ def parse(data, &block)
35
35
  @unpacker.feed_each(data) do |obj|
36
- yield convert_values(parse_time(obj), obj)
36
+ parse_unpacked_data(obj, &block)
37
37
  end
38
38
  end
39
39
  alias parse_partial_data parse
@@ -41,8 +41,29 @@ module Fluent
41
41
  def parse_io(io, &block)
42
42
  u = Fluent::MessagePackFactory.engine_factory.unpacker(io)
43
43
  u.each do |obj|
44
- time, record = convert_values(parse_time(obj), obj)
44
+ parse_unpacked_data(obj, &block)
45
+ end
46
+ end
47
+
48
+ def parse_unpacked_data(data)
49
+ if data.is_a?(Hash)
50
+ time, record = convert_values(parse_time(data), data)
45
51
  yield time, record
52
+ return
53
+ end
54
+
55
+ unless data.is_a?(Array)
56
+ yield nil, nil
57
+ return
58
+ end
59
+
60
+ data.each do |record|
61
+ unless record.is_a?(Hash)
62
+ yield nil, nil
63
+ next
64
+ end
65
+ time, converted_record = convert_values(parse_time(record), record)
66
+ yield time, converted_record
46
67
  end
47
68
  end
48
69
  end
@@ -65,9 +65,9 @@ module Fluent
65
65
  metrics.configure(config)
66
66
  # For multi workers environment, cmetrics should be distinguish with static labels.
67
67
  if Fluent::Engine.system_config.workers > 1
68
- labels.merge!(worker_id: fluentd_worker_id.to_s)
68
+ labels[:worker_id] = fluentd_worker_id.to_s
69
69
  end
70
- labels.merge!(plugin: @plugin_type_or_id)
70
+ labels[:plugin] = @plugin_type_or_id
71
71
  metrics.create(namespace: namespace, subsystem: subsystem, name: name, help_text: help_text, labels: labels)
72
72
 
73
73
  @_metrics["#{@plugin_type_or_id}_#{namespace}_#{subsystem}_#{name}"] = metrics
@@ -60,13 +60,13 @@ module Fluent
60
60
  # search from additional plugin directories
61
61
  if @dir_search_prefix
62
62
  path = "#{@dir_search_prefix}#{type}"
63
- files = @paths.map { |lp|
63
+ files = @paths.filter_map { |lp|
64
64
  lpath = File.expand_path(File.join(lp, "#{path}.rb"))
65
65
  File.exist?(lpath) ? lpath : nil
66
- }.compact
66
+ }
67
67
  unless files.empty?
68
68
  # prefer newer version
69
- require files.sort.last
69
+ require files.max
70
70
  return
71
71
  end
72
72
  end
@@ -74,17 +74,17 @@ module Fluent
74
74
  path = "#{@search_prefix}#{type}"
75
75
 
76
76
  # prefer LOAD_PATH than gems
77
- files = $LOAD_PATH.map { |lp|
77
+ files = $LOAD_PATH.filter_map { |lp|
78
78
  if lp == FLUENT_LIB_PATH
79
79
  nil
80
80
  else
81
81
  lpath = File.expand_path(File.join(lp, "#{path}.rb"))
82
82
  File.exist?(lpath) ? lpath : nil
83
83
  end
84
- }.compact
84
+ }
85
85
  unless files.empty?
86
86
  # prefer newer version
87
- require files.sort.last
87
+ require files.max
88
88
  return
89
89
  end
90
90
 
@@ -139,7 +139,7 @@ module Fluent
139
139
  assert_equal(@expected_buffer, buffer)
140
140
  end
141
141
 
142
- lines.keys.each do |meta|
142
+ lines.each_key do |meta|
143
143
  chunk = @instance.buffer.generate_chunk(meta).staged!
144
144
  chunk.append(lines[meta])
145
145
  begin
@@ -23,7 +23,7 @@ module Fluent
23
23
  end
24
24
 
25
25
  def self.hex(unique_id)
26
- unique_id.unpack('H*').first
26
+ unique_id.unpack1('H*')
27
27
  end
28
28
 
29
29
  module Mixin
@@ -16,6 +16,6 @@
16
16
 
17
17
  module Fluent
18
18
 
19
- VERSION = '1.16.4'
19
+ VERSION = '1.17.0'
20
20
 
21
21
  end
@@ -72,11 +72,18 @@ class ConsoleAdapterTest < Test::Unit::TestCase
72
72
  fatal: :fatal)
73
73
  def test_options(level)
74
74
  @console_logger.send(level, "subject", kwarg1: "opt1", kwarg2: "opt2")
75
+ lines = @logdev.logs[0].split("\n")
76
+ args = JSON.load(lines[1..].collect { |str| str.sub(/\s+\|/, "") }.join("\n"));
75
77
  assert_equal([
76
- "#{@timestamp_str} [#{level}]: 0.0s: subject\n" +
77
- " | {\"kwarg1\":\"opt1\",\"kwarg2\":\"opt2\"}\n"
78
+ 1,
79
+ "#{@timestamp_str} [#{level}]: 0.0s: subject",
80
+ { "kwarg1" => "opt1", "kwarg2" => "opt2" }
78
81
  ],
79
- @logdev.logs)
82
+ [
83
+ @logdev.logs.size,
84
+ lines[0],
85
+ args
86
+ ])
80
87
  end
81
88
 
82
89
  data(debug: :debug,
File without changes
File without changes
File without changes
File without changes
@@ -901,6 +901,65 @@ class BufferTest < Test::Unit::TestCase
901
901
 
902
902
  assert_equal 2, purge_count
903
903
  end
904
+
905
+ # https://github.com/fluent/fluentd/issues/4446
906
+ test "#write_step_by_step keeps chunks kept in locked in entire #write process" do
907
+ assert_equal 8 * 1024 * 1024, @p.chunk_limit_size
908
+ assert_equal 0.95, @p.chunk_full_threshold
909
+
910
+ mon_enter_counts_by_chunk = {}
911
+ mon_exit_counts_by_chunk = {}
912
+
913
+ stub.proxy(@p).generate_chunk(anything) do |chunk|
914
+ stub(chunk).mon_enter do
915
+ enter_count = 1 + mon_enter_counts_by_chunk.fetch(chunk, 0)
916
+ exit_count = mon_exit_counts_by_chunk.fetch(chunk, 0)
917
+ mon_enter_counts_by_chunk[chunk] = enter_count
918
+
919
+ # Assert that chunk is passed to &block of write_step_by_step before exiting the lock.
920
+ # (i.e. The lock count must be 2 greater than the exit count).
921
+ # Since ShouldRetry occurs once, the staged chunk takes the lock 3 times when calling the block.
922
+ if chunk.staged?
923
+ lock_in_block = enter_count == 3
924
+ assert_equal(enter_count - 2, exit_count) if lock_in_block
925
+ else
926
+ lock_in_block = enter_count == 2
927
+ assert_equal(enter_count - 2, exit_count) if lock_in_block
928
+ end
929
+ end
930
+ stub(chunk).mon_exit do
931
+ exit_count = 1 + mon_exit_counts_by_chunk.fetch(chunk, 0)
932
+ mon_exit_counts_by_chunk[chunk] = exit_count
933
+ end
934
+ chunk
935
+ end
936
+
937
+ m = @p.metadata(timekey: Time.parse('2016-04-11 16:40:00 +0000').to_i)
938
+ small_row = "x" * 1024 * 400
939
+ big_row = "x" * 1024 * 1024 * 8 # just `chunk_size_limit`, it does't cause BufferOverFlowError.
940
+
941
+ # Write 42 events in 1 event stream, last one is for triggering `ShouldRetry`
942
+ @p.write({m => [small_row] * 40 + [big_row] + ["x"]})
943
+
944
+ # Above event strem will be splitted twice by `Buffer#write_step_by_step`
945
+ #
946
+ # 1. `write_once`: 42 [events] * 1 [stream]
947
+ # 2. `write_step_by_step`: 4 [events]* 10 [streams] + 2 [events] * 1 [stream]
948
+ # 3. `write_step_by_step` (by `ShouldRetry`): 1 [event] * 42 [streams]
949
+ #
950
+ # Example of staged chunk lock behavior:
951
+ #
952
+ # 1. mon_enter in write_step_by_step
953
+ # 2. ShouldRetry occurs
954
+ # 3. mon_exit in write_step_by_step
955
+ # 4. mon_enter again in write_step_by_step (retry)
956
+ # 5. passed to &block of write_step_by_step
957
+ # 6. mon_enter in the block (write)
958
+ # 7. mon_exit in write_step_by_step
959
+ # 8. mon_exit in write
960
+
961
+ assert_equal(mon_enter_counts_by_chunk.values, mon_exit_counts_by_chunk.values)
962
+ end
904
963
  end
905
964
 
906
965
  sub_test_case 'standard format with configuration for test with lower chunk limit size' do