fluentd 1.19.4 → 1.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/AGENTS.md +46 -0
  3. data/CHANGELOG.md +75 -0
  4. data/GithubWorkflow.md +1 -1
  5. data/MAINTAINERS.md +2 -2
  6. data/README.md +3 -2
  7. data/Rakefile +2 -1
  8. data/SECURITY.md +26 -1
  9. data/lib/fluent/command/cat.rb +2 -2
  10. data/lib/fluent/compat/exec_util.rb +20 -5
  11. data/lib/fluent/config/literal_parser.rb +4 -3
  12. data/lib/fluent/config/yaml_parser/loader.rb +12 -1
  13. data/lib/fluent/config/yaml_parser/parser.rb +14 -13
  14. data/lib/fluent/event.rb +37 -0
  15. data/lib/fluent/load.rb +0 -1
  16. data/lib/fluent/log.rb +2 -2
  17. data/lib/fluent/plugin/buf_file.rb +2 -2
  18. data/lib/fluent/plugin/buf_file_single.rb +6 -2
  19. data/lib/fluent/plugin/buffer/file_chunk.rb +23 -10
  20. data/lib/fluent/plugin/buffer/file_single_chunk.rb +23 -10
  21. data/lib/fluent/plugin/buffer.rb +102 -36
  22. data/lib/fluent/plugin/formatter_out_file.rb +3 -4
  23. data/lib/fluent/plugin/in_exec.rb +6 -4
  24. data/lib/fluent/plugin/in_forward.rb +39 -29
  25. data/lib/fluent/plugin/in_object_space.rb +2 -2
  26. data/lib/fluent/plugin/in_syslog.rb +2 -2
  27. data/lib/fluent/plugin/in_tail/compatibility.rb +111 -0
  28. data/lib/fluent/plugin/in_tail/dir_watcher.rb +713 -0
  29. data/lib/fluent/plugin/in_tail/group_watch.rb +13 -13
  30. data/lib/fluent/plugin/in_tail/io_handler.rb +391 -0
  31. data/lib/fluent/plugin/in_tail/line_feeder.rb +230 -0
  32. data/lib/fluent/plugin/in_tail/stat_watcher.rb +52 -0
  33. data/lib/fluent/plugin/in_tail/tail_watcher.rb +276 -0
  34. data/lib/fluent/plugin/in_tail/worker_pool.rb +298 -0
  35. data/lib/fluent/plugin/in_tail.rb +324 -815
  36. data/lib/fluent/plugin/in_unix.rb +5 -3
  37. data/lib/fluent/plugin/out_exec_filter.rb +2 -2
  38. data/lib/fluent/plugin/out_file.rb +10 -4
  39. data/lib/fluent/plugin/out_forward.rb +8 -0
  40. data/lib/fluent/plugin/out_http.rb +0 -10
  41. data/lib/fluent/plugin/output.rb +49 -19
  42. data/lib/fluent/plugin/parser_csv.rb +7 -2
  43. data/lib/fluent/plugin/parser_json.rb +20 -12
  44. data/lib/fluent/plugin/parser_ltsv.rb +2 -4
  45. data/lib/fluent/plugin/parser_msgpack.rb +7 -6
  46. data/lib/fluent/plugin/parser_syslog.rb +52 -5
  47. data/lib/fluent/plugin/parser_tsv.rb +6 -1
  48. data/lib/fluent/plugin_helper/http_server/server.rb +29 -13
  49. data/lib/fluent/plugin_helper/http_server.rb +2 -1
  50. data/lib/fluent/root_agent.rb +1 -1
  51. data/lib/fluent/supervisor.rb +24 -5
  52. data/lib/fluent/system_config.rb +5 -2
  53. data/lib/fluent/time.rb +8 -7
  54. data/lib/fluent/version.rb +1 -1
  55. data/templates/new_gem/.gitignore +8 -0
  56. metadata +39 -16
@@ -0,0 +1,713 @@
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
+ require 'fluent/plugin/in_tail/stat_watcher'
19
+
20
+ module Fluent::Plugin
21
+ class TailInput < Fluent::Plugin::Input
22
+ # Discover files matching the configured paths and refresh file watchers.
23
+ # Periodic refreshes always run; enable_dir_watcher adds directory events
24
+ # to discover new files sooner than the next refresh_interval check.
25
+ #
26
+ # Coolio::StatWatcher uses libev's ev_stat, with inotify on Linux where
27
+ # available and stat polling as a fallback. Detection latency depends on
28
+ # the platform and file system.
29
+ #
30
+ # ev_stat compares several stat fields, including second-resolution
31
+ # timestamps. Changes can be missed when those fields remain unchanged;
32
+ # delaying a refresh does not guarantee detection. Periodic refreshes
33
+ # remain the fallback for missed changes.
34
+ module DirWatcher
35
+ # Directory changes may arrive close together. Collect requests during
36
+ # this interval and refresh once, without extending the pending timer.
37
+ DIR_WATCH_DEBOUNCE_INTERVAL = 1
38
+
39
+ # Brace alternatives can multiply the paths whose directories must be
40
+ # examined. Bound their expansion as well as the number of watchers.
41
+ MAX_DIR_PATTERNS = 100
42
+
43
+ # A component of a path which contains one of these characters may match a
44
+ # directory which is not named by the component itself, that is a
45
+ # directory which may be created later.
46
+ WILDCARD_CHARS = /[*?\[{]/
47
+
48
+ # A component of `**' stands for no directory at all or for directories at
49
+ # any depth, so it is expanded recursively instead of as a wildcard of one
50
+ # directory name.
51
+ GLOB_STAR = '**'
52
+
53
+ def initialize_dir_watchers
54
+ @dir_watchers = {}
55
+ @dir_watcher_offset = 0
56
+ @dir_watcher_start_dir = nil
57
+ @dir_watcher_limit_exceeded = false
58
+ @truncated_dir_patterns = {}
59
+ # The event loop schedules refreshes; shutdown detaches the pending
60
+ # timer from another thread. Protect only this scheduling state.
61
+ @refresh_request_mutex = Mutex.new
62
+ @refresh_timer = nil
63
+ @rotate_dir_watchers = false
64
+ # Directory watcher updates and shutdown detachment share this lock.
65
+ # Do not hold it while refreshing files: initial reads can take a long
66
+ # time, and shutdown must be able to signal the IO handlers promptly.
67
+ @dir_watchers_mutex = Mutex.new
68
+ end
69
+
70
+ def extended_glob_pattern(path)
71
+ path.include?('*') || path.include?('?') || /\[.*\]/.match?(path)
72
+ end
73
+
74
+ # Brace alternatives conflict with the default ',' path delimiter;
75
+ # configure rejects that combination with glob_policy :always.
76
+ def use_glob?(path)
77
+ if @glob_policy == :always
78
+ extended_glob_pattern(path) || /\{.*,.*\}/.match?(path)
79
+ elsif @glob_policy == :extended
80
+ extended_glob_pattern(path)
81
+ elsif @glob_policy == :backward_compatible
82
+ path.include?('*')
83
+ end
84
+ end
85
+
86
+ def expand_paths_raw
87
+ date = Fluent::EventTime.now
88
+ paths = []
89
+ @paths.each { |path|
90
+ path = if @path_timezone
91
+ @path_formatters[path].call(date)
92
+ else
93
+ date.to_time.strftime(path)
94
+ end
95
+ if use_glob?(path)
96
+ paths += Dir.glob(path).select { |p|
97
+ begin
98
+ is_file = !File.directory?(p)
99
+ if (File.readable?(p) || have_read_capability?) && is_file
100
+ if @limit_recently_modified && File.mtime(p) < (date.to_time - @limit_recently_modified)
101
+ false
102
+ else
103
+ true
104
+ end
105
+ else
106
+ if is_file
107
+ unless @ignore_list.include?(p)
108
+ log.warn "#{p} unreadable. It is excluded and would be examined next time."
109
+ @ignore_list << p if @ignore_repeated_permission_error
110
+ end
111
+ end
112
+ false
113
+ end
114
+ rescue Errno::ENOENT, Errno::EACCES
115
+ log.debug { "#{p} is missing after refresh file list" }
116
+ false
117
+ end
118
+ }
119
+ else
120
+ # When file is not created yet, Dir.glob returns an empty array. So just add when path is static.
121
+ paths << path
122
+ end
123
+ }
124
+ excluded = @exclude_path.map { |path|
125
+ path = if @path_timezone
126
+ @exclude_path_formatters[path].call(date)
127
+ else
128
+ date.to_time.strftime(path)
129
+ end
130
+ use_glob?(path) ? Dir.glob(path) : path
131
+ }.flatten.uniq
132
+ paths - excluded
133
+ end
134
+
135
+ def expand_paths
136
+ # filter out non existing files, so in case pattern is without '*' we don't do unnecessary work
137
+ hash = {}
138
+ expand_paths_raw.select { |path|
139
+ File.exist?(path)
140
+ }.each { |path|
141
+ # Even we just checked for existence, there is a race condition here as
142
+ # of which stat() might fail with ENOENT. See #3224.
143
+ begin
144
+ target_info = TargetInfo.new(path, Fluent::FileWrapper.stat(path).ino)
145
+ if @follow_inodes
146
+ hash[target_info.ino] = target_info
147
+ else
148
+ hash[target_info.path] = target_info
149
+ end
150
+ rescue Errno::ENOENT, Errno::EACCES => e
151
+ log.warn "expand_paths: stat() for #{path} failed with #{e.class.name}. Skip file."
152
+ end
153
+ }
154
+ hash
155
+ end
156
+
157
+ def existence_path
158
+ hash = {}
159
+ @tails.each {|path, tw|
160
+ if @follow_inodes
161
+ hash[tw.ino] = TargetInfo.new(tw.path, tw.ino)
162
+ else
163
+ hash[tw.path] = TargetInfo.new(tw.path, tw.ino)
164
+ end
165
+ }
166
+ hash
167
+ end
168
+
169
+ # Rotated files matching path can be discovered as new files and reread.
170
+ # follow_inodes avoids duplicate watchers for the same inode after rename,
171
+ # but not for a copytruncate archive, which has a different inode. Exclude
172
+ # archives from path to avoid rediscovering them. Directory-triggered
173
+ # refreshes can discover them sooner than periodic refreshes.
174
+ def refresh_watchers
175
+ @watcher_refresh_monitor.synchronize do
176
+ return if before_shutdown? || @shutdown_start_time
177
+
178
+ rotate = take_dir_watcher_rotation_request
179
+ # Arm directory watchers before scanning so changes during initial
180
+ # reads cannot become part of a new watcher's baseline.
181
+ update_dir_watchers(rotate: rotate)
182
+ refresh_watchers_raw
183
+ end
184
+ end
185
+
186
+ def refresh_watchers_raw
187
+ target_paths_hash = expand_paths
188
+ existence_paths_hash = existence_path
189
+
190
+ log.debug {
191
+ target_paths_str = target_paths_hash.collect { |key, target_info| target_info.path }.join(",")
192
+ existence_paths_str = existence_paths_hash.collect { |key, target_info| target_info.path }.join(",")
193
+ "tailing paths: target = #{target_paths_str} | existing = #{existence_paths_str}"
194
+ }
195
+
196
+ removed_hash = existence_paths_hash.reject {|key, value| target_paths_hash.key?(key)}
197
+ added_hash = target_paths_hash.reject {|key, value| existence_paths_hash.key?(key)}
198
+
199
+ if @follow_inodes
200
+ # A TailWatcher waiting for `rotate_wait` still reads its inode, so do not start a second one for it.
201
+ @tails_rotate_wait.each_value do |v|
202
+ target = added_hash.delete(v[:ino])
203
+ log.debug { "skip #{target.path} (inode: #{target.ino}) because a watcher waiting rotate_wait still reads it" } if target
204
+ end
205
+ end
206
+
207
+ # If an existing TailWatcher already follows a target path with the different inode,
208
+ # it means that the TailWatcher following the rotated file still exists. In this case,
209
+ # `refresh_watcher` can't start the new TailWatcher for the new current file. So, we
210
+ # should output a warning log in order to prevent silent collection stops.
211
+ # (Such as https://github.com/fluent/fluentd/pull/4327)
212
+ # (Usually, such a TailWatcher should be removed from `@tails` in `update_watcher`.)
213
+ # (The similar warning may work for `@follow_inodes true` too. Just limiting the case
214
+ # to suppress the impact to existing logics.)
215
+ unless @follow_inodes
216
+ target_paths_hash.each do |path, target|
217
+ next unless @tails.key?(path)
218
+ # We can't use `existence_paths_hash[path].ino` because it is from `TailWatcher.ino`,
219
+ # which is very unstable parameter. (It can be `nil` or old).
220
+ # So, we need to use `TailWatcher.pe.read_inode`.
221
+ existing_watcher_inode = @tails[path].pe.read_inode
222
+ if existing_watcher_inode != target.ino
223
+ log.warn "Could not follow a file (inode: #{target.ino}) because an existing watcher for that filepath follows a different inode: #{existing_watcher_inode} (e.g. keeps watching a already rotated file). If you keep getting this message, please restart Fluentd.",
224
+ filepath: target.path
225
+ end
226
+ end
227
+ end
228
+
229
+ stop_watchers(removed_hash, unwatched: !@follow_inodes) unless removed_hash.empty?
230
+ unwatch_removed_inodes(target_paths_hash) if @follow_inodes
231
+ start_watchers(added_hash) unless added_hash.empty?
232
+ @metrics.tracked.set(@tails.size)
233
+ @startup = false if @startup
234
+ end
235
+
236
+ # When using @follow_inodes, need this to unwatch the rotated old inode when it disappears.
237
+ # After `update_watcher` detaches an old TailWatcher, the inode is lost from the `@tails`.
238
+ # So that inode can't be contained in `removed_hash`, and can't be unwatched by `stop_watchers`.
239
+ #
240
+ # Keep inodes still read by watchers waiting for rotate_wait or worker completion.
241
+ # Compaction only updates entries remaining in the position file, so removing one
242
+ # early would let its watcher write into another file's line.
243
+ def unwatch_removed_inodes(target_paths_hash)
244
+ return unless @pf
245
+ draining_hash = @tails_rotate_wait.to_h { |tw, v| [v[:ino], TargetInfo.new(tw.path, v[:ino])] }
246
+ @worker_detached_watchers&.each do |tw|
247
+ draining_hash[tw.ino] = TargetInfo.new(tw.path, tw.ino)
248
+ end
249
+ @worker_deferred_unwatch&.each_value do |target|
250
+ draining_hash[target.ino] = target
251
+ end
252
+ @pf.unwatch_removed_targets(draining_hash.merge(target_paths_hash))
253
+ end
254
+
255
+ # Without directory events, keep the ordinary periodic refresh path.
256
+ def refresh_watchers_on_timer
257
+ if @enable_dir_watcher
258
+ request_refresh_watchers(:refresh_interval)
259
+ else
260
+ refresh_watchers
261
+ end
262
+ end
263
+
264
+ # Consume the rotation request before reading files, so a request arriving
265
+ # during that read remains pending for the next refresh.
266
+ def take_dir_watcher_rotation_request
267
+ return false unless @enable_dir_watcher
268
+
269
+ @refresh_request_mutex.synchronize do
270
+ requested = @rotate_dir_watchers
271
+ @rotate_dir_watchers = false
272
+ requested
273
+ end
274
+ end
275
+
276
+ # Merge directory changes and periodic requests into a pending refresh.
277
+ # With delay, schedule a one-shot timer without extending an existing one.
278
+ def request_refresh_watchers(trigger, delay: nil)
279
+ log.debug { "in_tail: #{trigger} requested to refresh watchers" }
280
+ refresh_now = false
281
+ @refresh_request_mutex.synchronize do
282
+ # Detaching a watcher cannot cancel an already dispatched callback.
283
+ return if before_shutdown? || @shutdown_start_time
284
+
285
+ # Preserve periodic rotation when it joins a directory-triggered refresh.
286
+ @rotate_dir_watchers = true if trigger == :refresh_interval
287
+ if @refresh_timer
288
+ log.debug { "in_tail: merge the request by #{trigger} into the scheduled refresh" }
289
+ elsif delay
290
+ @refresh_timer = TimerTrigger.new(delay, log, repeat: false) { trigger_refresh_watchers }
291
+ event_loop_attach(@refresh_timer)
292
+ else
293
+ # Do not hold the scheduling lock while reading files.
294
+ refresh_now = true
295
+ end
296
+ end
297
+ trigger_refresh_watchers if refresh_now
298
+ end
299
+
300
+ def trigger_refresh_watchers
301
+ # Detach the pending timer before refreshing, including immediate refreshes.
302
+ detach_refresh_timer
303
+ refresh_watchers
304
+ end
305
+
306
+ def detach_refresh_timer
307
+ timer = nil
308
+ @refresh_request_mutex.synchronize do
309
+ timer = @refresh_timer
310
+ @refresh_timer = nil
311
+ end
312
+ event_loop_detach(timer) if timer
313
+ end
314
+
315
+ # Derive watch directories from path patterns, not existing files, so
316
+ # files created later can also be discovered.
317
+ def expand_watch_dirs
318
+ date = Fluent::EventTime.now
319
+ dirs_by_path = @paths.map { |path|
320
+ expanded = if @path_timezone
321
+ @path_formatters[path].call(date)
322
+ else
323
+ date.to_time.strftime(path)
324
+ end
325
+ # Whether the path is expanded as a glob is decided on the whole path,
326
+ # exactly as #expand_paths_raw does. Deciding it on the directory part
327
+ # only would take `/base/[a]/*.log' as a literal directory name while
328
+ # #expand_paths_raw matches it as a wildcard.
329
+ expand_watch_dirs_of_pattern(path, expanded, use_glob?(expanded))
330
+ }
331
+ interleave_watch_dirs(dirs_by_path)
332
+ end
333
+
334
+ # The directories to watch for one path pattern. `path' is the pattern as
335
+ # it is configured and `expanded' is the pattern with the strftime
336
+ # directives of the current time replaced.
337
+ #
338
+ # Walk existing directories one component at a time, separately for each
339
+ # parent. Watch the final directories where files can be created, and
340
+ # parents where another matching directory can appear. For wildcard or
341
+ # time-dependent components, keep all parents; for literal components,
342
+ # keep only parents where that component is missing.
343
+ #
344
+ # For example, `/base/*/logs/*.log' with `/base/one/logs' and
345
+ # `/base/two' existing gives `/base', to notice a new directory matching
346
+ # `*', `/base/one/logs', the directory of the files, and `/base/two', to
347
+ # notice the creation of its own `logs'.
348
+ def expand_watch_dirs_of_pattern(path, expanded, glob)
349
+ # Expand braces before File.dirname; see #expand_brace_patterns.
350
+ patterns = glob ? expand_brace_patterns(expanded, warning_key: path) : [expanded]
351
+ configured_patterns = []
352
+ if glob
353
+ expand_brace_patterns_into(path, configured_patterns)
354
+ else
355
+ configured_patterns << path
356
+ end
357
+ patterns.each_with_index.flat_map { |pattern, index|
358
+ # Brace alternatives can have different depths. Compare each
359
+ # alternative with its own unformatted path, not the unsplit group.
360
+ time_dependent_from = time_dependent_component_index(configured_patterns[index] || path, pattern)
361
+ watch_dirs_of_path(pattern, glob, time_dependent_from)
362
+ }
363
+ end
364
+
365
+ # Walk one path after brace expansion, using the parent-selection rules above.
366
+ def watch_dirs_of_path(path, glob, time_dependent_from)
367
+ root, components = dir_path_parts(File.dirname(path))
368
+ dirs = []
369
+ # The existing directories which match the prefix walked so far.
370
+ parents = [root]
371
+ components.each_with_index do |component, i|
372
+ matched = []
373
+ missing = []
374
+ parents.each { |parent|
375
+ found = existing_dirs(parent, component, glob) { |link_parent|
376
+ # realpath follows a link, but replacing the link changes its
377
+ # parent, not the old target directory.
378
+ dirs << link_parent
379
+ }
380
+ if found.empty?
381
+ missing << parent
382
+ else
383
+ matched.concat(found)
384
+ end
385
+ }
386
+ if i >= time_dependent_from || (glob && WILDCARD_CHARS.match?(component))
387
+ dirs.concat(parents)
388
+ else
389
+ dirs.concat(missing)
390
+ end
391
+ parents = matched
392
+ break if parents.empty?
393
+ end
394
+ # Keep unresolved prefixes while walking: resolving a symbolic link
395
+ # before a later '..' can change Windows path lookup semantics.
396
+ dirs.concat(parents).filter_map { |dir| real_path(dir) }
397
+ end
398
+
399
+ # The existing directories matching one component of a path pattern inside
400
+ # one of the directories matched by the components above it. The component
401
+ # is expanded as a glob only when the path it comes from is expanded by
402
+ # Dir.glob in #expand_paths_raw: otherwise its glob characters are parts
403
+ # of the directory name. Even a component without wildcard characters
404
+ # must go through Dir.glob when glob is enabled, because backslashes
405
+ # escape characters there. Yield the parent of each matched symbolic
406
+ # link so that replacing the link can also be detected.
407
+ def existing_dirs(parent, component, glob)
408
+ dirs = if !glob
409
+ dir = File.join(parent, component)
410
+ File.directory?(dir) ? [dir] : []
411
+ elsif component == GLOB_STAR
412
+ # `**' stands for no directory at all or for directories at any
413
+ # depth. Dir.glob takes it recursively only when it is followed
414
+ # by a separator: `dir/**' gives the same as `dir/*' there.
415
+ Dir.glob(File.join(literal_glob_pattern(parent), GLOB_STAR) + File::SEPARATOR)
416
+ else
417
+ Dir.glob(File.join(literal_glob_pattern(parent), component))
418
+ end
419
+ dirs.filter_map { |dir|
420
+ next unless File.directory?(dir)
421
+ if block_given? && File.symlink?(dir.chomp(File::SEPARATOR))
422
+ yield File.dirname(dir.chomp(File::SEPARATOR))
423
+ end
424
+ dir
425
+ }
426
+ end
427
+
428
+ # Escape glob characters in matched directory names so later components
429
+ # do not reinterpret those names as patterns.
430
+ def literal_glob_pattern(path)
431
+ path.gsub(/[*?{}\[\]\\]/) { |char| "\\#{char}" }
432
+ end
433
+
434
+ # Resolve symbolic links and `..' through the file system and return a
435
+ # canonical path for deduplication, or nil if resolution fails. Callers
436
+ # check whether the path is a directory.
437
+ def real_path(dir)
438
+ # Win32 normalizes '..' before following symbolic links. These are
439
+ # already matched directory names, not unexpanded glob patterns.
440
+ dir = File.expand_path(dir) if Fluent.windows?
441
+ File.realpath(dir)
442
+ rescue SystemCallError
443
+ # A matched path may disappear or become inaccessible before resolution.
444
+ nil
445
+ end
446
+
447
+ # Split a directory path into the root directory it starts from and the
448
+ # components below it, so that the path can be walked from its topmost
449
+ # directory: `/base/logs' gives `/' and [`base', `logs'], and `C:/base'
450
+ # gives `C:/' and [`base'].
451
+ #
452
+ # Resolve relative paths from the working directory, like #expand_paths_raw.
453
+ # Preserve `..' until lookup so the platform decides its meaning. On
454
+ # Unix it refers to a symbolic link target's parent, unlike expand_path.
455
+ def dir_path_parts(dir_path)
456
+ components = path_components(dir_path)
457
+ if File::ALT_SEPARATOR && components[0, 2] == ['', ''] && components.size >= 4
458
+ # A UNC share is the root; walking from '/' would use the current
459
+ # drive and lose the server/share portion of the path.
460
+ ["//#{components[2]}/#{components[3]}/", components.drop(4)]
461
+ elsif components.empty?
462
+ # The path is a root directory itself, e.g. the directory of
463
+ # `/review.log', and there is no component to walk below it.
464
+ [File.expand_path(dir_path), []]
465
+ elsif components.first == ''
466
+ components.shift
467
+ [File.expand_path(File::SEPARATOR), components]
468
+ elsif File::ALT_SEPARATOR && components.first.match?(/\A[a-zA-Z]:\z/)
469
+ [components.shift + File::SEPARATOR, components]
470
+ else
471
+ dir_path_parts(File.join(Dir.pwd, dir_path))
472
+ end
473
+ end
474
+
475
+ # Normalize Windows file system separators before splitting. This is not
476
+ # glob parsing: Dir.glob patterns use '/' and treat backslashes as escapes.
477
+ def path_components(path)
478
+ path = path.tr(File::ALT_SEPARATOR, File::SEPARATOR) if File::ALT_SEPARATOR
479
+ path.split(File::SEPARATOR)
480
+ end
481
+
482
+ # The index of the first component of the directory part of a path whose
483
+ # value depends on the current time, or the number of the components when
484
+ # none of them does. Such a component may name another directory later,
485
+ # e.g. the directory of the next month of a `%Y%m' path, and that
486
+ # directory may be created inside one of the directories above it.
487
+ # If it already exists, the time boundary itself generates no directory
488
+ # event; periodic refreshes select the newly current directory.
489
+ def time_dependent_component_index(path, expanded)
490
+ configured = dir_path_parts(File.dirname(path))[1]
491
+ current = dir_path_parts(File.dirname(expanded))[1]
492
+ index = 0
493
+ while index < configured.size && index < current.size && configured[index] == current[index]
494
+ index += 1
495
+ end
496
+ index
497
+ end
498
+
499
+ # Expand the brace groups of a path pattern into the paths they stand for,
500
+ # the same way Dir.glob does: `{a,b}' stands for `a' and `b', the groups
501
+ # can be nested, and a group may contain a separator. The directories of
502
+ # `/base/{a/x.log,b/x.log}' are `/base/a' and `/base/b', which
503
+ # File.dirname can't give because it cuts the path inside the group.
504
+ def expand_brace_patterns(pattern, warning_key: pattern)
505
+ patterns = []
506
+ # One extra result distinguishes truncation from an exact fit.
507
+ expand_brace_patterns_into(pattern, patterns, limit: MAX_DIR_PATTERNS + 1)
508
+ if patterns.size > MAX_DIR_PATTERNS
509
+ patterns.pop
510
+ if @truncated_dir_patterns[warning_key]
511
+ log.debug {
512
+ "Directory watch pattern expansion remains limited to #{MAX_DIR_PATTERNS}" +
513
+ " paths for `#{pattern}'"
514
+ }
515
+ else
516
+ log.warn(
517
+ "Too many paths expanded from the braces of `#{pattern}'." +
518
+ " Only the first #{MAX_DIR_PATTERNS} paths are considered for directory watching;" +
519
+ " remaining paths rely on refresh_interval (#{@refresh_interval}s)"
520
+ )
521
+ end
522
+ @truncated_dir_patterns[warning_key] = true
523
+ else
524
+ @truncated_dir_patterns.delete(warning_key)
525
+ end
526
+ patterns
527
+ end
528
+
529
+ def expand_brace_patterns_into(pattern, patterns, limit: MAX_DIR_PATTERNS)
530
+ return if patterns.size >= limit
531
+ start, stop = brace_group(pattern)
532
+ unless start
533
+ patterns << pattern
534
+ return
535
+ end
536
+ head = pattern[0...start]
537
+ tail = pattern[(stop + 1)..]
538
+ split_brace_alternatives(pattern[(start + 1)...stop]).each { |alternative|
539
+ expand_brace_patterns_into(head + alternative + tail, patterns, limit: limit)
540
+ }
541
+ end
542
+
543
+ # Return the positions of the first unescaped '{' and its closing '}',
544
+ # or nil if there is no complete group. In that case the caller leaves
545
+ # the pattern unchanged for subsequent path/glob processing.
546
+ def brace_group(pattern)
547
+ start = nil
548
+ depth = 0
549
+ index = 0
550
+ while index < pattern.length
551
+ char = pattern[index]
552
+ if char == '\\'
553
+ index += 2
554
+ next
555
+ end
556
+ if char == '{'
557
+ start ||= index
558
+ depth += 1
559
+ elsif char == '}' && start
560
+ depth -= 1
561
+ return [start, index] if depth == 0
562
+ end
563
+ index += 1
564
+ end
565
+ nil
566
+ end
567
+
568
+ # Split the content of a brace group at the commas which are not inside
569
+ # another group.
570
+ def split_brace_alternatives(group)
571
+ alternatives = []
572
+ alternative = +''
573
+ depth = 0
574
+ index = 0
575
+ while index < group.length
576
+ char = group[index]
577
+ if char == '\\' && index + 1 < group.length
578
+ alternative << char << group[index + 1]
579
+ index += 2
580
+ next
581
+ end
582
+ case char
583
+ when '{'
584
+ depth += 1
585
+ when '}'
586
+ depth -= 1
587
+ when ','
588
+ if depth == 0
589
+ alternatives << alternative
590
+ alternative = +''
591
+ index += 1
592
+ next
593
+ end
594
+ end
595
+ alternative << char
596
+ index += 1
597
+ end
598
+ alternatives << alternative
599
+ end
600
+
601
+ # Interleave candidates so a large glob cannot monopolize the watcher
602
+ # limit and leave later paths entirely unwatched.
603
+ def interleave_watch_dirs(dirs_by_path)
604
+ # Deduplicate across paths only after interleaving. Otherwise a static
605
+ # path already included late in a large glob loses its own turn.
606
+ groups = dirs_by_path.map(&:uniq)
607
+
608
+ dirs = []
609
+ until groups.all?(&:empty?)
610
+ groups.each { |group|
611
+ dirs << group.shift unless group.empty?
612
+ }
613
+ end
614
+ dirs.uniq
615
+ end
616
+
617
+ # Take a circular window of the interleaved candidates. Advance only on
618
+ # periodic refreshes, not on directory events. Keep the window's starting
619
+ # directory when candidates change, or fall back to its previous index
620
+ # if that directory disappeared. Called under @dir_watchers_mutex.
621
+ def limit_dir_watchers(dirs, rotate:)
622
+ if dirs.size <= @dir_watcher_limit
623
+ @dir_watcher_offset = 0
624
+ @dir_watcher_start_dir = dirs.first
625
+ return dirs
626
+ end
627
+
628
+ offset = dirs.index(@dir_watcher_start_dir) || @dir_watcher_offset
629
+ offset += @dir_watcher_limit if rotate
630
+ @dir_watcher_offset = offset % dirs.size
631
+ @dir_watcher_start_dir = dirs[@dir_watcher_offset]
632
+ dirs.rotate(@dir_watcher_offset).first(@dir_watcher_limit)
633
+ end
634
+
635
+ # Attach the stat watchers of the directories which are needed now and
636
+ # detach the ones which are not needed anymore.
637
+ #
638
+ # Recompute and watch directories before discovering files, so changes
639
+ # during discovery or synchronous initial reads can request another refresh.
640
+ def update_dir_watchers(rotate: false)
641
+ return unless @enable_dir_watcher
642
+ # Do not add directory watchers once shutdown has begun.
643
+ return if before_shutdown? || @shutdown_start_time
644
+
645
+ dirs = expand_watch_dirs
646
+ @dir_watchers_mutex.synchronize do
647
+ # Shutdown may have detached the watchers during path expansion.
648
+ return if before_shutdown? || @shutdown_start_time
649
+
650
+ over_limit = dirs.size > @dir_watcher_limit
651
+ if over_limit
652
+ if @dir_watcher_limit_exceeded
653
+ log.debug {
654
+ "Directory watcher limit remains exceeded: #{dirs.size} candidates," +
655
+ " watching #{@dir_watcher_limit}"
656
+ }
657
+ else
658
+ log.warn(
659
+ "Too many directories to watch: #{dirs.size}." +
660
+ " Watching #{@dir_watcher_limit} of them, rotating at each periodic refresh," +
661
+ " so new files may not be detected until refresh_interval (#{@refresh_interval}s) passes"
662
+ )
663
+ end
664
+ end
665
+ @dir_watcher_limit_exceeded = over_limit
666
+
667
+ dirs = limit_dir_watchers(dirs, rotate: rotate)
668
+ # Release the old window before attaching the new one so rotation
669
+ # does not temporarily exceed the configured watcher limit.
670
+ (@dir_watchers.keys - dirs).each do |dir|
671
+ detach_dir_watcher(dir)
672
+ end
673
+ dirs.each do |dir|
674
+ attach_dir_watcher(dir) unless @dir_watchers.key?(dir)
675
+ end
676
+ end
677
+ end
678
+
679
+ def attach_dir_watcher(dir)
680
+ watcher = StatWatcher.new(dir, log) { on_dir_changed(dir) }
681
+ @dir_watchers[dir] = watcher
682
+ event_loop_attach(watcher)
683
+ log.debug { "in_tail: watching directory #{dir} to detect new files" }
684
+ rescue SystemCallError => e
685
+ # Failing to watch a directory only means that the new files in it are
686
+ # detected by refresh_interval, like before.
687
+ @dir_watchers.delete(dir)
688
+ log.debug { "in_tail: failed to watch directory #{dir}: #{e}" }
689
+ end
690
+
691
+ def detach_dir_watcher(dir)
692
+ watcher = @dir_watchers.delete(dir)
693
+ return unless watcher
694
+ event_loop_detach(watcher)
695
+ log.debug { "in_tail: stop watching directory #{dir}" }
696
+ end
697
+
698
+ def detach_dir_watchers
699
+ @dir_watchers_mutex.synchronize do
700
+ @dir_watchers.keys.each do |dir|
701
+ detach_dir_watcher(dir)
702
+ end
703
+ end
704
+ end
705
+
706
+ def on_dir_changed(dir)
707
+ # The directory may have been removed. #update_dir_watchers will drop it
708
+ # from the watched list at the next refresh.
709
+ request_refresh_watchers("change of #{dir}", delay: DIR_WATCH_DEBOUNCE_INTERVAL)
710
+ end
711
+ end
712
+ end
713
+ end