logfile_interval 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +35 -0
- data/LICENSE.txt +22 -0
- data/README.md +94 -0
- data/Rakefile +1 -0
- data/docs/design.rb +91 -0
- data/docs/design2.rb +77 -0
- data/docs/design3.rb +177 -0
- data/lib/logfile_interval.rb +12 -0
- data/lib/logfile_interval/file_backward.rb +49 -0
- data/lib/logfile_interval/interval.rb +42 -0
- data/lib/logfile_interval/interval_builder.rb +34 -0
- data/lib/logfile_interval/interval_length.rb +47 -0
- data/lib/logfile_interval/line_parser/aggregator.rb +61 -0
- data/lib/logfile_interval/line_parser/base.rb +94 -0
- data/lib/logfile_interval/line_parser/counter.rb +27 -0
- data/lib/logfile_interval/logfile.rb +33 -0
- data/lib/logfile_interval/logfile_set.rb +47 -0
- data/lib/logfile_interval/version.rb +3 -0
- data/logfile_interval.gemspec +25 -0
- data/spec/lib/interval_builder_spec.rb +49 -0
- data/spec/lib/interval_spec.rb +73 -0
- data/spec/lib/line_parser/aggregator_spec.rb +47 -0
- data/spec/lib/line_parser/base_spec.rb +84 -0
- data/spec/lib/logfile_set_spec.rb +39 -0
- data/spec/lib/logfile_spec.rb +39 -0
- data/spec/spec_helper.rb +9 -0
- data/spec/support/lib/access_log.rb +22 -0
- data/spec/support/lib/timing_log.rb +19 -0
- data/spec/support/logfiles/access.log +7 -0
- data/spec/support/logfiles/access.log.1 +3 -0
- data/spec/support/logfiles/access.log.2 +3 -0
- data/spec/support/logfiles/timing.log +3 -0
- data/spec/support/logfiles/timing.log.1 +3 -0
- metadata +149 -0
@@ -0,0 +1,12 @@
|
|
1
|
+
require "logfile_interval/version"
|
2
|
+
require "logfile_interval/file_backward"
|
3
|
+
require "logfile_interval/interval"
|
4
|
+
require "logfile_interval/interval_builder"
|
5
|
+
require "logfile_interval/logfile"
|
6
|
+
require "logfile_interval/logfile_set"
|
7
|
+
require "logfile_interval/line_parser/base"
|
8
|
+
require "logfile_interval/line_parser/aggregator"
|
9
|
+
require "logfile_interval/line_parser/counter"
|
10
|
+
|
11
|
+
module LogfileInterval
|
12
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module LogfileInterval
|
2
|
+
# Based on Perl's File::ReadBackwards module, by Uri Guttman.
|
3
|
+
class FileBackward
|
4
|
+
MAX_READ_SIZE = 1 << 10 # 1024
|
5
|
+
|
6
|
+
def initialize( *args )
|
7
|
+
return unless File.exist?(args[0])
|
8
|
+
@file = File.new(*args)
|
9
|
+
@file.seek(0, IO::SEEK_END)
|
10
|
+
|
11
|
+
@current_pos = @file.pos
|
12
|
+
|
13
|
+
@read_size = @file.pos % MAX_READ_SIZE
|
14
|
+
@read_size = MAX_READ_SIZE if @read_size.zero?
|
15
|
+
|
16
|
+
@line_buffer = Array.new
|
17
|
+
end
|
18
|
+
|
19
|
+
def gets( sep_string = $/ )
|
20
|
+
return nil unless @file
|
21
|
+
return @line_buffer.pop if @line_buffer.size > 2 or @current_pos.zero?
|
22
|
+
|
23
|
+
@current_pos -= @read_size
|
24
|
+
@file.seek(@current_pos, IO::SEEK_SET)
|
25
|
+
|
26
|
+
@line_buffer[0] = "#{@file.read(@read_size)}#{@line_buffer[0]}"
|
27
|
+
@read_size = MAX_READ_SIZE # Set a size for the next read.
|
28
|
+
|
29
|
+
@line_buffer[0] =
|
30
|
+
@line_buffer[0].scan(/.*?#{Regexp.escape(sep_string)}|.+/)
|
31
|
+
@line_buffer.flatten!
|
32
|
+
|
33
|
+
gets(sep_string)
|
34
|
+
end
|
35
|
+
|
36
|
+
def close
|
37
|
+
return unless @file
|
38
|
+
@file.close()
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# f = FileBackward.new('../log/development.log')
|
44
|
+
# i = 0
|
45
|
+
# while(line = f.gets())
|
46
|
+
# puts line
|
47
|
+
# i += 1
|
48
|
+
# break if i>30
|
49
|
+
# end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module LogfileInterval
|
2
|
+
class Interval
|
3
|
+
attr_reader :start_time, :end_time, :length, :parser
|
4
|
+
attr_reader :size
|
5
|
+
|
6
|
+
class OutOfRange < StandardError; end
|
7
|
+
class ParserMismatch < StandardError; end
|
8
|
+
|
9
|
+
def initialize(end_time, length, parser)
|
10
|
+
raise ArgumentError, 'end_time must be round' unless (end_time.to_i % length.to_i == 0)
|
11
|
+
@end_time = end_time
|
12
|
+
@start_time = end_time - length
|
13
|
+
@length = length
|
14
|
+
@parser = parser
|
15
|
+
@size = 0
|
16
|
+
|
17
|
+
@data = {}
|
18
|
+
parser.columns.each do |name, options|
|
19
|
+
next unless agg = options[:aggregator]
|
20
|
+
@data[name] = agg.new
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def [](name)
|
25
|
+
@data[name].value
|
26
|
+
end
|
27
|
+
|
28
|
+
def add_record(record)
|
29
|
+
return unless record.valid?
|
30
|
+
raise ParserMismatch unless record.class == parser
|
31
|
+
raise OutOfRange, 'too recent' if record.time>@end_time
|
32
|
+
raise OutOfRange, 'too old' if record.time<=@start_time
|
33
|
+
|
34
|
+
@size += 1
|
35
|
+
|
36
|
+
parser.columns.each do |name, options|
|
37
|
+
next unless @data[name]
|
38
|
+
@data[name].add(record[name])
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module LogfileInterval
|
2
|
+
class IntervalBuilder
|
3
|
+
attr_reader :logfile_set, :parser, :length
|
4
|
+
|
5
|
+
def initialize(logfile_set, length)
|
6
|
+
@logfile_set = logfile_set
|
7
|
+
@parser = logfile_set.parser
|
8
|
+
@length = length
|
9
|
+
end
|
10
|
+
|
11
|
+
def each_interval
|
12
|
+
secs = (Time.now.to_i / length.to_i) * length.to_i
|
13
|
+
rounded_end_time = Time.at(secs)
|
14
|
+
current_interval = Interval.new(rounded_end_time, length, parser)
|
15
|
+
|
16
|
+
logfile_set.each_parsed_line do |record|
|
17
|
+
next if record.time > current_interval.end_time
|
18
|
+
while record.time <= current_interval.start_time
|
19
|
+
yield current_interval
|
20
|
+
current_interval = Interval.new(current_interval.start_time, length, parser)
|
21
|
+
end
|
22
|
+
current_interval.add_record(record)
|
23
|
+
end
|
24
|
+
|
25
|
+
yield current_interval if current_interval.size>0
|
26
|
+
end
|
27
|
+
|
28
|
+
def last_interval
|
29
|
+
each_interval do |interval|
|
30
|
+
return interval
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module LogfileInterval
|
2
|
+
class IntervalLength
|
3
|
+
MAX_PERIODS = { 5 * 60 => 6 * 3600,
|
4
|
+
3600 => 3600 * 24,
|
5
|
+
3600 * 24 => 3600 * 24 * 30,
|
6
|
+
3600 * 24 * 30 => 365 * 3600 * 24 }
|
7
|
+
LENGTHS = MAX_PERIODS.keys.sort
|
8
|
+
|
9
|
+
attr_reader :length
|
10
|
+
|
11
|
+
def initialize(l)
|
12
|
+
raise ArgumentError unless LENGTHS.include?(l)
|
13
|
+
@length = l
|
14
|
+
end
|
15
|
+
|
16
|
+
def lower
|
17
|
+
pos = LENGTHS.index(@length)
|
18
|
+
return nil if pos==0
|
19
|
+
IntervalLength.new(LENGTHS[pos-1])
|
20
|
+
end
|
21
|
+
|
22
|
+
def higher
|
23
|
+
pos = LENGTHS.index(@length)
|
24
|
+
return nil if pos==LENGTHS.size-1
|
25
|
+
IntervalLength.new(LENGTHS[pos+1])
|
26
|
+
end
|
27
|
+
|
28
|
+
def smallest?
|
29
|
+
pos = LENGTHS.index(@length)
|
30
|
+
pos == 0
|
31
|
+
end
|
32
|
+
|
33
|
+
def start_time(t)
|
34
|
+
ts = (t.to_i / @length.to_i) * @length.to_i
|
35
|
+
ts -= @length.to_i if t.to_i % @length.to_i == 0
|
36
|
+
Time.at(ts)
|
37
|
+
end
|
38
|
+
|
39
|
+
def end_time(t)
|
40
|
+
start_time(t) + @length
|
41
|
+
end
|
42
|
+
|
43
|
+
def to_i
|
44
|
+
@length.to_i
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module LogfileInterval
|
2
|
+
module LineParser
|
3
|
+
module Aggregator
|
4
|
+
def self.klass(agg_function)
|
5
|
+
case agg_function
|
6
|
+
when :sum then Sum
|
7
|
+
when :average then Average
|
8
|
+
when :group then Group
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
class Sum
|
13
|
+
def initialize
|
14
|
+
@val = 0
|
15
|
+
end
|
16
|
+
|
17
|
+
def add(value)
|
18
|
+
@val += value
|
19
|
+
end
|
20
|
+
|
21
|
+
def value
|
22
|
+
@val
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
class Average
|
27
|
+
def initialize
|
28
|
+
@val = 0
|
29
|
+
@size = 0
|
30
|
+
end
|
31
|
+
|
32
|
+
def add(value)
|
33
|
+
@val += value
|
34
|
+
@size += 1
|
35
|
+
end
|
36
|
+
|
37
|
+
def value
|
38
|
+
if @size > 0
|
39
|
+
@val.to_f / @size.to_f
|
40
|
+
else
|
41
|
+
0
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
class Group
|
47
|
+
def initialize
|
48
|
+
@val = Counter.new
|
49
|
+
end
|
50
|
+
|
51
|
+
def add(value)
|
52
|
+
@val.increment(value)
|
53
|
+
end
|
54
|
+
|
55
|
+
def value
|
56
|
+
@val
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
module LogfileInterval
|
2
|
+
module LineParser
|
3
|
+
AGGREGATION_FUNCTIONS = [ :sum, :average, :timestamp, :group ]
|
4
|
+
|
5
|
+
class InvalidLine < StandardError; end
|
6
|
+
class ConfigurationError < StandardError; end
|
7
|
+
|
8
|
+
class Base
|
9
|
+
class << self
|
10
|
+
attr_reader :regex
|
11
|
+
|
12
|
+
def columns
|
13
|
+
@columns ||= {}
|
14
|
+
end
|
15
|
+
|
16
|
+
def set_regex(regex)
|
17
|
+
@regex = regex
|
18
|
+
end
|
19
|
+
|
20
|
+
def add_column(options)
|
21
|
+
name = options.fetch(:name)
|
22
|
+
pos = options.fetch(:pos)
|
23
|
+
agg_function = options.fetch(:agg_function)
|
24
|
+
conversion = options.fetch(:conversion, :string)
|
25
|
+
unless AGGREGATION_FUNCTIONS.include?(agg_function)
|
26
|
+
raise ArgumentError, "agg_function must be one of #{AGGREGATION_FUNCTIONS.join(', ')}"
|
27
|
+
end
|
28
|
+
|
29
|
+
aggregator = Aggregator.klass(agg_function)
|
30
|
+
columns[name] = { :pos => pos, :aggregator => aggregator, :conversion => conversion }
|
31
|
+
|
32
|
+
define_method(name) do
|
33
|
+
@data[name]
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def parse(line)
|
38
|
+
raise ConfigurationError, 'There must be at least 1 configured column' unless columns.any?
|
39
|
+
raise ConfigurationError, 'A regex must be set' unless regex
|
40
|
+
|
41
|
+
match_data = regex.match(line)
|
42
|
+
return nil unless match_data
|
43
|
+
return nil unless match_data.size >= columns.size+1
|
44
|
+
|
45
|
+
data = {}
|
46
|
+
columns.each do |name, options|
|
47
|
+
val = match_data[options[:pos]]
|
48
|
+
data[name] = convert(val, options[:conversion])
|
49
|
+
end
|
50
|
+
data
|
51
|
+
end
|
52
|
+
|
53
|
+
def create_record(line)
|
54
|
+
record = new(line)
|
55
|
+
return record if record.valid?
|
56
|
+
return nil
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def validate_column_options(options)
|
62
|
+
end
|
63
|
+
|
64
|
+
def convert(val, conversion)
|
65
|
+
case conversion
|
66
|
+
when :integer then val.to_i
|
67
|
+
when :float then val.to_f
|
68
|
+
else val
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def initialize(line)
|
74
|
+
@data = self.class.parse(line)
|
75
|
+
@valid = @data ? true : false
|
76
|
+
end
|
77
|
+
|
78
|
+
def valid?
|
79
|
+
@valid
|
80
|
+
end
|
81
|
+
|
82
|
+
def time
|
83
|
+
raise NotImplemented
|
84
|
+
end
|
85
|
+
|
86
|
+
def [](name)
|
87
|
+
@data[name]
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
|
94
|
+
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module LogfileInterval
|
2
|
+
module LineParser
|
3
|
+
class Counter < Hash
|
4
|
+
def increment(val)
|
5
|
+
if self.has_key?(val)
|
6
|
+
self[val] += 1
|
7
|
+
else
|
8
|
+
self[val] = 1
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def add(val, num)
|
13
|
+
if self.has_key?(val)
|
14
|
+
self[val] += num
|
15
|
+
else
|
16
|
+
self[val] = num
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def merge(c)
|
21
|
+
c.keys.each do |k|
|
22
|
+
self.add c[k]
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module LogfileInterval
|
2
|
+
class Logfile
|
3
|
+
attr_reader :filename, :parser
|
4
|
+
|
5
|
+
def initialize(filename, parser)
|
6
|
+
@filename = filename
|
7
|
+
@parser = parser
|
8
|
+
end
|
9
|
+
|
10
|
+
def first_timestamp
|
11
|
+
return nil unless File.exist?(@filename)
|
12
|
+
File.open(@filename) do |f|
|
13
|
+
line = parser.create_record(f.gets)
|
14
|
+
line.time
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def each_line
|
19
|
+
f = FileBackward.new(@filename)
|
20
|
+
while(line = f.gets)
|
21
|
+
yield line.chomp
|
22
|
+
end
|
23
|
+
f.close
|
24
|
+
end
|
25
|
+
|
26
|
+
def each_parsed_line
|
27
|
+
each_line do |line|
|
28
|
+
record = parser.create_record(line)
|
29
|
+
yield record if record
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module LogfileInterval
|
2
|
+
class LogfileSet
|
3
|
+
attr_reader :parser
|
4
|
+
|
5
|
+
def initialize(filenames, parser)
|
6
|
+
@parser = parser
|
7
|
+
@filenames = filenames
|
8
|
+
end
|
9
|
+
|
10
|
+
def existing_filenames
|
11
|
+
@existing_filenames ||= @filenames.select { |f| File.exist?(f) }
|
12
|
+
end
|
13
|
+
|
14
|
+
def ordered_filenames
|
15
|
+
time_for_file = existing_filenames.inject({}) do |h, filename|
|
16
|
+
file = Logfile.new(filename, parser)
|
17
|
+
h[filename] = file.first_timestamp
|
18
|
+
h
|
19
|
+
end
|
20
|
+
time_for_file.to_a.sort_by { |arr| arr[1] }.map { |arr| arr[0] }.reverse
|
21
|
+
end
|
22
|
+
|
23
|
+
def each_parsed_line
|
24
|
+
ordered_filenames.each do |filename|
|
25
|
+
tfile = Logfile.new(filename, parser)
|
26
|
+
tfile.each_parsed_line do |record|
|
27
|
+
yield record
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def each_line
|
33
|
+
ordered_filenames.each do |filename|
|
34
|
+
tfile = Logfile.new(filename, parser)
|
35
|
+
tfile.each_line do |line|
|
36
|
+
yield line
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def last_record
|
42
|
+
each_parsed_line do |record|
|
43
|
+
return record
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|