Skip to content

Commit 28c0534

Browse files
author
Guy Boertje
authored
Add string based time duration support. (#194)
* add string based time duration support. * redo the returned object as a Struct with a `to_a` method. Fixes #187
1 parent 323e669 commit 28c0534

File tree

3 files changed

+136
-5
lines changed

3 files changed

+136
-5
lines changed

Diff for: lib/logstash/inputs/file.rb

+20-5
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
require_relative "file_listener"
1212
require_relative "delete_completed_file_handler"
1313
require_relative "log_completed_file_handler"
14+
require_relative "friendly_durations"
1415
require "filewatch/bootstrap"
1516

1617
# Stream events from files, normally by tailing them in a manner
@@ -109,7 +110,7 @@ class File < LogStash::Inputs::Base
109110
# How often (in seconds) we stat files to see if they have been modified.
110111
# Increasing this interval will decrease the number of system calls we make,
111112
# but increase the time to detect new log lines.
112-
config :stat_interval, :validate => :number, :default => 1
113+
config :stat_interval, :validate => [FriendlyDurations, "seconds"], :default => 1
113114

114115
# How often (in seconds) we expand the filename patterns in the
115116
# `path` option to discover new files to watch.
@@ -123,7 +124,7 @@ class File < LogStash::Inputs::Base
123124

124125
# How often (in seconds) to write a since database with the current position of
125126
# monitored log files.
126-
config :sincedb_write_interval, :validate => :number, :default => 15
127+
config :sincedb_write_interval, :validate => [FriendlyDurations, "seconds"], :default => 15
127128

128129
# Choose where Logstash starts initially reading files: at the beginning or
129130
# at the end. The default behavior treats files like live streams and thus
@@ -145,7 +146,7 @@ class File < LogStash::Inputs::Base
145146
# After its discovery, if an ignored file is modified it is no
146147
# longer ignored and any new data is read. By default, this option is
147148
# disabled. Note this unit is in seconds.
148-
config :ignore_older, :validate => :number
149+
config :ignore_older, :validate => [FriendlyDurations, "seconds"]
149150

150151
# The file input closes any files that were last read the specified
151152
# timespan in seconds ago.
@@ -154,7 +155,7 @@ class File < LogStash::Inputs::Base
154155
# reopening when new data is detected. If reading, the file will be closed
155156
# after closed_older seconds from when the last bytes were read.
156157
# The default is 1 hour
157-
config :close_older, :validate => :number, :default => 1 * 60 * 60
158+
config :close_older, :validate => [FriendlyDurations, "seconds"], :default => "1 hour"
158159

159160
# What is the maximum number of file_handles that this input consumes
160161
# at any one time. Use close_older to close some files if you need to
@@ -191,7 +192,7 @@ class File < LogStash::Inputs::Base
191192
# If no changes are detected in tracked files in the last N days their sincedb
192193
# tracking record will expire and not be persisted.
193194
# This option protects against the well known inode recycling problem. (add reference)
194-
config :sincedb_clean_after, :validate => :number, :default => 14 # days
195+
config :sincedb_clean_after, :validate => [FriendlyDurations, "days"], :default => "14 days" # days
195196

196197
# File content is read off disk in blocks or chunks, then using whatever the set delimiter
197198
# is, lines are extracted from the chunk. Specify the size in bytes of each chunk.
@@ -222,6 +223,20 @@ class File < LogStash::Inputs::Base
222223
config :file_sort_direction, :validate => ["asc", "desc"], :default => "asc"
223224

224225
public
226+
227+
class << self
228+
alias_method :old_validate_value, :validate_value
229+
230+
def validate_value(value, validator)
231+
if validator.is_a?(Array) && validator.size == 2 && validator.first.respond_to?(:call)
232+
callable, units = *validator
233+
# returns a ValidatedStruct having a `to_a` method suitable to return to the config mixin caller
234+
return callable.call(value, units).to_a
235+
end
236+
old_validate_value(value, validator)
237+
end
238+
end
239+
225240
def register
226241
require "addressable/uri"
227242
require "digest/md5"

Diff for: lib/logstash/inputs/friendly_durations.rb

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# encoding: utf-8
2+
3+
module LogStash module Inputs
4+
module FriendlyDurations
5+
NUMBERS_RE = /^(?<number>\d+(\.\d+)?)\s?(?<units>s((ec)?(ond)?)(s)?|m((in)?(ute)?)(s)?|h(our)?(s)?|d(ay)?(s)?|w(eek)?(s)?|us(ec)?(s)?|ms(ec)?(s)?)?$/
6+
HOURS = 3600
7+
DAYS = 24 * HOURS
8+
MEGA = 10**6
9+
KILO = 10**3
10+
11+
ValidatedStruct = Struct.new(:value, :error_message) do
12+
def to_a
13+
error_message.nil? ? [true, value] : [false, error_message]
14+
end
15+
end
16+
17+
def self.call(value, unit = "sec")
18+
# coerce into seconds
19+
val_string = value.to_s.strip
20+
matched = NUMBERS_RE.match(val_string)
21+
if matched.nil?
22+
failed_message = "Value '#{val_string}' is not a valid duration string e.g. 200 usec, 250ms, 60 sec, 18h, 21.5d, 1 day, 2w, 6 weeks"
23+
return ValidatedStruct.new(nil, failed_message)
24+
end
25+
multiplier = matched[:units] || unit
26+
numeric = matched[:number].to_f
27+
case multiplier
28+
when "m","min","mins","minute","minutes"
29+
ValidatedStruct.new(numeric * 60, nil)
30+
when "h","hour","hours"
31+
ValidatedStruct.new(numeric * HOURS, nil)
32+
when "d","day","days"
33+
ValidatedStruct.new(numeric * DAYS, nil)
34+
when "w","week","weeks"
35+
ValidatedStruct.new(numeric * 7 * DAYS, nil)
36+
when "ms","msec","msecs"
37+
ValidatedStruct.new(numeric / KILO, nil)
38+
when "us","usec","usecs"
39+
ValidatedStruct.new(numeric / MEGA, nil)
40+
else
41+
ValidatedStruct.new(numeric, nil)
42+
end
43+
end
44+
end
45+
end end

Diff for: spec/inputs/friendly_durations_spec.rb

+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# encoding: utf-8
2+
3+
require "helpers/spec_helper"
4+
require "logstash/inputs/friendly_durations"
5+
6+
describe "FriendlyDurations module function call" do
7+
context "unacceptable strings" do
8+
it "gives an error message for 'foobar'" do
9+
result = LogStash::Inputs::FriendlyDurations.call("foobar","sec")
10+
expect(result.error_message).to start_with("Value 'foobar' is not a valid duration string e.g. 200 usec")
11+
end
12+
it "gives an error message for '5 5 days'" do
13+
result = LogStash::Inputs::FriendlyDurations.call("5 5 days","sec")
14+
expect(result.error_message).to start_with("Value '5 5 days' is not a valid duration string e.g. 200 usec")
15+
end
16+
end
17+
18+
context "when a unit is not specified, a unit override will affect the result" do
19+
it "coerces 14 to 1209600.0s as days" do
20+
result = LogStash::Inputs::FriendlyDurations.call(14,"d")
21+
expect(result.error_message).to eq(nil)
22+
expect(result.value).to eq(1209600.0)
23+
end
24+
it "coerces '30' to 1800.0s as minutes" do
25+
result = LogStash::Inputs::FriendlyDurations.call("30","minutes")
26+
expect(result.to_a).to eq([true, 1800.0])
27+
end
28+
end
29+
30+
context "acceptable strings" do
31+
[
32+
["10", 10.0],
33+
["10.5 s", 10.5],
34+
["10.75 secs", 10.75],
35+
["11 second", 11.0],
36+
["10 seconds", 10.0],
37+
["500 ms", 0.5],
38+
["750.9 msec", 0.7509],
39+
["750.9 msecs", 0.7509],
40+
["750.9 us", 0.0007509],
41+
["750.9 usec", 0.0007509],
42+
["750.9 usecs", 0.0007509],
43+
["1.5m", 90.0],
44+
["2.5 m", 150.0],
45+
["1.25 min", 75.0],
46+
["1 minute", 60.0],
47+
["2.5 minutes", 150.0],
48+
["2h", 7200.0],
49+
["2 h", 7200.0],
50+
["1 hour", 3600.0],
51+
["1hour", 3600.0],
52+
["3 hours", 10800.0],
53+
["0.5d", 43200.0],
54+
["1day", 86400.0],
55+
["1 day", 86400.0],
56+
["2days", 172800.0],
57+
["14 days", 1209600.0],
58+
["1w", 604800.0],
59+
["1 w", 604800.0],
60+
["1 week", 604800.0],
61+
["2weeks", 1209600.0],
62+
["2 weeks", 1209600.0],
63+
["1.5 weeks", 907200.0],
64+
].each do |input, coerced|
65+
it "coerces #{input.inspect.rjust(16)} to #{coerced.inspect}" do
66+
result = LogStash::Inputs::FriendlyDurations.call(input,"sec")
67+
expect(result.to_a).to eq([true, coerced])
68+
end
69+
end
70+
end
71+
end

0 commit comments

Comments
 (0)