logstash-input-file 4.2.1 → 4.3.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a3a31addcd028920e2e1206d56c6fbab1b5492ebfeef0d688a431c88685ec8b9
4
- data.tar.gz: 0c07d4dd1c1c821c5ef9fcbbac68375ba04ea4dd399116c6ce016ad1c102b076
3
+ metadata.gz: 1db5895e894194d27fa746f21e316c9bc8093f32d567023fd087e0be14a20839
4
+ data.tar.gz: 8c98ccd955718ecd48c46809345b665ab4f47a5dc1fe40fccf3dacedb8618283
5
5
  SHA512:
6
- metadata.gz: a1d95cd6b07d0076c3917e816c97ae832d3085b4ccc1029eb3325249a287b3f80e7ed41bfa5870ce9edb5af6ef5c98f8217a918fd0d3088e6ec25f985da1096a
7
- data.tar.gz: 2fafafe12fc3dbab4921029ae02e0e0949f4254f8fc50810de0c80cd49ab4158cacff9028d90c8d34dc28e17eabd7e6bc437ba2f28f0566f8bca9250b5733330
6
+ metadata.gz: 3d7448a61c0b4a732ba1625318db1c0e18a19413f0cd283ca604ca9bd63ecd8af77f10df99e9cba85d4e70abfac493011b2867618b072b02baf656dd85dda18f
7
+ data.tar.gz: fd7a0c304c711d12f2b89f1974a1d15a637fc01f721a7438144125cf3a30b2b0ed3e74b3b522ccefc1203d64daa8277f501238e351235142aa1b16b844ae88a2
data/CHANGELOG.md CHANGED
@@ -1,3 +1,22 @@
1
+ ## 4.3.1
2
+ - Add extra safety to `chown` call in `atomic_write`, avoiding plugin crashes and falling back to a
3
+ `non_atomic_write` in the event of failure [#295](https://github.com/logstash-plugins/logstash-input-file/pull/295)
4
+ - Refactor: unify event updates to happen in one place [#297](https://github.com/logstash-plugins/logstash-input-file/pull/297)
5
+ - Test: Actually retry tests on `RSpec::Expectations::ExpectationNotMetError` and retry instead of relying on timeout
6
+ [#297](https://github.com/logstash-plugins/logstash-input-file/pull/297)
7
+
8
+ ## 4.3.0
9
+ - Add ECS Compatibility Mode [#291](https://github.com/logstash-plugins/logstash-input-file/pull/291)
10
+
11
+ ## 4.2.4
12
+ - Fix: sincedb_write issue on Windows machines [#283](https://github.com/logstash-plugins/logstash-input-file/pull/283)
13
+
14
+ ## 4.2.3
15
+ - Refactor: improve debug logging (log catched exceptions) [#280](https://github.com/logstash-plugins/logstash-input-file/pull/280)
16
+
17
+ ## 4.2.2
18
+ - Fix: sincedb_clean_after not being respected [#276](https://github.com/logstash-plugins/logstash-input-file/pull/276)
19
+
1
20
  ## 4.2.1
2
21
  - Fix: skip sincedb eviction if read mode completion deletes file during flush [#273](https://github.com/logstash-plugins/logstash-input-file/pull/273)
3
22
 
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Logstash Plugin
2
2
  Travis Build
3
- [![Travis Build Status](https://travis-ci.org/logstash-plugins/logstash-input-file.svg)](https://travis-ci.org/logstash-plugins/logstash-input-file)
3
+ [![Travis Build Status](https://travis-ci.com/logstash-plugins/logstash-input-file.svg)](https://travis-ci.com/logstash-plugins/logstash-input-file)
4
4
 
5
5
  This is a plugin for [Logstash](https://github.com/elastic/logstash).
6
6
 
data/docs/index.asciidoc CHANGED
@@ -78,6 +78,21 @@ Read mode also allows for an action to take place after processing the file comp
78
78
  In the past attempts to simulate a Read mode while still assuming infinite streams
79
79
  was not ideal and a dedicated Read mode is an improvement.
80
80
 
81
+ [id="plugins-{type}s-{plugin}-ecs"]
82
+ ==== Compatibility with the Elastic Common Schema (ECS)
83
+
84
+ This plugin adds metadata about event's source, and can be configured to do so
85
+ in an {ecs-ref}[ECS-compatible] way with <<plugins-{type}s-{plugin}-ecs_compatibility>>.
86
+ This metadata is added after the event has been decoded by the appropriate codec,
87
+ and will never overwrite existing values.
88
+
89
+ |========
90
+ | ECS Disabled | ECS v1 | Description
91
+
92
+ | `host` | `[host][name]` | The name of the {ls} host that processed the event
93
+ | `path` | `[log][file][path]` | The full path to the log file from which the event originates
94
+ |========
95
+
81
96
  ==== Tracking of current position in watched files
82
97
 
83
98
  The plugin keeps track of the current position in each file by
@@ -168,6 +183,7 @@ see <<plugins-{type}s-{plugin}-string_duration,string_duration>> for the details
168
183
  | <<plugins-{type}s-{plugin}-close_older>> |<<number,number>> or <<plugins-{type}s-{plugin}-string_duration,string_duration>>|No
169
184
  | <<plugins-{type}s-{plugin}-delimiter>> |<<string,string>>|No
170
185
  | <<plugins-{type}s-{plugin}-discover_interval>> |<<number,number>>|No
186
+ | <<plugins-{type}s-{plugin}-ecs_compatibility>> |<<string,string>>|No
171
187
  | <<plugins-{type}s-{plugin}-exclude>> |<<array,array>>|No
172
188
  | <<plugins-{type}s-{plugin}-exit_after_read>> |<<boolean,boolean>>|No
173
189
  | <<plugins-{type}s-{plugin}-file_chunk_count>> |<<number,number>>|No
@@ -242,6 +258,20 @@ This value is a multiple to `stat_interval`, e.g. if `stat_interval` is "500 ms"
242
258
  files could be discovered every 15 X 500 milliseconds - 7.5 seconds.
243
259
  In practice, this will be the best case because the time taken to read new content needs to be factored in.
244
260
 
261
+ [id="plugins-{type}s-{plugin}-ecs_compatibility"]
262
+ ===== `ecs_compatibility`
263
+
264
+ * Value type is <<string,string>>
265
+ * Supported values are:
266
+ ** `disabled`: sets non-ECS metadata on event (such as top-level `host`, `path`)
267
+ ** `v1`: sets ECS-compatible metadata on event (such as `[host][name]`, `[log][file][path]`)
268
+ * Default value depends on which version of Logstash is running:
269
+ ** When Logstash provides a `pipeline.ecs_compatibility` setting, its value is used as the default
270
+ ** Otherwise, the default value is `disabled`.
271
+
272
+ Controls this plugin's compatibility with the
273
+ {ecs-ref}[Elastic Common Schema (ECS)].
274
+
245
275
  [id="plugins-{type}s-{plugin}-exclude"]
246
276
  ===== `exclude`
247
277
 
@@ -30,6 +30,7 @@ module FileHelper
30
30
  temp_file.binmode
31
31
  return_val = yield temp_file
32
32
  temp_file.close
33
+ new_stat = File.stat(temp_file)
33
34
 
34
35
  # Overwrite original file with temp file
35
36
  File.rename(temp_file.path, file_name)
@@ -37,8 +38,10 @@ module FileHelper
37
38
  # Unable to get permissions of the original file => return
38
39
  return return_val if old_stat.nil?
39
40
 
40
- # Set correct uid/gid on new file
41
- File.chown(old_stat.uid, old_stat.gid, file_name) if old_stat
41
+ # Set correct uid/gid on new file if ownership is different.
42
+ if old_stat && (old_stat.gid != new_stat.gid || old_stat.uid != new_stat.uid)
43
+ File.chown(old_stat.uid, old_stat.gid, file_name) if old_stat
44
+ end
42
45
 
43
46
  return_val
44
47
  end
@@ -83,15 +83,5 @@ module FileWatch
83
83
  # sincedb_write("shutting down")
84
84
  end
85
85
 
86
- # close_file(path) is to be used by external code
87
- # when it knows that it is completely done with a file.
88
- # Other files or folders may still be being watched.
89
- # Caution, once unwatched, a file can't be watched again
90
- # unless a new instance of this class begins watching again.
91
- # The sysadmin should rename, move or delete the file.
92
- def close_file(path)
93
- @watch.unwatch(path)
94
- sincedb_write
95
- end
96
86
  end
97
87
  end
@@ -19,7 +19,7 @@ module FileWatch module ReadMode module Handlers
19
19
  end
20
20
 
21
21
  def handle(watched_file)
22
- logger.trace("handling: #{watched_file.path}")
22
+ logger.trace? && logger.trace("handling:", :path => watched_file.path)
23
23
  unless watched_file.has_listener?
24
24
  watched_file.set_listener(@observer)
25
25
  end
@@ -41,14 +41,14 @@ module FileWatch module ReadMode module Handlers
41
41
  # don't emit this message too often. if a file that we can't
42
42
  # read is changing a lot, we'll try to open it more often, and spam the logs.
43
43
  now = Time.now.to_i
44
- logger.trace("opening OPEN_WARN_INTERVAL is '#{OPEN_WARN_INTERVAL}'")
44
+ logger.trace? && logger.trace("opening OPEN_WARN_INTERVAL is '#{OPEN_WARN_INTERVAL}'")
45
45
  if watched_file.last_open_warning_at.nil? || now - watched_file.last_open_warning_at > OPEN_WARN_INTERVAL
46
46
  backtrace = e.backtrace
47
47
  backtrace = backtrace.take(3) if backtrace && !logger.debug?
48
48
  logger.warn("failed to open", :path => watched_file.path, :exception => e.class, :message => e.message, :backtrace => backtrace)
49
49
  watched_file.last_open_warning_at = now
50
50
  else
51
- logger.trace("suppressed warning (failed to open)", :path => watched_file.path, :exception => e.class, :message => e.message)
51
+ logger.trace? && logger.trace("suppressed warning (failed to open)", :path => watched_file.path, :exception => e.class, :message => e.message)
52
52
  end
53
53
  watched_file.watch # set it back to watch so we can try it again
54
54
  end
@@ -67,8 +67,7 @@ module FileWatch module ReadMode module Handlers
67
67
  elsif sincedb_value.watched_file == watched_file
68
68
  update_existing_sincedb_collection_value(watched_file, sincedb_value)
69
69
  else
70
- msg = "add_or_update_sincedb_collection: the found sincedb_value has a watched_file - this is a rename, switching inode to this watched file"
71
- logger.trace(msg)
70
+ logger.trace? && logger.trace("add_or_update_sincedb_collection: the found sincedb_value has a watched_file - this is a rename, switching inode to this watched file")
72
71
  existing_watched_file = sincedb_value.watched_file
73
72
  if existing_watched_file.nil?
74
73
  sincedb_value.set_watched_file(watched_file)
@@ -77,7 +76,7 @@ module FileWatch module ReadMode module Handlers
77
76
  watched_file.update_bytes_read(sincedb_value.position)
78
77
  else
79
78
  sincedb_value.set_watched_file(watched_file)
80
- logger.trace("add_or_update_sincedb_collection: switching from", :watched_file => watched_file.details)
79
+ logger.trace? && logger.trace("add_or_update_sincedb_collection: switching from", :watched_file => watched_file.details)
81
80
  watched_file.rotate_from(existing_watched_file)
82
81
  end
83
82
 
@@ -86,7 +85,7 @@ module FileWatch module ReadMode module Handlers
86
85
  end
87
86
 
88
87
  def update_existing_sincedb_collection_value(watched_file, sincedb_value)
89
- logger.trace("update_existing_sincedb_collection_value: #{watched_file.path}, last value #{sincedb_value.position}, cur size #{watched_file.last_stat_size}")
88
+ logger.trace? && logger.trace("update_existing_sincedb_collection_value: #{watched_file.path}, last value #{sincedb_value.position}, cur size #{watched_file.last_stat_size}")
90
89
  # sincedb_value is the source of truth
91
90
  watched_file.update_bytes_read(sincedb_value.position)
92
91
  end
@@ -94,7 +93,7 @@ module FileWatch module ReadMode module Handlers
94
93
  def add_new_value_sincedb_collection(watched_file)
95
94
  sincedb_value = SincedbValue.new(0)
96
95
  sincedb_value.set_watched_file(watched_file)
97
- logger.trace("add_new_value_sincedb_collection:", :path => watched_file.path, :position => sincedb_value.position)
96
+ logger.trace? && logger.trace("add_new_value_sincedb_collection:", :path => watched_file.path, :position => sincedb_value.position)
98
97
  sincedb_collection.set(watched_file.sincedb_key, sincedb_value)
99
98
  end
100
99
  end
@@ -47,16 +47,16 @@ module FileWatch
47
47
  @time_sdb_opened = Time.now.to_f
48
48
  begin
49
49
  path.open do |file|
50
- logger.trace("open: reading from #{path}")
50
+ logger.debug("open: reading from #{path}")
51
51
  @serializer.deserialize(file) do |key, value|
52
- logger.trace("open: importing ... '#{key}' => '#{value}'")
52
+ logger.trace? && logger.trace("open: importing #{key.inspect} => #{value.inspect}")
53
53
  set_key_value(key, value)
54
54
  end
55
55
  end
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 => path, :exception => e.class, :message => e.message)
59
+ logger.debug("open: error opening #{path}", :exception => e.class, :message => e.message)
60
60
  end
61
61
  end
62
62
 
@@ -68,35 +68,32 @@ module FileWatch
68
68
  # and due to the window handling of many files
69
69
  # this file may not be opened in this session.
70
70
  # a new value will be added when the file is opened
71
- logger.trace("associate: unmatched")
71
+ logger.trace("associate: unmatched", :filename => watched_file.filename)
72
72
  return true
73
73
  end
74
74
  logger.trace? && logger.trace("associate: found sincedb record", :filename => watched_file.filename,
75
75
  :sincedb_key => watched_file.sincedb_key, :sincedb_value => sincedb_value)
76
- if sincedb_value.watched_file.nil?
77
- # not associated
76
+ if sincedb_value.watched_file.nil? # not associated
78
77
  if sincedb_value.path_in_sincedb.nil?
79
78
  handle_association(sincedb_value, watched_file)
80
- logger.trace("associate: inode matched but no path in sincedb")
79
+ logger.trace? && logger.trace("associate: inode matched but no path in sincedb", :filename => watched_file.filename)
81
80
  return true
82
81
  end
83
82
  if sincedb_value.path_in_sincedb == watched_file.path
84
- # the path on disk is the same as discovered path
85
- # and the inode is the same.
83
+ # the path on disk is the same as discovered path and the inode is the same.
86
84
  handle_association(sincedb_value, watched_file)
87
- logger.trace("associate: inode and path matched")
85
+ logger.trace? && logger.trace("associate: inode and path matched", :filename => watched_file.filename)
88
86
  return true
89
87
  end
90
- # the path on disk is different from discovered unassociated path
91
- # but they have the same key (inode)
88
+ # the path on disk is different from discovered unassociated path but they have the same key (inode)
92
89
  # treat as a new file, a new value will be added when the file is opened
93
90
  sincedb_value.clear_watched_file
94
91
  delete(watched_file.sincedb_key)
95
- logger.trace("associate: matched but allocated to another")
92
+ logger.trace? && logger.trace("associate: matched but allocated to another", :filename => watched_file.filename)
96
93
  return true
97
94
  end
98
95
  if sincedb_value.watched_file.equal?(watched_file) # pointer equals
99
- logger.trace("associate: already associated")
96
+ logger.trace? && logger.trace("associate: already associated", :filename => watched_file.filename)
100
97
  return true
101
98
  end
102
99
  # sincedb_value.watched_file is not this discovered watched_file but they have the same key (inode)
@@ -107,7 +104,7 @@ module FileWatch
107
104
  # after the original is deleted
108
105
  # are not yet in the delete phase, let this play out
109
106
  existing_watched_file = sincedb_value.watched_file
110
- logger.trace? && logger.trace("----------------- >> associate: the found sincedb_value has a watched_file - this is a rename",
107
+ logger.trace? && logger.trace("associate: found sincedb_value has a watched_file - this is a rename",
111
108
  :this_watched_file => watched_file.details, :existing_watched_file => existing_watched_file.details)
112
109
  watched_file.rotation_in_progress
113
110
  true
@@ -180,63 +177,74 @@ module FileWatch
180
177
  get(key).watched_file.nil?
181
178
  end
182
179
 
183
- private
184
-
185
180
  def flush_at_interval
186
- now = Time.now.to_i
187
- delta = now - @sincedb_last_write
181
+ now = Time.now
182
+ delta = now.to_i - @sincedb_last_write
188
183
  if delta >= @settings.sincedb_write_interval
189
184
  logger.debug("writing sincedb (delta since last write = #{delta})")
190
185
  sincedb_write(now)
191
186
  end
192
187
  end
193
188
 
189
+ private
190
+
194
191
  def handle_association(sincedb_value, watched_file)
195
192
  watched_file.update_bytes_read(sincedb_value.position)
196
193
  sincedb_value.set_watched_file(watched_file)
197
194
  watched_file.initial_completed
198
195
  if watched_file.all_read?
199
196
  watched_file.ignore
200
- logger.trace? && logger.trace("handle_association fully read, ignoring.....", :watched_file => watched_file.details, :sincedb_value => sincedb_value)
197
+ logger.trace? && logger.trace("handle_association fully read, ignoring", :watched_file => watched_file.details, :sincedb_value => sincedb_value)
201
198
  end
202
199
  end
203
200
 
204
201
  def set_key_value(key, value)
205
202
  if @time_sdb_opened < value.last_changed_at_expires(@settings.sincedb_expiry_duration)
206
- logger.trace("open: setting #{key.inspect} to #{value.inspect}")
207
203
  set(key, value)
208
204
  else
209
- logger.trace("open: record has expired, skipping: #{key.inspect} #{value.inspect}")
205
+ logger.debug("set_key_value: record has expired, skipping: #{key.inspect} => #{value.inspect}")
210
206
  end
211
207
  end
212
208
 
213
- def sincedb_write(time = Time.now.to_i)
214
- logger.trace("sincedb_write: to: #{path}")
209
+ def sincedb_write(time = Time.now)
210
+ logger.trace? && logger.trace("sincedb_write: #{path} (time = #{time})")
215
211
  begin
216
- @write_method.call
217
- @serializer.expired_keys.each do |key|
212
+ expired_keys = @write_method.call(time)
213
+ expired_keys.each do |key|
218
214
  @sincedb[key].unset_watched_file
219
215
  delete(key)
220
216
  logger.trace? && logger.trace("sincedb_write: cleaned", :key => key)
221
217
  end
222
- @sincedb_last_write = time
218
+ @sincedb_last_write = time.to_i
223
219
  @write_requested = false
224
- rescue Errno::EACCES
225
- # no file handles free perhaps
226
- # maybe it will work next time
227
- logger.trace("sincedb_write: error: #{path}: #{$!}")
220
+ rescue Errno::EACCES => e
221
+ # no file handles free perhaps - maybe it will work next time
222
+ logger.debug("sincedb_write: #{path} error:", :exception => e.class, :message => e.message)
228
223
  end
229
224
  end
230
225
 
231
- def atomic_write
232
- FileHelper.write_atomically(@full_path) do |io|
233
- @serializer.serialize(@sincedb, io)
226
+ # @return expired keys
227
+ def atomic_write(time)
228
+ logger.trace? && logger.trace("non_atomic_write: ", :time => time)
229
+ begin
230
+ FileHelper.write_atomically(@full_path) do |io|
231
+ @serializer.serialize(@sincedb, io, time.to_f)
232
+ end
233
+ rescue Errno::EPERM, Errno::EACCES => e
234
+ logger.warn("sincedb_write: unable to write atomically due to permissions error, falling back to non-atomic write: #{path} error:", :exception => e.class, :message => e.message)
235
+ @write_method = method(:non_atomic_write)
236
+ non_atomic_write(time)
237
+ rescue => e
238
+ logger.warn("sincedb_write: unable to write atomically, attempting non-atomic write: #{path} error:", :exception => e.class, :message => e.message)
239
+ non_atomic_write(time)
234
240
  end
235
241
  end
236
242
 
237
- def non_atomic_write
238
- IO.open(IO.sysopen(@full_path, "w+")) do |io|
239
- @serializer.serialize(@sincedb, io)
243
+ # @return expired keys
244
+ def non_atomic_write(time)
245
+ logger.trace? && logger.trace("non_atomic_write: ", :time => time)
246
+ File.open(@full_path, "w+") do |io|
247
+ @serializer.serialize(@sincedb, io, time.to_f)
240
248
  end
241
249
  end
242
250
  end
@@ -3,30 +3,25 @@
3
3
  module FileWatch
4
4
  class SincedbRecordSerializer
5
5
 
6
- attr_reader :expired_keys
7
-
8
6
  def self.days_to_seconds(days)
9
7
  (24 * 3600) * days.to_f
10
8
  end
11
9
 
12
10
  def initialize(sincedb_value_expiry)
13
11
  @sincedb_value_expiry = sincedb_value_expiry
14
- @expired_keys = []
15
- end
16
-
17
- def update_sincedb_value_expiry_from_days(days)
18
- @sincedb_value_expiry = SincedbRecordSerializer.days_to_seconds(days)
19
12
  end
20
13
 
14
+ # @return Array expired keys (ones that were not written to the file)
21
15
  def serialize(db, io, as_of = Time.now.to_f)
22
- @expired_keys.clear
16
+ expired_keys = []
23
17
  db.each do |key, value|
24
18
  if as_of > value.last_changed_at_expires(@sincedb_value_expiry)
25
- @expired_keys << key
19
+ expired_keys << key
26
20
  next
27
21
  end
28
22
  io.write(serialize_record(key, value))
29
23
  end
24
+ expired_keys
30
25
  end
31
26
 
32
27
  def deserialize(io)
@@ -36,8 +31,7 @@ module FileWatch
36
31
  end
37
32
 
38
33
  def serialize_record(k, v)
39
- # effectively InodeStruct#to_s SincedbValue#to_s
40
- "#{k} #{v}\n"
34
+ "#{k} #{v}\n" # effectively InodeStruct#to_s SincedbValue#to_s
41
35
  end
42
36
 
43
37
  def deserialize_record(record)
@@ -18,7 +18,7 @@ module FileWatch module TailMode module Handlers
18
18
  end
19
19
 
20
20
  def handle(watched_file)
21
- logger.trace("handling: #{watched_file.filename}")
21
+ logger.trace? && logger.trace("handling:", :path => watched_file.path)
22
22
  unless watched_file.has_listener?
23
23
  watched_file.set_listener(@observer)
24
24
  end
@@ -37,7 +37,7 @@ module FileWatch module TailMode module Handlers
37
37
 
38
38
  def controlled_read(watched_file, loop_control)
39
39
  changed = false
40
- logger.trace("reading...", "iterations" => loop_control.count, "amount" => loop_control.size, "filename" => watched_file.filename)
40
+ logger.trace? && logger.trace(__method__.to_s, :iterations => loop_control.count, :amount => loop_control.size, :filename => watched_file.filename)
41
41
  # from a real config (has 102 file inputs)
42
42
  # -- This cfg creates a file input for every log file to create a dedicated file pointer and read all file simultaneously
43
43
  # -- If we put all log files in one file input glob we will have indexing delay, because Logstash waits until the first file becomes EOF
@@ -48,7 +48,7 @@ module FileWatch module TailMode module Handlers
48
48
  loop_control.count.times do
49
49
  break if quit?
50
50
  begin
51
- logger.debug("read_to_eof: get chunk")
51
+ logger.debug? && logger.debug("#{__method__} get chunk")
52
52
  result = watched_file.read_extract_lines(loop_control.size) # expect BufferExtractResult
53
53
  logger.trace(result.warning, result.additional) unless result.warning.empty?
54
54
  changed = true
@@ -57,40 +57,42 @@ module FileWatch module TailMode module Handlers
57
57
  # sincedb position is now independent from the watched_file bytes_read
58
58
  sincedb_collection.increment(watched_file.sincedb_key, line.bytesize + @settings.delimiter_byte_size)
59
59
  end
60
- rescue EOFError
60
+ rescue EOFError => e
61
61
  # it only makes sense to signal EOF in "read" mode not "tail"
62
+ logger.debug(__method__.to_s, exception_details(watched_file.path, e, false))
62
63
  loop_control.flag_read_error
63
64
  break
64
- rescue Errno::EWOULDBLOCK, Errno::EINTR
65
+ rescue Errno::EWOULDBLOCK, Errno::EINTR => e
66
+ logger.debug(__method__.to_s, exception_details(watched_file.path, e, false))
65
67
  watched_file.listener.error
66
68
  loop_control.flag_read_error
67
69
  break
68
70
  rescue => e
69
- logger.error("read_to_eof: general error reading #{watched_file.path}", "error" => e.inspect, "backtrace" => e.backtrace.take(4))
71
+ logger.error("#{__method__} general error reading", exception_details(watched_file.path, e))
70
72
  watched_file.listener.error
71
73
  loop_control.flag_read_error
72
74
  break
73
75
  end
74
76
  end
75
- logger.debug("read_to_eof: exit due to quit") if quit?
77
+ logger.debug("#{__method__} stopped loop due quit") if quit?
76
78
  sincedb_collection.request_disk_flush if changed
77
79
  end
78
80
 
79
81
  def open_file(watched_file)
80
82
  return true if watched_file.file_open?
81
- logger.trace("opening #{watched_file.filename}")
83
+ logger.trace? && logger.trace("open_file", :filename => watched_file.filename)
82
84
  begin
83
85
  watched_file.open
84
- rescue
86
+ rescue => e
85
87
  # don't emit this message too often. if a file that we can't
86
88
  # read is changing a lot, we'll try to open it more often, and spam the logs.
87
89
  now = Time.now.to_i
88
- logger.trace("open_file OPEN_WARN_INTERVAL is '#{OPEN_WARN_INTERVAL}'")
90
+ logger.trace? && logger.trace("open_file OPEN_WARN_INTERVAL is '#{OPEN_WARN_INTERVAL}'")
89
91
  if watched_file.last_open_warning_at.nil? || now - watched_file.last_open_warning_at > OPEN_WARN_INTERVAL
90
- logger.warn("failed to open #{watched_file.path}: #{$!.inspect}, #{$!.backtrace.take(3)}")
92
+ logger.warn("failed to open file", exception_details(watched_file.path, e))
91
93
  watched_file.last_open_warning_at = now
92
94
  else
93
- logger.trace("suppressed warning for `failed to open` #{watched_file.path}: #{$!.inspect}")
95
+ logger.debug("open_file suppressed warning `failed to open file`", exception_details(watched_file.path, e, false))
94
96
  end
95
97
  watched_file.watch # set it back to watch so we can try it again
96
98
  else
@@ -108,26 +110,22 @@ module FileWatch module TailMode module Handlers
108
110
  update_existing_sincedb_collection_value(watched_file, sincedb_value)
109
111
  watched_file.initial_completed
110
112
  else
111
- msg = "add_or_update_sincedb_collection: found sincedb record"
112
- logger.trace(msg,
113
- "sincedb key" => watched_file.sincedb_key,
114
- "sincedb value" => sincedb_value
115
- )
113
+ logger.trace? && logger.trace("add_or_update_sincedb_collection: found sincedb record",
114
+ :sincedb_key => watched_file.sincedb_key, :sincedb_value => sincedb_value)
116
115
  # detected a rotation, Discoverer can't handle this because this watched file is not a new discovery.
117
116
  # we must handle it here, by transferring state and have the sincedb value track this watched file
118
117
  # rotate_as_file and rotate_from will switch the sincedb key to the inode that the path is now pointing to
119
118
  # and pickup the sincedb_value from before.
120
- msg = "add_or_update_sincedb_collection: the found sincedb_value has a watched_file - this is a rename, switching inode to this watched file"
121
- logger.trace(msg)
119
+ logger.debug("add_or_update_sincedb_collection: the found sincedb_value has a watched_file - this is a rename, switching inode to this watched file")
122
120
  existing_watched_file = sincedb_value.watched_file
123
121
  if existing_watched_file.nil?
124
122
  sincedb_value.set_watched_file(watched_file)
125
- logger.trace("add_or_update_sincedb_collection: switching as new file")
123
+ logger.trace? && logger.trace("add_or_update_sincedb_collection: switching as new file")
126
124
  watched_file.rotate_as_file
127
125
  watched_file.update_bytes_read(sincedb_value.position)
128
126
  else
129
127
  sincedb_value.set_watched_file(watched_file)
130
- logger.trace("add_or_update_sincedb_collection: switching from...", "watched_file details" => watched_file.details)
128
+ logger.trace? && logger.trace("add_or_update_sincedb_collection: switching from:", :watched_file => watched_file.details)
131
129
  watched_file.rotate_from(existing_watched_file)
132
130
  end
133
131
  end
@@ -135,13 +133,15 @@ module FileWatch module TailMode module Handlers
135
133
  end
136
134
 
137
135
  def update_existing_sincedb_collection_value(watched_file, sincedb_value)
138
- logger.trace("update_existing_sincedb_collection_value: #{watched_file.filename}, last value #{sincedb_value.position}, cur size #{watched_file.last_stat_size}")
136
+ logger.trace? && logger.trace("update_existing_sincedb_collection_value", :position => sincedb_value.position,
137
+ :filename => watched_file.filename, :last_stat_size => watched_file.last_stat_size)
139
138
  update_existing_specifically(watched_file, sincedb_value)
140
139
  end
141
140
 
142
141
  def add_new_value_sincedb_collection(watched_file)
143
142
  sincedb_value = get_new_value_specifically(watched_file)
144
- logger.trace("add_new_value_sincedb_collection", "position" => sincedb_value.position, "watched_file details" => watched_file.details)
143
+ logger.trace? && logger.trace("add_new_value_sincedb_collection", :position => sincedb_value.position,
144
+ :watched_file => watched_file.details)
145
145
  sincedb_collection.set(watched_file.sincedb_key, sincedb_value)
146
146
  sincedb_value
147
147
  end
@@ -153,5 +153,14 @@ module FileWatch module TailMode module Handlers
153
153
  watched_file.update_bytes_read(position)
154
154
  value
155
155
  end
156
+
157
+ private
158
+
159
+ def exception_details(path, e, trace = true)
160
+ details = { :path => path, :exception => e.class, :message => e.message }
161
+ details[:backtrace] = e.backtrace if trace && logger.debug?
162
+ details
163
+ end
164
+
156
165
  end
157
166
  end end end
@@ -7,7 +7,7 @@ module FileWatch module TailMode module Handlers
7
7
  # TODO consider trying to find the renamed file - it will have the same inode.
8
8
  # Needs a rotate scheme rename hint from user e.g. "<name>-YYYY-MM-DD-N.<ext>" or "<name>.<ext>.N"
9
9
  # send the found content to the same listener (stream identity)
10
- logger.trace("delete", :path => watched_file.path, :watched_file => watched_file.details)
10
+ logger.trace? && logger.trace(__method__.to_s, :path => watched_file.path, :watched_file => watched_file.details)
11
11
  if watched_file.bytes_unread > 0
12
12
  logger.warn(DATA_LOSS_WARNING, :path => watched_file.path, :unread_bytes => watched_file.bytes_unread)
13
13
  end
@@ -14,11 +14,10 @@ module FileWatch module TailMode module Handlers
14
14
  end
15
15
 
16
16
  def update_existing_specifically(watched_file, sincedb_value)
17
- # we have a match but size is smaller
18
- # set all to zero
17
+ # we have a match but size is smaller - set all to zero
19
18
  watched_file.reset_bytes_unread
20
19
  sincedb_value.update_position(0)
21
- logger.trace("update_existing_specifically: was truncated seeking to beginning", "watched file" => watched_file.details, "sincedb value" => sincedb_value)
20
+ logger.trace? && logger.trace("update_existing_specifically: was truncated seeking to beginning", :watched_file => watched_file.details, :sincedb_value => sincedb_value)
22
21
  end
23
22
  end
24
23
  end end end
@@ -13,9 +13,9 @@ module FileWatch module TailMode module Handlers
13
13
  # for file initially ignored their bytes_read was set to stat.size
14
14
  # use this value not the `start_new_files_at` for the position
15
15
  # logger.trace("get_new_value_specifically", "watched_file" => watched_file.inspect)
16
- SincedbValue.new(watched_file.bytes_read).tap do |val|
17
- val.set_watched_file(watched_file)
18
- logger.trace("-------------------- >>>>> get_new_value_specifically: unignore", "watched file" => watched_file.details, "sincedb value" => val)
16
+ SincedbValue.new(watched_file.bytes_read).tap do |sincedb_value|
17
+ sincedb_value.set_watched_file(watched_file)
18
+ logger.trace? && logger.trace("get_new_value_specifically: unignore", :watched_file => watched_file.details, :sincedb_value => sincedb_value)
19
19
  end
20
20
  end
21
21
 
@@ -26,7 +26,7 @@ module FileWatch module TailMode module Handlers
26
26
  # we will handle grow or shrink
27
27
  # for now we seek to where we were before the file got ignored (grow)
28
28
  # or to the start (shrink)
29
- logger.trace("-------------------- >>>>> update_existing_specifically: unignore", "watched file" => watched_file.details, "sincedb value" => sincedb_value)
29
+ logger.trace? && logger.trace("update_existing_specifically: unignore", :watched_file => watched_file.details, :sincedb_value => sincedb_value)
30
30
  position = 0
31
31
  if watched_file.shrunk?
32
32
  watched_file.update_bytes_read(0)
@@ -51,7 +51,10 @@ module FileWatch
51
51
  glob = 0
52
52
  end
53
53
  break if quit?
54
+ # NOTE: maybe the plugin should validate stat_interval <= sincedb_write_interval <= sincedb_clean_after
54
55
  sleep(@settings.stat_interval)
56
+ # we need to check potential expired keys (sincedb_clean_after) periodically
57
+ sincedb_collection.flush_at_interval
55
58
  end
56
59
  sincedb_collection.write_if_requested # does nothing if no requests to write were lodged.
57
60
  @watched_files_collection.close_all
Binary file
@@ -2,6 +2,7 @@
2
2
  require "logstash/namespace"
3
3
  require "logstash/inputs/base"
4
4
  require "logstash/codecs/identity_map_codec"
5
+ require 'logstash/plugin_mixins/ecs_compatibility_support'
5
6
 
6
7
  require "pathname"
7
8
  require "socket" # for Socket.gethostname
@@ -88,6 +89,8 @@ module LogStash module Inputs
88
89
  class File < LogStash::Inputs::Base
89
90
  config_name "file"
90
91
 
92
+ include PluginMixins::ECSCompatibilitySupport(:disabled, :v1)
93
+
91
94
  # The path(s) to the file(s) to use as an input.
92
95
  # You can use filename patterns here, such as `/var/log/*.log`.
93
96
  # If you use a pattern like `/var/log/**/*.log`, a recursive search
@@ -325,6 +328,9 @@ class File < LogStash::Inputs::Base
325
328
  @codec = LogStash::Codecs::IdentityMapCodec.new(@codec)
326
329
  @completely_stopped = Concurrent::AtomicBoolean.new
327
330
  @queue = Concurrent::AtomicReference.new
331
+
332
+ @source_host_field = ecs_select[disabled: 'host', v1:'[host][name]']
333
+ @source_path_field = ecs_select[disabled: 'path', v1:'[log][file][path]']
328
334
  end # def register
329
335
 
330
336
  def completely_stopped?
@@ -367,9 +373,12 @@ class File < LogStash::Inputs::Base
367
373
  @completely_stopped.make_true
368
374
  end # def run
369
375
 
370
- def post_process_this(event)
376
+ def post_process_this(event, path)
377
+ event.set("[@metadata][path]", path)
371
378
  event.set("[@metadata][host]", @host)
372
- event.set("host", @host) unless event.include?("host")
379
+ attempt_set(event, @source_host_field, @host)
380
+ attempt_set(event, @source_path_field, path) if path
381
+
373
382
  decorate(event)
374
383
  @queue.get << event
375
384
  end
@@ -377,12 +386,12 @@ class File < LogStash::Inputs::Base
377
386
  def handle_deletable_path(path)
378
387
  return if tail_mode?
379
388
  return if @completed_file_handlers.empty?
389
+ @logger.debug? && @logger.debug(__method__.to_s, :path => path)
380
390
  @completed_file_handlers.each { |handler| handler.handle(path) }
381
391
  end
382
392
 
383
393
  def log_line_received(path, line)
384
- return unless @logger.debug?
385
- @logger.debug("Received line", :path => path, :text => line)
394
+ @logger.debug? && @logger.debug("Received line", :path => path, :text => line)
386
395
  end
387
396
 
388
397
  def stop
@@ -407,6 +416,17 @@ class File < LogStash::Inputs::Base
407
416
  end
408
417
  end
409
418
 
419
+ # Attempt to set an event's field to the provided value
420
+ # without overwriting an existing value or producing an error
421
+ def attempt_set(event, field_reference, value)
422
+ return false if event.include?(field_reference)
423
+
424
+ event.set(field_reference, value)
425
+ rescue => e
426
+ logger.trace("failed to set #{field_reference} to `#{value}`", :exception => e.message)
427
+ false
428
+ end
429
+
410
430
  def build_sincedb_base_from_env
411
431
  # This section is going to be deprecated eventually, as path.data will be
412
432
  # the default, not an environment variable (SINCEDB_DIR or LOGSTASH_HOME)
@@ -7,9 +7,9 @@ module LogStash module Inputs
7
7
  class FileListener
8
8
  attr_reader :input, :path, :data
9
9
  # construct with link back to the input plugin instance.
10
- def initialize(path, input)
10
+ def initialize(path, input, data = nil)
11
11
  @path, @input = path, input
12
- @data = nil
12
+ @data = data
13
13
  end
14
14
 
15
15
  def opened
@@ -36,26 +36,13 @@ module LogStash module Inputs
36
36
  def accept(data)
37
37
  # and push transient data filled dup listener downstream
38
38
  input.log_line_received(path, data)
39
- input.codec.accept(dup_adding_state(data))
39
+ input.codec.accept(self.class.new(path, input, data))
40
40
  end
41
41
 
42
42
  def process_event(event)
43
- event.set("[@metadata][path]", path)
44
- event.set("path", path) unless event.include?("path")
45
- input.post_process_this(event)
46
- end
47
-
48
- def add_state(data)
49
- @data = data
50
- self
43
+ input.post_process_this(event, path)
51
44
  end
52
45
 
53
- private
54
-
55
- # duplicate and add state for downstream
56
- def dup_adding_state(line)
57
- self.class.new(path, input).add_state(line)
58
- end
59
46
  end
60
47
 
61
48
  class FlushableListener < FileListener
@@ -1,7 +1,7 @@
1
1
  Gem::Specification.new do |s|
2
2
 
3
3
  s.name = 'logstash-input-file'
4
- s.version = '4.2.1'
4
+ s.version = '4.3.1'
5
5
  s.licenses = ['Apache-2.0']
6
6
  s.summary = "Streams events from files"
7
7
  s.description = "This gem is a Logstash plugin required to be installed on top of the Logstash core pipeline using $LS_HOME/bin/logstash-plugin install gemname. This gem is not a stand-alone program"
@@ -33,6 +33,7 @@ Gem::Specification.new do |s|
33
33
 
34
34
  s.add_runtime_dependency 'concurrent-ruby', '~> 1.0'
35
35
  s.add_runtime_dependency 'logstash-codec-multiline', ['~> 3.0']
36
+ s.add_runtime_dependency 'logstash-mixin-ecs_compatibility_support', '~>1.1'
36
37
 
37
38
  s.add_development_dependency 'stud', ['~> 0.0.19']
38
39
  s.add_development_dependency 'logstash-devutils'
@@ -91,7 +91,7 @@ module FileWatch
91
91
  context "when watching a directory with files using striped reading" do
92
92
  let(:file_path2) { ::File.join(directory, "2.log") }
93
93
  # use a chunk size that does not align with the line boundaries
94
- let(:opts) { super.merge(:file_chunk_size => 10, :file_chunk_count => 1, :file_sort_by => "path")}
94
+ let(:opts) { super().merge(:file_chunk_size => 10, :file_chunk_count => 1, :file_sort_by => "path")}
95
95
  let(:lines) { [] }
96
96
  let(:observer) { TestObserver.new(lines) }
97
97
  let(:listener2) { observer.listener_for(file_path2) }
@@ -121,7 +121,7 @@ module FileWatch
121
121
  end
122
122
 
123
123
  context "when a non default delimiter is specified and it is not in the content" do
124
- let(:opts) { super.merge(:delimiter => "\nø") }
124
+ let(:opts) { super().merge(:delimiter => "\nø") }
125
125
  let(:actions) do
126
126
  RSpec::Sequencing.run("create file") do
127
127
  File.open(file_path, "wb") { |file| file.write("line1\nline2") }
@@ -154,7 +154,7 @@ module FileWatch
154
154
  let(:file_path2) { ::File.join(directory, "2.log") }
155
155
  let(:file_path3) { ::File.join(directory, "3.log") }
156
156
 
157
- let(:opts) { super.merge(:file_sort_by => "last_modified") }
157
+ let(:opts) { super().merge(:file_sort_by => "last_modified") }
158
158
  let(:lines) { [] }
159
159
  let(:observer) { TestObserver.new(lines) }
160
160
 
@@ -195,7 +195,7 @@ module FileWatch
195
195
  end
196
196
 
197
197
  context "when watching a directory with files using exit_after_read" do
198
- let(:opts) { super.merge(:exit_after_read => true, :max_open_files => 2) }
198
+ let(:opts) { super().merge(:exit_after_read => true, :max_open_files => 2) }
199
199
  let(:file_path3) { ::File.join(directory, "3.log") }
200
200
  let(:file_path4) { ::File.join(directory, "4.log") }
201
201
  let(:file_path5) { ::File.join(directory, "5.log") }
@@ -219,7 +219,7 @@ module FileWatch
219
219
  end
220
220
 
221
221
  context "create + rename rotation: when a new logfile is renamed to a path we have seen before but not all content from the previous the file is read" do
222
- let(:opts) { super.merge(
222
+ let(:opts) { super().merge(
223
223
  :file_chunk_size => line1.bytesize.succ,
224
224
  :file_chunk_count => 1
225
225
  ) }
@@ -296,7 +296,7 @@ module FileWatch
296
296
  end
297
297
 
298
298
  context "copy + truncate rotation: when a logfile is copied to a new path and truncated before the open file is fully read" do
299
- let(:opts) { super.merge(
299
+ let(:opts) { super().merge(
300
300
  :file_chunk_size => line1.bytesize.succ,
301
301
  :file_chunk_count => 1
302
302
  ) }
@@ -370,7 +370,7 @@ module FileWatch
370
370
  end
371
371
 
372
372
  context "? rotation: when an active file is renamed inside the glob and the reading lags behind" do
373
- let(:opts) { super.merge(
373
+ let(:opts) { super().merge(
374
374
  :file_chunk_size => line1.bytesize.succ,
375
375
  :file_chunk_count => 2
376
376
  ) }
@@ -409,7 +409,7 @@ module FileWatch
409
409
  end
410
410
 
411
411
  context "? rotation: when a not active file is rotated outside the glob before the file is read" do
412
- let(:opts) { super.merge(
412
+ let(:opts) { super().merge(
413
413
  :close_older => 3600,
414
414
  :max_open_files => 1,
415
415
  :file_sort_by => "path"
@@ -9,7 +9,9 @@ module FileWatch
9
9
  let(:io) { StringIO.new }
10
10
  let(:db) { Hash.new }
11
11
 
12
- subject { SincedbRecordSerializer.new(SincedbRecordSerializer.days_to_seconds(14)) }
12
+ let(:sincedb_value_expiry) { SincedbRecordSerializer.days_to_seconds(14) }
13
+
14
+ subject { SincedbRecordSerializer.new(sincedb_value_expiry) }
13
15
 
14
16
  context "deserialize from IO" do
15
17
  it 'reads V1 records' do
@@ -82,8 +84,10 @@ module FileWatch
82
84
  end
83
85
 
84
86
  context "given a non default `sincedb_clean_after`" do
87
+
88
+ let(:sincedb_value_expiry) { SincedbRecordSerializer.days_to_seconds(2) }
89
+
85
90
  it "does not write expired db entries to an IO object" do
86
- subject.update_sincedb_value_expiry_from_days(2)
87
91
  one_day_ago = Time.now.to_f - (1.0*24*3600)
88
92
  three_days_ago = one_day_ago - (2.0*24*3600)
89
93
  db[InodeStruct.new("42424242", 2, 5)] = SincedbValue.new(42, one_day_ago)
@@ -77,7 +77,7 @@ module FileWatch
77
77
 
78
78
  context "when close_older is set" do
79
79
  let(:wait_before_quit) { 0.8 }
80
- let(:opts) { super.merge(:close_older => 0.1, :max_open_files => 1, :stat_interval => 0.1) }
80
+ let(:opts) { super().merge(:close_older => 0.1, :max_open_files => 1, :stat_interval => 0.1) }
81
81
  let(:suffix) { "B" }
82
82
  it "opens both files" do
83
83
  actions.activate_quietly
@@ -278,7 +278,7 @@ module FileWatch
278
278
 
279
279
  context "when watching a directory with files and a file is renamed to match glob", :unix => true do
280
280
  let(:suffix) { "H" }
281
- let(:opts) { super.merge(:close_older => 0) }
281
+ let(:opts) { super().merge(:close_older => 0) }
282
282
  let(:listener2) { observer.listener_for(file_path2) }
283
283
  let(:actions) do
284
284
  RSpec::Sequencing
@@ -346,7 +346,7 @@ module FileWatch
346
346
  end
347
347
 
348
348
  context "when close older expiry is enabled" do
349
- let(:opts) { super.merge(:close_older => 1) }
349
+ let(:opts) { super().merge(:close_older => 1) }
350
350
  let(:suffix) { "J" }
351
351
  let(:actions) do
352
352
  RSpec::Sequencing.run("create file") do
@@ -370,7 +370,7 @@ module FileWatch
370
370
  end
371
371
 
372
372
  context "when close older expiry is enabled and after timeout the file is appended-to" do
373
- let(:opts) { super.merge(:close_older => 0.5) }
373
+ let(:opts) { super().merge(:close_older => 0.5) }
374
374
  let(:suffix) { "K" }
375
375
  let(:actions) do
376
376
  RSpec::Sequencing
@@ -406,7 +406,7 @@ module FileWatch
406
406
  end
407
407
 
408
408
  context "when ignore older expiry is enabled and all files are already expired" do
409
- let(:opts) { super.merge(:ignore_older => 1) }
409
+ let(:opts) { super().merge(:ignore_older => 1) }
410
410
  let(:suffix) { "L" }
411
411
  let(:actions) do
412
412
  RSpec::Sequencing
@@ -430,7 +430,7 @@ module FileWatch
430
430
 
431
431
  context "when a file is renamed before it gets activated", :unix => true do
432
432
  let(:max) { 1 }
433
- let(:opts) { super.merge(:file_chunk_count => 8, :file_chunk_size => 6, :close_older => 0.1, :discover_interval => 6) }
433
+ let(:opts) { super().merge(:file_chunk_count => 8, :file_chunk_size => 6, :close_older => 0.1, :discover_interval => 6) }
434
434
  let(:suffix) { "M" }
435
435
  let(:start_new_files_at) { :beginning } # we are creating files and sincedb record before hand
436
436
  let(:actions) do
@@ -469,7 +469,7 @@ module FileWatch
469
469
  end
470
470
 
471
471
  context "when ignore_older is less than close_older and all files are not expired" do
472
- let(:opts) { super.merge(:ignore_older => 1, :close_older => 1.1) }
472
+ let(:opts) { super().merge(:ignore_older => 1, :close_older => 1.1) }
473
473
  let(:suffix) { "N" }
474
474
  let(:start_new_files_at) { :beginning }
475
475
  let(:actions) do
@@ -497,7 +497,7 @@ module FileWatch
497
497
  end
498
498
 
499
499
  context "when ignore_older is less than close_older and all files are expired" do
500
- let(:opts) { super.merge(:ignore_older => 10, :close_older => 1) }
500
+ let(:opts) { super().merge(:ignore_older => 10, :close_older => 1) }
501
501
  let(:suffix) { "P" }
502
502
  let(:actions) do
503
503
  RSpec::Sequencing
@@ -522,7 +522,7 @@ module FileWatch
522
522
  end
523
523
 
524
524
  context "when ignore older and close older expiry is enabled and after timeout the file is appended-to" do
525
- let(:opts) { super.merge(:ignore_older => 20, :close_older => 0.5) }
525
+ let(:opts) { super().merge(:ignore_older => 20, :close_older => 0.5) }
526
526
  let(:suffix) { "Q" }
527
527
  let(:actions) do
528
528
  RSpec::Sequencing
@@ -551,7 +551,7 @@ module FileWatch
551
551
  end
552
552
 
553
553
  context "when a non default delimiter is specified and it is not in the content" do
554
- let(:opts) { super.merge(:ignore_older => 20, :close_older => 1, :delimiter => "\nø") }
554
+ let(:opts) { super().merge(:ignore_older => 20, :close_older => 1, :delimiter => "\nø") }
555
555
  let(:suffix) { "R" }
556
556
  let(:actions) do
557
557
  RSpec::Sequencing
@@ -267,7 +267,7 @@ describe LogStash::Inputs::File do
267
267
  describe 'delete on complete' do
268
268
 
269
269
  let(:options) do
270
- super.merge({ 'file_completed_action' => "delete", 'exit_after_read' => false })
270
+ super().merge({ 'file_completed_action' => "delete", 'exit_after_read' => false })
271
271
  end
272
272
 
273
273
  let(:sample_file) { File.join(temp_directory, "sample.log") }
@@ -301,25 +301,72 @@ describe LogStash::Inputs::File do
301
301
  watched_files = plugin.watcher.watch.watched_files_collection
302
302
  expect( watched_files ).to be_empty
303
303
  end
304
+ end
304
305
 
305
- private
306
+ describe 'sincedb cleanup' do
306
307
 
307
- def wait_for_start_processing(run_thread, timeout: 1.0)
308
- begin
309
- Timeout.timeout(timeout) do
310
- sleep(0.01) while run_thread.status != 'sleep'
311
- sleep(timeout) unless plugin.queue
312
- end
313
- rescue Timeout::Error
314
- raise "plugin did not start processing (timeout: #{timeout})" unless plugin.queue
315
- else
316
- raise "plugin did not start processing" unless plugin.queue
308
+ let(:options) do
309
+ super().merge(
310
+ 'sincedb_path' => sincedb_path,
311
+ 'sincedb_clean_after' => '1.0 seconds',
312
+ 'sincedb_write_interval' => 0.25,
313
+ 'stat_interval' => 0.1,
314
+ )
315
+ end
316
+
317
+ let(:sincedb_path) { "#{temp_directory}/.sincedb" }
318
+
319
+ let(:sample_file) { File.join(temp_directory, "sample.txt") }
320
+
321
+ before do
322
+ plugin.register
323
+ @run_thread = Thread.new(plugin) do |plugin|
324
+ Thread.current.abort_on_exception = true
325
+ plugin.run queue
317
326
  end
327
+
328
+ File.open(sample_file, 'w') { |fd| fd.write("line1\nline2\n") }
329
+
330
+ wait_for_start_processing(@run_thread)
318
331
  end
319
332
 
320
- def wait_for_file_removal(path, timeout: 3 * interval)
321
- wait(timeout).for { File.exist?(path) }.to be_falsey
333
+ after { plugin.stop }
334
+
335
+ it 'cleans up sincedb entry' do
336
+ wait_for_file_removal(sample_file) # watched discovery
337
+
338
+ sincedb_content = File.read(sincedb_path).strip
339
+ expect( sincedb_content ).to_not be_empty
340
+
341
+ try(3) do
342
+ sleep(1.5) # > sincedb_clean_after
343
+
344
+ sincedb_content = File.read(sincedb_path).strip
345
+ expect( sincedb_content ).to be_empty
346
+ end
347
+ end
348
+
349
+ end
350
+
351
+ private
352
+
353
+ def wait_for_start_processing(run_thread, timeout: 1.0)
354
+ begin
355
+ Timeout.timeout(timeout) do
356
+ sleep(0.01) while run_thread.status != 'sleep'
357
+ sleep(timeout) unless plugin.queue
358
+ end
359
+ rescue Timeout::Error
360
+ raise "plugin did not start processing (timeout: #{timeout})" unless plugin.queue
361
+ else
362
+ raise "plugin did not start processing" unless plugin.queue
322
363
  end
364
+ end
323
365
 
366
+ def wait_for_file_removal(path)
367
+ timeout = interval
368
+ try(5) do
369
+ wait(timeout).for { File.exist?(path) }.to be_falsey
370
+ end
324
371
  end
325
372
  end
@@ -3,7 +3,9 @@
3
3
  require "helpers/spec_helper"
4
4
  require "logstash/devutils/rspec/shared_examples"
5
5
  require "logstash/inputs/file"
6
+ require "logstash/plugin_mixins/ecs_compatibility_support/spec_helper"
6
7
 
8
+ require "json"
7
9
  require "tempfile"
8
10
  require "stud/temporary"
9
11
  require "logstash/codecs/multiline"
@@ -99,41 +101,59 @@ describe LogStash::Inputs::File do
99
101
  end
100
102
  end
101
103
 
102
- context "when path and host fields exist" do
103
- let(:name) { "C" }
104
- it "should not overwrite them" do
105
- conf = <<-CONFIG
106
- input {
107
- file {
108
- type => "blah"
109
- path => "#{path_path}"
110
- start_position => "beginning"
111
- sincedb_path => "#{sincedb_path}"
112
- delimiter => "#{TEST_FILE_DELIMITER}"
113
- codec => "json"
114
- }
115
- }
116
- CONFIG
117
104
 
118
- File.open(tmpfile_path, "w") do |fd|
119
- fd.puts('{"path": "my_path", "host": "my_host"}')
120
- fd.puts('{"my_field": "my_val"}')
121
- fd.fsync
105
+ context "when path and host fields exist", :ecs_compatibility_support do
106
+ ecs_compatibility_matrix(:disabled, :v1) do |ecs_select|
107
+
108
+ before(:each) do
109
+ allow_any_instance_of(described_class).to receive(:ecs_compatibility).and_return(ecs_compatibility)
122
110
  end
123
111
 
124
- events = input(conf) do |pipeline, queue|
125
- 2.times.collect { queue.pop }
112
+ let(:file_path_target_field ) { ecs_select[disabled: "path", v1: '[log][file][path]'] }
113
+ let(:source_host_target_field) { ecs_select[disabled: "host", v1: '[host][name]'] }
114
+
115
+ let(:event_with_existing) do
116
+ LogStash::Event.new.tap do |e|
117
+ e.set(file_path_target_field, 'my_path')
118
+ e.set(source_host_target_field, 'my_host')
119
+ end.to_hash
126
120
  end
127
121
 
128
- existing_path_index, added_path_index = "my_val" == events[0].get("my_field") ? [1,0] : [0,1]
122
+ let(:name) { "C" }
123
+ it "should not overwrite them" do
124
+ conf = <<-CONFIG
125
+ input {
126
+ file {
127
+ type => "blah"
128
+ path => "#{path_path}"
129
+ start_position => "beginning"
130
+ sincedb_path => "#{sincedb_path}"
131
+ delimiter => "#{TEST_FILE_DELIMITER}"
132
+ codec => "json"
133
+ }
134
+ }
135
+ CONFIG
129
136
 
130
- expect(events[existing_path_index].get("path")).to eq "my_path"
131
- expect(events[existing_path_index].get("host")).to eq "my_host"
132
- expect(events[existing_path_index].get("[@metadata][host]")).to eq "#{Socket.gethostname.force_encoding(Encoding::UTF_8)}"
137
+ File.open(tmpfile_path, "w") do |fd|
138
+ fd.puts(event_with_existing.to_json)
139
+ fd.puts('{"my_field": "my_val"}')
140
+ fd.fsync
141
+ end
133
142
 
134
- expect(events[added_path_index].get("path")).to eq "#{tmpfile_path}"
135
- expect(events[added_path_index].get("host")).to eq "#{Socket.gethostname.force_encoding(Encoding::UTF_8)}"
136
- expect(events[added_path_index].get("[@metadata][host]")).to eq "#{Socket.gethostname.force_encoding(Encoding::UTF_8)}"
143
+ events = input(conf) do |pipeline, queue|
144
+ 2.times.collect { queue.pop }
145
+ end
146
+
147
+ existing_path_index, added_path_index = "my_val" == events[0].get("my_field") ? [1,0] : [0,1]
148
+
149
+ expect(events[existing_path_index].get(file_path_target_field)).to eq "my_path"
150
+ expect(events[existing_path_index].get(source_host_target_field)).to eq "my_host"
151
+ expect(events[existing_path_index].get("[@metadata][host]")).to eq "#{Socket.gethostname.force_encoding(Encoding::UTF_8)}"
152
+
153
+ expect(events[added_path_index].get(file_path_target_field)).to eq "#{tmpfile_path}"
154
+ expect(events[added_path_index].get(source_host_target_field)).to eq "#{Socket.gethostname.force_encoding(Encoding::UTF_8)}"
155
+ expect(events[added_path_index].get("[@metadata][host]")).to eq "#{Socket.gethostname.force_encoding(Encoding::UTF_8)}"
156
+ end
137
157
  end
138
158
  end
139
159
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: logstash-input-file
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.2.1
4
+ version: 4.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Elastic
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-05-14 00:00:00.000000000 Z
11
+ date: 2021-06-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  requirement: !ruby/object:Gem::Requirement
@@ -86,6 +86,20 @@ dependencies:
86
86
  - - "~>"
87
87
  - !ruby/object:Gem::Version
88
88
  version: '3.0'
89
+ - !ruby/object:Gem::Dependency
90
+ requirement: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - "~>"
93
+ - !ruby/object:Gem::Version
94
+ version: '1.1'
95
+ name: logstash-mixin-ecs_compatibility_support
96
+ prerelease: false
97
+ type: :runtime
98
+ version_requirements: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: '1.1'
89
103
  - !ruby/object:Gem::Dependency
90
104
  requirement: !ruby/object:Gem::Requirement
91
105
  requirements: