fluentd 1.11.3-x86-mingw32 → 1.12.2-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 (97) hide show
  1. checksums.yaml +4 -4
  2. data/.deepsource.toml +13 -0
  3. data/.github/ISSUE_TEMPLATE/bug_report.md +1 -1
  4. data/.github/ISSUE_TEMPLATE/config.yml +5 -0
  5. data/.github/workflows/linux-test.yaml +36 -0
  6. data/.github/workflows/macos-test.yaml +30 -0
  7. data/.github/workflows/stale-actions.yml +22 -0
  8. data/.github/workflows/windows-test.yaml +30 -0
  9. data/CHANGELOG.md +138 -0
  10. data/MAINTAINERS.md +5 -2
  11. data/README.md +2 -2
  12. data/bin/fluent-cap-ctl +7 -0
  13. data/bin/fluent-ctl +7 -0
  14. data/fluentd.gemspec +4 -3
  15. data/lib/fluent/capability.rb +87 -0
  16. data/lib/fluent/command/bundler_injection.rb +1 -1
  17. data/lib/fluent/command/ca_generate.rb +6 -3
  18. data/lib/fluent/command/cap_ctl.rb +174 -0
  19. data/lib/fluent/command/cat.rb +0 -1
  20. data/lib/fluent/command/ctl.rb +177 -0
  21. data/lib/fluent/command/fluentd.rb +4 -0
  22. data/lib/fluent/command/plugin_config_formatter.rb +18 -2
  23. data/lib/fluent/compat/parser.rb +2 -2
  24. data/lib/fluent/config/section.rb +2 -2
  25. data/lib/fluent/config/types.rb +2 -2
  26. data/lib/fluent/env.rb +4 -0
  27. data/lib/fluent/event.rb +3 -13
  28. data/lib/fluent/load.rb +0 -1
  29. data/lib/fluent/plugin.rb +5 -0
  30. data/lib/fluent/plugin/buffer.rb +2 -21
  31. data/lib/fluent/plugin/formatter.rb +24 -0
  32. data/lib/fluent/plugin/formatter_csv.rb +1 -1
  33. data/lib/fluent/plugin/formatter_hash.rb +3 -1
  34. data/lib/fluent/plugin/formatter_json.rb +3 -1
  35. data/lib/fluent/plugin/formatter_ltsv.rb +7 -5
  36. data/lib/fluent/plugin/formatter_out_file.rb +6 -4
  37. data/lib/fluent/plugin/formatter_single_value.rb +4 -2
  38. data/lib/fluent/plugin/formatter_tsv.rb +4 -2
  39. data/lib/fluent/plugin/in_http.rb +24 -3
  40. data/lib/fluent/plugin/in_monitor_agent.rb +1 -1
  41. data/lib/fluent/plugin/in_tail.rb +128 -41
  42. data/lib/fluent/plugin/in_tail/position_file.rb +39 -14
  43. data/lib/fluent/plugin/in_tcp.rb +1 -0
  44. data/lib/fluent/plugin/out_copy.rb +18 -5
  45. data/lib/fluent/plugin/out_exec_filter.rb +3 -3
  46. data/lib/fluent/plugin/out_forward.rb +61 -28
  47. data/lib/fluent/plugin/out_http.rb +29 -4
  48. data/lib/fluent/plugin/output.rb +14 -6
  49. data/lib/fluent/plugin/storage_local.rb +3 -3
  50. data/lib/fluent/plugin_helper/http_server/compat/server.rb +1 -1
  51. data/lib/fluent/plugin_helper/inject.rb +4 -1
  52. data/lib/fluent/plugin_helper/retry_state.rb +4 -0
  53. data/lib/fluent/supervisor.rb +153 -48
  54. data/lib/fluent/system_config.rb +2 -1
  55. data/lib/fluent/time.rb +58 -1
  56. data/lib/fluent/version.rb +1 -1
  57. data/lib/fluent/winsvc.rb +22 -4
  58. data/templates/plugin_config_formatter/param.md-table.erb +10 -0
  59. data/test/command/test_binlog_reader.rb +22 -6
  60. data/test/command/test_cap_ctl.rb +100 -0
  61. data/test/command/test_ctl.rb +57 -0
  62. data/test/command/test_fluentd.rb +38 -0
  63. data/test/command/test_plugin_config_formatter.rb +124 -2
  64. data/test/config/test_configurable.rb +1 -1
  65. data/test/plugin/in_tail/test_position_file.rb +46 -26
  66. data/test/plugin/out_forward/test_connection_manager.rb +6 -0
  67. data/test/plugin/test_filter_stdout.rb +6 -1
  68. data/test/plugin/test_formatter_hash.rb +6 -3
  69. data/test/plugin/test_formatter_json.rb +14 -4
  70. data/test/plugin/test_formatter_ltsv.rb +13 -5
  71. data/test/plugin/test_formatter_out_file.rb +35 -14
  72. data/test/plugin/test_formatter_single_value.rb +12 -6
  73. data/test/plugin/test_formatter_tsv.rb +12 -4
  74. data/test/plugin/test_in_exec.rb +1 -1
  75. data/test/plugin/test_in_http.rb +25 -0
  76. data/test/plugin/test_in_tail.rb +470 -32
  77. data/test/plugin/test_out_copy.rb +87 -0
  78. data/test/plugin/test_out_file.rb +23 -18
  79. data/test/plugin/test_out_forward.rb +74 -0
  80. data/test/plugin/test_out_http.rb +20 -1
  81. data/test/plugin/test_output.rb +12 -0
  82. data/test/plugin/test_parser_syslog.rb +2 -2
  83. data/test/plugin/test_sd_file.rb +1 -1
  84. data/test/plugin_helper/test_child_process.rb +5 -2
  85. data/test/plugin_helper/test_compat_parameters.rb +7 -2
  86. data/test/plugin_helper/test_http_server_helper.rb +3 -1
  87. data/test/plugin_helper/test_inject.rb +42 -0
  88. data/test/plugin_helper/test_server.rb +18 -5
  89. data/test/test_capability.rb +74 -0
  90. data/test/test_event.rb +16 -0
  91. data/test/test_formatter.rb +64 -10
  92. data/test/test_output.rb +6 -1
  93. data/test/test_supervisor.rb +150 -1
  94. data/test/test_time_parser.rb +109 -0
  95. metadata +61 -29
  96. data/.travis.yml +0 -57
  97. data/appveyor.yml +0 -28
@@ -244,10 +244,10 @@ module Fluent
244
244
  end
245
245
 
246
246
  def convert_value_to_nil(value)
247
- if value and @null_empty_string
247
+ if value && @null_empty_string
248
248
  value = (value == '') ? nil : value
249
249
  end
250
- if value and @null_value_pattern
250
+ if value && @null_value_pattern
251
251
  value = ::Fluent::StringUtil.match_regexp(@null_value_pattern, value) ? nil : value
252
252
  end
253
253
  value
@@ -179,7 +179,7 @@ module Fluent
179
179
  end
180
180
 
181
181
  if section_params[varname].nil?
182
- unless proxy.defaults.has_key?(varname) and proxy.defaults[varname].nil?
182
+ unless proxy.defaults.has_key?(varname) && proxy.defaults[varname].nil?
183
183
  logger.error "config error in:\n#{conf}" if logger
184
184
  raise ConfigError, "'#{name}' parameter is required but nil is specified"
185
185
  end
@@ -247,7 +247,7 @@ module Fluent
247
247
  def self.check_unused_section(proxy, conf, plugin_class)
248
248
  elems = conf.respond_to?(:elements) ? conf.elements : []
249
249
  elems.each { |e|
250
- next if plugin_class.nil? && Fluent::Config::V1Parser::ELEM_SYMBOLS.include?(e.name) # skip pre-defined non-plugin elements because it doens't have proxy section
250
+ next if plugin_class.nil? && Fluent::Config::V1Parser::ELEM_SYMBOLS.include?(e.name) # skip pre-defined non-plugin elements because it doesn't have proxy section
251
251
  next if e.unused_in && e.unused_in.empty? # the section is used at least once
252
252
 
253
253
  if proxy.sections.any? { |name, subproxy| e.name == subproxy.name.to_s || e.name == subproxy.alias.to_s }
@@ -186,7 +186,7 @@ module Fluent
186
186
  return nil if val.nil?
187
187
 
188
188
  param = if val.is_a?(String)
189
- val.start_with?('{') ? JSON.load(val) : Hash[val.strip.split(/\s*,\s*/).map{|v| v.split(':', 2)}]
189
+ val.start_with?('{') ? JSON.parse(val) : Hash[val.strip.split(/\s*,\s*/).map{|v| v.split(':', 2)}]
190
190
  else
191
191
  val
192
192
  end
@@ -213,7 +213,7 @@ module Fluent
213
213
  return nil if val.nil?
214
214
 
215
215
  param = if val.is_a?(String)
216
- val.start_with?('[') ? JSON.load(val) : val.strip.split(/\s*,\s*/)
216
+ val.start_with?('[') ? JSON.parse(val) : val.strip.split(/\s*,\s*/)
217
217
  else
218
218
  val
219
219
  end
data/lib/fluent/env.rb CHANGED
@@ -28,4 +28,8 @@ module Fluent
28
28
  def self.windows?
29
29
  ServerEngine.windows?
30
30
  end
31
+
32
+ def self.linux?
33
+ /linux/ === RUBY_PLATFORM
34
+ end
31
35
  end
data/lib/fluent/event.rb CHANGED
@@ -254,19 +254,9 @@ module Fluent
254
254
  end
255
255
 
256
256
  def each(unpacker: nil, &block)
257
- if @unpacked_times
258
- @unpacked_times.each_with_index do |time, i|
259
- block.call(time, @unpacked_records[i])
260
- end
261
- else
262
- @unpacked_times = []
263
- @unpacked_records = []
264
- (unpacker || Fluent::MessagePackFactory.msgpack_unpacker).feed_each(@data) do |time, record|
265
- @unpacked_times << time
266
- @unpacked_records << record
267
- block.call(time, record)
268
- end
269
- @size = @unpacked_times.size
257
+ ensure_unpacked!(unpacker: unpacker)
258
+ @unpacked_times.each_with_index do |time, i|
259
+ block.call(time, @unpacked_records[i])
270
260
  end
271
261
  nil
272
262
  end
data/lib/fluent/load.rb CHANGED
@@ -1,4 +1,3 @@
1
- require 'thread'
2
1
  require 'socket'
3
2
  require 'fcntl'
4
3
  require 'time'
data/lib/fluent/plugin.rb CHANGED
@@ -121,6 +121,11 @@ module Fluent
121
121
  new_impl('sd', SD_REGISTRY, type, parent)
122
122
  end
123
123
 
124
+ class << self
125
+ # This should be defined for fluent-plugin-config-formatter type arguments.
126
+ alias_method :new_service_discovery, :new_sd
127
+ end
128
+
124
129
  def self.new_parser(type, parent: nil)
125
130
  if type[0] == '/' && type[-1] == '/'
126
131
  # This usage is not recommended for new API... create RegexpParser directly
@@ -143,33 +143,14 @@ module Fluent
143
143
  end
144
144
  end
145
145
 
146
- # timekey should be unixtime as usual.
147
- # So, unixtime should be bigger than 2^30 - 1 (= 1073741823) nowadays.
148
- # We should check object_id stability to use object_id as optimization for comparing operations.
149
- # e.g.)
150
- # irb> Time.parse("2020/07/31 18:30:00+09:00").to_i
151
- # => 1596187800
152
- # irb> Time.parse("2020/07/31 18:30:00+09:00").to_i > 2**30 -1
153
- # => true
154
- def self.enable_optimize?
155
- a1 = 2**30 - 1
156
- a2 = 2**30 - 1
157
- b1 = 2**62 - 1
158
- b2 = 2**62 - 1
159
- (a1.object_id == a2.object_id) && (b1.object_id == b2.object_id)
160
- end
161
-
162
146
  # This is an optimization code. Current Struct's implementation is comparing all data.
163
147
  # https://github.com/ruby/ruby/blob/0623e2b7cc621b1733a760b72af246b06c30cf96/struct.c#L1200-L1203
164
148
  # Actually this overhead is very small but this class is generated *per chunk* (and used in hash object).
165
149
  # This means that this class is one of the most called object in Fluentd.
166
150
  # See https://github.com/fluent/fluentd/pull/2560
167
- # But, this optimization has a side effect on Windows and 32bit environment(s) due to differing object_id.
168
- # This difference causes flood of buffer files.
169
- # So, this optimization should be enabled on `enable_optimize?` as true platforms.
170
151
  def hash
171
- timekey.object_id
172
- end if enable_optimize?
152
+ timekey.hash
153
+ end
173
154
  end
174
155
 
175
156
  # for tests
@@ -46,5 +46,29 @@ module Fluent
46
46
  @proc.call(tag, time, record)
47
47
  end
48
48
  end
49
+
50
+ module Newline
51
+ module Mixin
52
+ include Fluent::Configurable
53
+
54
+ DEFAULT_NEWLINE = if Fluent.windows?
55
+ :crlf
56
+ else
57
+ :lf
58
+ end
59
+
60
+ config_param :newline, :enum, list: [:lf, :crlf], default: DEFAULT_NEWLINE
61
+
62
+ def configure(conf)
63
+ super
64
+ @newline = case newline
65
+ when :lf
66
+ "\n".freeze
67
+ when :crlf
68
+ "\r\n".freeze
69
+ end
70
+ end
71
+ end
72
+ end
49
73
  end
50
74
  end
@@ -27,7 +27,7 @@ module Fluent
27
27
  helpers :record_accessor
28
28
 
29
29
  config_param :delimiter, default: ',' do |val|
30
- ['\t', 'TAB'].include?(val) ? "\t" : val
30
+ ['\t', 'TAB'].include?(val) ? "\t".freeze : val.freeze
31
31
  end
32
32
  config_param :force_quotes, :bool, default: true
33
33
  # "array" looks good for type of :fields, but this implementation removes tailing comma
@@ -19,13 +19,15 @@ require 'fluent/plugin/formatter'
19
19
  module Fluent
20
20
  module Plugin
21
21
  class HashFormatter < Formatter
22
+ include Fluent::Plugin::Newline::Mixin
23
+
22
24
  Plugin.register_formatter('hash', self)
23
25
 
24
26
  config_param :add_newline, :bool, default: true
25
27
 
26
28
  def format(tag, time, record)
27
29
  line = record.to_s
28
- line << "\n".freeze if @add_newline
30
+ line << @newline if @add_newline
29
31
  line
30
32
  end
31
33
  end
@@ -20,6 +20,8 @@ require 'fluent/env'
20
20
  module Fluent
21
21
  module Plugin
22
22
  class JSONFormatter < Formatter
23
+ include Fluent::Plugin::Newline::Mixin
24
+
23
25
  Plugin.register_formatter('json', self)
24
26
 
25
27
  config_param :json_parser, :string, default: 'oj'
@@ -44,7 +46,7 @@ module Fluent
44
46
  end
45
47
 
46
48
  def format(tag, time, record)
47
- "#{@dump_proc.call(record)}\n"
49
+ "#{@dump_proc.call(record)}#{@newline}"
48
50
  end
49
51
 
50
52
  def format_without_nl(tag, time, record)
@@ -19,22 +19,24 @@ require 'fluent/plugin/formatter'
19
19
  module Fluent
20
20
  module Plugin
21
21
  class LabeledTSVFormatter < Formatter
22
+ include Fluent::Plugin::Newline::Mixin
23
+
22
24
  Plugin.register_formatter('ltsv', self)
23
25
 
24
26
  # http://ltsv.org/
25
27
 
26
- config_param :delimiter, :string, default: "\t"
27
- config_param :label_delimiter, :string, default: ":"
28
+ config_param :delimiter, :string, default: "\t".freeze
29
+ config_param :label_delimiter, :string, default: ":".freeze
30
+ config_param :replacement, :string, default: " ".freeze
28
31
  config_param :add_newline, :bool, default: true
29
32
 
30
- # TODO: escaping for \t in values
31
33
  def format(tag, time, record)
32
34
  formatted = ""
33
35
  record.each do |label, value|
34
36
  formatted << @delimiter if formatted.length.nonzero?
35
- formatted << "#{label}#{@label_delimiter}#{value}"
37
+ formatted << "#{label}#{@label_delimiter}#{value.to_s.gsub(@delimiter, @replacement)}"
36
38
  end
37
- formatted << "\n".freeze if @add_newline
39
+ formatted << @newline if @add_newline
38
40
  formatted
39
41
  end
40
42
  end
@@ -21,15 +21,17 @@ require 'yajl'
21
21
  module Fluent
22
22
  module Plugin
23
23
  class OutFileFormatter < Formatter
24
+ include Fluent::Plugin::Newline::Mixin
25
+
24
26
  Plugin.register_formatter('out_file', self)
25
27
 
26
28
  config_param :output_time, :bool, default: true
27
29
  config_param :output_tag, :bool, default: true
28
30
  config_param :delimiter, default: "\t" do |val|
29
31
  case val
30
- when /SPACE/i then ' '
31
- when /COMMA/i then ','
32
- else "\t"
32
+ when /SPACE/i then ' '.freeze
33
+ when /COMMA/i then ','.freeze
34
+ else "\t".freeze
33
35
  end
34
36
  end
35
37
  config_set_default :time_type, :string
@@ -44,7 +46,7 @@ module Fluent
44
46
  header = ''
45
47
  header << "#{@timef.format(time)}#{@delimiter}" if @output_time
46
48
  header << "#{tag}#{@delimiter}" if @output_tag
47
- "#{header}#{Yajl.dump(record)}\n"
49
+ "#{header}#{Yajl.dump(record)}#{@newline}"
48
50
  end
49
51
  end
50
52
  end
@@ -19,14 +19,16 @@ require 'fluent/plugin/formatter'
19
19
  module Fluent
20
20
  module Plugin
21
21
  class SingleValueFormatter < Formatter
22
+ include Fluent::Plugin::Newline::Mixin
23
+
22
24
  Plugin.register_formatter('single_value', self)
23
25
 
24
- config_param :message_key, :string, default: 'message'
26
+ config_param :message_key, :string, default: 'message'.freeze
25
27
  config_param :add_newline, :bool, default: true
26
28
 
27
29
  def format(tag, time, record)
28
30
  text = record[@message_key].to_s.dup
29
- text << "\n" if @add_newline
31
+ text << @newline if @add_newline
30
32
  text
31
33
  end
32
34
  end
@@ -19,18 +19,20 @@ require 'fluent/plugin/formatter'
19
19
  module Fluent
20
20
  module Plugin
21
21
  class TSVFormatter < Formatter
22
+ include Fluent::Plugin::Newline::Mixin
23
+
22
24
  Plugin.register_formatter('tsv', self)
23
25
 
24
26
  desc 'Field names included in each lines'
25
27
  config_param :keys, :array, value_type: :string
26
28
  desc 'The delimiter character (or string) of TSV values'
27
- config_param :delimiter, :string, default: "\t"
29
+ config_param :delimiter, :string, default: "\t".freeze
28
30
  desc 'The parameter to enable writing to new lines'
29
31
  config_param :add_newline, :bool, default: true
30
32
 
31
33
  def format(tag, time, record)
32
34
  formatted = @keys.map{|k| record[k].to_s }.join(@delimiter)
33
- formatted << "\n".freeze if @add_newline
35
+ formatted << @newline if @add_newline
34
36
  formatted
35
37
  end
36
38
  end
@@ -80,6 +80,8 @@ module Fluent::Plugin
80
80
  config_param :use_204_response, :bool, default: false
81
81
  desc 'Dump error log or not'
82
82
  config_param :dump_error_log, :bool, default: true
83
+ desc 'Add QUERY_ prefix query params to record'
84
+ config_param :add_query_params, :bool, default: false
83
85
 
84
86
  config_section :parse do
85
87
  config_set_default :@type, 'in_http'
@@ -277,7 +279,7 @@ module Fluent::Plugin
277
279
  private
278
280
 
279
281
  def on_server_connect(conn)
280
- handler = Handler.new(conn, @km, method(:on_request), @body_size_limit, @format_name, log, @cors_allow_origins)
282
+ handler = Handler.new(conn, @km, method(:on_request), @body_size_limit, @format_name, log, @cors_allow_origins, @add_query_params)
281
283
 
282
284
  conn.on(:data) do |data|
283
285
  handler.on_read(data)
@@ -326,6 +328,14 @@ module Fluent::Plugin
326
328
  }
327
329
  end
328
330
 
331
+ if @add_query_params
332
+ params.each_pair { |k, v|
333
+ if k.start_with?("QUERY_".freeze)
334
+ record[k] = v
335
+ end
336
+ }
337
+ end
338
+
329
339
  if @add_remote_addr
330
340
  record['REMOTE_ADDR'] = params['REMOTE_ADDR']
331
341
  end
@@ -346,7 +356,7 @@ module Fluent::Plugin
346
356
  class Handler
347
357
  attr_reader :content_type
348
358
 
349
- def initialize(io, km, callback, body_size_limit, format_name, log, cors_allow_origins)
359
+ def initialize(io, km, callback, body_size_limit, format_name, log, cors_allow_origins, add_query_params)
350
360
  @io = io
351
361
  @km = km
352
362
  @callback = callback
@@ -356,6 +366,7 @@ module Fluent::Plugin
356
366
  @log = log
357
367
  @cors_allow_origins = cors_allow_origins
358
368
  @idle = 0
369
+ @add_query_params = add_query_params
359
370
  @km.add(self)
360
371
 
361
372
  @remote_port, @remote_addr = io.remote_port, io.remote_addr
@@ -492,7 +503,7 @@ module Fluent::Plugin
492
503
  # For every incoming request, we check if we have some CORS
493
504
  # restrictions and allow listed origins through @cors_allow_origins.
494
505
  unless @cors_allow_origins.nil?
495
- unless @cors_allow_origins.include?('*') or include_cors_allow_origin
506
+ unless @cors_allow_origins.include?('*') || include_cors_allow_origin
496
507
  send_response_and_close(RES_403_STATUS, {'Connection' => 'close'}, "")
497
508
  return
498
509
  end
@@ -533,7 +544,17 @@ module Fluent::Plugin
533
544
  end
534
545
  path_info = uri.path
535
546
 
547
+ if (@add_query_params)
548
+
549
+ query_params = WEBrick::HTTPUtils.parse_query(uri.query)
550
+
551
+ query_params.each_pair {|k,v|
552
+ params["QUERY_#{k.gsub('-','_').upcase}"] = v
553
+ }
554
+ end
555
+
536
556
  params.merge!(@env)
557
+
537
558
  @env.clear
538
559
 
539
560
  code, header, body = @callback.call(path_info, params)
@@ -338,7 +338,7 @@ module Fluent::Plugin
338
338
  obj.merge!(pe.statistics['output'] || {})
339
339
  end
340
340
 
341
- obj['retry'] = get_retry_info(pe.retry) if opts[:with_retry] and pe.instance_variable_defined?(:@retry)
341
+ obj['retry'] = get_retry_info(pe.retry) if opts[:with_retry] && pe.instance_variable_defined?(:@retry)
342
342
 
343
343
  # include all instance variables if :with_debug_info is set
344
344
  if opts[:with_debug_info]
@@ -22,6 +22,7 @@ require 'fluent/event'
22
22
  require 'fluent/plugin/buffer'
23
23
  require 'fluent/plugin/parser_multiline'
24
24
  require 'fluent/variable_store'
25
+ require 'fluent/capability'
25
26
  require 'fluent/plugin/in_tail/position_file'
26
27
 
27
28
  if Fluent.windows?
@@ -104,6 +105,8 @@ module Fluent::Plugin
104
105
  config_param :ignore_repeated_permission_error, :bool, default: false
105
106
  desc 'Format path with the specified timezone'
106
107
  config_param :path_timezone, :string, default: nil
108
+ desc 'Follow inodes instead of following file names. Guarantees more stable delivery and allows to use * in path pattern with rotating files'
109
+ config_param :follow_inodes, :bool, default: false
107
110
 
108
111
  config_section :parse, required: false, multi: true, init: true, param_name: :parser_configs do
109
112
  config_argument :usage, :string, default: 'in_tail_parser'
@@ -154,6 +157,9 @@ module Fluent::Plugin
154
157
  end
155
158
  @variable_store[@pos_file] = self.plugin_id
156
159
  else
160
+ if @follow_inodes
161
+ raise Fluent::ConfigError, "Can't follow inodes without pos_file configuration parameter"
162
+ end
157
163
  $log.warn "'pos_file PATH' parameter is not set to a 'tail' source."
158
164
  $log.warn "this parameter is highly recommended to save the position to resume tailing."
159
165
  end
@@ -171,6 +177,7 @@ module Fluent::Plugin
171
177
  @dir_perm = system_config.dir_permission || Fluent::DEFAULT_DIR_PERMISSION
172
178
  # parser is already created by parser helper
173
179
  @parser = parser_create(usage: parser_config['usage'] || @parser_configs.first.usage)
180
+ @capability = Fluent::Capability.new(:current_process)
174
181
  end
175
182
 
176
183
  def configure_tag
@@ -214,7 +221,7 @@ module Fluent::Plugin
214
221
  FileUtils.mkdir_p(pos_file_dir, mode: @dir_perm) unless Dir.exist?(pos_file_dir)
215
222
  @pf_file = File.open(@pos_file, File::RDWR|File::CREAT|File::BINARY, @file_perm)
216
223
  @pf_file.sync = true
217
- @pf = PositionFile.load(@pf_file, logger: log)
224
+ @pf = PositionFile.load(@pf_file, @follow_inodes, expand_paths, logger: log)
218
225
 
219
226
  if @pos_file_compaction_interval
220
227
  timer_execute(:in_tail_refresh_compact_pos_file, @pos_file_compaction_interval) do
@@ -238,7 +245,7 @@ module Fluent::Plugin
238
245
 
239
246
  def shutdown
240
247
  # during shutdown phase, don't close io. It should be done in close after all threads are stopped. See close.
241
- stop_watchers(@tails.keys, immediate: true, remove_watcher: false)
248
+ stop_watchers(existence_path, immediate: true, remove_watcher: false)
242
249
  @pf_file.close if @pf_file
243
250
 
244
251
  super
@@ -250,6 +257,11 @@ module Fluent::Plugin
250
257
  close_watcher_handles
251
258
  end
252
259
 
260
+ def have_read_capability?
261
+ @capability.have_capability?(:effective, :dac_read_search) ||
262
+ @capability.have_capability?(:effective, :dac_override)
263
+ end
264
+
253
265
  def expand_paths
254
266
  date = Fluent::EventTime.now
255
267
  paths = []
@@ -263,7 +275,7 @@ module Fluent::Plugin
263
275
  paths += Dir.glob(path).select { |p|
264
276
  begin
265
277
  is_file = !File.directory?(p)
266
- if File.readable?(p) && is_file
278
+ if (File.readable?(p) || have_read_capability?) && is_file
267
279
  if @limit_recently_modified && File.mtime(p) < (date.to_time - @limit_recently_modified)
268
280
  false
269
281
  else
@@ -296,7 +308,37 @@ module Fluent::Plugin
296
308
  end
297
309
  path.include?('*') ? Dir.glob(path) : path
298
310
  }.flatten.uniq
299
- paths.uniq - excluded
311
+ # filter out non existing files, so in case pattern is without '*' we don't do unnecessary work
312
+ hash = {}
313
+ (paths - excluded).select { |path|
314
+ FileTest.exist?(path)
315
+ }.each { |path|
316
+ # Even we just checked for existence, there is a race condition here as
317
+ # of which stat() might fail with ENOENT. See #3224.
318
+ begin
319
+ target_info = TargetInfo.new(path, Fluent::FileWrapper.stat(path).ino)
320
+ if @follow_inodes
321
+ hash[target_info.ino] = target_info
322
+ else
323
+ hash[target_info.path] = target_info
324
+ end
325
+ rescue Errno::ENOENT
326
+ $log.warn "expand_paths: stat() for #{path} failed with ENOENT. Skip file."
327
+ end
328
+ }
329
+ hash
330
+ end
331
+
332
+ def existence_path
333
+ hash = {}
334
+ @tails.each_key {|target_info|
335
+ if @follow_inodes
336
+ hash[target_info.ino] = target_info
337
+ else
338
+ hash[target_info.path] = target_info
339
+ end
340
+ }
341
+ hash
300
342
  end
301
343
 
302
344
  # in_tail with '*' path doesn't check rotation file equality at refresh phase.
@@ -305,21 +347,21 @@ module Fluent::Plugin
305
347
  # In such case, you should separate log directory and specify two paths in path parameter.
306
348
  # e.g. path /path/to/dir/*,/path/to/rotated_logs/target_file
307
349
  def refresh_watchers
308
- target_paths = expand_paths
309
- existence_paths = @tails.keys
350
+ target_paths_hash = expand_paths
351
+ existence_paths_hash = existence_path
310
352
 
311
353
  log.debug { "tailing paths: target = #{target_paths.join(",")} | existing = #{existence_paths.join(",")}" }
312
354
 
313
- unwatched = existence_paths - target_paths
314
- added = target_paths - existence_paths
355
+ unwatched_hash = existence_paths_hash.reject {|key, value| target_paths_hash.key?(key)}
356
+ added_hash = target_paths_hash.reject {|key, value| existence_paths_hash.key?(key)}
315
357
 
316
- stop_watchers(unwatched, immediate: false, unwatched: true) unless unwatched.empty?
317
- start_watchers(added) unless added.empty?
358
+ stop_watchers(unwatched_hash, immediate: false, unwatched: true) unless unwatched_hash.empty?
359
+ start_watchers(added_hash) unless added_hash.empty?
318
360
  end
319
361
 
320
- def setup_watcher(path, pe)
362
+ def setup_watcher(target_info, pe)
321
363
  line_buffer_timer_flusher = @multiline_mode ? TailWatcher::LineBufferTimerFlusher.new(log, @multiline_flush_interval, &method(:flush_buffer)) : nil
322
- tw = TailWatcher.new(path, pe, log, @read_from_head, method(:update_watcher), line_buffer_timer_flusher, method(:io_handler))
364
+ tw = TailWatcher.new(target_info, pe, log, @read_from_head, @follow_inodes, method(:update_watcher), line_buffer_timer_flusher, method(:io_handler))
323
365
 
324
366
  if @enable_watch_timer
325
367
  tt = TimerTrigger.new(1, log) { tw.on_notify }
@@ -350,47 +392,59 @@ module Fluent::Plugin
350
392
  raise e
351
393
  end
352
394
 
353
- def start_watchers(paths)
354
- paths.each { |path|
395
+ def start_watchers(targets_info)
396
+ targets_info.each_value { |target_info|
355
397
  pe = nil
356
398
  if @pf
357
- pe = @pf[path]
399
+ pe = @pf[target_info]
358
400
  if @read_from_head && pe.read_inode.zero?
359
401
  begin
360
- pe.update(Fluent::FileWrapper.stat(path).ino, 0)
402
+ pe.update(Fluent::FileWrapper.stat(target_info.path).ino, 0)
361
403
  rescue Errno::ENOENT
362
- $log.warn "#{path} not found. Continuing without tailing it."
404
+ $log.warn "#{target_info.path} not found. Continuing without tailing it."
363
405
  end
364
406
  end
365
407
  end
366
408
 
367
409
  begin
368
- tw = setup_watcher(path, pe)
410
+ tw = setup_watcher(target_info, pe)
369
411
  rescue WatcherSetupError => e
370
- log.warn "Skip #{path} because unexpected setup error happens: #{e}"
412
+ log.warn "Skip #{target_info.path} because unexpected setup error happens: #{e}"
371
413
  next
372
414
  end
373
- @tails[path] = tw
415
+
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
+ stop_watchers(target_info, immediate: true, unwatched: true)
423
+ end
374
424
  }
375
425
  end
376
426
 
377
- def stop_watchers(paths, immediate: false, unwatched: false, remove_watcher: true)
378
- paths.each { |path|
379
- tw = remove_watcher ? @tails.delete(path) : @tails[path]
427
+ def stop_watchers(targets_info, immediate: false, unwatched: false, remove_watcher: true)
428
+ targets_info.each_value { |target_info|
429
+ if remove_watcher
430
+ tw = @tails.delete(target_info)
431
+ else
432
+ tw = @tails[target_info]
433
+ end
380
434
  if tw
381
435
  tw.unwatched = unwatched
382
436
  if immediate
383
- detach_watcher(tw, false)
437
+ detach_watcher(tw, target_info.ino, false)
384
438
  else
385
- detach_watcher_after_rotate_wait(tw)
439
+ detach_watcher_after_rotate_wait(tw, target_info.ino)
386
440
  end
387
441
  end
388
442
  }
389
443
  end
390
444
 
391
445
  def close_watcher_handles
392
- @tails.keys.each do |path|
393
- tw = @tails.delete(path)
446
+ @tails.keys.each do |target_info|
447
+ tw = @tails.delete(target_info)
394
448
  if tw
395
449
  tw.close
396
450
  end
@@ -398,25 +452,39 @@ module Fluent::Plugin
398
452
  end
399
453
 
400
454
  # refresh_watchers calls @tails.keys so we don't use stop_watcher -> start_watcher sequence for safety.
401
- def update_watcher(path, pe)
402
- log.info("detected rotation of #{path}; waiting #{@rotate_wait} seconds")
455
+ def update_watcher(target_info, pe)
456
+ log.info("detected rotation of #{target_info.path}; waiting #{@rotate_wait} seconds")
403
457
 
404
458
  if @pf
405
- unless pe.read_inode == @pf[path].read_inode
459
+ pe_inode = pe.read_inode
460
+ target_info_from_position_entry = TargetInfo.new(target_info.path, pe_inode)
461
+ unless pe_inode == @pf[target_info_from_position_entry].read_inode
406
462
  log.debug "Skip update_watcher because watcher has been already updated by other inotify event"
407
463
  return
408
464
  end
409
465
  end
410
- rotated_tw = @tails[path]
411
- @tails[path] = setup_watcher(path, pe)
412
- detach_watcher_after_rotate_wait(rotated_tw) if rotated_tw
466
+
467
+ rotated_target_info = TargetInfo.new(target_info.path, pe.read_inode)
468
+ rotated_tw = @tails[rotated_target_info]
469
+ new_target_info = target_info.dup
470
+
471
+ if @follow_inodes
472
+ new_position_entry = @pf[target_info]
473
+
474
+ if new_position_entry.read_inode == 0
475
+ @tails[new_target_info] = setup_watcher(new_target_info, new_position_entry)
476
+ end
477
+ else
478
+ @tails[new_target_info] = setup_watcher(new_target_info, pe)
479
+ end
480
+ detach_watcher_after_rotate_wait(rotated_tw, pe.read_inode) if rotated_tw
413
481
  end
414
482
 
415
483
  # TailWatcher#close is called by another thread at shutdown phase.
416
484
  # It causes 'can't modify string; temporarily locked' error in IOHandler
417
485
  # so adding close_io argument to avoid this problem.
418
486
  # At shutdown, IOHandler's io will be released automatically after detached the event loop
419
- def detach_watcher(tw, close_io = true)
487
+ def detach_watcher(tw, ino, close_io = true)
420
488
  tw.watchers.each do |watcher|
421
489
  event_loop_detach(watcher)
422
490
  end
@@ -425,15 +493,16 @@ module Fluent::Plugin
425
493
  tw.close if close_io
426
494
 
427
495
  if tw.unwatched && @pf
428
- @pf.unwatch(tw.path)
496
+ target_info = TargetInfo.new(tw.path, ino)
497
+ @pf.unwatch(target_info)
429
498
  end
430
499
  end
431
500
 
432
- def detach_watcher_after_rotate_wait(tw)
501
+ def detach_watcher_after_rotate_wait(tw, ino)
433
502
  # Call event_loop_attach/event_loop_detach is high-cost for short-live object.
434
503
  # If this has a problem with large number of files, use @_event_loop directly instead of timer_execute.
435
504
  timer_execute(:in_tail_close_watcher, @rotate_wait, repeat: false) do
436
- detach_watcher(tw)
505
+ detach_watcher(tw, ino)
437
506
  end
438
507
  end
439
508
 
@@ -601,10 +670,12 @@ module Fluent::Plugin
601
670
  end
602
671
 
603
672
  class TailWatcher
604
- def initialize(path, pe, log, read_from_head, update_watcher, line_buffer_timer_flusher, io_handler_build)
605
- @path = path
673
+ def initialize(target_info, pe, log, read_from_head, follow_inodes, update_watcher, line_buffer_timer_flusher, io_handler_build)
674
+ @path = target_info.path
675
+ @ino = target_info.ino
606
676
  @pe = pe || MemoryPositionEntry.new
607
677
  @read_from_head = read_from_head
678
+ @follow_inodes = follow_inodes
608
679
  @update_watcher = update_watcher
609
680
  @log = log
610
681
  @rotate_handler = RotateHandler.new(log, &method(:on_rotate))
@@ -614,7 +685,7 @@ module Fluent::Plugin
614
685
  @watchers = []
615
686
  end
616
687
 
617
- attr_reader :path
688
+ attr_reader :path, :ino
618
689
  attr_reader :pe
619
690
  attr_reader :line_buffer_timer_flusher
620
691
  attr_accessor :unwatched # This is used for removing position entry from PositionFile
@@ -709,7 +780,23 @@ module Fluent::Plugin
709
780
  end
710
781
 
711
782
  if watcher_needs_update
712
- @update_watcher.call(@path, swap_state(@pe))
783
+ if @follow_inodes
784
+ # No need to update a watcher if stat is nil (file not present), because moving to inodes will create
785
+ # new watcher, and old watcher will be closed by stop_watcher in refresh_watchers method
786
+ # don't want to swap state because we need latest read offset in pos file even after rotate_wait
787
+ if stat
788
+ target_info = TargetInfo.new(@path, stat)
789
+ @update_watcher.call(target_info, @pe)
790
+ end
791
+ else
792
+ # Permit to handle if stat is nil (file not present).
793
+ # If a file is mv-ed and a new file is created during
794
+ # calling `#refresh_watchers`s, and `#refresh_watchers` won't run `#start_watchers`
795
+ # and `#stop_watchers()` for the path because `target_paths_hash`
796
+ # always contains the path.
797
+ target_info = TargetInfo.new(@path, stat ? stat.ino : nil)
798
+ @update_watcher.call(target_info, swap_state(@pe))
799
+ end
713
800
  else
714
801
  @log.info "detected rotation of #{@path}"
715
802
  @io_handler = io_handler