logstash-input-file 4.1.16 → 4.2.2

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 (35) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +20 -0
  3. data/LICENSE +199 -10
  4. data/docs/index.asciidoc +23 -7
  5. data/lib/filewatch/discoverer.rb +9 -8
  6. data/lib/filewatch/observing_base.rb +1 -12
  7. data/lib/filewatch/processor.rb +55 -0
  8. data/lib/filewatch/read_mode/handlers/base.rb +8 -6
  9. data/lib/filewatch/read_mode/handlers/read_file.rb +26 -8
  10. data/lib/filewatch/read_mode/handlers/read_zip_file.rb +63 -34
  11. data/lib/filewatch/read_mode/processor.rb +22 -36
  12. data/lib/filewatch/settings.rb +3 -2
  13. data/lib/filewatch/sincedb_collection.rb +23 -21
  14. data/lib/filewatch/stat/generic.rb +8 -13
  15. data/lib/filewatch/stat/windows_path.rb +7 -9
  16. data/lib/filewatch/tail_mode/handlers/delete.rb +2 -4
  17. data/lib/filewatch/tail_mode/processor.rb +47 -54
  18. data/lib/filewatch/watch.rb +12 -14
  19. data/lib/filewatch/watched_file.rb +25 -14
  20. data/lib/filewatch/watched_files_collection.rb +11 -74
  21. data/lib/jars/filewatch-1.0.1.jar +0 -0
  22. data/lib/logstash/inputs/delete_completed_file_handler.rb +5 -0
  23. data/lib/logstash/inputs/file.rb +32 -11
  24. data/logstash-input-file.gemspec +3 -2
  25. data/spec/filewatch/reading_spec.rb +60 -9
  26. data/spec/filewatch/rotate_spec.rb +2 -1
  27. data/spec/filewatch/settings_spec.rb +3 -0
  28. data/spec/filewatch/spec_helper.rb +13 -15
  29. data/spec/filewatch/tailing_spec.rb +14 -12
  30. data/spec/filewatch/watched_file_spec.rb +30 -0
  31. data/spec/filewatch/watched_files_collection_spec.rb +62 -8
  32. data/spec/helpers/spec_helper.rb +8 -0
  33. data/spec/inputs/file_read_spec.rb +154 -4
  34. data/spec/inputs/file_tail_spec.rb +3 -2
  35. metadata +21 -6
@@ -19,16 +19,21 @@ module FileWatch module ReadMode module Handlers
19
19
  watched_file.listener.eof
20
20
  watched_file.file_close
21
21
  key = watched_file.sincedb_key
22
- sincedb_collection.reading_completed(key)
23
- sincedb_collection.clear_watched_file(key)
22
+ if sincedb_collection.get(key)
23
+ sincedb_collection.reading_completed(key)
24
+ sincedb_collection.clear_watched_file(key)
25
+ end
24
26
  watched_file.listener.deleted
27
+ # NOTE: on top of un-watching we should also remove from the watched files collection
28
+ # if the file is getting deleted (on completion), that part currently resides in
29
+ # DeleteCompletedFileHandler - triggered above using `watched_file.listener.deleted`
25
30
  watched_file.unwatch
26
31
  end
27
32
  end
28
33
  end
29
34
 
30
35
  def controlled_read(watched_file, loop_control)
31
- logger.trace("reading...", "iterations" => loop_control.count, "amount" => loop_control.size, "filename" => watched_file.filename)
36
+ logger.trace? && logger.trace("reading...", :filename => watched_file.filename, :iterations => loop_control.count, :amount => loop_control.size)
32
37
  loop_control.count.times do
33
38
  break if quit?
34
39
  begin
@@ -40,22 +45,35 @@ module FileWatch module ReadMode module Handlers
40
45
  delta = line.bytesize + @settings.delimiter_byte_size
41
46
  sincedb_collection.increment(watched_file.sincedb_key, delta)
42
47
  end
43
- rescue EOFError
44
- logger.error("controlled_read: eof error reading file", "path" => watched_file.path, "error" => e.inspect, "backtrace" => e.backtrace.take(8))
48
+ rescue EOFError => e
49
+ log_error("controlled_read: eof error reading file", watched_file, e)
45
50
  loop_control.flag_read_error
46
51
  break
47
- rescue Errno::EWOULDBLOCK, Errno::EINTR
48
- logger.error("controlled_read: block or interrupt error reading file", "path" => watched_file.path, "error" => e.inspect, "backtrace" => e.backtrace.take(8))
52
+ rescue Errno::EWOULDBLOCK, Errno::EINTR => e
53
+ log_error("controlled_read: block or interrupt error reading file", watched_file, e)
49
54
  watched_file.listener.error
50
55
  loop_control.flag_read_error
51
56
  break
52
57
  rescue => e
53
- logger.error("controlled_read: general error reading file", "path" => watched_file.path, "error" => e.inspect, "backtrace" => e.backtrace.take(8))
58
+ log_error("controlled_read: general error reading file", watched_file, e)
54
59
  watched_file.listener.error
55
60
  loop_control.flag_read_error
56
61
  break
57
62
  end
58
63
  end
59
64
  end
65
+
66
+ def log_error(msg, watched_file, error)
67
+ details = { :path => watched_file.path,
68
+ :exception => error.class,
69
+ :message => error.message,
70
+ :backtrace => error.backtrace }
71
+ if logger.debug?
72
+ details[:file] = watched_file
73
+ else
74
+ details[:backtrace] = details[:backtrace].take(8) if details[:backtrace]
75
+ end
76
+ logger.error(msg, details)
77
+ end
60
78
  end
61
79
  end end end
@@ -1,13 +1,15 @@
1
1
  # encoding: utf-8
2
2
  require 'java'
3
- java_import java.io.InputStream
4
- java_import java.io.InputStreamReader
5
- java_import java.io.FileInputStream
6
- java_import java.io.BufferedReader
7
- java_import java.util.zip.GZIPInputStream
8
- java_import java.util.zip.ZipException
9
3
 
10
4
  module FileWatch module ReadMode module Handlers
5
+
6
+ java_import java.io.InputStream
7
+ java_import java.io.InputStreamReader
8
+ java_import java.io.FileInputStream
9
+ java_import java.io.BufferedReader
10
+ java_import java.util.zip.GZIPInputStream
11
+ java_import java.util.zip.ZipException
12
+
11
13
  class ReadZipFile < Base
12
14
  def handle_specifically(watched_file)
13
15
  add_or_update_sincedb_collection(watched_file) unless sincedb_collection.member?(watched_file.sincedb_key)
@@ -18,34 +20,40 @@ module FileWatch module ReadMode module Handlers
18
20
  # fast forward through the lines until we reach unseen content?
19
21
  # meaning that we can quit in the middle of a zip file
20
22
  key = watched_file.sincedb_key
21
- begin
22
- file_stream = FileInputStream.new(watched_file.path)
23
- gzip_stream = GZIPInputStream.new(file_stream)
24
- decoder = InputStreamReader.new(gzip_stream, "UTF-8")
25
- buffered = BufferedReader.new(decoder)
26
- while (line = buffered.readLine(false))
27
- watched_file.listener.accept(line)
28
- # can't quit, if we did then we would incorrectly write a 'completed' sincedb entry
29
- # what do we do about quit when we have just begun reading the zipped file (e.g. pipeline reloading)
30
- # should we track lines read in the sincedb and
31
- # fast forward through the lines until we reach unseen content?
32
- # meaning that we can quit in the middle of a zip file
33
- end
34
- watched_file.listener.eof
35
- rescue ZipException => e
36
- logger.error("Cannot decompress the gzip file at path: #{watched_file.path}")
37
- watched_file.listener.error
38
- else
39
- sincedb_collection.store_last_read(key, watched_file.last_stat_size)
40
- sincedb_collection.request_disk_flush
41
- watched_file.listener.deleted
23
+
24
+ if @settings.check_archive_validity && corrupted?(watched_file)
42
25
  watched_file.unwatch
43
- ensure
44
- # rescue each close individually so all close attempts are tried
45
- close_and_ignore_ioexception(buffered) unless buffered.nil?
46
- close_and_ignore_ioexception(decoder) unless decoder.nil?
47
- close_and_ignore_ioexception(gzip_stream) unless gzip_stream.nil?
48
- close_and_ignore_ioexception(file_stream) unless file_stream.nil?
26
+ else
27
+ begin
28
+ file_stream = FileInputStream.new(watched_file.path)
29
+ gzip_stream = GZIPInputStream.new(file_stream)
30
+ decoder = InputStreamReader.new(gzip_stream, "UTF-8")
31
+ buffered = BufferedReader.new(decoder)
32
+ while (line = buffered.readLine(false))
33
+ watched_file.listener.accept(line)
34
+ # can't quit, if we did then we would incorrectly write a 'completed' sincedb entry
35
+ # what do we do about quit when we have just begun reading the zipped file (e.g. pipeline reloading)
36
+ # should we track lines read in the sincedb and
37
+ # fast forward through the lines until we reach unseen content?
38
+ # meaning that we can quit in the middle of a zip file
39
+ end
40
+ watched_file.listener.eof
41
+ rescue ZipException => e
42
+ logger.error("Cannot decompress the gzip file at path: #{watched_file.path}", :exception => e.class,
43
+ :message => e.message, :backtrace => e.backtrace)
44
+ watched_file.listener.error
45
+ else
46
+ sincedb_collection.store_last_read(key, watched_file.last_stat_size)
47
+ sincedb_collection.request_disk_flush
48
+ watched_file.listener.deleted
49
+ watched_file.unwatch
50
+ ensure
51
+ # rescue each close individually so all close attempts are tried
52
+ close_and_ignore_ioexception(buffered) unless buffered.nil?
53
+ close_and_ignore_ioexception(decoder) unless decoder.nil?
54
+ close_and_ignore_ioexception(gzip_stream) unless gzip_stream.nil?
55
+ close_and_ignore_ioexception(file_stream) unless file_stream.nil?
56
+ end
49
57
  end
50
58
  sincedb_collection.clear_watched_file(key)
51
59
  end
@@ -56,7 +64,28 @@ module FileWatch module ReadMode module Handlers
56
64
  begin
57
65
  closeable.close
58
66
  rescue Exception => e # IOException can be thrown by any of the Java classes that implement the Closable interface.
59
- logger.warn("Ignoring an IOException when closing an instance of #{closeable.class.name}", "exception" => e)
67
+ logger.warn("Ignoring an IOException when closing an instance of #{closeable.class.name}",
68
+ :exception => e.class, :message => e.message, :backtrace => e.backtrace)
69
+ end
70
+ end
71
+
72
+ def corrupted?(watched_file)
73
+ begin
74
+ file_stream = FileInputStream.new(watched_file.path)
75
+ gzip_stream = GZIPInputStream.new(file_stream)
76
+ buffer = Java::byte[8192].new
77
+ start = Time.new
78
+ until gzip_stream.read(buffer) == -1
79
+ end
80
+ return false
81
+ rescue ZipException => e
82
+ duration = Time.now - start
83
+ logger.warn("Detected corrupted archive #{watched_file.path} file won't be processed", :message => e.message,
84
+ :duration => duration.round(3))
85
+ return true
86
+ ensure
87
+ close_and_ignore_ioexception(gzip_stream) unless gzip_stream.nil?
88
+ close_and_ignore_ioexception(file_stream) unless file_stream.nil?
60
89
  end
61
90
  end
62
91
  end
@@ -1,6 +1,5 @@
1
1
  # encoding: utf-8
2
- require "logstash/util/loggable"
3
-
2
+ require 'filewatch/processor'
4
3
  require_relative "handlers/base"
5
4
  require_relative "handlers/read_file"
6
5
  require_relative "handlers/read_zip_file"
@@ -9,20 +8,7 @@ module FileWatch module ReadMode
9
8
  # Must handle
10
9
  # :read_file
11
10
  # :read_zip_file
12
- class Processor
13
- include LogStash::Util::Loggable
14
-
15
- attr_reader :watch, :deletable_filepaths
16
-
17
- def initialize(settings)
18
- @settings = settings
19
- @deletable_filepaths = []
20
- end
21
-
22
- def add_watch(watch)
23
- @watch = watch
24
- self
25
- end
11
+ class Processor < FileWatch::Processor
26
12
 
27
13
  def initialize_handlers(sincedb_collection, observer)
28
14
  # we deviate from the tail mode handler initialization here
@@ -48,24 +34,23 @@ module FileWatch module ReadMode
48
34
  private
49
35
 
50
36
  def process_watched(watched_files)
51
- logger.trace("Watched processing")
37
+ logger.trace(__method__.to_s)
52
38
  # Handles watched_files in the watched state.
53
39
  # for a slice of them:
54
40
  # move to the active state
55
41
  # should never have been active before
56
42
  # how much of the max active window is available
57
- to_take = @settings.max_active - watched_files.count{|wf| wf.active?}
43
+ to_take = @settings.max_active - watched_files.count { |wf| wf.active? }
58
44
  if to_take > 0
59
- watched_files.select {|wf| wf.watched?}.take(to_take).each do |watched_file|
60
- path = watched_file.path
45
+ watched_files.select(&:watched?).take(to_take).each do |watched_file|
61
46
  begin
62
- watched_file.restat
47
+ restat(watched_file)
63
48
  watched_file.activate
64
49
  rescue Errno::ENOENT
65
- common_deleted_reaction(watched_file, "Watched")
50
+ common_deleted_reaction(watched_file, __method__)
66
51
  next
67
52
  rescue => e
68
- common_error_reaction(path, e, "Watched")
53
+ common_error_reaction(watched_file, e, __method__)
69
54
  next
70
55
  end
71
56
  break if watch.quit?
@@ -74,7 +59,7 @@ module FileWatch module ReadMode
74
59
  now = Time.now.to_i
75
60
  if (now - watch.lastwarn_max_files) > MAX_FILES_WARN_INTERVAL
76
61
  waiting = watched_files.size - @settings.max_active
77
- logger.warn(@settings.max_warn_msg + ", files yet to open: #{waiting}")
62
+ logger.warn("#{@settings.max_warn_msg}, files yet to open: #{waiting}")
78
63
  watch.lastwarn_max_files = now
79
64
  end
80
65
  end
@@ -83,17 +68,18 @@ module FileWatch module ReadMode
83
68
  ## TODO add process_rotation_in_progress
84
69
 
85
70
  def process_active(watched_files)
86
- logger.trace("Active processing")
71
+ logger.trace(__method__.to_s)
87
72
  # Handles watched_files in the active state.
88
- watched_files.select {|wf| wf.active? }.each do |watched_file|
89
- path = watched_file.path
73
+ watched_files.each do |watched_file|
74
+ next unless watched_file.active?
75
+
90
76
  begin
91
- watched_file.restat
77
+ restat(watched_file)
92
78
  rescue Errno::ENOENT
93
- common_deleted_reaction(watched_file, "Active")
79
+ common_deleted_reaction(watched_file, __method__)
94
80
  next
95
81
  rescue => e
96
- common_error_reaction(path, e, "Active")
82
+ common_error_reaction(watched_file, e, __method__)
97
83
  next
98
84
  end
99
85
  break if watch.quit?
@@ -114,19 +100,19 @@ module FileWatch module ReadMode
114
100
  def common_detach_when_allread(watched_file)
115
101
  watched_file.unwatch
116
102
  watched_file.listener.reading_completed
117
- deletable_filepaths << watched_file.path
118
- logger.trace("Whole file read: #{watched_file.path}, removing from collection")
103
+ add_deletable_path watched_file.path
104
+ logger.trace? && logger.trace("whole file read, removing from collection", :path => watched_file.path)
119
105
  end
120
106
 
121
107
  def common_deleted_reaction(watched_file, action)
122
108
  # file has gone away or we can't read it anymore.
123
109
  watched_file.unwatch
124
- deletable_filepaths << watched_file.path
125
- logger.trace("#{action} - stat failed: #{watched_file.path}, removing from collection")
110
+ add_deletable_path watched_file.path
111
+ logger.trace? && logger.trace("#{action} - stat failed, removing from collection", :path => watched_file.path)
126
112
  end
127
113
 
128
- def common_error_reaction(path, error, action)
129
- logger.error("#{action} - other error #{path}: (#{error.message}, #{error.backtrace.take(8).inspect})")
114
+ def common_error_reaction(watched_file, error, action)
115
+ logger.error("#{action} - other error", error_details(error, watched_file))
130
116
  end
131
117
  end
132
118
  end end
@@ -6,9 +6,10 @@ module FileWatch
6
6
  attr_reader :max_active, :max_warn_msg, :lastwarn_max_files
7
7
  attr_reader :sincedb_write_interval, :stat_interval, :discover_interval
8
8
  attr_reader :exclude, :start_new_files_at, :file_chunk_count, :file_chunk_size
9
- attr_reader :sincedb_path, :sincedb_write_interval, :sincedb_expiry_duration
9
+ attr_reader :sincedb_path, :sincedb_expiry_duration
10
10
  attr_reader :file_sort_by, :file_sort_direction
11
11
  attr_reader :exit_after_read
12
+ attr_reader :check_archive_validity
12
13
 
13
14
  def self.from_options(opts)
14
15
  new.add_options(opts)
@@ -40,7 +41,6 @@ module FileWatch
40
41
  @file_chunk_size = @opts[:file_chunk_size]
41
42
  @close_older = @opts[:close_older]
42
43
  @ignore_older = @opts[:ignore_older]
43
- @sincedb_write_interval = @opts[:sincedb_write_interval]
44
44
  @stat_interval = @opts[:stat_interval]
45
45
  @discover_interval = @opts[:discover_interval]
46
46
  @exclude = Array(@opts[:exclude])
@@ -52,6 +52,7 @@ module FileWatch
52
52
  @file_sort_by = @opts[:file_sort_by]
53
53
  @file_sort_direction = @opts[:file_sort_direction]
54
54
  @exit_after_read = @opts[:exit_after_read]
55
+ @check_archive_validity = @opts[:check_archive_validity]
55
56
  self
56
57
  end
57
58
 
@@ -56,12 +56,12 @@ module FileWatch
56
56
  logger.trace("open: count of keys read: #{@sincedb.keys.size}")
57
57
  rescue => e
58
58
  #No existing sincedb to load
59
- logger.trace("open: error: #{path}: #{e.inspect}")
59
+ logger.trace("open: error:", :path => path, :exception => e.class, :message => e.message)
60
60
  end
61
61
  end
62
62
 
63
63
  def associate(watched_file)
64
- logger.trace("associate: finding", "inode" => watched_file.sincedb_key.inode, "path" => watched_file.path)
64
+ logger.trace? && logger.trace("associate: finding", :path => watched_file.path, :inode => watched_file.sincedb_key.inode)
65
65
  sincedb_value = find(watched_file)
66
66
  if sincedb_value.nil?
67
67
  # sincedb has no record of this inode
@@ -71,7 +71,8 @@ module FileWatch
71
71
  logger.trace("associate: unmatched")
72
72
  return true
73
73
  end
74
- logger.trace("associate: found sincedb record", "filename" => watched_file.filename, "sincedb key" => watched_file.sincedb_key,"sincedb_value" => sincedb_value)
74
+ logger.trace? && logger.trace("associate: found sincedb record", :filename => watched_file.filename,
75
+ :sincedb_key => watched_file.sincedb_key, :sincedb_value => sincedb_value)
75
76
  if sincedb_value.watched_file.nil?
76
77
  # not associated
77
78
  if sincedb_value.path_in_sincedb.nil?
@@ -106,7 +107,8 @@ module FileWatch
106
107
  # after the original is deleted
107
108
  # are not yet in the delete phase, let this play out
108
109
  existing_watched_file = sincedb_value.watched_file
109
- logger.trace("----------------- >> associate: the found sincedb_value has a watched_file - this is a rename", "this watched_file details" => watched_file.details, "other watched_file details" => existing_watched_file.details)
110
+ logger.trace? && logger.trace("----------------- >> associate: the found sincedb_value has a watched_file - this is a rename",
111
+ :this_watched_file => watched_file.details, :existing_watched_file => existing_watched_file.details)
110
112
  watched_file.rotation_in_progress
111
113
  true
112
114
  end
@@ -149,8 +151,8 @@ module FileWatch
149
151
  end
150
152
 
151
153
  def watched_file_deleted(watched_file)
152
- return unless member?(watched_file.sincedb_key)
153
- get(watched_file.sincedb_key).unset_watched_file
154
+ value = @sincedb[watched_file.sincedb_key]
155
+ value.unset_watched_file if value
154
156
  end
155
157
 
156
158
  def store_last_read(key, pos)
@@ -178,24 +180,24 @@ module FileWatch
178
180
  get(key).watched_file.nil?
179
181
  end
180
182
 
181
- private
182
-
183
183
  def flush_at_interval
184
- now = Time.now.to_i
185
- delta = now - @sincedb_last_write
184
+ now = Time.now
185
+ delta = now.to_i - @sincedb_last_write
186
186
  if delta >= @settings.sincedb_write_interval
187
187
  logger.debug("writing sincedb (delta since last write = #{delta})")
188
188
  sincedb_write(now)
189
189
  end
190
190
  end
191
191
 
192
+ private
193
+
192
194
  def handle_association(sincedb_value, watched_file)
193
195
  watched_file.update_bytes_read(sincedb_value.position)
194
196
  sincedb_value.set_watched_file(watched_file)
195
197
  watched_file.initial_completed
196
198
  if watched_file.all_read?
197
199
  watched_file.ignore
198
- logger.trace("handle_association fully read, ignoring.....", "watched file" => watched_file.details, "sincedb value" => sincedb_value)
200
+ logger.trace? && logger.trace("handle_association fully read, ignoring.....", :watched_file => watched_file.details, :sincedb_value => sincedb_value)
199
201
  end
200
202
  end
201
203
 
@@ -208,33 +210,33 @@ module FileWatch
208
210
  end
209
211
  end
210
212
 
211
- def sincedb_write(time = Time.now.to_i)
212
- logger.trace("sincedb_write: to: #{path}")
213
+ def sincedb_write(time = Time.now)
214
+ logger.trace("sincedb_write: #{path} (time = #{time})")
213
215
  begin
214
- @write_method.call
216
+ @write_method.call(time)
215
217
  @serializer.expired_keys.each do |key|
216
218
  @sincedb[key].unset_watched_file
217
219
  delete(key)
218
- logger.trace("sincedb_write: cleaned", "key" => "'#{key}'")
220
+ logger.trace? && logger.trace("sincedb_write: cleaned", :key => key)
219
221
  end
220
- @sincedb_last_write = time
222
+ @sincedb_last_write = time.to_i
221
223
  @write_requested = false
222
224
  rescue Errno::EACCES
223
225
  # no file handles free perhaps
224
226
  # maybe it will work next time
225
- logger.trace("sincedb_write: error: #{path}: #{$!}")
227
+ logger.trace("sincedb_write: #{path} error: #{$!}")
226
228
  end
227
229
  end
228
230
 
229
- def atomic_write
231
+ def atomic_write(time)
230
232
  FileHelper.write_atomically(@full_path) do |io|
231
- @serializer.serialize(@sincedb, io)
233
+ @serializer.serialize(@sincedb, io, time.to_f)
232
234
  end
233
235
  end
234
236
 
235
- def non_atomic_write
237
+ def non_atomic_write(time)
236
238
  IO.open(IO.sysopen(@full_path, "w+")) do |io|
237
- @serializer.serialize(@sincedb, io)
239
+ @serializer.serialize(@sincedb, io, time.to_f)
238
240
  end
239
241
  end
240
242
  end
@@ -3,24 +3,19 @@
3
3
  module FileWatch module Stat
4
4
  class Generic
5
5
 
6
- attr_reader :identifier, :inode, :modified_at, :size, :inode_struct
6
+ attr_reader :inode, :modified_at, :size, :inode_struct
7
7
 
8
8
  def initialize(source)
9
- @source = source
10
- @identifier = nil
9
+ @source = source # Pathname
11
10
  restat
12
11
  end
13
12
 
14
- def add_identifier(identifier) self; end
15
-
16
13
  def restat
17
- @inner_stat = @source.stat
18
- @inode = @inner_stat.ino.to_s
19
- @modified_at = @inner_stat.mtime.to_f
20
- @size = @inner_stat.size
21
- @dev_major = @inner_stat.dev_major
22
- @dev_minor = @inner_stat.dev_minor
23
- @inode_struct = InodeStruct.new(@inode, @dev_major, @dev_minor)
14
+ stat = @source.stat
15
+ @inode = stat.ino.to_s
16
+ @modified_at = stat.mtime.to_f
17
+ @size = stat.size
18
+ @inode_struct = InodeStruct.new(@inode, stat.dev_major, stat.dev_minor)
24
19
  end
25
20
 
26
21
  def windows?
@@ -28,7 +23,7 @@ module FileWatch module Stat
28
23
  end
29
24
 
30
25
  def inspect
31
- "<Generic size='#{@size}', modified_at='#{@modified_at}', inode='#{@inode}', inode_struct='#{@inode_struct}'>"
26
+ "<#{self.class.name} size=#{@size}, modified_at=#{@modified_at}, inode='#{@inode}', inode_struct=#{@inode_struct}>"
32
27
  end
33
28
  end
34
29
  end end