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.
- checksums.yaml +4 -4
- data/AGENTS.md +46 -0
- data/CHANGELOG.md +75 -0
- data/GithubWorkflow.md +1 -1
- data/MAINTAINERS.md +2 -2
- data/README.md +3 -2
- data/Rakefile +2 -1
- data/SECURITY.md +26 -1
- data/lib/fluent/command/cat.rb +2 -2
- data/lib/fluent/compat/exec_util.rb +20 -5
- data/lib/fluent/config/literal_parser.rb +4 -3
- data/lib/fluent/config/yaml_parser/loader.rb +12 -1
- data/lib/fluent/config/yaml_parser/parser.rb +14 -13
- data/lib/fluent/event.rb +37 -0
- data/lib/fluent/load.rb +0 -1
- data/lib/fluent/log.rb +2 -2
- data/lib/fluent/plugin/buf_file.rb +2 -2
- data/lib/fluent/plugin/buf_file_single.rb +6 -2
- data/lib/fluent/plugin/buffer/file_chunk.rb +23 -10
- data/lib/fluent/plugin/buffer/file_single_chunk.rb +23 -10
- data/lib/fluent/plugin/buffer.rb +102 -36
- data/lib/fluent/plugin/formatter_out_file.rb +3 -4
- data/lib/fluent/plugin/in_exec.rb +6 -4
- data/lib/fluent/plugin/in_forward.rb +39 -29
- data/lib/fluent/plugin/in_object_space.rb +2 -2
- data/lib/fluent/plugin/in_syslog.rb +2 -2
- data/lib/fluent/plugin/in_tail/compatibility.rb +111 -0
- data/lib/fluent/plugin/in_tail/dir_watcher.rb +713 -0
- data/lib/fluent/plugin/in_tail/group_watch.rb +13 -13
- data/lib/fluent/plugin/in_tail/io_handler.rb +391 -0
- data/lib/fluent/plugin/in_tail/line_feeder.rb +230 -0
- data/lib/fluent/plugin/in_tail/stat_watcher.rb +52 -0
- data/lib/fluent/plugin/in_tail/tail_watcher.rb +276 -0
- data/lib/fluent/plugin/in_tail/worker_pool.rb +298 -0
- data/lib/fluent/plugin/in_tail.rb +324 -815
- data/lib/fluent/plugin/in_unix.rb +5 -3
- data/lib/fluent/plugin/out_exec_filter.rb +2 -2
- data/lib/fluent/plugin/out_file.rb +10 -4
- data/lib/fluent/plugin/out_forward.rb +8 -0
- data/lib/fluent/plugin/out_http.rb +0 -10
- data/lib/fluent/plugin/output.rb +49 -19
- data/lib/fluent/plugin/parser_csv.rb +7 -2
- data/lib/fluent/plugin/parser_json.rb +20 -12
- data/lib/fluent/plugin/parser_ltsv.rb +2 -4
- data/lib/fluent/plugin/parser_msgpack.rb +7 -6
- data/lib/fluent/plugin/parser_syslog.rb +52 -5
- data/lib/fluent/plugin/parser_tsv.rb +6 -1
- data/lib/fluent/plugin_helper/http_server/server.rb +29 -13
- data/lib/fluent/plugin_helper/http_server.rb +2 -1
- data/lib/fluent/root_agent.rb +1 -1
- data/lib/fluent/supervisor.rb +24 -5
- data/lib/fluent/system_config.rb +5 -2
- data/lib/fluent/time.rb +8 -7
- data/lib/fluent/version.rb +1 -1
- data/templates/new_gem/.gitignore +8 -0
- 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
|