indy 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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