indy 0.2.0 → 0.3.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.
@@ -3,10 +3,9 @@ class Indy
3
3
  #
4
4
  # LogFormats defines the building blocks of Indy's built in log patterns.
5
5
  #
6
- # See indy/patterns.rb for the following Constants:
7
- # Indy::COMMON_LOG_PATTERN
8
- # Indy::COMBINED_LOG_PATTERN
9
- # Indy::LOG4R_DEFAULT_PATTERN
6
+ # Indy::COMMON_LOG_FORMAT
7
+ # Indy::COMBINED_LOG_FORMAT
8
+ # Indy::LOG4R_DEFAULT_FORMAT
10
9
  #
11
10
  module LogFormats
12
11
 
@@ -36,4 +35,27 @@ class Indy
36
35
  LOG4R_DEFAULT_FIELDS = [:level, :application, :message]
37
36
  LOG4R_DEFAULT_REGEXP = /(?:\s+)?([A-Z]+) (\S+): (.*)$/
38
37
  end
38
+
39
+ #
40
+ # Indy default log format
41
+ # e.g.:
42
+ # INFO 2000-09-07 MyApp - Entering APPLICATION.
43
+ #
44
+ DEFAULT_LOG_FORMAT = [LogFormats::DEFAULT_LOG_REGEXP, LogFormats::DEFAULT_LOG_FIELDS].flatten
45
+
46
+ #
47
+ # Uncustomized Log4r log format
48
+ #
49
+ LOG4R_DEFAULT_FORMAT = [LogFormats::LOG4R_DEFAULT_REGEXP, LogFormats::LOG4R_DEFAULT_FIELDS].flatten
50
+
51
+ #
52
+ # NCSA Common Log Format log format
53
+ #
54
+ COMMON_LOG_FORMAT = [LogFormats::COMMON_REGEXP, LogFormats::COMMON_FIELDS].flatten
55
+
56
+ #
57
+ # NCSA Combined Log Format log format
58
+ #
59
+ COMBINED_LOG_FORMAT = [LogFormats::COMBINED_REGEXP, LogFormats::COMBINED_FIELDS].flatten
60
+
39
61
  end
@@ -0,0 +1,9 @@
1
+
2
+ bisect_log (by time)
3
+
4
+ first_entry._time
5
+ last_entry._time
6
+ log_size
7
+ median_entry
8
+ tail( entries )
9
+
@@ -1,8 +1,8 @@
1
- #
2
- # The current implementation extends an Array. However, as this
3
- # has not been tested with a large data set it may need some new
4
- # Enumerable object that is multi-threaded and performance friendly
5
- #
6
- class ResultSet < Array
7
-
8
- end
1
+ #
2
+ # The current implementation extends an Array. However, as this
3
+ # has not been tested with a large data set it may need some new
4
+ # Enumerable object that is multi-threaded and performance friendly
5
+ #
6
+ class ResultSet < Array
7
+
8
+ end
@@ -0,0 +1,129 @@
1
+ class Indy
2
+
3
+ #
4
+ # A StringIO interface to the underlying log source.
5
+ #
6
+ class Source
7
+
8
+ # log source type. :cmd, :file, or :string
9
+ attr_reader :type
10
+
11
+ # log source connection string (cmd, filename or log data)
12
+ attr_reader :connection
13
+
14
+ # the StringIO object
15
+ attr_reader :io
16
+
17
+ # Exception raised when unable to open source
18
+ class Invalid < Exception; end
19
+
20
+ ##
21
+ # Creates a Source object.
22
+ #
23
+ # @param [String, Hash] param The source content String, filepath String, or :cmd => 'command' Hash
24
+ #
25
+ def initialize(param)
26
+ raise Indy::Source::Invalid if param.nil?
27
+ if param.respond_to?(:keys)
28
+ set_connection(:cmd, param[:cmd]) if param[:cmd]
29
+ set_connection(:file, param[:file]) if ( param[:file] and param[:file].size > 0 )
30
+ set_connection(:string, param[:string]) if param[:string]
31
+ elsif param.respond_to?(:read) and param.respond_to?(:rewind)
32
+ set_connection(:file, param)
33
+ elsif param.respond_to?(:to_s) and param.respond_to?(:length)
34
+ # fall back to source being the string passed in
35
+ set_connection(:string, param)
36
+ else
37
+ raise Indy::Source::Invalid
38
+ end
39
+
40
+
41
+ end
42
+
43
+ #
44
+ # set the source connection type and connection_string
45
+ #
46
+ def set_connection(type, value)
47
+ @type = type
48
+ @connection = value
49
+ end
50
+
51
+ #
52
+ # Return a StringIO object to provide access to the underlying log source
53
+ #
54
+ def open(time_search=nil)
55
+ begin
56
+
57
+ case @type
58
+ when :cmd
59
+ @io = StringIO.new( exec_command(@connection).read )
60
+ raise "Failed to execute command (#{@connection})" if @io.nil?
61
+
62
+ when :file
63
+ @connection.rewind
64
+ @io = StringIO.new(@connection.read)
65
+ raise "Failed to open file: #{@connection}" if @io.nil?
66
+
67
+ when :string
68
+ @io = StringIO.new( @connection )
69
+
70
+ else
71
+ raise RuntimeError, "Invalid log source type: #{@type.inspect}"
72
+ end
73
+
74
+ rescue Exception => e
75
+ raise Indy::Source::Invalid, "Unable to open log source. (#{e.message})"
76
+ end
77
+
78
+ # scope_by_time(source_io) if time_search
79
+
80
+ @io
81
+ end
82
+
83
+ #
84
+ # Execute the source's connection string, returning an IO object
85
+ #
86
+ # @param [String] command_string string of command that will return log contents
87
+ #
88
+ def exec_command(command_string)
89
+ begin
90
+ io = IO.popen(command_string)
91
+ return nil if io.eof?
92
+ rescue
93
+ nil
94
+ end
95
+ io
96
+ end
97
+
98
+
99
+
100
+ #
101
+ # the number of lines in the source
102
+ #
103
+ def num_lines
104
+ load_data unless @num_lines
105
+ @num_lines
106
+ end
107
+
108
+ #
109
+ # array of log lines from source
110
+ #
111
+ def lines
112
+ load_data unless @lines
113
+ @lines
114
+ end
115
+
116
+ #
117
+ # read source data and populate instance variables
118
+ #
119
+ # TODO: hmmm... not called when Source#open is called directly, but #load_data would call open again. :(
120
+ #
121
+ def load_data
122
+ self.open
123
+ @lines = @io.readlines
124
+ @io.rewind
125
+ @num_lines = @lines.count
126
+ end
127
+
128
+ end
129
+ end
@@ -0,0 +1,13 @@
1
+
2
+ require 'scanf'
3
+
4
+ f = File.open('spec/multiline.log')
5
+
6
+ first_capture_regexp = '\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}'
7
+
8
+ full_regexp = "^(#{first_capture_regexp})\\s+([A-Z]+)\\s+(.+?)(?=^#{first_capture_regexp}|\\z)"
9
+
10
+ f.read.scan(/#{full_regexp}/m) do |entry|
11
+ puts "Entry::::\n#{entry}"
12
+ end
13
+
@@ -4,7 +4,7 @@ describe "Search Performance" do
4
4
 
5
5
  context "with a 10000 line log file" do
6
6
 
7
- large_file = "#{File.dirname(__FILE__)}/large.log"
7
+ large_file = File.open("#{File.dirname(__FILE__)}/large.log", 'r')
8
8
  before(:all) do
9
9
  @indy = Indy.search(large_file).with([/^\[([^\|]+)\|([^\]]+)\] (.*)$/,:severity, :time, :message])
10
10
  end
@@ -33,11 +33,11 @@ describe "Search Performance" do
33
33
 
34
34
  context "with a 10000 line log file" do
35
35
 
36
- large_file = "#{File.dirname(__FILE__)}/large.log"
36
+ large_file = File.open("#{File.dirname(__FILE__)}/large.log", 'r')
37
37
  before(:all) do
38
38
  @indy = Indy.new(
39
39
  :source => large_file,
40
- :pattern => [/^\[([^\|]+)\|([^\]]+)\] (.*)$/,:severity, :time, :message],
40
+ :log_format => [/^\[([^\|]+)\|([^\]]+)\] (.*)$/,:severity, :time, :message],
41
41
  :time_format => '%d-%m-%Y %H:%M:%S')
42
42
  end
43
43
 
@@ -1,4 +1,8 @@
1
+ require 'simplecov'
2
+ SimpleCov.start if ENV["COVERAGE"]
3
+ # SimpleCov.root("#{File.dirname(__FILE__)}/../..")
4
+
1
5
  require File.expand_path("#{File.dirname(__FILE__)}/../lib/indy") unless
2
6
  $:.include? File.expand_path("#{File.dirname(__FILE__)}/../lib/indy")
3
7
 
4
- require 'rspec'
8
+ # require 'rspec'
@@ -6,30 +6,31 @@ describe 'Indy' do
6
6
 
7
7
  # http://log4r.rubyforge.org/rdoc/Log4r/rdoc/patternformatter.html
8
8
  it "should accept a log4r pattern string without error" do
9
- lambda { Indy.new(:pattern => ["(%d) (%i) (%c) - (%m)", :time, :info, :class, :message]) }.should_not raise_error
9
+ Indy.new(:log_format => ["(%d) (%i) (%c) - (%m)", :time, :info, :class, :message]).class.should == Indy
10
10
  end
11
11
 
12
12
  # http://logging.apache.org/log4j/1.2/apidocs/org/apache/log4j/PatternLayout.html
13
13
  it "should accept a log4j pattern string without error" do
14
- lambda { Indy.new(:pattern => ["%d [%M] %p %C{1} - %m", :time, :info, :class, :message])}
14
+ Indy.new(:log_format => ["%d [%M] %p %C{1} - %m", :time, :info, :class, :message]).class.should == Indy
15
15
  end
16
16
 
17
17
  it "should not raise error with non-conforming data" do
18
- @indy = Indy.new(:source => " \nfoobar\n\n baz", :pattern => ['([^\s]+) (\w+)', :time, :message])
19
- lambda{ @indy.for(:all) }.should_not raise_error
18
+ @indy = Indy.new(:source => " \nfoobar\n\n baz", :log_format => ['([^\s]+) (\w+)', :time, :message])
19
+ @indy.for(:all).class.should == Array
20
20
  end
21
21
 
22
22
  it "should accept time_format parameter" do
23
- @indy = Indy.new(:time_format => '%d-%m-%Y', :source => "1-13-2000 yes", :pattern => ['^([^\s]+) (\w+)$', :time, :message])
24
- lambda{ @indy.for(:all) }.should_not raise_error
23
+ @indy = Indy.new(:time_format => '%d-%m-%Y', :source => "1-13-2000 yes", :log_format => ['^([^\s]+) (\w+)$', :time, :message])
24
+ @indy.for(:all).class.should == Array
25
25
  @indy.instance_variable_get(:@time_format).should == '%d-%m-%Y'
26
26
  end
27
27
 
28
28
  it "should accept an initialization hash passed to #search" do
29
29
  hash = {:time_format => '%d-%m-%Y',
30
30
  :source => "1-13-2000 yes",
31
- :pattern => ['^([^\s]+) (\w+)$', :time, :message]}
32
- lambda{ @indy = Indy.search( hash ) }.should_not raise_error
31
+ :log_format => ['^([^\s]+) (\w+)$', :time, :message]}
32
+ @indy = Indy.search(hash)
33
+ @indy.class.should == Indy
33
34
  @indy.for(:all).length.should == 1
34
35
  end
35
36
 
@@ -39,7 +40,7 @@ describe 'Indy' do
39
40
  context 'instance' do
40
41
 
41
42
  before(:all) do
42
- @indy = Indy.new(:source => '1/2/2002 string', :pattern => ['([^\s]+) (\w+)', :time, :message])
43
+ @indy = Indy.new(:source => '1/2/2002 string', :log_format => ['([^\s]+) (\w+)', :time, :message])
43
44
  end
44
45
 
45
46
  context "method" do
@@ -60,16 +61,18 @@ describe 'Indy' do
60
61
 
61
62
  context ':search' do
62
63
 
64
+ let(:log_file) { "#{File.dirname(__FILE__)}/data.log" }
65
+
63
66
  it "should be a class method" do
64
67
  Indy.should respond_to(:search)
65
68
  end
66
69
 
67
70
  it "should accept a string parameter" do
68
- lambda{ Indy.search("String Log") }.should_not raise_error
71
+ Indy.search("String Log").class.should == Indy
69
72
  end
70
73
 
71
74
  it "should accept a :cmd symbol and a command string parameter" do
72
- lambda{ Indy.search(:cmd =>"ls") }.should_not raise_error
75
+ Indy.search(:cmd =>"ls").class.should == Indy
73
76
  end
74
77
 
75
78
  it "should return an instance of Indy" do
@@ -77,63 +80,46 @@ describe 'Indy' do
77
80
  Indy.search(:cmd => "ls").should be_kind_of(Indy)
78
81
  end
79
82
 
83
+ it "should return an instance of Indy" do
84
+ Indy.search(:source => {:cmd => 'ls'}, :log_format => Indy::DEFAULT_LOG_FORMAT).class.should == Indy
85
+ end
86
+
87
+
88
+ it "should create an instance of Indy::Source" do
89
+ Indy.search("source string").instance_variable_get(:@source).should be_kind_of(Indy::Source)
90
+ end
91
+
80
92
  it "the instance should have the source specified" do
81
93
  Indy.search("source string").source.should_not be_nil
82
94
  Indy.search(:cmd => "ls").source.should_not be_nil
83
95
  end
84
96
 
85
- it "the instance should raise an exception when passed an invalid source: Fixnum" do
86
- lambda{ Indy.search(9) }.should raise_error Indy::InvalidSource
97
+ it "the instance should raise an exception when passed an invalid source" do
98
+ lambda{ Indy.search(nil) }.should raise_error Indy::Source::Invalid
87
99
  end
88
100
 
89
101
  it "the instance should raise an exception when passed an invalid source: nil" do
90
- lambda{ Indy.search(nil) }.should raise_error Indy::InvalidSource
102
+ lambda{ Indy.search(nil) }.should raise_error Indy::Source::Invalid
91
103
  end
92
104
 
93
105
  it "the instance should raise an exception when the arity is incorrect" do
94
- lambda{ Indy.search( ) }.should raise_error Indy::InvalidSource
106
+ lambda{ Indy.search( ) }.should raise_error Indy::Source::Invalid
95
107
  end
96
108
 
97
- context "for a String" do
98
-
99
- let(:log_file) { "#{File.dirname(__FILE__)}/data.log" }
100
-
101
- context "treat it first like a file" do
102
-
103
- it "should attempt to open the file" do
104
- File.should_receive(:exist?).with("possible_file.ext").ordered
105
- Indy.search("possible_file.ext")
106
- end
107
-
108
- it "should not throw an error for a non-existent file" do
109
- lambda { Indy.search("possible_file.ext") }.should_not raise_error
110
- end
111
-
112
- it "should return an IO object when there is a file" do
113
- File.should_receive(:exist?).with("file_exists.ext").and_return( true )
114
- File.should_receive(:open).and_return(StringIO.new("2000-09-07 14:07:41 INFO MyApp - Entering APPLICATION."))
115
- Indy.search("file_exists.ext").for(:application => 'MyApp').length.should == 1
116
- end
117
-
118
- it "should handle a real file" do
119
- Indy.search(log_file).for(:application => 'MyApp').length.should == 2
120
- end
109
+ context "treat it second like a string" do
121
110
 
111
+ it "should attempt to treat it as a string" do
112
+ string = "2000-09-07 14:07:41 INFO MyApp - Entering APPLICATION."
113
+ string_io = StringIO.new(string)
114
+ StringIO.should_receive(:new).with(string).ordered.and_return(string_io)
115
+ Indy.search(string).for(:application => 'MyApp').length.should == 1
122
116
  end
123
117
 
124
- context "treat it second like a string" do
125
-
126
- it "should attempt to treat it as a string" do
127
- string = "2000-09-07 14:07:41 INFO MyApp - Entering APPLICATION."
128
- string_io = StringIO.new(string)
129
- StringIO.should_receive(:new).with(string).ordered.and_return(string_io)
130
- Indy.search(string).for(:application => 'MyApp').length.should == 1
131
- end
132
-
133
- end
118
+ end
134
119
 
120
+ context "with explicit source hash" do
135
121
 
136
- context "treat it optionally like a command" do
122
+ context ":cmd" do
137
123
 
138
124
  it "should attempt open the command" do
139
125
  IO.stub!(:popen).with('ssh user@system "bash --login -c \"cat /var/log/standard.log\" "')
@@ -142,7 +128,7 @@ describe 'Indy' do
142
128
 
143
129
  it "should not throw an error for an invalid command" do
144
130
  IO.stub!(:popen).with('an invalid command').and_return('')
145
- lambda { Indy.search(:cmd => "an invalid command") }.should_not raise_error
131
+ Indy.search(:cmd => "an invalid command").class.should == Indy
146
132
  end
147
133
 
148
134
  it "should return an IO object upon a successful command" do
@@ -154,6 +140,32 @@ describe 'Indy' do
154
140
  Indy.search(:cmd => "cat #{log_file}").for(:application => 'MyApp').length.should == 2
155
141
  end
156
142
 
143
+ it "should return an IO object upon a successful command" do
144
+ IO.stub!(:popen).with("zzzzzzzzzzzz").and_return('Invalid command')
145
+ lambda{ Indy.search(:cmd => "zzzzzzzzzzzz").for(:all) }.should raise_error( Indy::Source::Invalid, /Unable to open log source/)
146
+ end
147
+
148
+ it "should raise error for an invalid command" do
149
+ lambda{ Indy.search(:cmd => "zzzzzzzzzzzz").for(:all) }.should raise_error( Indy::Source::Invalid, /Unable to open log source/)
150
+ end
151
+
152
+ end
153
+
154
+ it ":file" do
155
+ require 'tempfile'
156
+ file = stub!(:size).and_return(1)
157
+ lambda{ Indy.search(:file => file) }
158
+ end
159
+
160
+ it ":string" do
161
+ string = "2000-09-07 14:07:41 INFO MyApp - Entering APPLICATION."
162
+ string_io = StringIO.new(string)
163
+ StringIO.should_receive(:new).with(string).ordered.and_return(string_io)
164
+ Indy.search(:string => string).for(:application => 'MyApp').length.should == 1
165
+ end
166
+
167
+ it "should raise error when invalid" do
168
+ lambda{ Indy.search(:foo => "a string").for(:all) }.should raise_error( Indy::Source::Invalid )
157
169
  end
158
170
 
159
171
  end
@@ -183,7 +195,7 @@ describe 'Indy' do
183
195
  "2000-09-07 14:07:42 DEBUG MyApp - Initializing APPLICATION.",
184
196
  "2000-09-07 14:07:43 INFO MyApp - Exiting APPLICATION with data:\nApplications data\nMore data\n\tlast Application data."].join("\n")
185
197
  regexp = "^((#{Indy::LogFormats::DEFAULT_DATE_TIME})\\s+(#{Indy::LogFormats::DEFAULT_SEVERITY_PATTERN})\\s+(#{Indy::LogFormats::DEFAULT_APPLICATION})\\s+-\\s+(.*?)(?=#{Indy::LogFormats::DEFAULT_DATE_TIME}|\\z))"
186
- @indy = Indy.new(:source => log, :pattern => [regexp, :time,:severity,:application,:message], :multiline => true )
198
+ @indy = Indy.new(:source => log, :log_format => [regexp, :time,:severity,:application,:message], :multiline => true )
187
199
  end
188
200
 
189
201
  it "should find the first row" do
@@ -197,8 +209,51 @@ describe 'Indy' do
197
209
  results.length.should == 5
198
210
  end
199
211
 
212
+ it "should find using time based search" do
213
+ results = @indy.before(:time => '2000-09-07 14:07:42', :inclusive => false).for(:all)
214
+ results.length.should == 2
215
+ end
216
+
200
217
  end
201
218
 
219
+ context "support for blocks" do
220
+
221
+ def log
222
+ [ "2000-09-07 14:07:41 INFO MyApp - Entering APPLICATION.",
223
+ "2000-09-07 14:07:42 DEBUG MyApp - Initializing APPLICATION.",
224
+ "2000-09-07 14:07:43 INFO MyApp - Exiting APPLICATION."].join("\n")
225
+ end
226
+
227
+ it "should allow a block with :for and yield on each line of the results using :all" do
228
+ actual_yield_count = 0
229
+ Indy.search(log).for(:all) do |result|
230
+ result.should be_kind_of(Struct::Line)
231
+ actual_yield_count = actual_yield_count + 1
232
+ end
233
+ actual_yield_count.should == 3
234
+ end
235
+
236
+ it "should allow a block with :for and yield on each line of the results" do
237
+ actual_yield_count = 0
238
+ Indy.search(log).for(:severity => 'INFO') do |result|
239
+ result.should be_kind_of(Struct::Line)
240
+ actual_yield_count = actual_yield_count + 1
241
+ end
242
+ actual_yield_count.should == 2
243
+ end
244
+
245
+ it "should allow a block with :like and yield on each line of the results" do
246
+ actual_yield_count = 0
247
+ Indy.search(log).like(:message => '\be\S+ing') do |result|
248
+ result.should be_kind_of(Struct::Line)
249
+ actual_yield_count = actual_yield_count + 1
250
+ end
251
+ actual_yield_count.should == 2
252
+ end
253
+
254
+ end
255
+
256
+
202
257
  context "instance" do
203
258
 
204
259
  before(:each) do
@@ -213,11 +268,11 @@ describe 'Indy' do
213
268
  end
214
269
 
215
270
  it "with() should accept the log4r default pattern const without error" do
216
- lambda { @indy.with(Indy::LOG4R_DEFAULT_PATTERN) }.should_not raise_error
271
+ @indy.with(Indy::LOG4R_DEFAULT_FORMAT).class.should == Indy
217
272
  end
218
273
 
219
274
  it "with() should accept :default without error" do
220
- lambda { @indy.with(:default) }.should_not raise_error
275
+ @indy.with(:default).class.should == Indy
221
276
  end
222
277
 
223
278
  it "with() should use default log pattern when passed :default" do
@@ -225,7 +280,7 @@ describe 'Indy' do
225
280
  end
226
281
 
227
282
  it "with() should accept no params without error" do
228
- lambda { @indy.with() }.should_not raise_error
283
+ @indy.with().class.should == Indy
229
284
  end
230
285
 
231
286
  it "should return itself" do
@@ -238,7 +293,7 @@ describe 'Indy' do
238
293
  end
239
294
 
240
295
  it "#{method}() should accept a hash of search criteria" do
241
- lambda { @indy.send(method,:severity => "INFO") }.should_not raise_error
296
+ @indy.send(method,:severity => "INFO").class.should == Array
242
297
  end
243
298
 
244
299
  it "#{method}() should return a set of results" do
@@ -300,4 +355,21 @@ describe 'Indy' do
300
355
 
301
356
  end
302
357
 
358
+ context 'source' do
359
+ before(:each) do
360
+ @indy = Indy.search(
361
+ [ "2000-09-07 14:07:41 INFO MyApp - Entering APPLICATION.",
362
+ "2000-09-07 14:08:41 INFO MyApp - Initializing APPLICATION.",
363
+ "2000-09-07 14:09:41 INFO MyApp - Configuring APPLICATION.",
364
+ "2000-09-07 14:10:50 INFO MyApp - Running APPLICATION.",
365
+ "2000-09-07 14:11:42 INFO MyApp - Exiting APPLICATION.",
366
+ "2000-09-07 14:12:15 INFO MyApp - Exiting APPLICATION."
367
+ ].join("\n") )
368
+ end
369
+
370
+ it "should know how many lines it contains" do
371
+ @indy.source.send(:num_lines).should == 6
372
+ end
373
+ end
374
+
303
375
  end