eventmachine-tail 0.0.1 → 0.6.1
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.
- data/bin/emtail +83 -0
- data/bin/rtail +83 -0
- data/lib/em/filetail.rb +372 -87
- data/lib/em/globwatcher.rb +244 -51
- data/lib/eventmachine-tail.rb +2 -0
- data/samples/glob-tail.rb +47 -0
- data/samples/globwatch.rb +23 -0
- data/samples/tail-with-block.rb +30 -0
- data/samples/tail.rb +38 -0
- data/test/alltests.rb +5 -0
- data/test/logrotate.conf +4 -0
- data/test/test_filetail.rb +232 -0
- data/test/test_glob.rb +146 -0
- data/test/testcase_helpers.rb +13 -0
- metadata +43 -14
data/lib/em/globwatcher.rb
CHANGED
|
@@ -1,87 +1,280 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
2
|
|
|
3
|
-
require "
|
|
4
|
-
require "set"
|
|
3
|
+
require "em/filetail"
|
|
5
4
|
require "eventmachine"
|
|
6
|
-
require "ap"
|
|
7
5
|
require "logger"
|
|
6
|
+
require "set"
|
|
8
7
|
|
|
9
|
-
|
|
8
|
+
EventMachine.epoll if EventMachine.epoll?
|
|
9
|
+
EventMachine.kqueue = true if EventMachine.kqueue?
|
|
10
10
|
|
|
11
|
+
# A file glob pattern watcher for EventMachine.
|
|
12
|
+
#
|
|
13
|
+
# If you are unfamiliar with globs, see Wikipedia:
|
|
14
|
+
# http://en.wikipedia.org/wiki/Glob_(programming)
|
|
15
|
+
#
|
|
16
|
+
# Any glob supported by Dir#glob will work with
|
|
17
|
+
# this class.
|
|
18
|
+
#
|
|
19
|
+
# This class will allow you to get notified whenever a file
|
|
20
|
+
# is created or deleted that matches your glob.
|
|
21
|
+
#
|
|
22
|
+
# If you are subclassing, here are the methods you should implement:
|
|
23
|
+
# file_found(path)
|
|
24
|
+
# file_deleted(path)
|
|
25
|
+
#
|
|
26
|
+
# See alsoe
|
|
27
|
+
# * EventMachine::watch_glob
|
|
28
|
+
# * EventMachine::FileGlobWatch#file_found
|
|
29
|
+
# * EventMachine::FileGlobWatch#file_deleted
|
|
30
|
+
#
|
|
11
31
|
class EventMachine::FileGlobWatch
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
32
|
+
# Watch a glob
|
|
33
|
+
#
|
|
34
|
+
# * glob - a string path or glob, such as "/var/log/*.log"
|
|
35
|
+
# * interval - number of seconds between scanning the glob for changes
|
|
36
|
+
def initialize(glob, interval=60)
|
|
37
|
+
@glob = glob
|
|
38
|
+
@files = Hash.new
|
|
39
|
+
@watches = Hash.new
|
|
40
|
+
@logger = Logger.new(STDOUT)
|
|
41
|
+
@logger.level = ($DEBUG and Logger::DEBUG or Logger::WARN)
|
|
16
42
|
|
|
43
|
+
# We periodically check here because it is easier than writing our own glob
|
|
44
|
+
# parser (so we can smartly watch globs like /foo/*/bar*/*.log)
|
|
45
|
+
#
|
|
46
|
+
# Reasons to fix this -
|
|
47
|
+
# This will likely perform badly on globs that result in a large number of
|
|
48
|
+
# files.
|
|
17
49
|
EM.next_tick do
|
|
18
50
|
find_files
|
|
19
51
|
EM.add_periodic_timer(interval) do
|
|
20
52
|
find_files
|
|
21
53
|
end
|
|
22
|
-
end
|
|
54
|
+
end # EM.next_tick
|
|
23
55
|
end # def initialize
|
|
24
56
|
|
|
57
|
+
# This method is called when a new file is found
|
|
58
|
+
#
|
|
59
|
+
# * path - the string path of the file found
|
|
60
|
+
#
|
|
61
|
+
# You must implement this in your subclass or module for it
|
|
62
|
+
# to work with EventMachine::watch_glob
|
|
63
|
+
public
|
|
64
|
+
def file_found(path)
|
|
65
|
+
raise NotImplementedError.new("#{self.class.name}#file_found is not "\
|
|
66
|
+
"implemented. Did you forget to implement this in your subclass or "\
|
|
67
|
+
"module?")
|
|
68
|
+
end # def file_found
|
|
69
|
+
|
|
70
|
+
# This method is called when a file is deleted.
|
|
71
|
+
#
|
|
72
|
+
# * path - the string path of the file deleted
|
|
73
|
+
#
|
|
74
|
+
# You must implement this in your subclass or module for it
|
|
75
|
+
# to work with EventMachine::watch_glob
|
|
76
|
+
public
|
|
77
|
+
def file_deleted(path)
|
|
78
|
+
raise NotImplementedError.new("#{self.class.name}#file_deleted is not "\
|
|
79
|
+
"implemented. Did you forget to implement this in your subclass or "\
|
|
80
|
+
"module?")
|
|
81
|
+
end # def file_found
|
|
82
|
+
|
|
83
|
+
private
|
|
25
84
|
def find_files
|
|
26
|
-
|
|
85
|
+
@logger.info("Searching for files in #{@glob}")
|
|
86
|
+
list = Dir.glob(@glob)
|
|
87
|
+
|
|
88
|
+
known_files = @files.clone
|
|
27
89
|
list.each do |path|
|
|
28
|
-
|
|
29
|
-
|
|
90
|
+
fileinfo = FileInfo.new(path) rescue next
|
|
91
|
+
# Skip files that have the same inode (renamed or hardlinked)
|
|
92
|
+
known_files.delete(fileinfo.stat.ino)
|
|
93
|
+
next if @files.include?(fileinfo.stat.ino)
|
|
94
|
+
|
|
95
|
+
track(fileinfo)
|
|
96
|
+
file_found(path)
|
|
30
97
|
end
|
|
31
98
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
99
|
+
# Report missing files.
|
|
100
|
+
known_files.each do |inode, fileinfo|
|
|
101
|
+
remove(fileinfo)
|
|
35
102
|
end
|
|
36
103
|
end # def find_files
|
|
37
104
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
@
|
|
105
|
+
# Remove a file from being watched and notify file_deleted()
|
|
106
|
+
private
|
|
107
|
+
def remove(fileinfo)
|
|
108
|
+
@files.delete(fileinfo.stat.ino)
|
|
109
|
+
@watches.delete(fileinfo.path)
|
|
110
|
+
file_deleted(fileinfo.path)
|
|
111
|
+
end # def remove
|
|
112
|
+
|
|
113
|
+
# Add a file to watch and notify file_found()
|
|
114
|
+
private
|
|
115
|
+
def track(fileinfo)
|
|
116
|
+
@files[fileinfo.stat.ino] = fileinfo
|
|
117
|
+
|
|
118
|
+
# If EventMachine::watch_file fails, that's ok, I guess.
|
|
119
|
+
# We'll still find the file 'missing' from the next glob attempt.
|
|
120
|
+
#begin
|
|
121
|
+
# EM currently has a bug that only the first handler for a watch_file
|
|
122
|
+
# on each file gets events. This causes globtails to never get data
|
|
123
|
+
# since the glob is watching the file already.
|
|
124
|
+
# Until we fix that, let's skip file watching here.
|
|
125
|
+
#@watches[path] = EventMachine::watch_file(path, FileWatcher, self) do |path|
|
|
126
|
+
# remove(path)
|
|
127
|
+
#end
|
|
128
|
+
#rescue Errno::EACCES => e
|
|
129
|
+
#@logger.warn(e)
|
|
130
|
+
#end
|
|
42
131
|
end # def watch
|
|
132
|
+
|
|
133
|
+
private
|
|
134
|
+
class FileWatcher < EventMachine::FileWatch
|
|
135
|
+
def initialize(globwatch, &block)
|
|
136
|
+
@globwatch = globwatch
|
|
137
|
+
@block = block
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def file_moved
|
|
141
|
+
stop_watching
|
|
142
|
+
block.call path
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def file_deleted
|
|
146
|
+
block.call path
|
|
147
|
+
end
|
|
148
|
+
end # class EventMachine::FileGlobWatch::FileWatcher < EventMachine::FileWatch
|
|
149
|
+
|
|
150
|
+
private
|
|
151
|
+
class FileInfo
|
|
152
|
+
attr_reader :path
|
|
153
|
+
attr_reader :stat
|
|
154
|
+
|
|
155
|
+
def initialize(path)
|
|
156
|
+
@path = path
|
|
157
|
+
@stat = File.stat(path)
|
|
158
|
+
end
|
|
159
|
+
end # class FileInfo
|
|
43
160
|
end # class EventMachine::FileGlobWatch
|
|
44
161
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
162
|
+
# A glob tailer for EventMachine
|
|
163
|
+
#
|
|
164
|
+
# This class combines features of EventMachine::file_tail and
|
|
165
|
+
# EventMachine::watch_glob.
|
|
166
|
+
#
|
|
167
|
+
# You won't generally subclass this class (See EventMachine::FileGlobWatch)
|
|
168
|
+
#
|
|
169
|
+
# See also: EventMachine::glob_tail
|
|
170
|
+
#
|
|
171
|
+
class EventMachine::FileGlobWatchTail < EventMachine::FileGlobWatch
|
|
172
|
+
# Initialize a new file glob tail.
|
|
173
|
+
#
|
|
174
|
+
# * path - glob or file path (string)
|
|
175
|
+
# * handler - a module or subclass of EventMachine::FileTail
|
|
176
|
+
# See also EventMachine::file_tail
|
|
177
|
+
# * interval - how often (seconds) the glob path should be scanned
|
|
178
|
+
# * exclude - an array of Regexp (or anything with .match) for
|
|
179
|
+
# excluding from things to tail
|
|
180
|
+
#
|
|
181
|
+
# The remainder of arguments are passed to EventMachine::file_tail as
|
|
182
|
+
# EventMachine::file_tail(path_found, handler, *args, &block)
|
|
183
|
+
public
|
|
184
|
+
def initialize(path, handler=nil, interval=60, exclude=[], *args, &block)
|
|
185
|
+
super(path, interval)
|
|
48
186
|
@handler = handler
|
|
49
|
-
|
|
187
|
+
@args = args
|
|
188
|
+
@exclude = exclude
|
|
189
|
+
|
|
190
|
+
if block_given?
|
|
191
|
+
@handler = block
|
|
192
|
+
end
|
|
193
|
+
end # def initialize
|
|
50
194
|
|
|
195
|
+
public
|
|
51
196
|
def file_found(path)
|
|
52
|
-
|
|
53
|
-
|
|
197
|
+
begin
|
|
198
|
+
@logger.info "#{self.class}: Trying #{path}"
|
|
199
|
+
@exclude.each do |exclude|
|
|
200
|
+
@logger.info "#{self.class}: Testing #{exclude} =~ #{path} == #{exclude.match(path) != nil}"
|
|
201
|
+
if exclude.match(path) != nil
|
|
202
|
+
file_excluded(path)
|
|
203
|
+
return
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
@logger.info "#{self.class}: Watching #{path}"
|
|
207
|
+
|
|
208
|
+
if @handler.is_a? Proc
|
|
209
|
+
EventMachine::file_tail(path, nil, *@args, &@handler)
|
|
210
|
+
else
|
|
211
|
+
EventMachine::file_tail(path, @handler, *@args)
|
|
212
|
+
end
|
|
213
|
+
rescue Errno::EACCES => e
|
|
214
|
+
file_error(path, e)
|
|
215
|
+
rescue Errno::EISDIR => e
|
|
216
|
+
file_error(path, e)
|
|
217
|
+
end
|
|
218
|
+
end # def file_found
|
|
219
|
+
|
|
220
|
+
public
|
|
221
|
+
def file_excluded(path)
|
|
222
|
+
@logger.info "#{self.class}: Skipping path #{path} due to exclude rule"
|
|
223
|
+
end # def file_excluded
|
|
54
224
|
|
|
55
|
-
|
|
225
|
+
public
|
|
226
|
+
def file_deleted(path)
|
|
56
227
|
# Nothing to do
|
|
57
|
-
end
|
|
228
|
+
end # def file_deleted
|
|
229
|
+
|
|
230
|
+
public
|
|
231
|
+
def file_error(path, e)
|
|
232
|
+
$stderr.puts "#{e.class} while trying to tail #{path}"
|
|
233
|
+
# otherwise, drop the error by default
|
|
234
|
+
end # def file_error
|
|
58
235
|
end # class EventMachine::FileGlobWatchHandler
|
|
59
236
|
|
|
60
237
|
module EventMachine
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
238
|
+
# Watch a glob and tail any files found.
|
|
239
|
+
#
|
|
240
|
+
# * glob - a string path or glob, such as /var/log/*.log
|
|
241
|
+
# * handler - a module or subclass of EventMachine::FileGlobWatchTail.
|
|
242
|
+
# handler can be omitted if you give a block.
|
|
243
|
+
#
|
|
244
|
+
# If you give a block and omit the handler parameter, then the behavior
|
|
245
|
+
# is that your block is called for every line read from any file the same
|
|
246
|
+
# way EventMachine::file_tail does when called with a block.
|
|
247
|
+
#
|
|
248
|
+
# See EventMachine::FileGlobWatchTail for the callback methods.
|
|
249
|
+
# See EventMachine::file_tail for more information about block behavior.
|
|
250
|
+
def self.glob_tail(glob, handler=nil, *args, &block)
|
|
251
|
+
handler = EventMachine::FileGlobWatchTail if handler == nil
|
|
252
|
+
args.unshift(glob)
|
|
253
|
+
klass = klass_from_handler(EventMachine::FileGlobWatchTail, handler, *args)
|
|
254
|
+
c = klass.new(*args, &block)
|
|
255
|
+
return c
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# Watch a glob for any files.
|
|
259
|
+
#
|
|
260
|
+
# * glob - a string path or glob, such as "/var/log/*.log"
|
|
261
|
+
# * handler - must be a module or a subclass of EventMachine::FileGlobWatch
|
|
262
|
+
#
|
|
263
|
+
# The remaining (optional) arguments are passed to your handler like this:
|
|
264
|
+
# If you call this:
|
|
265
|
+
# EventMachine.watch_glob("/var/log/*.log", YourHandler, 1, 2, 3, ...)
|
|
266
|
+
# This will be invoked when new matching files are found:
|
|
267
|
+
# YourHandler.new(path_found, 1, 2, 3, ...)
|
|
268
|
+
# ^ path_found is the new path found by the glob
|
|
269
|
+
#
|
|
270
|
+
# See EventMachine::FileGlobWatch for the callback methods.
|
|
271
|
+
def self.watch_glob(glob, handler=nil, *args)
|
|
272
|
+
# This code mostly styled on what EventMachine does in many of it's other
|
|
273
|
+
# methods.
|
|
274
|
+
args = [glob, *args]
|
|
275
|
+
klass = klass_from_handler(EventMachine::FileGlobWatch, handler, *args);
|
|
64
276
|
c = klass.new(*args)
|
|
65
277
|
yield c if block_given?
|
|
66
278
|
return c
|
|
67
|
-
end
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
if __FILE__ == $0
|
|
71
|
-
class Reader < EventMachine::FileTail
|
|
72
|
-
def initialize(*args)
|
|
73
|
-
super(*args)
|
|
74
|
-
@buffer = BufferedTokenizer.new
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
def receive_data(data)
|
|
78
|
-
@buffer.extract(data).each do |line|
|
|
79
|
-
ap [path, line]
|
|
80
|
-
end
|
|
81
|
-
end
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
EventMachine.run do
|
|
85
|
-
EventMachine::FileGlobWatch.new("/var/log/*.log", EventMachine::FileGlobWatchHandler.new(Reader))
|
|
86
|
-
end
|
|
87
|
-
end
|
|
279
|
+
end # def EventMachine::watch_glob
|
|
280
|
+
end # module EventMachine
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
#
|
|
3
|
+
# Sample that uses eventmachine-tail to watch a file or set of files.
|
|
4
|
+
# Basically, this example implements 'tail -f' but can accept globs
|
|
5
|
+
# that are also watched.
|
|
6
|
+
#
|
|
7
|
+
# For example, '/var/log/*.log' will be periodically watched for new
|
|
8
|
+
# matching files which will additionally be watched.
|
|
9
|
+
#
|
|
10
|
+
# Usage example:
|
|
11
|
+
# glob-tail.rb "/var/log/*.log" "/var/log/httpd/*.log"
|
|
12
|
+
#
|
|
13
|
+
# (Important to use quotes or otherwise escape the '*' chars, otherwise
|
|
14
|
+
# your shell will interpret them)
|
|
15
|
+
|
|
16
|
+
require "rubygems"
|
|
17
|
+
require "eventmachine"
|
|
18
|
+
require "eventmachine-tail"
|
|
19
|
+
|
|
20
|
+
class Reader < EventMachine::FileTail
|
|
21
|
+
def initialize(path, startpos=-1)
|
|
22
|
+
super(path, startpos)
|
|
23
|
+
puts "Tailing #{path}"
|
|
24
|
+
@buffer = BufferedTokenizer.new
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def receive_data(data)
|
|
28
|
+
@buffer.extract(data).each do |line|
|
|
29
|
+
puts "#{path}: #{line}"
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def main(args)
|
|
35
|
+
if args.length == 0
|
|
36
|
+
puts "Usage: #{$0} <path_or_glob> [path_or_glob2] [...]"
|
|
37
|
+
return 1
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
EventMachine.run do
|
|
41
|
+
args.each do |path|
|
|
42
|
+
EventMachine::FileGlobWatchTail.new(path, Reader)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end # def main
|
|
46
|
+
|
|
47
|
+
exit(main(ARGV))
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
require "rubygems"
|
|
4
|
+
require "eventmachine"
|
|
5
|
+
require "eventmachine-tail"
|
|
6
|
+
|
|
7
|
+
class Watcher < EventMachine::FileGlobWatch
|
|
8
|
+
def initialize(pathglob, interval=5)
|
|
9
|
+
super(pathglob, interval)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def file_deleted(path)
|
|
13
|
+
puts "Removed: #{path}"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def file_found(path)
|
|
17
|
+
puts "Found: #{path}"
|
|
18
|
+
end
|
|
19
|
+
end # class Watcher
|
|
20
|
+
|
|
21
|
+
EM.run do
|
|
22
|
+
Watcher.new("/var/log/*")
|
|
23
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
#
|
|
3
|
+
# Simple 'tail -f' example. This one uses a block instead of a separate handler
|
|
4
|
+
# class
|
|
5
|
+
# Usage example:
|
|
6
|
+
# tail-with-block.rb /var/log/messages
|
|
7
|
+
|
|
8
|
+
require "rubygems"
|
|
9
|
+
require "eventmachine"
|
|
10
|
+
require "eventmachine-tail"
|
|
11
|
+
|
|
12
|
+
def main(args)
|
|
13
|
+
if args.length == 0
|
|
14
|
+
puts "Usage: #{$0} <path> [path2] [...]"
|
|
15
|
+
return 1
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
EventMachine.run do
|
|
19
|
+
args.each do |path|
|
|
20
|
+
EventMachine::file_tail(path) do |filetail, line|
|
|
21
|
+
# filetail is the 'EventMachine::FileTail' instance for this file.
|
|
22
|
+
# line is the line read from thefile.
|
|
23
|
+
# this block is invoked for every line read.
|
|
24
|
+
puts line
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end # def main
|
|
29
|
+
|
|
30
|
+
exit(main(ARGV))
|
data/samples/tail.rb
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
#
|
|
3
|
+
# Simple 'tail -f' example.
|
|
4
|
+
# Usage example:
|
|
5
|
+
# tail.rb /var/log/messages
|
|
6
|
+
|
|
7
|
+
require "rubygems"
|
|
8
|
+
require "eventmachine"
|
|
9
|
+
require "eventmachine-tail"
|
|
10
|
+
|
|
11
|
+
class Reader < EventMachine::FileTail
|
|
12
|
+
def initialize(path, startpos=-1)
|
|
13
|
+
super(path, startpos)
|
|
14
|
+
puts "Tailing #{path}"
|
|
15
|
+
@buffer = BufferedTokenizer.new
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def receive_data(data)
|
|
19
|
+
@buffer.extract(data).each do |line|
|
|
20
|
+
puts "#{path}: #{line}"
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def main(args)
|
|
26
|
+
if args.length == 0
|
|
27
|
+
puts "Usage: #{$0} <path> [path2] [...]"
|
|
28
|
+
return 1
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
EventMachine.run do
|
|
32
|
+
args.each do |path|
|
|
33
|
+
EventMachine::file_tail(path, Reader)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end # def main
|
|
37
|
+
|
|
38
|
+
exit(main(ARGV))
|
data/test/alltests.rb
ADDED
data/test/logrotate.conf
ADDED