fluentd 1.12.4-x86-mingw32 → 1.13.3-x86-mingw32

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 (71) hide show
  1. checksums.yaml +4 -4
  2. data/.github/ISSUE_TEMPLATE/bug_report.yaml +69 -0
  3. data/.github/ISSUE_TEMPLATE/feature_request.yaml +38 -0
  4. data/.github/workflows/linux-test.yaml +1 -1
  5. data/.github/workflows/windows-test.yaml +14 -3
  6. data/.gitlab-ci.yml +0 -22
  7. data/CHANGELOG.md +129 -0
  8. data/CONTRIBUTING.md +2 -2
  9. data/MAINTAINERS.md +1 -1
  10. data/README.md +3 -3
  11. data/bin/fluentd +8 -1
  12. data/example/counter.conf +1 -1
  13. data/example/v0_12_filter.conf +2 -2
  14. data/fluentd.gemspec +1 -1
  15. data/lib/fluent/command/cat.rb +19 -3
  16. data/lib/fluent/command/fluentd.rb +1 -2
  17. data/lib/fluent/command/plugin_generator.rb +15 -5
  18. data/lib/fluent/config.rb +1 -1
  19. data/lib/fluent/config/section.rb +5 -0
  20. data/lib/fluent/config/types.rb +15 -0
  21. data/lib/fluent/config/v1_parser.rb +3 -2
  22. data/lib/fluent/env.rb +2 -1
  23. data/lib/fluent/log.rb +1 -0
  24. data/lib/fluent/oj_options.rb +62 -0
  25. data/lib/fluent/plugin/file_wrapper.rb +35 -4
  26. data/lib/fluent/plugin/formatter.rb +1 -0
  27. data/lib/fluent/plugin/formatter_json.rb +9 -7
  28. data/lib/fluent/plugin/in_http.rb +10 -0
  29. data/lib/fluent/plugin/in_tail.rb +159 -41
  30. data/lib/fluent/plugin/in_tail/position_file.rb +20 -18
  31. data/lib/fluent/plugin/out_forward.rb +14 -33
  32. data/lib/fluent/plugin/parser_json.rb +2 -3
  33. data/lib/fluent/plugin/service_discovery.rb +0 -15
  34. data/lib/fluent/plugin_helper/http_server/router.rb +1 -1
  35. data/lib/fluent/plugin_helper/service_discovery.rb +39 -1
  36. data/lib/fluent/plugin_helper/service_discovery/manager.rb +11 -5
  37. data/lib/fluent/supervisor.rb +15 -0
  38. data/lib/fluent/system_config.rb +14 -0
  39. data/lib/fluent/test/driver/storage.rb +30 -0
  40. data/lib/fluent/version.rb +1 -1
  41. data/templates/new_gem/lib/fluent/plugin/storage.rb.erb +40 -0
  42. data/templates/new_gem/test/plugin/test_storage.rb.erb +18 -0
  43. data/test/command/test_cat.rb +99 -0
  44. data/test/command/test_plugin_generator.rb +2 -1
  45. data/test/config/test_section.rb +9 -0
  46. data/test/config/test_system_config.rb +46 -0
  47. data/test/config/test_types.rb +7 -0
  48. data/test/plugin/in_tail/test_io_handler.rb +4 -4
  49. data/test/plugin/in_tail/test_position_file.rb +23 -5
  50. data/test/plugin/test_file_wrapper.rb +22 -1
  51. data/test/plugin/test_in_forward.rb +59 -83
  52. data/test/plugin/test_in_http.rb +58 -40
  53. data/test/plugin/test_in_syslog.rb +66 -56
  54. data/test/plugin/test_in_tail.rb +341 -1
  55. data/test/plugin/test_in_tcp.rb +45 -32
  56. data/test/plugin/test_in_udp.rb +47 -33
  57. data/test/plugin/test_out_forward.rb +114 -95
  58. data/test/plugin/test_out_stream.rb +18 -8
  59. data/test/plugin_helper/http_server/test_route.rb +1 -1
  60. data/test/plugin_helper/test_child_process.rb +1 -1
  61. data/test/plugin_helper/test_http_server_helper.rb +33 -26
  62. data/test/plugin_helper/test_server.rb +137 -138
  63. data/test/plugin_helper/test_service_discovery.rb +74 -14
  64. data/test/plugin_helper/test_socket.rb +16 -9
  65. data/test/test_config.rb +2 -1
  66. data/test/test_event_time.rb +2 -2
  67. data/test/test_oj_options.rb +55 -0
  68. data/test/test_supervisor.rb +35 -0
  69. metadata +15 -7
  70. data/.github/ISSUE_TEMPLATE/bug_report.md +0 -40
  71. data/.github/ISSUE_TEMPLATE/feature_request.md +0 -23
@@ -85,9 +85,8 @@ op.on('-o', '--log PATH', "log file path") {|s|
85
85
  opts[:log_path] = s
86
86
  }
87
87
 
88
- ROTATE_AGE = %w(daily weekly monthly)
89
88
  op.on('--log-rotate-age AGE', 'generations to keep rotated log files') {|age|
90
- if ROTATE_AGE.include?(age)
89
+ if Fluent::Log::LOG_ROTATE_AGE.include?(age)
91
90
  opts[:log_rotate_age] = age
92
91
  else
93
92
  begin
@@ -27,7 +27,7 @@ class FluentPluginGenerator
27
27
  attr_reader :type, :name
28
28
  attr_reader :license_name
29
29
 
30
- SUPPORTED_TYPES = ["input", "output", "filter", "parser", "formatter"]
30
+ SUPPORTED_TYPES = ["input", "output", "filter", "parser", "formatter", "storage"]
31
31
 
32
32
  def initialize(argv = ARGV)
33
33
  @argv = argv
@@ -164,8 +164,13 @@ BANNER
164
164
  end
165
165
 
166
166
  def locked_gem_version(gem_name)
167
- d = Bundler::Definition.build(gem_file_path, lock_file_path, false)
168
- d.locked_gems.dependencies[gem_name].requirement.requirements.first.last.version
167
+ if File.exist?(lock_file_path)
168
+ d = Bundler::Definition.build(gem_file_path, lock_file_path, false)
169
+ d.locked_gems.dependencies[gem_name].requirement.requirements.first.last.version
170
+ else
171
+ # fallback even though Fluentd is installed without bundler
172
+ Gem::Specification.find_by_name(gem_name).version.version
173
+ end
169
174
  end
170
175
 
171
176
  def rake_version
@@ -177,8 +182,13 @@ BANNER
177
182
  end
178
183
 
179
184
  def bundler_version
180
- d = Bundler::Definition.build(gem_file_path, lock_file_path, false)
181
- d.locked_gems.bundler_version.version
185
+ if File.exist?(lock_file_path)
186
+ d = Bundler::Definition.build(gem_file_path, lock_file_path, false)
187
+ d.locked_gems.bundler_version.version
188
+ else
189
+ # fallback even though Fluentd is installed without bundler
190
+ Gem::Specification.find_by_name("bundler").version.version
191
+ end
182
192
  end
183
193
 
184
194
  def class_name
data/lib/fluent/config.rb CHANGED
@@ -62,7 +62,7 @@ module Fluent
62
62
  Parser.parse(str, fname, basepath)
63
63
  when :ruby
64
64
  require 'fluent/config/dsl'
65
- $log.warn("Ruby DSL configuration format is deprecated. Please use original configuration format. https://docs.fluentd.org/configuration/config-file")
65
+ $log.warn("Ruby DSL configuration format is deprecated. Please use original configuration format. https://docs.fluentd.org/configuration/config-file") if $log
66
66
  Config::DSL::Parser.parse(str, File.join(basepath, fname))
67
67
  else
68
68
  raise "[BUG] unknown configuration parser specification:'#{parser}'"
@@ -50,6 +50,11 @@ module Fluent
50
50
  "<Fluent::Config::Section #{@params.to_json}>"
51
51
  end
52
52
 
53
+ # Used by PP and Pry
54
+ def pretty_print(q)
55
+ q.text(inspect)
56
+ end
57
+
53
58
  def nil?
54
59
  false
55
60
  end
@@ -20,6 +20,10 @@ require 'fluent/config/error'
20
20
 
21
21
  module Fluent
22
22
  module Config
23
+ def self.reformatted_value(type, val, opts = {}, name = nil)
24
+ REFORMAT_VALUE.call(type, val, opts, name)
25
+ end
26
+
23
27
  def self.size_value(str, opts = {}, name = nil)
24
28
  return nil if str.nil?
25
29
 
@@ -104,6 +108,16 @@ module Fluent
104
108
  Config.string_value(val, opts, name)
105
109
  }
106
110
 
111
+ def self.symbol_value(val, opts = {}, name = nil)
112
+ return nil if val.nil? || val.empty?
113
+
114
+ val.delete_prefix(":").to_sym
115
+ end
116
+
117
+ SYMBOL_TYPE = Proc.new { |val, opts = {}, name = nil|
118
+ Config.symbol_value(val, opts, name)
119
+ }
120
+
107
121
  def self.enum_value(val, opts = {}, name = nil)
108
122
  return nil if val.nil?
109
123
 
@@ -176,6 +190,7 @@ module Fluent
176
190
  when :bool then Config.bool_value(value, opts, name)
177
191
  when :time then Config.time_value(value, opts, name)
178
192
  when :regexp then Config.regexp_value(value, opts, name)
193
+ when :symbol then Config.symbol_value(value, opts, name)
179
194
  else
180
195
  raise "unknown type in REFORMAT: #{type}"
181
196
  end
@@ -37,6 +37,7 @@ module Fluent
37
37
  super(strscan, eval_context)
38
38
  @include_basepath = include_basepath
39
39
  @fname = fname
40
+ @logger = defined?($log) ? $log : nil
40
41
  end
41
42
 
42
43
  def parse!
@@ -99,7 +100,7 @@ module Fluent
99
100
 
100
101
  elsif root_element && skip(/(\@include|include)#{SPACING}/)
101
102
  if !prev_match.start_with?('@')
102
- $log.warn "'include' is deprecated. Use '@include' instead"
103
+ @logger.warn "'include' is deprecated. Use '@include' instead" if @logger
103
104
  end
104
105
  parse_include(attrs, elems)
105
106
 
@@ -123,7 +124,7 @@ module Fluent
123
124
  parse_error! "'@' is the system reserved prefix. Don't use '@' prefix parameter in the configuration: #{k}"
124
125
  else
125
126
  # TODO: This is for backward compatibility. It will throw an error in the future.
126
- $log.warn "'@' is the system reserved prefix. It works in the nested configuration for now but it will be rejected: #{k}"
127
+ @logger.warn "'@' is the system reserved prefix. It works in the nested configuration for now but it will be rejected: #{k}" if @logger
127
128
  end
128
129
  end
129
130
 
data/lib/fluent/env.rb CHANGED
@@ -15,13 +15,14 @@
15
15
  #
16
16
 
17
17
  require 'serverengine/utils'
18
+ require 'fluent/oj_options'
18
19
 
19
20
  module Fluent
20
21
  DEFAULT_CONFIG_PATH = ENV['FLUENT_CONF'] || '/etc/fluent/fluent.conf'
21
22
  DEFAULT_PLUGIN_DIR = ENV['FLUENT_PLUGIN'] || '/etc/fluent/plugin'
22
23
  DEFAULT_SOCKET_PATH = ENV['FLUENT_SOCKET'] || '/var/run/fluent/fluent.sock'
23
24
  DEFAULT_BACKUP_DIR = ENV['FLUENT_BACKUP_DIR'] || '/tmp/fluent'
24
- DEFAULT_OJ_OPTIONS = {bigdecimal_load: :float, mode: :compat, use_to_json: true}
25
+ DEFAULT_OJ_OPTIONS = Fluent::OjOptions.load_env
25
26
  DEFAULT_DIR_PERMISSION = 0755
26
27
  DEFAULT_FILE_PERMISSION = 0644
27
28
 
data/lib/fluent/log.rb CHANGED
@@ -49,6 +49,7 @@ module Fluent
49
49
  LOG_TYPE_DEFAULT = :default # show logs in all supervisor/workers, with worker id in workers (default)
50
50
 
51
51
  LOG_TYPES = [LOG_TYPE_SUPERVISOR, LOG_TYPE_WORKER0, LOG_TYPE_DEFAULT].freeze
52
+ LOG_ROTATE_AGE = %w(daily weekly monthly)
52
53
 
53
54
  def self.str_to_level(log_level_str)
54
55
  case log_level_str.downcase
@@ -0,0 +1,62 @@
1
+ require 'fluent/config/types'
2
+
3
+ module Fluent
4
+ class OjOptions
5
+ OPTIONS = {
6
+ 'bigdecimal_load': :symbol,
7
+ 'max_nesting': :integer,
8
+ 'mode': :symbol,
9
+ 'use_to_json': :bool
10
+ }
11
+
12
+ ALLOWED_VALUES = {
13
+ 'bigdecimal_load': %i[bigdecimal float auto],
14
+ 'mode': %i[strict null compat json rails object custom]
15
+ }
16
+
17
+ DEFAULTS = {
18
+ 'bigdecimal_load': :float,
19
+ 'mode': :compat,
20
+ 'use_to_json': true
21
+ }
22
+
23
+ @@available = false
24
+
25
+ def self.available?
26
+ @@available
27
+ end
28
+
29
+ def self.load_env
30
+ options = self.get_options
31
+ begin
32
+ require 'oj'
33
+ Oj.default_options = options
34
+ @@available = true
35
+ rescue LoadError
36
+ @@available = false
37
+ end
38
+ options
39
+ end
40
+
41
+ private
42
+
43
+ def self.get_options
44
+ options = {}
45
+ DEFAULTS.each { |key, value| options[key] = value }
46
+
47
+ OPTIONS.each do |key, type|
48
+ env_value = ENV["FLUENT_OJ_OPTION_#{key.upcase}"]
49
+ next if env_value.nil?
50
+
51
+ cast_value = Fluent::Config.reformatted_value(OPTIONS[key], env_value, { strict: true })
52
+ next if cast_value.nil?
53
+
54
+ next if ALLOWED_VALUES[key] && !ALLOWED_VALUES[key].include?(cast_value)
55
+
56
+ options[key.to_sym] = cast_value
57
+ end
58
+
59
+ options
60
+ end
61
+ end
62
+ end
@@ -52,6 +52,8 @@ module Fluent
52
52
 
53
53
  attr_reader :errcode, :msg
54
54
 
55
+ WSABASEERR = 10000
56
+
55
57
  def initialize(errcode, msg = nil)
56
58
  @errcode = errcode
57
59
  @msg = msg
@@ -80,6 +82,10 @@ module Fluent
80
82
  return false if other.class != Win32Error
81
83
  @errcode == other.errcode && @msg == other.msg
82
84
  end
85
+
86
+ def wsaerr?
87
+ @errcode >= WSABASEERR
88
+ end
83
89
  end
84
90
 
85
91
  # To open and get stat with setting FILE_SHARE_DELETE
@@ -113,11 +119,14 @@ module Fluent
113
119
  @file_handle = CreateFile.call(@path, access, sharemode,
114
120
  0, creationdisposition, FILE_ATTRIBUTE_NORMAL, 0)
115
121
  if @file_handle == INVALID_HANDLE_VALUE
116
- err = Win32::API.last_error
117
- if err == ERROR_FILE_NOT_FOUND || err == ERROR_PATH_NOT_FOUND || err == ERROR_ACCESS_DENIED
118
- raise Errno::ENOENT
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)
119
129
  end
120
- raise Win32Error.new(err, path)
121
130
  end
122
131
  end
123
132
 
@@ -146,7 +155,29 @@ module Fluent
146
155
  by_handle_file_information.unpack("I11Q1")[11] # fileindex
147
156
  end
148
157
 
158
+ # DeletePending is a Windows-specific file state that roughly means
159
+ # "this file is queued for deletion, so close any open handlers"
160
+ #
161
+ # This flag can be retrieved via GetFileInformationByHandleEx().
162
+ #
163
+ # https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-getfileinformationbyhandleex
164
+ #
165
+ def delete_pending
166
+ file_standard_info = 0x01
167
+ bufsize = 1024
168
+ buf = '\0' * bufsize
169
+
170
+ unless GetFileInformationByHandleEx.call(@file_handle, file_standard_info, buf, bufsize)
171
+ return false
172
+ end
173
+
174
+ return buf.unpack("QQICC")[3] != 0
175
+ end
176
+
177
+ private :delete_pending
178
+
149
179
  def stat
180
+ raise Errno::ENOENT if delete_pending
150
181
  s = File.stat(@path)
151
182
  s.instance_variable_set :@ino, self.ino
152
183
  def s.ino; @ino; end
@@ -14,6 +14,7 @@
14
14
  # limitations under the License.
15
15
  #
16
16
 
17
+ require 'fluent/env'
17
18
  require 'fluent/plugin/base'
18
19
  require 'fluent/plugin/owned_by_mixin'
19
20
  require 'fluent/time'
@@ -15,7 +15,7 @@
15
15
  #
16
16
 
17
17
  require 'fluent/plugin/formatter'
18
- require 'fluent/env'
18
+ require 'fluent/oj_options'
19
19
 
20
20
  module Fluent
21
21
  module Plugin
@@ -30,12 +30,14 @@ module Fluent
30
30
  def configure(conf)
31
31
  super
32
32
 
33
- begin
34
- raise LoadError unless @json_parser == 'oj'
35
- require 'oj'
36
- Oj.default_options = Fluent::DEFAULT_OJ_OPTIONS
37
- @dump_proc = Oj.method(:dump)
38
- rescue LoadError
33
+ if @json_parser == 'oj'
34
+ if Fluent::OjOptions.available?
35
+ @dump_proc = Oj.method(:dump)
36
+ else
37
+ log.info "Oj isn't installed, fallback to Yajl as json parser"
38
+ @dump_proc = Yajl.method(:dump)
39
+ end
40
+ else
39
41
  @dump_proc = Yajl.method(:dump)
40
42
  end
41
43
 
@@ -461,6 +461,12 @@ module Fluent::Plugin
461
461
  RES_200_STATUS = "200 OK".freeze
462
462
  RES_403_STATUS = "403 Forbidden".freeze
463
463
 
464
+ # Azure App Service sends GET requests for health checking purpose.
465
+ # Respond with `200 OK` to accommodate it.
466
+ def handle_get_request
467
+ return send_response_and_close(RES_200_STATUS, {}, "")
468
+ end
469
+
464
470
  # Web browsers can send an OPTIONS request before performing POST
465
471
  # to check if cross-origin requests are supported.
466
472
  def handle_options_request
@@ -494,6 +500,10 @@ module Fluent::Plugin
494
500
  def on_message_complete
495
501
  return if closing?
496
502
 
503
+ if @parser.http_method == 'GET'.freeze
504
+ return handle_get_request()
505
+ end
506
+
497
507
  if @parser.http_method == 'OPTIONS'.freeze
498
508
  return handle_options_request()
499
509
  end
@@ -56,6 +56,7 @@ module Fluent::Plugin
56
56
  @pf_file = nil
57
57
  @pf = nil
58
58
  @ignore_list = []
59
+ @shutdown_start_time = nil
59
60
  end
60
61
 
61
62
  desc 'The paths to read. Multiple paths can be specified, separated by comma.'
@@ -81,6 +82,8 @@ module Fluent::Plugin
81
82
  config_param :refresh_interval, :time, default: 60
82
83
  desc 'The number of reading lines at each IO.'
83
84
  config_param :read_lines_limit, :integer, default: 1000
85
+ desc 'The number of reading bytes per second'
86
+ config_param :read_bytes_limit_per_second, :size, default: -1
84
87
  desc 'The interval of flushing the buffer for multiline format'
85
88
  config_param :multiline_flush_interval, :time, default: nil
86
89
  desc 'Enable the option to emit unmatched lines.'
@@ -178,6 +181,16 @@ module Fluent::Plugin
178
181
  # parser is already created by parser helper
179
182
  @parser = parser_create(usage: parser_config['usage'] || @parser_configs.first.usage)
180
183
  @capability = Fluent::Capability.new(:current_process)
184
+ if @read_bytes_limit_per_second > 0
185
+ if !@enable_watch_timer
186
+ raise Fluent::ConfigError, "Need to enable watch timer when using log throttling feature"
187
+ end
188
+ min_bytes = TailWatcher::IOHandler::BYTES_TO_READ
189
+ if @read_bytes_limit_per_second < min_bytes
190
+ log.warn "Should specify greater equal than #{min_bytes}. Use #{min_bytes} for read_bytes_limit_per_second"
191
+ @read_bytes_limit_per_second = min_bytes
192
+ end
193
+ end
181
194
  end
182
195
 
183
196
  def configure_tag
@@ -244,6 +257,7 @@ module Fluent::Plugin
244
257
  end
245
258
 
246
259
  def shutdown
260
+ @shutdown_start_time = Fluent::Clock.now
247
261
  # during shutdown phase, don't close io. It should be done in close after all threads are stopped. See close.
248
262
  stop_watchers(existence_path, immediate: true, remove_watcher: false)
249
263
  @pf_file.close if @pf_file
@@ -290,7 +304,7 @@ module Fluent::Plugin
290
304
  end
291
305
  false
292
306
  end
293
- rescue Errno::ENOENT
307
+ rescue Errno::ENOENT, Errno::EACCES
294
308
  log.debug("#{p} is missing after refresh file list")
295
309
  false
296
310
  end
@@ -322,8 +336,8 @@ module Fluent::Plugin
322
336
  else
323
337
  hash[target_info.path] = target_info
324
338
  end
325
- rescue Errno::ENOENT
326
- $log.warn "expand_paths: stat() for #{path} failed with ENOENT. Skip file."
339
+ rescue Errno::ENOENT, Errno::EACCES => e
340
+ $log.warn "expand_paths: stat() for #{path} failed with #{e.class.name}. Skip file."
327
341
  end
328
342
  }
329
343
  hash
@@ -373,8 +387,6 @@ module Fluent::Plugin
373
387
  tw.register_watcher(tt)
374
388
  end
375
389
 
376
- tw.on_notify
377
-
378
390
  tw.watchers.each do |watcher|
379
391
  event_loop_attach(watcher)
380
392
  end
@@ -386,42 +398,49 @@ module Fluent::Plugin
386
398
  event_loop_detach(watcher)
387
399
  end
388
400
 
389
- tw.detach
401
+ tw.detach(@shutdown_start_time)
390
402
  tw.close
391
403
  end
392
404
  raise e
393
405
  end
394
406
 
395
- def start_watchers(targets_info)
396
- targets_info.each_value { |target_info|
397
- pe = nil
398
- if @pf
399
- pe = @pf[target_info]
400
- if @read_from_head && pe.read_inode.zero?
401
- begin
402
- pe.update(Fluent::FileWrapper.stat(target_info.path).ino, 0)
403
- rescue Errno::ENOENT
404
- $log.warn "#{target_info.path} not found. Continuing without tailing it."
405
- end
407
+ def construct_watcher(target_info)
408
+ pe = nil
409
+ if @pf
410
+ pe = @pf[target_info]
411
+ if @read_from_head && pe.read_inode.zero?
412
+ begin
413
+ pe.update(Fluent::FileWrapper.stat(target_info.path).ino, 0)
414
+ rescue Errno::ENOENT, Errno::EACCES
415
+ $log.warn "stat() for #{target_info.path} failed. Continuing without tailing it."
406
416
  end
407
417
  end
418
+ end
408
419
 
409
- begin
410
- tw = setup_watcher(target_info, pe)
411
- rescue WatcherSetupError => e
412
- log.warn "Skip #{target_info.path} because unexpected setup error happens: #{e}"
413
- next
414
- end
420
+ begin
421
+ tw = setup_watcher(target_info, pe)
422
+ rescue WatcherSetupError => e
423
+ log.warn "Skip #{target_info.path} because unexpected setup error happens: #{e}"
424
+ return
425
+ end
415
426
 
416
- begin
417
- target_info = TargetInfo.new(target_info.path, Fluent::FileWrapper.stat(target_info.path).ino)
418
- @tails[target_info] = tw
419
- rescue Errno::ENOENT
420
- $log.warn "stat() for #{target_info.path} failed with ENOENT. Drop tail watcher for now."
421
- # explicitly detach and unwatch watcher `tw`.
422
- tw.unwatched = true
423
- detach_watcher(tw, target_info.ino, false)
424
- end
427
+ begin
428
+ target_info = TargetInfo.new(target_info.path, Fluent::FileWrapper.stat(target_info.path).ino)
429
+ @tails.delete(target_info)
430
+ @tails[target_info] = tw
431
+ tw.on_notify
432
+ rescue Errno::ENOENT, Errno::EACCES => e
433
+ $log.warn "stat() for #{target_info.path} failed with #{e.class.name}. Drop tail watcher for now."
434
+ # explicitly detach and unwatch watcher `tw`.
435
+ tw.unwatched = true
436
+ detach_watcher(tw, target_info.ino, false)
437
+ end
438
+ end
439
+
440
+ def start_watchers(targets_info)
441
+ targets_info.each_value {|target_info|
442
+ construct_watcher(target_info)
443
+ break if before_shutdown?
425
444
  }
426
445
  end
427
446
 
@@ -473,10 +492,19 @@ module Fluent::Plugin
473
492
  new_position_entry = @pf[target_info]
474
493
 
475
494
  if new_position_entry.read_inode == 0
495
+ # When follow_inodes is true, it's not cleaned up by refresh_watcher.
496
+ # So it should be unwatched here explicitly.
497
+ rotated_tw.unwatched = true
498
+ # Make sure to delete old key, it has a different ino while the hash key is same.
499
+ @tails.delete(rotated_target_info)
476
500
  @tails[new_target_info] = setup_watcher(new_target_info, new_position_entry)
501
+ @tails[new_target_info].on_notify
477
502
  end
478
503
  else
504
+ # Make sure to delete old key, it has a different ino while the hash key is same.
505
+ @tails.delete(rotated_target_info)
479
506
  @tails[new_target_info] = setup_watcher(new_target_info, pe)
507
+ @tails[new_target_info].on_notify
480
508
  end
481
509
  detach_watcher_after_rotate_wait(rotated_tw, pe.read_inode) if rotated_tw
482
510
  end
@@ -489,7 +517,7 @@ module Fluent::Plugin
489
517
  tw.watchers.each do |watcher|
490
518
  event_loop_detach(watcher)
491
519
  end
492
- tw.detach
520
+ tw.detach(@shutdown_start_time)
493
521
 
494
522
  tw.close if close_io
495
523
 
@@ -502,8 +530,25 @@ module Fluent::Plugin
502
530
  def detach_watcher_after_rotate_wait(tw, ino)
503
531
  # Call event_loop_attach/event_loop_detach is high-cost for short-live object.
504
532
  # If this has a problem with large number of files, use @_event_loop directly instead of timer_execute.
505
- timer_execute(:in_tail_close_watcher, @rotate_wait, repeat: false) do
533
+ if @open_on_every_update
534
+ # Detach now because it's already closed, waiting it doesn't make sense.
506
535
  detach_watcher(tw, ino)
536
+ elsif @read_bytes_limit_per_second < 0
537
+ # throttling isn't enabled, just wait @rotate_wait
538
+ timer_execute(:in_tail_close_watcher, @rotate_wait, repeat: false) do
539
+ detach_watcher(tw, ino)
540
+ end
541
+ else
542
+ # When the throttling feature is enabled, it might not reach EOF yet.
543
+ # Should ensure to read all contents before closing it, with keeping throttling.
544
+ start_time_to_wait = Fluent::Clock.now
545
+ timer = timer_execute(:in_tail_close_watcher, 1, repeat: true) do
546
+ elapsed = Fluent::Clock.now - start_time_to_wait
547
+ if tw.eof? && elapsed >= @rotate_wait
548
+ timer.detach
549
+ detach_watcher(tw, ino)
550
+ end
551
+ end
507
552
  end
508
553
  end
509
554
 
@@ -633,6 +678,7 @@ module Fluent::Plugin
633
678
  path: path,
634
679
  log: log,
635
680
  read_lines_limit: @read_lines_limit,
681
+ read_bytes_limit_per_second: @read_bytes_limit_per_second,
636
682
  open_on_every_update: @open_on_every_update,
637
683
  from_encoding: @from_encoding,
638
684
  encoding: @encoding,
@@ -700,8 +746,11 @@ module Fluent::Plugin
700
746
  @watchers << watcher
701
747
  end
702
748
 
703
- def detach
704
- @io_handler.on_notify if @io_handler
749
+ def detach(shutdown_start_time = nil)
750
+ if @io_handler
751
+ @io_handler.ready_to_shutdown(shutdown_start_time)
752
+ @io_handler.on_notify
753
+ end
705
754
  @line_buffer_timer_flusher&.close(self)
706
755
  end
707
756
 
@@ -712,10 +761,14 @@ module Fluent::Plugin
712
761
  end
713
762
  end
714
763
 
764
+ def eof?
765
+ @io_handler.eof?
766
+ end
767
+
715
768
  def on_notify
716
769
  begin
717
770
  stat = Fluent::FileWrapper.stat(@path)
718
- rescue Errno::ENOENT
771
+ rescue Errno::ENOENT, Errno::EACCES
719
772
  # moved or deleted
720
773
  stat = nil
721
774
  end
@@ -786,7 +839,7 @@ module Fluent::Plugin
786
839
  # new watcher, and old watcher will be closed by stop_watcher in refresh_watchers method
787
840
  # don't want to swap state because we need latest read offset in pos file even after rotate_wait
788
841
  if stat
789
- target_info = TargetInfo.new(@path, stat)
842
+ target_info = TargetInfo.new(@path, stat.ino)
790
843
  @update_watcher.call(target_info, @pe)
791
844
  end
792
845
  else
@@ -876,10 +929,16 @@ module Fluent::Plugin
876
929
  end
877
930
 
878
931
  class IOHandler
879
- def initialize(watcher, path:, read_lines_limit:, log:, open_on_every_update:, from_encoding: nil, encoding: nil, &receive_lines)
932
+ BYTES_TO_READ = 8192
933
+ SHUTDOWN_TIMEOUT = 5
934
+
935
+ attr_accessor :shutdown_timeout
936
+
937
+ def initialize(watcher, path:, read_lines_limit:, read_bytes_limit_per_second:, log:, open_on_every_update:, from_encoding: nil, encoding: nil, &receive_lines)
880
938
  @watcher = watcher
881
939
  @path = path
882
940
  @read_lines_limit = read_lines_limit
941
+ @read_bytes_limit_per_second = read_bytes_limit_per_second
883
942
  @receive_lines = receive_lines
884
943
  @open_on_every_update = open_on_every_update
885
944
  @fifo = FIFO.new(from_encoding || Encoding::ASCII_8BIT, encoding || Encoding::ASCII_8BIT)
@@ -888,6 +947,12 @@ module Fluent::Plugin
888
947
  @io = nil
889
948
  @notify_mutex = Mutex.new
890
949
  @log = log
950
+ @start_reading_time = nil
951
+ @number_bytes_read = 0
952
+ @shutdown_start_time = nil
953
+ @shutdown_timeout = SHUTDOWN_TIMEOUT
954
+ @shutdown_mutex = Mutex.new
955
+ @eof = false
891
956
 
892
957
  @log.info "following tail of #{@path}"
893
958
  end
@@ -896,6 +961,13 @@ module Fluent::Plugin
896
961
  @notify_mutex.synchronize { handle_notify }
897
962
  end
898
963
 
964
+ def ready_to_shutdown(shutdown_start_time = nil)
965
+ @shutdown_mutex.synchronize {
966
+ @shutdown_start_time =
967
+ shutdown_start_time || Fluent::Clock.now
968
+ }
969
+ end
970
+
899
971
  def close
900
972
  if @io && !@io.closed?
901
973
  @io.close
@@ -907,9 +979,41 @@ module Fluent::Plugin
907
979
  !!@io
908
980
  end
909
981
 
982
+ def eof?
983
+ @eof
984
+ end
985
+
910
986
  private
911
987
 
988
+ def limit_bytes_per_second_reached?
989
+ return false if @read_bytes_limit_per_second < 0 # not enabled by conf
990
+ return false if @number_bytes_read < @read_bytes_limit_per_second
991
+
992
+ @start_reading_time ||= Fluent::Clock.now
993
+ time_spent_reading = Fluent::Clock.now - @start_reading_time
994
+ @log.debug("time_spent_reading: #{time_spent_reading} #{ @watcher.path}")
995
+
996
+ if time_spent_reading < 1
997
+ true
998
+ else
999
+ @start_reading_time = nil
1000
+ @number_bytes_read = 0
1001
+ false
1002
+ end
1003
+ end
1004
+
1005
+ def should_shutdown_now?
1006
+ # Ensure to read all remaining lines, but abort immediately if it
1007
+ # seems to take too long time.
1008
+ @shutdown_mutex.synchronize {
1009
+ return false if @shutdown_start_time.nil?
1010
+ return Fluent::Clock.now - @shutdown_start_time > @shutdown_timeout
1011
+ }
1012
+ end
1013
+
912
1014
  def handle_notify
1015
+ return if limit_bytes_per_second_reached?
1016
+
913
1017
  with_io do |io|
914
1018
  begin
915
1019
  read_more = false
@@ -917,8 +1021,18 @@ module Fluent::Plugin
917
1021
  if !io.nil? && @lines.empty?
918
1022
  begin
919
1023
  while true
920
- @fifo << io.readpartial(8192, @iobuf)
1024
+ @start_reading_time ||= Fluent::Clock.now
1025
+ data = io.readpartial(BYTES_TO_READ, @iobuf)
1026
+ @eof = false
1027
+ @number_bytes_read += data.bytesize
1028
+ @fifo << data
921
1029
  @fifo.read_lines(@lines)
1030
+
1031
+ if limit_bytes_per_second_reached? || should_shutdown_now?
1032
+ # Just get out from tailing loop.
1033
+ read_more = false
1034
+ break
1035
+ end
922
1036
  if @lines.size >= @read_lines_limit
923
1037
  # not to use too much memory in case the file is very large
924
1038
  read_more = true
@@ -926,6 +1040,7 @@ module Fluent::Plugin
926
1040
  end
927
1041
  end
928
1042
  rescue EOFError
1043
+ @eof = true
929
1044
  end
930
1045
  end
931
1046
 
@@ -948,7 +1063,7 @@ module Fluent::Plugin
948
1063
  rescue RangeError
949
1064
  io.close if io
950
1065
  raise WatcherSetupError, "seek error with #{@path}: file position = #{@watcher.pe.read_pos.to_s(16)}, reading bytesize = #{@fifo.bytesize.to_s(16)}"
951
- rescue Errno::ENOENT
1066
+ rescue Errno::ENOENT, Errno::EACCES
952
1067
  nil
953
1068
  end
954
1069
 
@@ -963,14 +1078,17 @@ module Fluent::Plugin
963
1078
  else
964
1079
  @io ||= open
965
1080
  yield @io
1081
+ @eof = true if @io.nil?
966
1082
  end
967
1083
  rescue WatcherSetupError => e
968
1084
  close
1085
+ @eof = true
969
1086
  raise e
970
1087
  rescue
971
1088
  @log.error $!.to_s
972
1089
  @log.error_backtrace
973
1090
  close
1091
+ @eof = true
974
1092
  end
975
1093
  end
976
1094