fluentd 1.14.6 → 1.15.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of fluentd might be problematic. Click here for more details.

Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/linux-test.yaml +1 -1
  3. data/.github/workflows/windows-test.yaml +4 -1
  4. data/CHANGELOG.md +53 -1
  5. data/fluentd.gemspec +2 -1
  6. data/lib/fluent/command/ctl.rb +4 -1
  7. data/lib/fluent/command/fluentd.rb +10 -0
  8. data/lib/fluent/config/literal_parser.rb +2 -2
  9. data/lib/fluent/config/yaml_parser/fluent_value.rb +47 -0
  10. data/lib/fluent/config/yaml_parser/loader.rb +91 -0
  11. data/lib/fluent/config/yaml_parser/parser.rb +166 -0
  12. data/lib/fluent/config/yaml_parser/section_builder.rb +107 -0
  13. data/lib/fluent/config/yaml_parser.rb +56 -0
  14. data/lib/fluent/config.rb +14 -1
  15. data/lib/fluent/plugin/file_wrapper.rb +52 -107
  16. data/lib/fluent/plugin/in_tail/group_watch.rb +204 -0
  17. data/lib/fluent/plugin/in_tail/position_file.rb +1 -15
  18. data/lib/fluent/plugin/in_tail.rb +66 -47
  19. data/lib/fluent/plugin/out_forward/socket_cache.rb +2 -0
  20. data/lib/fluent/plugin/output.rb +2 -1
  21. data/lib/fluent/plugin/parser_syslog.rb +1 -1
  22. data/lib/fluent/plugin_helper/server.rb +3 -1
  23. data/lib/fluent/plugin_helper/service_discovery.rb +2 -2
  24. data/lib/fluent/supervisor.rb +109 -25
  25. data/lib/fluent/system_config.rb +2 -1
  26. data/lib/fluent/version.rb +1 -1
  27. data/lib/fluent/winsvc.rb +2 -0
  28. data/test/command/test_ctl.rb +0 -1
  29. data/test/command/test_fluentd.rb +33 -0
  30. data/test/config/test_system_config.rb +3 -1
  31. data/test/config/test_types.rb +1 -1
  32. data/test/plugin/in_tail/test_io_handler.rb +14 -4
  33. data/test/plugin/in_tail/test_position_file.rb +0 -63
  34. data/test/plugin/out_forward/test_socket_cache.rb +26 -1
  35. data/test/plugin/test_file_wrapper.rb +0 -68
  36. data/test/plugin/test_in_object_space.rb +9 -3
  37. data/test/plugin/test_in_syslog.rb +1 -1
  38. data/test/plugin/test_in_tail.rb +629 -353
  39. data/test/plugin/test_out_forward.rb +30 -20
  40. data/test/plugin/test_parser_syslog.rb +1 -1
  41. data/test/plugin_helper/test_cert_option.rb +1 -1
  42. data/test/plugin_helper/test_child_process.rb +16 -4
  43. data/test/test_config.rb +135 -4
  44. data/test/test_supervisor.rb +155 -0
  45. metadata +11 -5
@@ -16,8 +16,8 @@
16
16
 
17
17
  module Fluent
18
18
  module FileWrapper
19
- def self.open(*args)
20
- io = WindowsFile.new(*args).io
19
+ def self.open(path, mode='r')
20
+ io = WindowsFile.new(path, mode).io
21
21
  if block_given?
22
22
  v = yield io
23
23
  io.close
@@ -35,116 +35,36 @@ module Fluent
35
35
  end
36
36
  end
37
37
 
38
- module WindowsFileExtension
39
- attr_reader :path
40
-
41
- def stat
42
- s = super
43
- s.instance_variable_set :@ino, @ino
44
- def s.ino; @ino; end
45
- s
46
- end
47
- end
48
-
49
- class Win32Error < StandardError
50
- require 'windows/error'
51
- include Windows::Error
52
-
53
- attr_reader :errcode, :msg
54
-
55
- WSABASEERR = 10000
56
-
57
- def initialize(errcode, msg = nil)
58
- @errcode = errcode
59
- @msg = msg
60
- end
61
-
62
- def format_english_message(errcode)
63
- buf = 0.chr * 260
64
- flags = FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_ARGUMENT_ARRAY
65
- english_lang_id = 1033 # The result of MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US)
66
- FormatMessageA.call(flags, 0, errcode, english_lang_id, buf, buf.size, 0)
67
- buf.force_encoding(Encoding.default_external).strip
68
- end
69
-
70
- def to_s
71
- msg = super
72
- msg << ": code: #{@errcode}, #{format_english_message(@errcode)}"
73
- msg << " - #{@msg}" if @msg
74
- msg
75
- end
76
-
77
- def inspect
78
- "#<#{to_s}>"
79
- end
80
-
81
- def ==(other)
82
- return false if other.class != Win32Error
83
- @errcode == other.errcode && @msg == other.msg
84
- end
85
-
86
- def wsaerr?
87
- @errcode >= WSABASEERR
88
- end
89
- end
90
-
91
- # To open and get stat with setting FILE_SHARE_DELETE
92
38
  class WindowsFile
93
39
  require 'windows/file'
94
- require 'windows/error'
95
40
  require 'windows/handle'
96
- require 'windows/nio'
97
41
 
98
- include Windows::Error
42
+ include File::Constants
99
43
  include Windows::File
100
44
  include Windows::Handle
101
- include Windows::NIO
102
-
103
- def initialize(path, mode='r', sharemode=FILE_SHARE_READ|FILE_SHARE_WRITE|FILE_SHARE_DELETE)
104
- @path = path
105
- @file_handle = INVALID_HANDLE_VALUE
106
- @mode = mode
107
45
 
46
+ attr_reader :io
108
47
 
109
- access, creationdisposition, seektoend = case mode.delete('b')
110
- when "r" ; [FILE_GENERIC_READ , OPEN_EXISTING, false]
111
- when "r+"; [FILE_GENERIC_READ | FILE_GENERIC_WRITE, OPEN_ALWAYS , false]
112
- when "w" ; [FILE_GENERIC_WRITE , CREATE_ALWAYS, false]
113
- when "w+"; [FILE_GENERIC_READ | FILE_GENERIC_WRITE, CREATE_ALWAYS, false]
114
- when "a" ; [FILE_GENERIC_WRITE , OPEN_ALWAYS , true]
115
- when "a+"; [FILE_GENERIC_READ | FILE_GENERIC_WRITE, OPEN_ALWAYS , true]
116
- else raise "unknown mode '#{mode}'"
117
- end
118
-
119
- @file_handle = CreateFile.call(@path, access, sharemode,
120
- 0, creationdisposition, FILE_ATTRIBUTE_NORMAL, 0)
121
- if @file_handle == INVALID_HANDLE_VALUE
122
- win32err = Win32Error.new(Win32::API.last_error, path)
123
- errno = ServerEngine::RbWinSock.rb_w32_map_errno(win32err.errcode)
124
- if errno == Errno::EINVAL::Errno || win32err.wsaerr?
125
- # maybe failed to map
126
- raise win32err
127
- else
128
- raise SystemCallError.new(win32err.message, errno)
129
- end
48
+ def initialize(path, mode='r')
49
+ @path = path
50
+ @io = File.open(path, mode2flags(mode))
51
+ @file_handle = _get_osfhandle(@io.to_i)
52
+ @io.instance_variable_set(:@file_index, self.ino)
53
+ def @io.ino
54
+ @file_index
130
55
  end
131
56
  end
132
57
 
133
58
  def close
134
- CloseHandle.call(@file_handle)
59
+ @io.close
135
60
  @file_handle = INVALID_HANDLE_VALUE
136
61
  end
137
62
 
138
- def io
139
- fd = _open_osfhandle(@file_handle, 0)
140
- raise Errno::ENOENT if fd == -1
141
- io = File.for_fd(fd, @mode)
142
- io.instance_variable_set :@ino, self.ino
143
- io.instance_variable_set :@path, @path
144
- io.extend WindowsFileExtension
145
- io
146
- end
147
-
63
+ # To keep backward compatibility, we continue to use GetFileInformationByHandle()
64
+ # to get file id.
65
+ # Note that Ruby's File.stat uses GetFileInformationByHandleEx() with FileIdInfo
66
+ # and returned value is different with above one, former one is 64 bit while
67
+ # later one is 128bit.
148
68
  def ino
149
69
  by_handle_file_information = '\0'*(4+8+8+8+4+4+4+4+4+4) #72bytes
150
70
 
@@ -155,6 +75,41 @@ module Fluent
155
75
  by_handle_file_information.unpack("I11Q1")[11] # fileindex
156
76
  end
157
77
 
78
+ def stat
79
+ raise Errno::ENOENT if delete_pending
80
+ s = File.stat(@path)
81
+ s.instance_variable_set :@ino, self.ino
82
+ def s.ino; @ino; end
83
+ s
84
+ end
85
+
86
+ private
87
+
88
+ def mode2flags(mode)
89
+ # Always inject File::Constants::SHARE_DELETE
90
+ # https://github.com/fluent/fluentd/pull/3585#issuecomment-1101502617
91
+ # To enable SHARE_DELETE, BINARY is also required.
92
+ # https://bugs.ruby-lang.org/issues/11218
93
+ # https://github.com/ruby/ruby/blob/d6684f063bc53e3cab025bd39526eca3b480b5e7/win32/win32.c#L6332-L6345
94
+ flags = BINARY | SHARE_DELETE
95
+ case mode.delete("b")
96
+ when "r"
97
+ flags |= RDONLY
98
+ when "r+"
99
+ flags |= RDWR
100
+ when "w"
101
+ flags |= WRONLY | CREAT | TRUNC
102
+ when "w+"
103
+ flags |= RDWR | CREAT | TRUNC
104
+ when "a"
105
+ flags |= WRONLY | CREAT | APPEND
106
+ when "a+"
107
+ flags |= RDWR | CREAT | APPEND
108
+ else
109
+ raise Errno::EINVAL.new("Unsupported mode by Fluent::FileWrapper: #{mode}")
110
+ end
111
+ end
112
+
158
113
  # DeletePending is a Windows-specific file state that roughly means
159
114
  # "this file is queued for deletion, so close any open handlers"
160
115
  #
@@ -173,15 +128,5 @@ module Fluent
173
128
 
174
129
  return buf.unpack("QQICC")[3] != 0
175
130
  end
176
-
177
- private :delete_pending
178
-
179
- def stat
180
- raise Errno::ENOENT if delete_pending
181
- s = File.stat(@path)
182
- s.instance_variable_set :@ino, self.ino
183
- def s.ino; @ino; end
184
- s
185
- end
186
131
  end
187
132
  end if Fluent.windows?
@@ -0,0 +1,204 @@
1
+ #
2
+ # Fluentd
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+
17
+ require 'fluent/plugin/input'
18
+
19
+ module Fluent::Plugin
20
+ class TailInput < Fluent::Plugin::Input
21
+ module GroupWatchParams
22
+ include Fluent::Configurable
23
+
24
+ DEFAULT_KEY = /.*/
25
+ DEFAULT_LIMIT = -1
26
+ REGEXP_JOIN = "_"
27
+
28
+ config_section :group, param_name: :group, required: false, multi: false do
29
+ desc 'Regex for extracting group\'s metadata'
30
+ config_param :pattern,
31
+ :regexp,
32
+ default: /^\/var\/log\/containers\/(?<podname>[a-z0-9]([-a-z0-9]*[a-z0-9])?(\/[a-z0-9]([-a-z0-9]*[a-z0-9])?)*)_(?<namespace>[^_]+)_(?<container>.+)-(?<docker_id>[a-z0-9]{64})\.log$/
33
+
34
+ desc 'Period of time in which the group_line_limit is applied'
35
+ config_param :rate_period, :time, default: 5
36
+
37
+ config_section :rule, param_name: :rule, required: true, multi: true do
38
+ desc 'Key-value pairs for grouping'
39
+ config_param :match, :hash, value_type: :regexp, default: { namespace: [DEFAULT_KEY], podname: [DEFAULT_KEY] }
40
+ desc 'Maximum number of log lines allowed per group over a period of rate_period'
41
+ config_param :limit, :integer, default: DEFAULT_LIMIT
42
+ end
43
+ end
44
+ end
45
+
46
+ module GroupWatch
47
+ def self.included(mod)
48
+ mod.include GroupWatchParams
49
+ end
50
+
51
+ attr_reader :group_watchers, :default_group_key
52
+
53
+ def initialize
54
+ super
55
+ @group_watchers = {}
56
+ @group_keys = nil
57
+ @default_group_key = nil
58
+ end
59
+
60
+ def configure(conf)
61
+ super
62
+
63
+ unless @group.nil?
64
+ ## Ensuring correct time period syntax
65
+ @group.rule.each { |rule|
66
+ raise "Metadata Group Limit >= DEFAULT_LIMIT" unless rule.limit >= GroupWatchParams::DEFAULT_LIMIT
67
+ }
68
+
69
+ @group_keys = Regexp.compile(@group.pattern).named_captures.keys
70
+ @default_group_key = ([GroupWatchParams::DEFAULT_KEY] * @group_keys.length).join(GroupWatchParams::REGEXP_JOIN)
71
+
72
+ ## Ensures that "specific" rules (with larger number of `rule.match` keys)
73
+ ## have a higher priority against "generic" rules (with less number of `rule.match` keys).
74
+ ## This will be helpful when a file satisfies more than one rule.
75
+ @group.rule.sort_by! { |rule| -rule.match.length() }
76
+ construct_groupwatchers
77
+ @group_watchers[@default_group_key] ||= GroupWatcher.new(@group.rate_period, GroupWatchParams::DEFAULT_LIMIT)
78
+ end
79
+ end
80
+
81
+ def add_path_to_group_watcher(path)
82
+ return nil if @group.nil?
83
+ group_watcher = find_group_from_metadata(path)
84
+ group_watcher.add(path) unless group_watcher.include?(path)
85
+ group_watcher
86
+ end
87
+
88
+ def remove_path_from_group_watcher(path)
89
+ return if @group.nil?
90
+ group_watcher = find_group_from_metadata(path)
91
+ group_watcher.delete(path)
92
+ end
93
+
94
+ def construct_group_key(named_captures)
95
+ match_rule = []
96
+ @group_keys.each { |key|
97
+ match_rule.append(named_captures.fetch(key, GroupWatchParams::DEFAULT_KEY))
98
+ }
99
+ match_rule = match_rule.join(GroupWatchParams::REGEXP_JOIN)
100
+
101
+ match_rule
102
+ end
103
+
104
+ def construct_groupwatchers
105
+ @group.rule.each { |rule|
106
+ match_rule = construct_group_key(rule.match)
107
+ @group_watchers[match_rule] ||= GroupWatcher.new(@group.rate_period, rule.limit)
108
+ }
109
+ end
110
+
111
+ def find_group(metadata)
112
+ metadata_key = construct_group_key(metadata)
113
+ gw_key = @group_watchers.keys.find { |regexp| metadata_key.match?(regexp) && regexp != @default_group_key }
114
+ gw_key ||= @default_group_key
115
+
116
+ @group_watchers[gw_key]
117
+ end
118
+
119
+ def find_group_from_metadata(path)
120
+ begin
121
+ metadata = @group.pattern.match(path).named_captures
122
+ group_watcher = find_group(metadata)
123
+ rescue
124
+ log.warn "Cannot find group from metadata, Adding file in the default group"
125
+ group_watcher = @group_watchers[@default_group_key]
126
+ end
127
+
128
+ group_watcher
129
+ end
130
+ end
131
+
132
+ class GroupWatcher
133
+ attr_accessor :current_paths, :limit, :number_lines_read, :start_reading_time, :rate_period
134
+
135
+ FileCounter = Struct.new(
136
+ :number_lines_read,
137
+ :start_reading_time,
138
+ )
139
+
140
+ def initialize(rate_period = 60, limit = -1)
141
+ @current_paths = {}
142
+ @rate_period = rate_period
143
+ @limit = limit
144
+ end
145
+
146
+ def add(path)
147
+ @current_paths[path] = FileCounter.new(0, nil)
148
+ end
149
+
150
+ def include?(path)
151
+ @current_paths.key?(path)
152
+ end
153
+
154
+ def size
155
+ @current_paths.size
156
+ end
157
+
158
+ def delete(path)
159
+ @current_paths.delete(path)
160
+ end
161
+
162
+ def update_reading_time(path)
163
+ @current_paths[path].start_reading_time ||= Fluent::Clock.now
164
+ end
165
+
166
+ def update_lines_read(path, value)
167
+ @current_paths[path].number_lines_read += value
168
+ end
169
+
170
+ def reset_counter(path)
171
+ @current_paths[path].start_reading_time = nil
172
+ @current_paths[path].number_lines_read = 0
173
+ end
174
+
175
+ def time_spent_reading(path)
176
+ Fluent::Clock.now - @current_paths[path].start_reading_time
177
+ end
178
+
179
+ def limit_time_period_reached?(path)
180
+ time_spent_reading(path) < @rate_period
181
+ end
182
+
183
+ def limit_lines_reached?(path)
184
+ return true unless include?(path)
185
+ return true if @limit == 0
186
+
187
+ return false if @limit < 0
188
+ return false if @current_paths[path].number_lines_read < @limit / size
189
+
190
+ # update_reading_time(path)
191
+ if limit_time_period_reached?(path) # Exceeds limit
192
+ true
193
+ else # Does not exceed limit
194
+ reset_counter(path)
195
+ false
196
+ end
197
+ end
198
+
199
+ def to_s
200
+ super + " current_paths: #{@current_paths} rate_period: #{@rate_period} limit: #{@limit}"
201
+ end
202
+ end
203
+ end
204
+ end
@@ -250,20 +250,6 @@ module Fluent::Plugin
250
250
  end
251
251
  end
252
252
 
253
- TargetInfo = Struct.new(:path, :ino) do
254
- def ==(other)
255
- return false unless other.is_a?(TargetInfo)
256
- self.path == other.path
257
- end
258
-
259
- def hash
260
- self.path.hash
261
- end
262
-
263
- def eql?(other)
264
- return false unless other.is_a?(TargetInfo)
265
- self.path == other.path
266
- end
267
- end
253
+ TargetInfo = Struct.new(:path, :ino)
268
254
  end
269
255
  end
@@ -24,6 +24,7 @@ require 'fluent/plugin/parser_multiline'
24
24
  require 'fluent/variable_store'
25
25
  require 'fluent/capability'
26
26
  require 'fluent/plugin/in_tail/position_file'
27
+ require 'fluent/plugin/in_tail/group_watch'
27
28
 
28
29
  if Fluent.windows?
29
30
  require_relative 'file_wrapper'
@@ -33,6 +34,8 @@ end
33
34
 
34
35
  module Fluent::Plugin
35
36
  class TailInput < Fluent::Plugin::Input
37
+ include GroupWatch
38
+
36
39
  Fluent::Plugin.register_input('tail', self)
37
40
 
38
41
  helpers :timer, :event_loop, :parser, :compat_parameters
@@ -354,11 +357,11 @@ module Fluent::Plugin
354
357
 
355
358
  def existence_path
356
359
  hash = {}
357
- @tails.each_key {|target_info|
360
+ @tails.each {|path, tw|
358
361
  if @follow_inodes
359
- hash[target_info.ino] = target_info
362
+ hash[tw.ino] = TargetInfo.new(tw.path, tw.ino)
360
363
  else
361
- hash[target_info.path] = target_info
364
+ hash[tw.path] = TargetInfo.new(tw.path, tw.ino)
362
365
  end
363
366
  }
364
367
  hash
@@ -406,6 +409,8 @@ module Fluent::Plugin
406
409
  event_loop_attach(watcher)
407
410
  end
408
411
 
412
+ tw.group_watcher = add_path_to_group_watcher(target_info.path)
413
+
409
414
  tw
410
415
  rescue => e
411
416
  if tw
@@ -420,36 +425,31 @@ module Fluent::Plugin
420
425
  end
421
426
 
422
427
  def construct_watcher(target_info)
428
+ path = target_info.path
429
+
430
+ # The file might be rotated or removed after collecting paths, so check inode again here.
431
+ begin
432
+ target_info.ino = Fluent::FileWrapper.stat(path).ino
433
+ rescue Errno::ENOENT, Errno::EACCES
434
+ $log.warn "stat() for #{path} failed. Continuing without tailing it."
435
+ return
436
+ end
437
+
423
438
  pe = nil
424
439
  if @pf
425
440
  pe = @pf[target_info]
426
- if @read_from_head && pe.read_inode.zero?
427
- begin
428
- pe.update(Fluent::FileWrapper.stat(target_info.path).ino, 0)
429
- rescue Errno::ENOENT, Errno::EACCES
430
- $log.warn "stat() for #{target_info.path} failed. Continuing without tailing it."
431
- end
432
- end
441
+ pe.update(target_info.ino, 0) if @read_from_head && pe.read_inode.zero?
433
442
  end
434
443
 
435
444
  begin
436
445
  tw = setup_watcher(target_info, pe)
437
446
  rescue WatcherSetupError => e
438
- log.warn "Skip #{target_info.path} because unexpected setup error happens: #{e}"
447
+ log.warn "Skip #{path} because unexpected setup error happens: #{e}"
439
448
  return
440
449
  end
441
450
 
442
- begin
443
- target_info = TargetInfo.new(target_info.path, Fluent::FileWrapper.stat(target_info.path).ino)
444
- @tails.delete(target_info)
445
- @tails[target_info] = tw
446
- tw.on_notify
447
- rescue Errno::ENOENT, Errno::EACCES => e
448
- $log.warn "stat() for #{target_info.path} failed with #{e.class.name}. Drop tail watcher for now."
449
- # explicitly detach and unwatch watcher `tw`.
450
- tw.unwatched = true
451
- detach_watcher(tw, target_info.ino, false)
452
- end
451
+ @tails[path] = tw
452
+ tw.on_notify
453
453
  end
454
454
 
455
455
  def start_watchers(targets_info)
@@ -461,10 +461,12 @@ module Fluent::Plugin
461
461
 
462
462
  def stop_watchers(targets_info, immediate: false, unwatched: false, remove_watcher: true)
463
463
  targets_info.each_value { |target_info|
464
+ remove_path_from_group_watcher(target_info.path)
465
+
464
466
  if remove_watcher
465
- tw = @tails.delete(target_info)
467
+ tw = @tails.delete(target_info.path)
466
468
  else
467
- tw = @tails[target_info]
469
+ tw = @tails[target_info.path]
468
470
  end
469
471
  if tw
470
472
  tw.unwatched = unwatched
@@ -478,8 +480,8 @@ module Fluent::Plugin
478
480
  end
479
481
 
480
482
  def close_watcher_handles
481
- @tails.keys.each do |target_info|
482
- tw = @tails.delete(target_info)
483
+ @tails.keys.each do |path|
484
+ tw = @tails.delete(path)
483
485
  if tw
484
486
  tw.close
485
487
  end
@@ -488,20 +490,20 @@ module Fluent::Plugin
488
490
 
489
491
  # refresh_watchers calls @tails.keys so we don't use stop_watcher -> start_watcher sequence for safety.
490
492
  def update_watcher(target_info, pe)
491
- log.info("detected rotation of #{target_info.path}; waiting #{@rotate_wait} seconds")
493
+ path = target_info.path
494
+
495
+ log.info("detected rotation of #{path}; waiting #{@rotate_wait} seconds")
492
496
 
493
497
  if @pf
494
498
  pe_inode = pe.read_inode
495
- target_info_from_position_entry = TargetInfo.new(target_info.path, pe_inode)
499
+ target_info_from_position_entry = TargetInfo.new(path, pe_inode)
496
500
  unless pe_inode == @pf[target_info_from_position_entry].read_inode
497
501
  log.debug "Skip update_watcher because watcher has been already updated by other inotify event"
498
502
  return
499
503
  end
500
504
  end
501
505
 
502
- rotated_target_info = TargetInfo.new(target_info.path, pe.read_inode)
503
- rotated_tw = @tails[rotated_target_info]
504
- new_target_info = target_info.dup
506
+ rotated_tw = @tails[path]
505
507
 
506
508
  if @follow_inodes
507
509
  new_position_entry = @pf[target_info]
@@ -509,17 +511,13 @@ module Fluent::Plugin
509
511
  if new_position_entry.read_inode == 0
510
512
  # When follow_inodes is true, it's not cleaned up by refresh_watcher.
511
513
  # So it should be unwatched here explicitly.
512
- rotated_tw.unwatched = true
513
- # Make sure to delete old key, it has a different ino while the hash key is same.
514
- @tails.delete(rotated_target_info)
515
- @tails[new_target_info] = setup_watcher(new_target_info, new_position_entry)
516
- @tails[new_target_info].on_notify
514
+ rotated_tw.unwatched = true if rotated_tw
515
+ @tails[path] = setup_watcher(target_info, new_position_entry)
516
+ @tails[path].on_notify
517
517
  end
518
518
  else
519
- # Make sure to delete old key, it has a different ino while the hash key is same.
520
- @tails.delete(rotated_target_info)
521
- @tails[new_target_info] = setup_watcher(new_target_info, pe)
522
- @tails[new_target_info].on_notify
519
+ @tails[path] = setup_watcher(target_info, pe)
520
+ @tails[path].on_notify
523
521
  end
524
522
  detach_watcher_after_rotate_wait(rotated_tw, pe.read_inode) if rotated_tw
525
523
  end
@@ -542,18 +540,19 @@ module Fluent::Plugin
542
540
  end
543
541
  end
544
542
 
543
+ def throttling_is_enabled?(tw)
544
+ return true if @read_bytes_limit_per_second > 0
545
+ return true if tw.group_watcher && tw.group_watcher.limit >= 0
546
+ false
547
+ end
548
+
545
549
  def detach_watcher_after_rotate_wait(tw, ino)
546
550
  # Call event_loop_attach/event_loop_detach is high-cost for short-live object.
547
551
  # If this has a problem with large number of files, use @_event_loop directly instead of timer_execute.
548
552
  if @open_on_every_update
549
553
  # Detach now because it's already closed, waiting it doesn't make sense.
550
554
  detach_watcher(tw, ino)
551
- elsif @read_bytes_limit_per_second < 0
552
- # throttling isn't enabled, just wait @rotate_wait
553
- timer_execute(:in_tail_close_watcher, @rotate_wait, repeat: false) do
554
- detach_watcher(tw, ino)
555
- end
556
- else
555
+ elsif throttling_is_enabled?(tw)
557
556
  # When the throttling feature is enabled, it might not reach EOF yet.
558
557
  # Should ensure to read all contents before closing it, with keeping throttling.
559
558
  start_time_to_wait = Fluent::Clock.now
@@ -564,6 +563,11 @@ module Fluent::Plugin
564
563
  detach_watcher(tw, ino)
565
564
  end
566
565
  end
566
+ else
567
+ # when the throttling feature isn't enabled, just wait @rotate_wait
568
+ timer_execute(:in_tail_close_watcher, @rotate_wait, repeat: false) do
569
+ detach_watcher(tw, ino)
570
+ end
567
571
  end
568
572
  end
569
573
 
@@ -775,6 +779,7 @@ module Fluent::Plugin
775
779
  attr_reader :line_buffer_timer_flusher
776
780
  attr_accessor :unwatched # This is used for removing position entry from PositionFile
777
781
  attr_reader :watchers
782
+ attr_accessor :group_watcher
778
783
 
779
784
  def tag
780
785
  @parsed_tag ||= @path.tr('/', '.').gsub(/\.+/, '.').gsub(/^\./, '')
@@ -997,6 +1002,10 @@ module Fluent::Plugin
997
1002
  @log.info "following tail of #{@path}"
998
1003
  end
999
1004
 
1005
+ def group_watcher
1006
+ @watcher.group_watcher
1007
+ end
1008
+
1000
1009
  def on_notify
1001
1010
  @notify_mutex.synchronize { handle_notify }
1002
1011
  end
@@ -1054,6 +1063,7 @@ module Fluent::Plugin
1054
1063
 
1055
1064
  def handle_notify
1056
1065
  return if limit_bytes_per_second_reached?
1066
+ return if group_watcher&.limit_lines_reached?(@path)
1057
1067
 
1058
1068
  with_io do |io|
1059
1069
  begin
@@ -1063,17 +1073,26 @@ module Fluent::Plugin
1063
1073
  begin
1064
1074
  while true
1065
1075
  @start_reading_time ||= Fluent::Clock.now
1076
+ group_watcher&.update_reading_time(@path)
1077
+
1066
1078
  data = io.readpartial(BYTES_TO_READ, @iobuf)
1067
1079
  @eof = false
1068
1080
  @number_bytes_read += data.bytesize
1069
1081
  @fifo << data
1082
+
1083
+ n_lines_before_read = @lines.size
1070
1084
  @fifo.read_lines(@lines)
1085
+ group_watcher&.update_lines_read(@path, @lines.size - n_lines_before_read)
1086
+
1087
+ group_watcher_limit = group_watcher&.limit_lines_reached?(@path)
1088
+ @log.debug "Reading Limit exceeded #{@path} #{group_watcher.number_lines_read}" if group_watcher_limit
1071
1089
 
1072
- if limit_bytes_per_second_reached? || should_shutdown_now?
1090
+ if group_watcher_limit || limit_bytes_per_second_reached? || should_shutdown_now?
1073
1091
  # Just get out from tailing loop.
1074
1092
  read_more = false
1075
1093
  break
1076
1094
  end
1095
+
1077
1096
  if @lines.size >= @read_lines_limit
1078
1097
  # not to use too much memory in case the file is very large
1079
1098
  read_more = true
@@ -50,6 +50,7 @@ module Fluent::Plugin
50
50
  def checkin(sock)
51
51
  @mutex.synchronize do
52
52
  if (s = @inflight_sockets.delete(sock))
53
+ s.timeout = timeout
53
54
  @available_sockets[s.key] << s
54
55
  else
55
56
  @log.debug("there is no socket #{sock}")
@@ -122,6 +123,7 @@ module Fluent::Plugin
122
123
  t = Time.now
123
124
  if (s = @available_sockets[key].find { |sock| !expired_socket?(sock, time: t) })
124
125
  @inflight_sockets[s.sock] = @available_sockets[key].delete(s)
126
+ s.timeout = timeout
125
127
  s
126
128
  else
127
129
  nil