fluentd 1.16.3 → 1.17.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.
Files changed (52) 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/{linux-test.yaml → test.yml} +9 -13
  6. data/CHANGELOG.md +66 -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 +95 -86
  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 +52 -8
  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/command/test_fluentd.rb +8 -2
  33. data/test/log/test_console_adapter.rb +10 -3
  34. data/test/plugin/data/log_numeric/01.log +0 -0
  35. data/test/plugin/data/log_numeric/02.log +0 -0
  36. data/test/plugin/data/log_numeric/12.log +0 -0
  37. data/test/plugin/data/log_numeric/14.log +0 -0
  38. data/test/plugin/test_buffer.rb +110 -0
  39. data/test/plugin/test_in_http.rb +23 -1
  40. data/test/plugin/test_in_tail.rb +307 -3
  41. data/test/plugin/test_out_forward.rb +34 -39
  42. data/test/plugin/test_out_http.rb +128 -0
  43. data/test/plugin/test_owned_by.rb +0 -1
  44. data/test/plugin/test_parser_json.rb +106 -0
  45. data/test/plugin/test_parser_msgpack.rb +127 -0
  46. data/test/plugin/test_storage.rb +0 -1
  47. data/test/plugin_helper/test_child_process.rb +17 -7
  48. metadata +104 -9
  49. data/.github/workflows/macos-test.yaml +0 -34
  50. data/.github/workflows/windows-test.yaml +0 -49
  51. /data/.github/ISSUE_TEMPLATE/{bug_report.yaml → bug_report.yml} +0 -0
  52. /data/.github/ISSUE_TEMPLATE/{feature_request.yaml → feature_request.yml} +0 -0
@@ -580,7 +580,7 @@ module Fluent
580
580
  chunk = @dequeued.delete(chunk_id)
581
581
  return false unless chunk # already purged by other thread
582
582
  @queue.unshift(chunk)
583
- log.trace "chunk taken back", instance: self.object_id, chunk_id: dump_unique_id_hex(chunk_id), metadata: chunk.metadata
583
+ log.on_trace { log.trace "chunk taken back", instance: self.object_id, chunk_id: dump_unique_id_hex(chunk_id), metadata: chunk.metadata }
584
584
  @queued_num[chunk.metadata] += 1 # BUG if nil
585
585
  @dequeued_num[chunk.metadata] -= 1
586
586
  end
@@ -610,7 +610,7 @@ module Fluent
610
610
  @queued_num.delete(metadata)
611
611
  @dequeued_num.delete(metadata)
612
612
  end
613
- log.trace "chunk purged", instance: self.object_id, chunk_id: dump_unique_id_hex(chunk_id), metadata: metadata
613
+ log.on_trace { log.trace "chunk purged", instance: self.object_id, chunk_id: dump_unique_id_hex(chunk_id), metadata: metadata }
614
614
  end
615
615
 
616
616
  nil
@@ -728,7 +728,6 @@ module Fluent
728
728
 
729
729
  def write_step_by_step(metadata, data, format, splits_count, &block)
730
730
  splits = []
731
- errors = []
732
731
  if splits_count > data.size
733
732
  splits_count = data.size
734
733
  end
@@ -749,16 +748,14 @@ module Fluent
749
748
  modified_chunks = []
750
749
  modified_metadata = metadata
751
750
  get_next_chunk = ->(){
752
- c = if staged_chunk_used
753
- # Staging new chunk here is bad idea:
754
- # Recovering whole state including newly staged chunks is much harder than current implementation.
755
- modified_metadata = modified_metadata.dup_next
756
- generate_chunk(modified_metadata)
757
- else
758
- synchronize { @stage[modified_metadata] ||= generate_chunk(modified_metadata).staged! }
759
- end
760
- modified_chunks << c
761
- c
751
+ if staged_chunk_used
752
+ # Staging new chunk here is bad idea:
753
+ # Recovering whole state including newly staged chunks is much harder than current implementation.
754
+ modified_metadata = modified_metadata.dup_next
755
+ generate_chunk(modified_metadata)
756
+ else
757
+ synchronize { @stage[modified_metadata] ||= generate_chunk(modified_metadata).staged! }
758
+ end
762
759
  }
763
760
 
764
761
  writing_splits_index = 0
@@ -766,104 +763,116 @@ module Fluent
766
763
 
767
764
  while writing_splits_index < splits.size
768
765
  chunk = get_next_chunk.call
769
- chunk.synchronize do
770
- raise ShouldRetry unless chunk.writable?
771
- staged_chunk_used = true if chunk.staged?
772
-
773
- original_bytesize = committed_bytesize = chunk.bytesize
774
- begin
775
- while writing_splits_index < splits.size
776
- split = splits[writing_splits_index]
777
- formatted_split = format ? format.call(split) : nil
766
+ errors = []
767
+ # The chunk must be locked until being passed to &block.
768
+ chunk.mon_enter
769
+ modified_chunks << {chunk: chunk, adding_bytesize: 0, errors: errors}
778
770
 
779
- if split.size == 1 # Check BufferChunkOverflowError
780
- determined_bytesize = nil
781
- if @compress != :text
782
- determined_bytesize = nil
783
- elsif formatted_split
784
- determined_bytesize = formatted_split.bytesize
785
- elsif split.first.respond_to?(:bytesize)
786
- determined_bytesize = split.first.bytesize
787
- end
771
+ raise ShouldRetry unless chunk.writable?
772
+ staged_chunk_used = true if chunk.staged?
788
773
 
789
- if determined_bytesize && determined_bytesize > @chunk_limit_size
790
- # It is a obvious case that BufferChunkOverflowError should be raised here.
791
- # But if it raises here, already processed 'split' or
792
- # the proceeding 'split' will be lost completely.
793
- # So it is a last resort to delay raising such a exception
794
- errors << "a #{determined_bytesize} bytes record (nth: #{writing_splits_index}) is larger than buffer chunk limit size (#{@chunk_limit_size})"
795
- writing_splits_index += 1
796
- next
797
- end
774
+ original_bytesize = committed_bytesize = chunk.bytesize
775
+ begin
776
+ while writing_splits_index < splits.size
777
+ split = splits[writing_splits_index]
778
+ formatted_split = format ? format.call(split) : nil
798
779
 
799
- if determined_bytesize.nil? || chunk.bytesize + determined_bytesize > @chunk_limit_size
800
- # The split will (might) cause size over so keep already processed
801
- # 'split' content here (allow performance regression a bit).
802
- chunk.commit
803
- committed_bytesize = chunk.bytesize
804
- end
780
+ if split.size == 1 # Check BufferChunkOverflowError
781
+ determined_bytesize = nil
782
+ if @compress != :text
783
+ determined_bytesize = nil
784
+ elsif formatted_split
785
+ determined_bytesize = formatted_split.bytesize
786
+ elsif split.first.respond_to?(:bytesize)
787
+ determined_bytesize = split.first.bytesize
805
788
  end
806
789
 
807
- if format
808
- chunk.concat(formatted_split, split.size)
809
- else
810
- chunk.append(split, compress: @compress)
790
+ if determined_bytesize && determined_bytesize > @chunk_limit_size
791
+ # It is a obvious case that BufferChunkOverflowError should be raised here.
792
+ # But if it raises here, already processed 'split' or
793
+ # the proceeding 'split' will be lost completely.
794
+ # So it is a last resort to delay raising such a exception
795
+ errors << "a #{determined_bytesize} bytes record (nth: #{writing_splits_index}) is larger than buffer chunk limit size (#{@chunk_limit_size})"
796
+ writing_splits_index += 1
797
+ next
811
798
  end
812
- adding_bytes = chunk.bytesize - committed_bytesize
813
799
 
814
- if chunk_size_over?(chunk) # split size is larger than difference between size_full? and size_over?
815
- chunk.rollback
800
+ if determined_bytesize.nil? || chunk.bytesize + determined_bytesize > @chunk_limit_size
801
+ # The split will (might) cause size over so keep already processed
802
+ # 'split' content here (allow performance regression a bit).
803
+ chunk.commit
816
804
  committed_bytesize = chunk.bytesize
805
+ end
806
+ end
817
807
 
818
- if split.size == 1 # Check BufferChunkOverflowError again
819
- if adding_bytes > @chunk_limit_size
820
- errors << "concatenated/appended a #{adding_bytes} bytes record (nth: #{writing_splits_index}) is larger than buffer chunk limit size (#{@chunk_limit_size})"
821
- writing_splits_index += 1
822
- next
823
- else
824
- # As already processed content is kept after rollback, then unstaged chunk should be queued.
825
- # After that, re-process current split again.
826
- # New chunk should be allocated, to do it, modify @stage and so on.
827
- synchronize { @stage.delete(modified_metadata) }
828
- staged_chunk_used = false
829
- chunk.unstaged!
830
- break
831
- end
832
- end
808
+ if format
809
+ chunk.concat(formatted_split, split.size)
810
+ else
811
+ chunk.append(split, compress: @compress)
812
+ end
813
+ adding_bytes = chunk.bytesize - committed_bytesize
833
814
 
834
- if chunk_size_full?(chunk) || split.size == 1
835
- enqueue_chunk_before_retry = true
815
+ if chunk_size_over?(chunk) # split size is larger than difference between size_full? and size_over?
816
+ chunk.rollback
817
+ committed_bytesize = chunk.bytesize
818
+
819
+ if split.size == 1 # Check BufferChunkOverflowError again
820
+ if adding_bytes > @chunk_limit_size
821
+ errors << "concatenated/appended a #{adding_bytes} bytes record (nth: #{writing_splits_index}) is larger than buffer chunk limit size (#{@chunk_limit_size})"
822
+ writing_splits_index += 1
823
+ next
836
824
  else
837
- splits_count *= 10
825
+ # As already processed content is kept after rollback, then unstaged chunk should be queued.
826
+ # After that, re-process current split again.
827
+ # New chunk should be allocated, to do it, modify @stage and so on.
828
+ synchronize { @stage.delete(modified_metadata) }
829
+ staged_chunk_used = false
830
+ chunk.unstaged!
831
+ break
838
832
  end
833
+ end
839
834
 
840
- raise ShouldRetry
835
+ if chunk_size_full?(chunk) || split.size == 1
836
+ enqueue_chunk_before_retry = true
837
+ else
838
+ splits_count *= 10
841
839
  end
842
840
 
843
- writing_splits_index += 1
841
+ raise ShouldRetry
842
+ end
843
+
844
+ writing_splits_index += 1
844
845
 
845
- if chunk_size_full?(chunk)
846
- break
847
- end
846
+ if chunk_size_full?(chunk)
847
+ break
848
848
  end
849
- rescue
850
- chunk.purge if chunk.unstaged? # unstaged chunk will leak unless purge it
851
- raise
852
849
  end
853
-
854
- block.call(chunk, chunk.bytesize - original_bytesize, errors)
855
- errors = []
850
+ rescue
851
+ chunk.purge if chunk.unstaged? # unstaged chunk will leak unless purge it
852
+ raise
856
853
  end
854
+
855
+ modified_chunks.last[:adding_bytesize] = chunk.bytesize - original_bytesize
856
+ end
857
+ modified_chunks.each do |data|
858
+ block.call(data[:chunk], data[:adding_bytesize], data[:errors])
857
859
  end
858
860
  rescue ShouldRetry
859
- modified_chunks.each do |mc|
860
- mc.rollback rescue nil
861
- if mc.unstaged?
862
- mc.purge rescue nil
861
+ modified_chunks.each do |data|
862
+ chunk = data[:chunk]
863
+ chunk.rollback rescue nil
864
+ if chunk.unstaged?
865
+ chunk.purge rescue nil
863
866
  end
867
+ chunk.mon_exit rescue nil
864
868
  end
865
869
  enqueue_chunk(metadata) if enqueue_chunk_before_retry
866
870
  retry
871
+ ensure
872
+ modified_chunks.each do |data|
873
+ chunk = data[:chunk]
874
+ chunk.mon_exit
875
+ end
867
876
  end
868
877
 
869
878
  STATS_KEYS = [
@@ -70,6 +70,13 @@ module Fluent::Plugin
70
70
  end
71
71
  end
72
72
  begin
73
+ # Note: https://github.com/fluent/fluentd/issues/4100
74
+ # If the parser returns multiple records from one raw_value,
75
+ # this returns only the first one record.
76
+ # This should be fixed in the future version.
77
+ result_time = nil
78
+ result_record = nil
79
+
73
80
  @parser.parse(raw_value) do |t, values|
74
81
  if values
75
82
  t = if @reserve_time
@@ -79,20 +86,31 @@ module Fluent::Plugin
79
86
  end
80
87
  @accessor.delete(record) if @remove_key_name_field
81
88
  r = handle_parsed(tag, record, t, values)
82
- return t, r
89
+
90
+ if result_record.nil?
91
+ result_time = t
92
+ result_record = r
93
+ else
94
+ if @emit_invalid_record_to_error
95
+ router.emit_error_event(tag, t, r, Fluent::Plugin::Parser::ParserError.new(
96
+ "Could not emit the event. The parser returned multiple results, but currently filter_parser plugin only returns the first parsed result. Raw data: '#{raw_value}'"
97
+ ))
98
+ end
99
+ end
83
100
  else
84
101
  if @emit_invalid_record_to_error
85
102
  router.emit_error_event(tag, time, record, Fluent::Plugin::Parser::ParserError.new("pattern not matched with data '#{raw_value}'"))
86
103
  end
87
- if @reserve_data
88
- t = time
89
- r = handle_parsed(tag, record, time, {})
90
- return t, r
91
- else
92
- return FAILED_RESULT
93
- end
104
+
105
+ next unless @reserve_data
106
+ next unless result_record.nil?
107
+
108
+ result_time = time
109
+ result_record = handle_parsed(tag, record, time, {})
94
110
  end
95
111
  end
112
+
113
+ return result_time, result_record
96
114
  rescue Fluent::Plugin::Parser::ParserError => e
97
115
  if @emit_invalid_record_to_error
98
116
  raise e
@@ -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
 
@@ -52,6 +52,7 @@ module Fluent::Plugin
52
52
  super
53
53
  @paths = []
54
54
  @tails = {}
55
+ @tails_rotate_wait = {}
55
56
  @pf_file = nil
56
57
  @pf = nil
57
58
  @ignore_list = []
@@ -64,6 +65,8 @@ module Fluent::Plugin
64
65
  config_param :path, :string
65
66
  desc 'path delimiter used for spliting path config'
66
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
67
70
  desc 'The tag of the event.'
68
71
  config_param :tag, :string
69
72
  desc 'The paths to exclude the files from watcher list.'
@@ -140,6 +143,14 @@ module Fluent::Plugin
140
143
  raise Fluent::ConfigError, "either of enable_watch_timer or enable_stat_watcher must be true"
141
144
  end
142
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
+
143
154
  if RESERVED_CHARS.include?(@path_delimiter)
144
155
  rc = RESERVED_CHARS.join(', ')
145
156
  raise Fluent::ConfigError, "#{rc} are reserved words: #{@path_delimiter}"
@@ -267,6 +278,9 @@ module Fluent::Plugin
267
278
  @shutdown_start_time = Fluent::Clock.now
268
279
  # during shutdown phase, don't close io. It should be done in close after all threads are stopped. See close.
269
280
  stop_watchers(existence_path, immediate: true, remove_watcher: false)
281
+ @tails_rotate_wait.keys.each do |tw|
282
+ detach_watcher(tw, @tails_rotate_wait[tw][:ino], false)
283
+ end
270
284
  @pf_file.close if @pf_file
271
285
 
272
286
  super
@@ -275,6 +289,7 @@ module Fluent::Plugin
275
289
  def close
276
290
  super
277
291
  # close file handles after all threads stopped (in #close of thread plugin helper)
292
+ # It may be because we need to wait IOHanlder.ready_to_shutdown()
278
293
  close_watcher_handles
279
294
  end
280
295
 
@@ -283,6 +298,28 @@ module Fluent::Plugin
283
298
  @capability.have_capability?(:effective, :dac_override)
284
299
  end
285
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
+
286
323
  def expand_paths
287
324
  date = Fluent::EventTime.now
288
325
  paths = []
@@ -292,7 +329,7 @@ module Fluent::Plugin
292
329
  else
293
330
  date.to_time.strftime(path)
294
331
  end
295
- if path.include?('*')
332
+ if use_glob?(path)
296
333
  paths += Dir.glob(path).select { |p|
297
334
  begin
298
335
  is_file = !File.directory?(p)
@@ -327,7 +364,7 @@ module Fluent::Plugin
327
364
  else
328
365
  date.to_time.strftime(path)
329
366
  end
330
- path.include?('*') ? Dir.glob(path) : path
367
+ use_glob?(path) ? Dir.glob(path) : path
331
368
  }.flatten.uniq
332
369
  # filter out non existing files, so in case pattern is without '*' we don't do unnecessary work
333
370
  hash = {}
@@ -516,6 +553,9 @@ module Fluent::Plugin
516
553
  tw.close
517
554
  end
518
555
  end
556
+ @tails_rotate_wait.keys.each do |tw|
557
+ tw.close
558
+ end
519
559
  end
520
560
 
521
561
  # refresh_watchers calls @tails.keys so we don't use stop_watcher -> start_watcher sequence for safety.
@@ -570,10 +610,6 @@ module Fluent::Plugin
570
610
  detach_watcher_after_rotate_wait(tail_watcher, pe.read_inode)
571
611
  end
572
612
 
573
- # TailWatcher#close is called by another thread at shutdown phase.
574
- # It causes 'can't modify string; temporarily locked' error in IOHandler
575
- # so adding close_io argument to avoid this problem.
576
- # At shutdown, IOHandler's io will be released automatically after detached the event loop
577
613
  def detach_watcher(tw, ino, close_io = true)
578
614
  if @follow_inodes && tw.ino != ino
579
615
  log.warn("detach_watcher could be detaching an unexpected tail_watcher with a different ino.",
@@ -604,7 +640,11 @@ module Fluent::Plugin
604
640
  if @open_on_every_update
605
641
  # Detach now because it's already closed, waiting it doesn't make sense.
606
642
  detach_watcher(tw, ino)
607
- elsif throttling_is_enabled?(tw)
643
+ end
644
+
645
+ return if @tails_rotate_wait[tw]
646
+
647
+ if throttling_is_enabled?(tw)
608
648
  # When the throttling feature is enabled, it might not reach EOF yet.
609
649
  # Should ensure to read all contents before closing it, with keeping throttling.
610
650
  start_time_to_wait = Fluent::Clock.now
@@ -612,14 +652,18 @@ module Fluent::Plugin
612
652
  elapsed = Fluent::Clock.now - start_time_to_wait
613
653
  if tw.eof? && elapsed >= @rotate_wait
614
654
  timer.detach
655
+ @tails_rotate_wait.delete(tw)
615
656
  detach_watcher(tw, ino)
616
657
  end
617
658
  end
659
+ @tails_rotate_wait[tw] = { ino: ino, timer: timer }
618
660
  else
619
661
  # when the throttling feature isn't enabled, just wait @rotate_wait
620
- timer_execute(:in_tail_close_watcher, @rotate_wait, repeat: false) do
662
+ timer = timer_execute(:in_tail_close_watcher, @rotate_wait, repeat: false) do
663
+ @tails_rotate_wait.delete(tw)
621
664
  detach_watcher(tw, ino)
622
665
  end
666
+ @tails_rotate_wait[tw] = { ino: ino, timer: timer }
623
667
  end
624
668
  end
625
669