fluentd 1.12.0.rc1 → 1.12.0.rc2

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.

checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ea33aa9fbbca10538b329676df8dc024f38836b8c0a3bd78ff8afe9b21e59a27
4
- data.tar.gz: d3500418626d7f49305b4dee07ab6dfa10c84687ee754fd967aee1200a815dc2
3
+ metadata.gz: 7233372dc2b1c0ab6ae93682dc0a3ecd8ba2eb4956dfecc2456173cf30729090
4
+ data.tar.gz: e61aa30c17b3f27322a10fbc4b2e81a219eeeb0f061c4ccfd0b6c8c10777a607
5
5
  SHA512:
6
- metadata.gz: 7a43d215a3a026042028643ca627f81411379efe8eb5090c4c98f8886c22efe33595362b82273889b97ecc8a95af8c93d0d2dbf5d8494c3c8e680942dd8c0c67
7
- data.tar.gz: a17be6c1b92193649412e223ce7bba96d94f1122b309e459dc72a6e7f0a5339874e2a9d9d352a88612c3d7e603c2276b8109af97b8b26cc3967d81cae7925071
6
+ metadata.gz: 737b0727cd390374168ed176a19a73b94d3010a7e93d5951a5580cfbca5bdd697c082afbde07ba1b266edd77be057ec7ccef6ea4383f626e7a11bcf4ad9d9c40
7
+ data.tar.gz: 94f055d9d2d88359a49caf597bc9076cee741d61e80d84aa3cd1cefe8b01e7ee78721502b5b0dd4e329cb0fceeec952b43b51efd666ee7c9742fc763d1c7cb11
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: Bug Report
3
- about: Create a report to help us improve. If you have questions about Fluentd and plugins, please direct these to https://groups.google.com/forum/#!forum/fluentd
3
+ about: Create a report with a procedure for reproducing the bug
4
4
 
5
5
  ---
6
6
 
@@ -0,0 +1,5 @@
1
+ blank_issues_enabled: false
2
+ contact_links:
3
+ - name: Ask a Question
4
+ url: https://groups.google.com/forum/#!forum/fluentd
5
+ about: I have questions about Fluentd and plugins. Please ask and answer questions here
@@ -0,0 +1,22 @@
1
+ name: "Mark or close stale issues and PRs"
2
+ on:
3
+ schedule:
4
+ - cron: "00 10 * * *"
5
+
6
+ jobs:
7
+ stale:
8
+ runs-on: ubuntu-latest
9
+ steps:
10
+ - uses: actions/stale@v3
11
+ with:
12
+ repo-token: ${{ secrets.GITHUB_TOKEN }}
13
+ days-before-stale: 90
14
+ days-before-close: 30
15
+ stale-issue-message: "This issue has been automatically marked as stale because it has been open 90 days with no activity. Remove stale label or comment or this issue will be closed in 30 days"
16
+ stale-pr-message: "This PR has been automatically marked as stale because it has been open 90 days with no activity. Remove stale label or comment or this PR will be closed in 30 days"
17
+ close-issue-message: "This issue was automatically closed because of stale in 30 days"
18
+ close-pr-message: "This PR was automatically closed because of stale in 30 days"
19
+ stale-pr-label: "stale"
20
+ stale-issue-label: "stale"
21
+ exempt-issue-labels: "bug,enhancement,feature request,pending,work_in_progress,v1,v2"
22
+ exempt-pr-labels: "bug,enhancement,feature request,pending,work_in_progress,v1,v2"
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ here = File.dirname(__FILE__)
4
+ $LOAD_PATH << File.expand_path(File.join(here, '..', 'lib'))
5
+ require 'fluent/command/ctl'
6
+
7
+ Fluent::Ctl.new.call
@@ -0,0 +1,177 @@
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 'optparse'
18
+ require 'fluent/env'
19
+ require 'fluent/version'
20
+ if Fluent.windows?
21
+ require 'win32/event'
22
+ require 'win32/service'
23
+ end
24
+
25
+ module Fluent
26
+ class Ctl
27
+ DEFAULT_OPTIONS = {}
28
+
29
+ if Fluent.windows?
30
+ include Windows::ServiceConstants
31
+ include Windows::ServiceStructs
32
+ include Windows::ServiceFunctions
33
+
34
+ COMMAND_MAP = {
35
+ shutdown: "",
36
+ restart: "HUP",
37
+ flush: "USR1",
38
+ reload: "USR2",
39
+ }
40
+ WINSVC_CONTROL_CODE_MAP = {
41
+ shutdown: SERVICE_CONTROL_STOP,
42
+ # 128 - 255: user-defined control code
43
+ # See https://docs.microsoft.com/en-us/windows/win32/api/winsvc/nf-winsvc-controlservice
44
+ restart: 128,
45
+ flush: 129,
46
+ reload: SERVICE_CONTROL_PARAMCHANGE,
47
+ }
48
+ else
49
+ COMMAND_MAP = {
50
+ shutdown: :TERM,
51
+ restart: :HUP,
52
+ flush: :USR1,
53
+ reload: :USR2,
54
+ }
55
+ end
56
+
57
+ def initialize(argv = ARGV)
58
+ @argv = argv
59
+ @options = {}
60
+ @opt_parser = OptionParser.new
61
+ configure_option_parser
62
+ @options.merge!(DEFAULT_OPTIONS)
63
+ parse_options!
64
+ end
65
+
66
+ def help_text
67
+ text = "\n"
68
+ if Fluent.windows?
69
+ text << "Usage: #{$PROGRAM_NAME} COMMAND [PID_OR_SVCNAME]\n"
70
+ else
71
+ text << "Usage: #{$PROGRAM_NAME} COMMAND PID\n"
72
+ end
73
+ text << "\n"
74
+ text << "Commands: \n"
75
+ COMMAND_MAP.each do |key, value|
76
+ text << " #{key}\n"
77
+ end
78
+ text
79
+ end
80
+
81
+ def usage(msg = nil)
82
+ puts help_text
83
+ if msg
84
+ puts
85
+ puts "Error: #{msg}"
86
+ end
87
+ exit 1
88
+ end
89
+
90
+ def call
91
+ if Fluent.windows?
92
+ if @pid_or_svcname =~ /^[0-9]+$/
93
+ # Use as PID
94
+ return call_windows_event(@command, "fluentd_#{@pid_or_svcname}")
95
+ end
96
+
97
+ unless call_winsvc_control_code(@command, @pid_or_svcname)
98
+ puts "Cannot send control code to #{@pid_or_svcname} service, try to send an event with this name ..."
99
+ call_windows_event(@command, @pid_or_svcname)
100
+ end
101
+ else
102
+ call_signal(@command, @pid_or_svcname)
103
+ end
104
+ end
105
+
106
+ private
107
+
108
+ def call_signal(command, pid)
109
+ signal = COMMAND_MAP[command.to_sym]
110
+ Process.kill(signal, pid.to_i)
111
+ end
112
+
113
+ def call_winsvc_control_code(command, pid_or_svcname)
114
+ status = SERVICE_STATUS.new
115
+
116
+ begin
117
+ handle_scm = OpenSCManager(nil, nil, SC_MANAGER_CONNECT)
118
+ FFI.raise_windows_error('OpenSCManager') if handle_scm == 0
119
+
120
+ handle_scs = OpenService(handle_scm, "fluentdwinsvc", SERVICE_STOP | SERVICE_PAUSE_CONTINUE | SERVICE_USER_DEFINED_CONTROL)
121
+ FFI.raise_windows_error('OpenService') if handle_scs == 0
122
+
123
+ control_code = WINSVC_CONTROL_CODE_MAP[command.to_sym]
124
+
125
+ unless ControlService(handle_scs, control_code, status)
126
+ FFI.raise_windows_error('ControlService')
127
+ end
128
+ rescue => e
129
+ puts e
130
+ state = status[:dwCurrentState]
131
+ return state == SERVICE_STOPPED || state == SERVICE_STOP_PENDING
132
+ ensure
133
+ CloseServiceHandle(handle_scs)
134
+ CloseServiceHandle(handle_scm)
135
+ end
136
+
137
+ return true
138
+ end
139
+
140
+ def call_windows_event(command, pid_or_svcname)
141
+ prefix = pid_or_svcname
142
+ event_name = COMMAND_MAP[command.to_sym]
143
+ suffix = event_name.empty? ? "" : "_#{event_name}"
144
+
145
+ begin
146
+ event = Win32::Event.open("#{prefix}#{suffix}")
147
+ event.set
148
+ event.close
149
+ rescue Errno::ENOENT => e
150
+ puts "Error: Cannot find the fluentd process with the event name: \"#{prefix}\""
151
+ end
152
+ end
153
+
154
+ def configure_option_parser
155
+ @opt_parser.banner = help_text
156
+ @opt_parser.version = Fluent::VERSION
157
+ end
158
+
159
+ def parse_options!
160
+ @opt_parser.parse!(@argv)
161
+
162
+ @command = @argv[0]
163
+ @pid_or_svcname = @argv[1] || "fluentdwinsvc"
164
+
165
+ usage("Command isn't specified!") if @command.nil? || @command.empty?
166
+ usage("Unknown command: #{@command}") unless COMMAND_MAP.has_key?(@command.to_sym)
167
+
168
+ if Fluent.windows?
169
+ usage("PID or SVCNAME isn't specified!") if @pid_or_svcname.nil? || @pid_or_svcname.empty?
170
+ else
171
+ usage("PID isn't specified!") if @pid_or_svcname.nil? || @pid_or_svcname.empty?
172
+ usage("Invalid PID: #{pid}") unless @pid_or_svcname =~ /^[0-9]+$/
173
+ end
174
+ end
175
+ end
176
+ end
177
+
@@ -29,7 +29,8 @@ class FluentPluginConfigFormatter
29
29
  AVAILABLE_FORMATS = [:markdown, :txt, :json]
30
30
  SUPPORTED_TYPES = [
31
31
  "input", "output", "filter",
32
- "buffer", "parser", "formatter", "storage"
32
+ "buffer", "parser", "formatter", "storage",
33
+ "service_discovery"
33
34
  ]
34
35
 
35
36
  DOCS_BASE_URL = "https://docs.fluentd.org/v/1.0"
@@ -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
@@ -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
@@ -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)
@@ -105,6 +105,8 @@ module Fluent::Plugin
105
105
  config_param :ignore_repeated_permission_error, :bool, default: false
106
106
  desc 'Format path with the specified timezone'
107
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
108
110
 
109
111
  config_section :parse, required: false, multi: true, init: true, param_name: :parser_configs do
110
112
  config_argument :usage, :string, default: 'in_tail_parser'
@@ -155,6 +157,9 @@ module Fluent::Plugin
155
157
  end
156
158
  @variable_store[@pos_file] = self.plugin_id
157
159
  else
160
+ if @follow_inodes
161
+ raise Fluent::ConfigError, "Can't follow inodes without pos_file configuration parameter"
162
+ end
158
163
  $log.warn "'pos_file PATH' parameter is not set to a 'tail' source."
159
164
  $log.warn "this parameter is highly recommended to save the position to resume tailing."
160
165
  end
@@ -216,7 +221,7 @@ module Fluent::Plugin
216
221
  FileUtils.mkdir_p(pos_file_dir, mode: @dir_perm) unless Dir.exist?(pos_file_dir)
217
222
  @pf_file = File.open(@pos_file, File::RDWR|File::CREAT|File::BINARY, @file_perm)
218
223
  @pf_file.sync = true
219
- @pf = PositionFile.load(@pf_file, logger: log)
224
+ @pf = PositionFile.load(@pf_file, @follow_inodes, expand_paths, logger: log)
220
225
 
221
226
  if @pos_file_compaction_interval
222
227
  timer_execute(:in_tail_refresh_compact_pos_file, @pos_file_compaction_interval) do
@@ -240,7 +245,7 @@ module Fluent::Plugin
240
245
 
241
246
  def shutdown
242
247
  # during shutdown phase, don't close io. It should be done in close after all threads are stopped. See close.
243
- stop_watchers(@tails.keys, immediate: true, remove_watcher: false)
248
+ stop_watchers(existence_path, immediate: true, remove_watcher: false)
244
249
  @pf_file.close if @pf_file
245
250
 
246
251
  super
@@ -303,7 +308,31 @@ module Fluent::Plugin
303
308
  end
304
309
  path.include?('*') ? Dir.glob(path) : path
305
310
  }.flatten.uniq
306
- 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
+ target_info = TargetInfo.new(path, Fluent::FileWrapper.stat(path).ino)
317
+ if @follow_inodes
318
+ hash[target_info.ino] = target_info
319
+ else
320
+ hash[target_info.path] = target_info
321
+ end
322
+ }
323
+ hash
324
+ end
325
+
326
+ def existence_path
327
+ hash = {}
328
+ @tails.each_key {|target_info|
329
+ if @follow_inodes
330
+ hash[target_info.ino] = target_info
331
+ else
332
+ hash[target_info.path] = target_info
333
+ end
334
+ }
335
+ hash
307
336
  end
308
337
 
309
338
  # in_tail with '*' path doesn't check rotation file equality at refresh phase.
@@ -312,21 +341,21 @@ module Fluent::Plugin
312
341
  # In such case, you should separate log directory and specify two paths in path parameter.
313
342
  # e.g. path /path/to/dir/*,/path/to/rotated_logs/target_file
314
343
  def refresh_watchers
315
- target_paths = expand_paths
316
- existence_paths = @tails.keys
344
+ target_paths_hash = expand_paths
345
+ existence_paths_hash = existence_path
317
346
 
318
347
  log.debug { "tailing paths: target = #{target_paths.join(",")} | existing = #{existence_paths.join(",")}" }
319
348
 
320
- unwatched = existence_paths - target_paths
321
- added = target_paths - existence_paths
349
+ unwatched_hash = existence_paths_hash.reject {|key, value| target_paths_hash.key?(key)}
350
+ added_hash = target_paths_hash.reject {|key, value| existence_paths_hash.key?(key)}
322
351
 
323
- stop_watchers(unwatched, immediate: false, unwatched: true) unless unwatched.empty?
324
- start_watchers(added) unless added.empty?
352
+ stop_watchers(unwatched_hash, immediate: false, unwatched: true) unless unwatched_hash.empty?
353
+ start_watchers(added_hash) unless added_hash.empty?
325
354
  end
326
355
 
327
- def setup_watcher(path, pe)
356
+ def setup_watcher(target_info, pe)
328
357
  line_buffer_timer_flusher = @multiline_mode ? TailWatcher::LineBufferTimerFlusher.new(log, @multiline_flush_interval, &method(:flush_buffer)) : nil
329
- tw = TailWatcher.new(path, pe, log, @read_from_head, method(:update_watcher), line_buffer_timer_flusher, method(:io_handler))
358
+ tw = TailWatcher.new(target_info, pe, log, @read_from_head, @follow_inodes, method(:update_watcher), line_buffer_timer_flusher, method(:io_handler))
330
359
 
331
360
  if @enable_watch_timer
332
361
  tt = TimerTrigger.new(1, log) { tw.on_notify }
@@ -357,47 +386,52 @@ module Fluent::Plugin
357
386
  raise e
358
387
  end
359
388
 
360
- def start_watchers(paths)
361
- paths.each { |path|
389
+ def start_watchers(targets_info)
390
+ targets_info.each_value { |target_info|
362
391
  pe = nil
363
392
  if @pf
364
- pe = @pf[path]
393
+ pe = @pf[target_info]
365
394
  if @read_from_head && pe.read_inode.zero?
366
395
  begin
367
- pe.update(Fluent::FileWrapper.stat(path).ino, 0)
396
+ pe.update(Fluent::FileWrapper.stat(target_info.path).ino, 0)
368
397
  rescue Errno::ENOENT
369
- $log.warn "#{path} not found. Continuing without tailing it."
398
+ $log.warn "#{target_info.path} not found. Continuing without tailing it."
370
399
  end
371
400
  end
372
401
  end
373
402
 
374
403
  begin
375
- tw = setup_watcher(path, pe)
404
+ tw = setup_watcher(target_info, pe)
376
405
  rescue WatcherSetupError => e
377
- log.warn "Skip #{path} because unexpected setup error happens: #{e}"
406
+ log.warn "Skip #{target_info.path} because unexpected setup error happens: #{e}"
378
407
  next
379
408
  end
380
- @tails[path] = tw
409
+ target_info = TargetInfo.new(target_info.path, Fluent::FileWrapper.stat(target_info.path).ino)
410
+ @tails[target_info] = tw
381
411
  }
382
412
  end
383
413
 
384
- def stop_watchers(paths, immediate: false, unwatched: false, remove_watcher: true)
385
- paths.each { |path|
386
- tw = remove_watcher ? @tails.delete(path) : @tails[path]
414
+ def stop_watchers(targets_info, immediate: false, unwatched: false, remove_watcher: true)
415
+ targets_info.each_value { |target_info|
416
+ if remove_watcher
417
+ tw = @tails.delete(target_info)
418
+ else
419
+ tw = @tails[target_info]
420
+ end
387
421
  if tw
388
422
  tw.unwatched = unwatched
389
423
  if immediate
390
- detach_watcher(tw, false)
424
+ detach_watcher(tw, target_info.ino, false)
391
425
  else
392
- detach_watcher_after_rotate_wait(tw)
426
+ detach_watcher_after_rotate_wait(tw, target_info.ino)
393
427
  end
394
428
  end
395
429
  }
396
430
  end
397
431
 
398
432
  def close_watcher_handles
399
- @tails.keys.each do |path|
400
- tw = @tails.delete(path)
433
+ @tails.keys.each do |target_info|
434
+ tw = @tails.delete(target_info)
401
435
  if tw
402
436
  tw.close
403
437
  end
@@ -405,25 +439,39 @@ module Fluent::Plugin
405
439
  end
406
440
 
407
441
  # refresh_watchers calls @tails.keys so we don't use stop_watcher -> start_watcher sequence for safety.
408
- def update_watcher(path, pe)
409
- log.info("detected rotation of #{path}; waiting #{@rotate_wait} seconds")
442
+ def update_watcher(target_info, pe)
443
+ log.info("detected rotation of #{target_info.path}; waiting #{@rotate_wait} seconds")
410
444
 
411
445
  if @pf
412
- unless pe.read_inode == @pf[path].read_inode
446
+ pe_inode = pe.read_inode
447
+ target_info_from_position_entry = TargetInfo.new(target_info.path, pe_inode)
448
+ unless pe_inode == @pf[target_info_from_position_entry].read_inode
413
449
  log.debug "Skip update_watcher because watcher has been already updated by other inotify event"
414
450
  return
415
451
  end
416
452
  end
417
- rotated_tw = @tails[path]
418
- @tails[path] = setup_watcher(path, pe)
419
- detach_watcher_after_rotate_wait(rotated_tw) if rotated_tw
453
+
454
+ rotated_target_info = TargetInfo.new(target_info.path, pe.read_inode)
455
+ rotated_tw = @tails[rotated_target_info]
456
+ new_target_info = target_info.dup
457
+
458
+ if @follow_inodes
459
+ new_position_entry = @pf[target_info]
460
+
461
+ if new_position_entry.read_inode == 0
462
+ @tails[new_target_info] = setup_watcher(new_target_info, new_position_entry)
463
+ end
464
+ else
465
+ @tails[new_target_info] = setup_watcher(new_target_info, pe)
466
+ end
467
+ detach_watcher_after_rotate_wait(rotated_tw, pe.read_inode) if rotated_tw
420
468
  end
421
469
 
422
470
  # TailWatcher#close is called by another thread at shutdown phase.
423
471
  # It causes 'can't modify string; temporarily locked' error in IOHandler
424
472
  # so adding close_io argument to avoid this problem.
425
473
  # At shutdown, IOHandler's io will be released automatically after detached the event loop
426
- def detach_watcher(tw, close_io = true)
474
+ def detach_watcher(tw, ino, close_io = true)
427
475
  tw.watchers.each do |watcher|
428
476
  event_loop_detach(watcher)
429
477
  end
@@ -432,15 +480,16 @@ module Fluent::Plugin
432
480
  tw.close if close_io
433
481
 
434
482
  if tw.unwatched && @pf
435
- @pf.unwatch(tw.path)
483
+ target_info = TargetInfo.new(tw.path, ino)
484
+ @pf.unwatch(target_info)
436
485
  end
437
486
  end
438
487
 
439
- def detach_watcher_after_rotate_wait(tw)
488
+ def detach_watcher_after_rotate_wait(tw, ino)
440
489
  # Call event_loop_attach/event_loop_detach is high-cost for short-live object.
441
490
  # If this has a problem with large number of files, use @_event_loop directly instead of timer_execute.
442
491
  timer_execute(:in_tail_close_watcher, @rotate_wait, repeat: false) do
443
- detach_watcher(tw)
492
+ detach_watcher(tw, ino)
444
493
  end
445
494
  end
446
495
 
@@ -608,10 +657,12 @@ module Fluent::Plugin
608
657
  end
609
658
 
610
659
  class TailWatcher
611
- def initialize(path, pe, log, read_from_head, update_watcher, line_buffer_timer_flusher, io_handler_build)
612
- @path = path
660
+ def initialize(target_info, pe, log, read_from_head, follow_inodes, update_watcher, line_buffer_timer_flusher, io_handler_build)
661
+ @path = target_info.path
662
+ @ino = target_info.ino
613
663
  @pe = pe || MemoryPositionEntry.new
614
664
  @read_from_head = read_from_head
665
+ @follow_inodes = follow_inodes
615
666
  @update_watcher = update_watcher
616
667
  @log = log
617
668
  @rotate_handler = RotateHandler.new(log, &method(:on_rotate))
@@ -621,7 +672,7 @@ module Fluent::Plugin
621
672
  @watchers = []
622
673
  end
623
674
 
624
- attr_reader :path
675
+ attr_reader :path, :ino
625
676
  attr_reader :pe
626
677
  attr_reader :line_buffer_timer_flusher
627
678
  attr_accessor :unwatched # This is used for removing position entry from PositionFile
@@ -716,7 +767,17 @@ module Fluent::Plugin
716
767
  end
717
768
 
718
769
  if watcher_needs_update
719
- @update_watcher.call(@path, swap_state(@pe))
770
+ # No need to update a watcher if stat is nil (file not present), because moving to inodes will create
771
+ # new watcher, and old watcher will be closed by stop_watcher in refresh_watchers method
772
+ if stat
773
+ target_info = TargetInfo.new(@path, stat.ino)
774
+ if @follow_inodes
775
+ # don't want to swap state because we need latest read offset in pos file even after rotate_wait
776
+ @update_watcher.call(target_info, @pe)
777
+ else
778
+ @update_watcher.call(target_info, swap_state(@pe))
779
+ end
780
+ end
720
781
  else
721
782
  @log.info "detected rotation of #{@path}"
722
783
  @io_handler = io_handler