indy 0.3.4 → 0.4.0.pre
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 +9 -0
- data/.travis.yml +8 -0
- data/Guardfile +12 -0
- data/History.txt +6 -0
- data/README.md +43 -14
- data/Rakefile +7 -0
- data/features/step_definitions/find_by.steps.rb +1 -1
- data/features/step_definitions/log_file.steps.rb +1 -1
- data/features/step_definitions/time.steps.rb +6 -6
- data/indy.gemspec +21 -13
- data/lib/indy.rb +10 -2
- data/lib/indy/indy.rb +87 -408
- data/lib/indy/log_definition.rb +115 -0
- data/lib/indy/log_formats.rb +15 -7
- data/lib/indy/search.rb +147 -0
- data/lib/indy/source.rb +143 -50
- data/lib/indy/time.rb +78 -0
- data/lib/indy/version.rb +1 -1
- data/performance/large.log +40000 -0
- data/performance/profile_spec.rb +7 -7
- data/performance/time_large_file_spec.rb +18 -0
- data/spec/helper.rb +5 -3
- data/spec/indy_private_spec.rb +24 -0
- data/spec/indy_spec.rb +153 -226
- data/spec/indy_struct_spec.rb +43 -0
- data/spec/log_definition_spec.rb +75 -0
- data/spec/log_format_spec.rb +62 -50
- data/spec/search_spec.rb +15 -25
- data/spec/source_spec.rb +43 -35
- data/spec/time_scope_spec.rb +162 -0
- data/spec/time_spec.rb +26 -192
- metadata +264 -164
- data/.autotest +0 -18
- data/.rvmrc +0 -1
- data/autotest/discover.rb +0 -2
- data/lib/indy/formats.rb +0 -3
- data/lib/indy/notes.txt +0 -9
- data/lib/indy/result_set.rb +0 -8
- data/lib/scanf.rb +0 -13
- data/spec/last_spec.rb +0 -42
- data/spec/result_set_spec.rb +0 -36
@@ -0,0 +1,115 @@
|
|
1
|
+
class Indy
|
2
|
+
|
3
|
+
class LogDefinition
|
4
|
+
|
5
|
+
attr_accessor :entry_regexp, :entry_fields, :time_format, :multiline
|
6
|
+
|
7
|
+
def initialize(args=:default)
|
8
|
+
case args
|
9
|
+
when :default, {}
|
10
|
+
params_hash = set_defaults
|
11
|
+
when Array, Hash
|
12
|
+
params_hash = parse_enumerable_params(args)
|
13
|
+
end
|
14
|
+
raise ArgumentError, "Values for entry_regexp and/or entry_fields were not supplied" unless (params_hash[:entry_fields] && params_hash[:entry_regexp])
|
15
|
+
if params_hash[:multiline]
|
16
|
+
@entry_regexp = Regexp.new(params_hash[:entry_regexp], Regexp::MULTILINE)
|
17
|
+
@multiline = true
|
18
|
+
else
|
19
|
+
@entry_regexp = Regexp.new(params_hash[:entry_regexp])
|
20
|
+
end
|
21
|
+
@entry_fields = params_hash[:entry_fields]
|
22
|
+
@time_format = params_hash[:time_format]
|
23
|
+
define_struct
|
24
|
+
end
|
25
|
+
|
26
|
+
def set_defaults
|
27
|
+
params_hash = {}
|
28
|
+
params_hash[:entry_regexp] = Indy::LogFormats::DEFAULT_ENTRY_REGEXP
|
29
|
+
params_hash[:entry_fields] = Indy::LogFormats::DEFAULT_ENTRY_FIELDS
|
30
|
+
params_hash
|
31
|
+
end
|
32
|
+
|
33
|
+
def parse_enumerable_params(args)
|
34
|
+
params_hash = {}
|
35
|
+
params_hash.merge!(args)
|
36
|
+
if args.keys.include? :log_format
|
37
|
+
# support 0.3.4 params
|
38
|
+
params_hash[:entry_regexp] = args[:log_format][0]
|
39
|
+
params_hash[:entry_fields] = args[:log_format][1..-1]
|
40
|
+
params_hash.delete :log_format
|
41
|
+
end
|
42
|
+
params_hash
|
43
|
+
end
|
44
|
+
|
45
|
+
#
|
46
|
+
# Return a Struct::Entry object from a hash of values from a log entry
|
47
|
+
#
|
48
|
+
# @param [Hash] entry_hash a hash of :field_name => value pairs for one log entry
|
49
|
+
#
|
50
|
+
def create_struct( entry_hash )
|
51
|
+
values = entry_hash.keys.sort_by{|entry|entry.to_s}.collect {|key| entry_hash[key]}
|
52
|
+
result = Struct::Entry.new( *values )
|
53
|
+
result
|
54
|
+
end
|
55
|
+
|
56
|
+
#
|
57
|
+
# Define Struct::Entry with the fields from @log_definition. Ignore warnings.
|
58
|
+
#
|
59
|
+
def define_struct
|
60
|
+
fields = (@entry_fields + [:raw_entry]).sort_by{|key|key.to_s}
|
61
|
+
verbose = $VERBOSE
|
62
|
+
$VERBOSE = nil
|
63
|
+
Struct.new( "Entry", *fields )
|
64
|
+
$VERBOSE = verbose
|
65
|
+
end
|
66
|
+
|
67
|
+
#
|
68
|
+
# Convert log entry into hash
|
69
|
+
#
|
70
|
+
def entry_hash(values)
|
71
|
+
assert_valid_field_list(values) unless @field_list_is_valid # just do it once
|
72
|
+
raw_entry = values.shift
|
73
|
+
hash = Hash[ *@entry_fields.zip( values ).flatten ]
|
74
|
+
hash[:raw_entry] = raw_entry.strip
|
75
|
+
hash
|
76
|
+
end
|
77
|
+
|
78
|
+
|
79
|
+
#
|
80
|
+
# Return a hash of field=>value pairs for the log entry
|
81
|
+
#
|
82
|
+
# @param [String] raw_entry The raw log entry
|
83
|
+
#
|
84
|
+
def parse_entry(raw_entry)
|
85
|
+
match_data = /#{@entry_regexp}/.match(raw_entry)
|
86
|
+
return nil unless match_data
|
87
|
+
values = match_data.captures
|
88
|
+
entry_hash([raw_entry, values].flatten)
|
89
|
+
end
|
90
|
+
|
91
|
+
#
|
92
|
+
# Return a hash of field=>value pairs for the array of captured values from a log entry
|
93
|
+
#
|
94
|
+
# @param [Array] capture_array The array of values captured by the @log_definition.entry_regexp
|
95
|
+
#
|
96
|
+
def parse_entry_captures( capture_array )
|
97
|
+
entire_entry = capture_array.shift
|
98
|
+
values = capture_array
|
99
|
+
entry_hash([entire_entry, values].flatten)
|
100
|
+
end
|
101
|
+
|
102
|
+
#
|
103
|
+
# Ensure number of fields is expected
|
104
|
+
#
|
105
|
+
def assert_valid_field_list(values)
|
106
|
+
if values.length == @entry_fields.length + 1 # values also includes raw_entry
|
107
|
+
@field_list_is_valid = true
|
108
|
+
else
|
109
|
+
raise ArgumentError, "Field mismatch between log pattern and log data. The data is: '#{values.join(':::')}'"
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
end
|
114
|
+
|
115
|
+
end
|
data/lib/indy/log_formats.rb
CHANGED
@@ -23,8 +23,8 @@ class Indy
|
|
23
23
|
DEFAULT_APPLICATION = '\w+'
|
24
24
|
DEFAULT_MESSAGE = '.+'
|
25
25
|
|
26
|
-
|
27
|
-
|
26
|
+
DEFAULT_ENTRY_FIELDS = [:time,:severity,:application,:message]
|
27
|
+
DEFAULT_ENTRY_REGEXP = /^(#{DEFAULT_DATE_TIME})\s+(#{DEFAULT_SEVERITY_PATTERN})\s+(#{DEFAULT_APPLICATION})\s+-\s+(#{DEFAULT_MESSAGE})$/
|
28
28
|
|
29
29
|
COMMON_FIELDS = [:host, :ident, :authuser, :time, :request, :status, :bytes]
|
30
30
|
COMMON_REGEXP = /^#{IPV4_REGEXP} #{SPACE_DELIM_REGEXP} #{SPACE_DELIM_REGEXP} #{BRACKET_DELIM_REGEXP} #{DQUOTE_DELIM_REGEXP} #{HTTP_STATUS_REGEXP} #{NUMBER_REGEXP}$/
|
@@ -33,7 +33,10 @@ class Indy
|
|
33
33
|
COMBINED_REGEXP = /^#{IPV4_REGEXP} #{SPACE_DELIM_REGEXP} #{SPACE_DELIM_REGEXP} #{BRACKET_DELIM_REGEXP} #{DQUOTE_DELIM_REGEXP} #{HTTP_STATUS_REGEXP} #{NUMBER_REGEXP} #{DQUOTE_DELIM_REGEXP} #{DQUOTE_DELIM_REGEXP}$/
|
34
34
|
|
35
35
|
LOG4R_DEFAULT_FIELDS = [:level, :application, :message]
|
36
|
-
LOG4R_DEFAULT_REGEXP =
|
36
|
+
LOG4R_DEFAULT_REGEXP = /^(?:\s*)([A-Z]+) (\S+): (.*)$/
|
37
|
+
|
38
|
+
LOG4J_DEFAULT_FIELDS = [:message]
|
39
|
+
LOG4J_DEFAULT_REGEXP = /^(.+?)$/
|
37
40
|
end
|
38
41
|
|
39
42
|
#
|
@@ -41,21 +44,26 @@ class Indy
|
|
41
44
|
# e.g.:
|
42
45
|
# INFO 2000-09-07 MyApp - Entering APPLICATION.
|
43
46
|
#
|
44
|
-
DEFAULT_LOG_FORMAT =
|
47
|
+
DEFAULT_LOG_FORMAT = {:entry_regexp => LogFormats::DEFAULT_ENTRY_REGEXP, :entry_fields => LogFormats::DEFAULT_ENTRY_FIELDS}
|
45
48
|
|
46
49
|
#
|
47
50
|
# Uncustomized Log4r log format
|
48
51
|
#
|
49
|
-
LOG4R_DEFAULT_FORMAT =
|
52
|
+
LOG4R_DEFAULT_FORMAT = {:entry_regexp => LogFormats::LOG4R_DEFAULT_REGEXP, :entry_fields => LogFormats::LOG4R_DEFAULT_FIELDS}
|
53
|
+
|
54
|
+
#
|
55
|
+
# Uncustomized Log4j log format (message field only!)
|
56
|
+
#
|
57
|
+
LOG4J_DEFAULT_FORMAT = {:entry_regexp => LogFormats::LOG4J_DEFAULT_REGEXP, :entry_fields => LogFormats::LOG4J_DEFAULT_FIELDS}
|
50
58
|
|
51
59
|
#
|
52
60
|
# NCSA Common Log Format log format
|
53
61
|
#
|
54
|
-
COMMON_LOG_FORMAT =
|
62
|
+
COMMON_LOG_FORMAT = {:entry_regexp => LogFormats::COMMON_REGEXP, :entry_fields => LogFormats::COMMON_FIELDS}
|
55
63
|
|
56
64
|
#
|
57
65
|
# NCSA Combined Log Format log format
|
58
66
|
#
|
59
|
-
COMBINED_LOG_FORMAT =
|
67
|
+
COMBINED_LOG_FORMAT = {:entry_regexp => LogFormats::COMBINED_REGEXP, :entry_fields => LogFormats::COMBINED_FIELDS}
|
60
68
|
|
61
69
|
end
|
data/lib/indy/search.rb
ADDED
@@ -0,0 +1,147 @@
|
|
1
|
+
class Indy
|
2
|
+
|
3
|
+
class Search
|
4
|
+
|
5
|
+
attr_accessor :source
|
6
|
+
attr_accessor :log_definition
|
7
|
+
|
8
|
+
attr_accessor :start_time, :end_time, :inclusive
|
9
|
+
|
10
|
+
def initialize(params_hash)
|
11
|
+
while (param = params_hash.shift) do
|
12
|
+
send("#{param.first}=",param.last)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
#
|
17
|
+
# Helper function called by Indy#for, Indy#like and Indy#all
|
18
|
+
#
|
19
|
+
# @param [Symbol] type The symbol :for, :like or :all
|
20
|
+
#
|
21
|
+
# @param [Hash] search_criteria the field to search for as the key and the
|
22
|
+
# value to compare against the log entries.
|
23
|
+
#
|
24
|
+
def iterate_and_compare(type,search_criteria,&block)
|
25
|
+
results = []
|
26
|
+
results += search do |entry|
|
27
|
+
if type == :all || is_match?(type,entry,search_criteria)
|
28
|
+
result_struct = @log_definition.create_struct(entry)
|
29
|
+
if block_given?
|
30
|
+
block.call(result_struct)
|
31
|
+
else
|
32
|
+
result_struct
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
results.compact
|
37
|
+
end
|
38
|
+
|
39
|
+
#
|
40
|
+
# Search the @source and yield to the block the entry that was found
|
41
|
+
# with @log_definition
|
42
|
+
#
|
43
|
+
# This method is supposed to be used internally.
|
44
|
+
#
|
45
|
+
def search(&block)
|
46
|
+
if @log_definition.multiline
|
47
|
+
multiline_search(&block)
|
48
|
+
else
|
49
|
+
standard_search(&block)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
#
|
54
|
+
# Performs #search for line based entries
|
55
|
+
#
|
56
|
+
def standard_search(&block)
|
57
|
+
is_time_search = use_time_criteria?
|
58
|
+
results = []
|
59
|
+
source_lines = (is_time_search ? @source.open([@start_time,@end_time]) : @source.open)
|
60
|
+
source_lines.each do |single_line|
|
61
|
+
hash = @log_definition.parse_entry(single_line)
|
62
|
+
next unless hash
|
63
|
+
next unless Indy::Time.inside_time_window?(hash[:time],@start_time,@end_time,@inclusive) if is_time_search
|
64
|
+
results << (block.call(hash) if block_given?)
|
65
|
+
end
|
66
|
+
results.compact
|
67
|
+
end
|
68
|
+
|
69
|
+
#
|
70
|
+
# Performs #search for multi-line based entries
|
71
|
+
#
|
72
|
+
def multiline_search(&block)
|
73
|
+
is_time_search = use_time_criteria?
|
74
|
+
source_io = StringIO.new( (is_time_search ? @source.open([@start_time,@end_time]) : @source.open ).join("\n") )
|
75
|
+
results = source_io.read.scan(@log_definition.entry_regexp).collect do |entry|
|
76
|
+
hash = @log_definition.parse_entry_captures(entry)
|
77
|
+
next unless hash
|
78
|
+
next unless Indy::Time.inside_time_window?(hash[:time],@start_time,@end_time,@inclusive) if is_time_search
|
79
|
+
block.call(hash) if block_given?
|
80
|
+
end
|
81
|
+
results.compact
|
82
|
+
end
|
83
|
+
|
84
|
+
#
|
85
|
+
# Return true if start or end time has been set, and a :time field exists
|
86
|
+
#
|
87
|
+
def use_time_criteria?
|
88
|
+
if @start_time || @end_time
|
89
|
+
# ensure both boundaries are set
|
90
|
+
@start_time ||= Indy::Time.forever_ago(@log_definition.time_format)
|
91
|
+
@end_time ||= Indy::Time.forever(@log_definition.time_format)
|
92
|
+
end
|
93
|
+
@start_time && @end_time
|
94
|
+
end
|
95
|
+
|
96
|
+
#
|
97
|
+
# Evaluates if field => value criteria is an exact match on entry
|
98
|
+
#
|
99
|
+
# @param [Hash] result The entry_hash
|
100
|
+
# @param [Hash] search_criteria The field => value criteria to match
|
101
|
+
#
|
102
|
+
def is_match?(type, result, search_criteria)
|
103
|
+
if type == :for
|
104
|
+
search_criteria.reject {|criteria,value| result[criteria] == value }.empty?
|
105
|
+
elsif type == :like
|
106
|
+
search_criteria.reject {|criteria,value| result[criteria] =~ /#{value}/i }.empty?
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
#
|
111
|
+
# Parse hash to set @start_time, @end_time and @inclusive
|
112
|
+
#
|
113
|
+
def time_scope(params_hash)
|
114
|
+
if params_hash[:time]
|
115
|
+
time_scope_from_direction(params_hash[:direction], params_hash[:span], params_hash[:time])
|
116
|
+
else
|
117
|
+
@start_time = Indy::Time.parse_date(params_hash[:start_time]) if params_hash[:start_time]
|
118
|
+
@end_time = Indy::Time.parse_date(params_hash[:end_time]) if params_hash[:end_time]
|
119
|
+
end
|
120
|
+
@inclusive = params_hash[:inclusive]
|
121
|
+
end
|
122
|
+
|
123
|
+
#
|
124
|
+
# Parse direction, span, and time to set @start_time and @end_time
|
125
|
+
#
|
126
|
+
def time_scope_from_direction(direction, span, time)
|
127
|
+
time = Indy::Time.parse_date(time)
|
128
|
+
span = (span.to_i * 60).seconds if span
|
129
|
+
if direction == :before
|
130
|
+
@end_time = time
|
131
|
+
@start_time = time - span if span
|
132
|
+
elsif direction == :after
|
133
|
+
@start_time = time
|
134
|
+
@end_time = time + span if span
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
#
|
139
|
+
# Clear time scope settings
|
140
|
+
#
|
141
|
+
def reset_scope
|
142
|
+
@inclusive = @start_time = @end_time = nil
|
143
|
+
end
|
144
|
+
|
145
|
+
end
|
146
|
+
|
147
|
+
end
|
data/lib/indy/source.rb
CHANGED
@@ -14,6 +14,9 @@ class Indy
|
|
14
14
|
# the StringIO object
|
15
15
|
attr_reader :io
|
16
16
|
|
17
|
+
# log definition
|
18
|
+
attr_reader :log_definition
|
19
|
+
|
17
20
|
# Exception raised when unable to open source
|
18
21
|
class Invalid < Exception; end
|
19
22
|
|
@@ -22,22 +25,30 @@ class Indy
|
|
22
25
|
#
|
23
26
|
# @param [String, Hash] param The source content String, filepath String, or :cmd => 'command' Hash
|
24
27
|
#
|
25
|
-
def initialize(param)
|
26
|
-
raise Indy::Source::Invalid if param.nil?
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
set_connection(:
|
31
|
-
elsif param
|
32
|
-
|
28
|
+
def initialize(param,log_definition=nil)
|
29
|
+
raise Indy::Source::Invalid, "No source specified." if param.nil?
|
30
|
+
@log_definition = log_definition || LogDefinition.new()
|
31
|
+
return discover_connection(param) unless param.respond_to?(:keys)
|
32
|
+
if param[:cmd]
|
33
|
+
set_connection(:cmd, param[:cmd])
|
34
|
+
elsif param[:file]
|
35
|
+
set_connection(:file, open_or_return_file(param[:file]))
|
36
|
+
elsif param[:string]
|
37
|
+
set_connection(:string, param[:string])
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
#
|
42
|
+
# Support source being passed in without key indicating type
|
43
|
+
#
|
44
|
+
def discover_connection(param)
|
45
|
+
if param.respond_to?(:read) and param.respond_to?(:rewind)
|
46
|
+
set_connection(:file, param)
|
33
47
|
elsif param.respond_to?(:to_s) and param.respond_to?(:length)
|
34
|
-
# fall back to source being the string passed in
|
35
48
|
set_connection(:string, param)
|
36
49
|
else
|
37
50
|
raise Indy::Source::Invalid
|
38
51
|
end
|
39
|
-
|
40
|
-
|
41
52
|
end
|
42
53
|
|
43
54
|
#
|
@@ -48,82 +59,164 @@ class Indy
|
|
48
59
|
@connection = value
|
49
60
|
end
|
50
61
|
|
62
|
+
def open_or_return_file(param)
|
63
|
+
return param if param.respond_to? :pos
|
64
|
+
file = File.open(param, 'r')
|
65
|
+
raise ArgumentError, "Unable to open file parameter: '#{file}'" unless file.respond_to? :pos
|
66
|
+
file
|
67
|
+
end
|
68
|
+
|
51
69
|
#
|
52
70
|
# Return a StringIO object to provide access to the underlying log source
|
53
71
|
#
|
54
|
-
def open(
|
72
|
+
def open(time_boundaries=nil)
|
55
73
|
begin
|
74
|
+
open_method = ('open_' + @type.to_s).intern
|
75
|
+
self.send(open_method)
|
76
|
+
rescue Exception => e
|
77
|
+
raise Indy::Source::Invalid, "Unable to open log source. (#{e.message})"
|
78
|
+
end
|
79
|
+
load_data
|
80
|
+
scope_by_time(time_boundaries) if time_boundaries
|
81
|
+
@entries
|
82
|
+
end
|
56
83
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
84
|
+
def open_cmd
|
85
|
+
@io = StringIO.new(exec_command(@connection).read)
|
86
|
+
raise "Failed to execute command (#{@connection.inspect})" if @io.nil?
|
87
|
+
end
|
61
88
|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
89
|
+
def open_file
|
90
|
+
@connection.rewind
|
91
|
+
@io = StringIO.new(@connection.read)
|
92
|
+
raise "Failed to open file: #{@connection.inspect}" if @io.nil?
|
93
|
+
end
|
66
94
|
|
67
|
-
|
68
|
-
|
95
|
+
def open_string
|
96
|
+
@io = StringIO.new(@connection)
|
97
|
+
raise "Failed to create StringIO from source (#{@connection.inspect})" if @io.nil?
|
98
|
+
end
|
69
99
|
|
70
|
-
else
|
71
|
-
raise RuntimeError, "Invalid log source type: #{@type.inspect}"
|
72
|
-
end
|
73
100
|
|
74
|
-
|
75
|
-
|
101
|
+
#
|
102
|
+
# Return entries that meet time criteria
|
103
|
+
#
|
104
|
+
def scope_by_time(time_boundaries)
|
105
|
+
start_time, end_time = time_boundaries
|
106
|
+
scope_end = num_entries - 1
|
107
|
+
# short circuit the search if possible
|
108
|
+
if (time_at(0) > end_time) or (time_at(-1) < start_time)
|
109
|
+
@entries = []
|
110
|
+
return @entries
|
111
|
+
end
|
112
|
+
scope_begin = find_first(start_time, 0, scope_end)
|
113
|
+
scope_end = find_last(end_time, scope_begin, scope_end)
|
114
|
+
@entries = @entries[scope_begin..scope_end]
|
115
|
+
end
|
116
|
+
|
117
|
+
#
|
118
|
+
# find index of first record to match value
|
119
|
+
#
|
120
|
+
def find_first(value,start,stop)
|
121
|
+
return start if time_at(start) > value
|
122
|
+
find(:first,value,start,stop)
|
123
|
+
end
|
124
|
+
|
125
|
+
#
|
126
|
+
# find index of last record to match value
|
127
|
+
#
|
128
|
+
def find_last(value,start,stop)
|
129
|
+
return stop if time_at(stop) < value
|
130
|
+
find(:last,value,start,stop)
|
131
|
+
end
|
132
|
+
|
133
|
+
#
|
134
|
+
# Find index and time at mid point
|
135
|
+
#
|
136
|
+
def find_middle(start, stop)
|
137
|
+
index = ((stop - start) / 2) + start
|
138
|
+
time = time_at(index)
|
139
|
+
[index, time]
|
140
|
+
end
|
141
|
+
|
142
|
+
#
|
143
|
+
# Step forward or backward by one, looking for the boundary of the value
|
144
|
+
#
|
145
|
+
def find_adjacent(boundary,value,start,stop,mid_index)
|
146
|
+
case boundary
|
147
|
+
when :first
|
148
|
+
(time_at(mid_index,-1) == value) ? find_first(value,start-1,stop) : mid_index
|
149
|
+
when :last
|
150
|
+
(time_at(mid_index,1) == value) ? find_last(value,start,stop+1) : mid_index
|
76
151
|
end
|
152
|
+
end
|
77
153
|
|
78
|
-
|
154
|
+
#
|
155
|
+
# Return the time of a log entry index, with an optional offset
|
156
|
+
#
|
157
|
+
def time_at(index, delta=0)
|
158
|
+
::Time.parse(@entries[index + delta])
|
159
|
+
end
|
79
160
|
|
80
|
-
|
161
|
+
#
|
162
|
+
# Binary search for a time condition
|
163
|
+
#
|
164
|
+
def find(boundary,value,start,stop)
|
165
|
+
return start if start == stop
|
166
|
+
mid_index, mid_time = find_middle(start,stop)
|
167
|
+
if mid_time == value
|
168
|
+
find_adjacent(boundary,value,start,stop,mid_index)
|
169
|
+
elsif mid_time > value
|
170
|
+
mid_index -= 1 if mid_index == stop
|
171
|
+
find(boundary, value, start, mid_index)
|
172
|
+
elsif mid_time < value
|
173
|
+
mid_index += 1 if mid_index == start
|
174
|
+
find(boundary, value, mid_index, stop)
|
175
|
+
end
|
81
176
|
end
|
82
|
-
|
177
|
+
|
83
178
|
#
|
84
179
|
# Execute the source's connection string, returning an IO object
|
85
180
|
#
|
86
181
|
# @param [String] command_string string of command that will return log contents
|
87
182
|
#
|
88
183
|
def exec_command(command_string)
|
89
|
-
|
90
|
-
|
91
|
-
return nil if io.eof?
|
92
|
-
rescue
|
93
|
-
nil
|
94
|
-
end
|
184
|
+
io = IO.popen(command_string)
|
185
|
+
raise Indy::Source::Invalid, "No data returned from command string execution" if io.eof?
|
95
186
|
io
|
96
187
|
end
|
97
188
|
|
98
|
-
|
99
|
-
|
100
189
|
#
|
101
190
|
# the number of lines in the source
|
102
191
|
#
|
103
|
-
def
|
104
|
-
load_data unless @
|
105
|
-
@
|
192
|
+
def num_entries
|
193
|
+
load_data unless @num_entries
|
194
|
+
@num_entries
|
106
195
|
end
|
107
196
|
|
108
197
|
#
|
109
198
|
# array of log lines from source
|
110
199
|
#
|
111
|
-
def
|
112
|
-
load_data unless @
|
113
|
-
@
|
200
|
+
def entries
|
201
|
+
load_data unless @entries
|
202
|
+
@entries
|
114
203
|
end
|
115
204
|
|
116
205
|
#
|
117
206
|
# read source data and populate instance variables
|
118
207
|
#
|
119
|
-
# TODO: hmmm... not called when Source#open is called directly, but #load_data would call open again. :(
|
120
|
-
#
|
121
208
|
def load_data
|
122
|
-
self.open
|
123
|
-
|
209
|
+
self.open if @io.nil?
|
210
|
+
if @log_definition.multiline
|
211
|
+
entire_log = @io.read
|
212
|
+
@entries = entire_log.scan(@log_definition.entry_regexp).map{|matchdata|matchdata[0]}
|
213
|
+
else
|
214
|
+
@entries = @io.readlines
|
215
|
+
end
|
124
216
|
@io.rewind
|
125
|
-
@
|
217
|
+
@entries.delete_if {|entry| entry.match(/^\s*$/)}
|
218
|
+
@num_entries = @entries.count
|
126
219
|
end
|
127
220
|
|
128
221
|
end
|
129
|
-
end
|
222
|
+
end
|