indy 0.1.5 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "http://rubygems.org"
2
+
3
+ gemspec
@@ -1,3 +1,14 @@
1
+ === 0.2.0 / 2011-03-08
2
+
3
+ * Support for Multiline log entries. See README.md
4
+ * Time scopes now respect inclusive flag
5
+ * #last() no longer supports number of rows as a parameter. Use :span => minutes.
6
+ * Fixes for Ruby 1.9.2
7
+
8
+ === 0.1.6 / 2011-03-07
9
+
10
+ * Unsupported. Gem version 0.1.6 == 0.2.0
11
+
1
12
  === 0.1.5 / 2011-01-21
2
13
 
3
14
  * Searching with time scopes (#after, #within, #before) are much faster
data/README.md CHANGED
@@ -40,16 +40,22 @@ Usage
40
40
 
41
41
  Indy.search(log_string).for(:message => 'Entering application')
42
42
 
43
- ## Specify your Pattern
44
-
45
- The default search pattern resembles something you might find:
46
-
47
- YYYY-MM-DD HH:MM:SS SEVERITY APPLICATION_NAME - MESSAGE
43
+ ## Log Pattern
48
44
 
49
45
  ### Default Log Pattern
50
46
 
51
- Indy.search(source).for(:severity => 'INFO')
52
- Indy.search(source).for(:application => 'MyApp', :severity => 'DEBUG')
47
+ The default search pattern follows this form:
48
+ YYYY-MM-DD HH:MM:SS SEVERITY APPLICATION_NAME - MESSAGE
49
+
50
+ Which uses this regexp:
51
+ /^(\d{4}.\d{2}.\d{2}\s+\d{2}.\d{2}.\d{2})\s+(TRACE|DEBUG|INFO|WARN|ERROR|FATAL)\s+(\w+)\s+-\s+(.+)$/
52
+
53
+ and specifies these fields:
54
+ [:time, :severity, :application, :message]
55
+
56
+ For example:
57
+ Indy.search(source).for(:severity => 'INFO')
58
+ Indy.search(source).for(:application => 'MyApp', :severity => 'DEBUG')
53
59
 
54
60
  ### Custom Log Pattern
55
61
 
@@ -73,6 +79,36 @@ Several log formats have been predefined for ease of configuration. See indy/pat
73
79
  # INFO mylog: This is a message with level INFO
74
80
  Indy.new(:source => 'logfile.txt', :pattern => Indy::LOG4R_DEFAULT_PATTERN).for(:application => 'mylog')
75
81
 
82
+ ### Multiline log entries
83
+
84
+ By default, Indy assumes that log lines are separated by new lines. Any lines that don't match the active pattern are ignored. To enable multiline log entries you must do two things:
85
+
86
+ 1. Use `Indy.new()` and include the `:multiline => true` parameter
87
+ 2. Use a log entry regexp that does not use `$` and/or `\n` to define the end of the entry.
88
+
89
+ #### Multiline Regexp tips
90
+
91
+ * Use non-greedy matching when needed: `.*?` instead of `.*`
92
+ * Assuming your log entries do not include a unique line ending, you can use a zero-width positive lookahead assertion to verify that each line is followed by the start of a valid log entry, or the end of the string. e.g.: `(?=^foo|\z)`
93
+
94
+ Check out [Regexp Extensions](http://www.ruby-doc.org/docs/ProgrammingRuby/html/language.html#UN)
95
+
96
+ Example:
97
+
98
+ # Given this log containing two entries:
99
+ #
100
+ # INFO MyApp - Multiline message begins here
101
+ # and ends here
102
+ # DEBUG MyOtherApp - Single line message
103
+
104
+ severity_string = 'DEBUG|INFO|WARN|ERROR|FATAL'
105
+
106
+ # single line regexp would be:
107
+ # /^(#{severity_string}) (\w+) - (.*)$/
108
+ multiline_regexp = /^(#{severity_string}) (\w+) - (.*?)(?=^#{severity_string}|\z)/
109
+
110
+ Indy.new( :multiline => true, :pattern => [multiline_regexp, :severity, :application, :message], :source => MY_LOG)
111
+
76
112
  ### Explicit Time Format
77
113
 
78
114
  By default, Indy tries to guess your time format (courtesy of DateTime#parse). If you supply an explicit time format, it will use DateTime#strptime, as well as try to guess.
@@ -94,13 +130,6 @@ This is required when log data uses a non-standard date format, e.g.: U.S. forma
94
130
  Indy.search(source).for(:message => 'Entering Application', :application => 'MyApp')
95
131
  Indy.search(source).for(:severity => 'INFO', :application => 'MyApp')
96
132
 
97
- ### Time Scope
98
-
99
- Indy.search(source).after(:time => '2011-01-13 13:40:00').for(:all)
100
- Indy.search(source).before(:time => '2010-12-31 23:59:59').for(:all)
101
- Indy.search(source).around(:time => '2011-01-01 00:00:00', :span => 2).for(:all) # 2 minutes around New Year's Eve
102
- Indy.search(source).within(:time => ['2011-01-01 00:00:00','2011-02-01 00:00:00']).for(:severity => 'ERROR', :application => 'MyApp')
103
-
104
133
  ### Partial Match
105
134
 
106
135
  Indy.search(source).like(:message => 'Memory')
@@ -109,17 +138,46 @@ This is required when log data uses a non-standard date format, e.g.: U.S. forma
109
138
 
110
139
  Indy.search(source).like(:severity => '(?:INFO|DEBUG)', :message => 'Memory')
111
140
 
141
+ ## Log Scopes
142
+
143
+ Multiple scope methods can be called on an instance. Use #reset_scope to remove scope constrints on the instance.
144
+
145
+ ### Time Scope
146
+
147
+ # After Dec 1
148
+ Indy.search(source).after(:time => '2010-12-01 23:59:59').for(:all)
149
+
150
+ # 20 minutes Around New Year's eve
151
+ Indy.search(source).around(:time => '2011-01-01 00:00:00', :span => 20).for(:all)
152
+
153
+ # After Jan 1 but Before Feb 1
154
+ @log = Indy.search(source)
155
+ @log.after(:time => '2011-01-01 00:00:00').before(:time => '2011-02-01 00:00:00')
156
+ @log.for(:all)
157
+
158
+ # Within Jan 1 and Feb 1 (same time scope as above)
159
+ Indy.search(source).within(:time => ['2011-01-01 00:00:00','2011-02-01 00:00:00']).for(:all)
160
+
161
+ # After Jan 1
162
+ @log = Indy.search(source)
163
+ @log.after(:time => '2011-01-01 00:00:00')
164
+ @log.for(:all)
165
+ # Reset the time scope to include entries before Jan 1
166
+ @log.reset_scope
167
+ # Before Feb 1
168
+ @log.before(:time => '2011-02-01 00:00:00')
169
+ @log.for(:all)
170
+
112
171
  ## Process the Results
113
172
 
114
- A ResultSet (Array) is returned by #for, #like, #after, etc.
173
+ A ResultSet is returned by #for and #like, which is an Enumerable containing a hash for each log entry.
115
174
 
116
175
  entries = Indy.search(source).for(:message => 'Entering Application')
176
+ entries.first.keys
177
+ # => [:line, :time, :severity, :application, :message]
117
178
 
118
179
  Indy.search(source).for(:message => 'Entering Application').each do |entry|
119
-
120
- # each log line entry returned is an OpenStruct object
121
180
  puts "[#{entry.time}] #{entry.message}: #{entry.application}"
122
-
123
181
  end
124
182
 
125
183
  LICENSE
data/Rakefile CHANGED
@@ -1,9 +1,9 @@
1
1
  require 'rake'
2
2
  require 'rspec/core'
3
3
  require 'rspec/core/rake_task'
4
+ require 'rcov/rcovtask'
4
5
  require "cucumber/rake/task"
5
6
  require "yard"
6
- require "city"
7
7
 
8
8
  desc 'Default: run tests'
9
9
  task :default => :test
@@ -55,15 +55,16 @@ task :flog_detail do
55
55
  system('find lib -name \*.rb | xargs flog -d')
56
56
  end
57
57
 
58
- desc "Generate code coverage"
59
- RSpec::Core::RakeTask.new(:coverage) do |t|
60
- t.pattern = "./spec/**/*_spec.rb" # don't need this, it's default.
61
- t.rcov = true
58
+ # Task :rcov -- Run RCOV to Generate code coverage report
59
+ Rcov::RcovTask.new do |t|
60
+ t.libs << "lib"
61
+ t.test_files = FileList['spec/*.rb']
62
62
  t.rcov_opts = ['--exclude', 'spec', '--exclude', 'gems', '-T']
63
+ t.verbose = true
63
64
  end
64
65
 
65
-
66
- YARD::Rake::CitydocTask.new do |t|
66
+ # Task :yard -- Generate yard + yard-cucumber docs
67
+ YARD::Rake::YardocTask.new do |t|
67
68
  t.files = ['features/**/*', 'lib/**/*.rb']
68
69
  t.options = ['--private']
69
70
  end
@@ -0,0 +1,4 @@
1
+ Indy
2
+ ====
3
+
4
+ Searching through logs includes before, around, after, and exact matching.
@@ -30,5 +30,5 @@ Scenario: Count of entries for a time span before and including a specified time
30
30
  Then I expect to have found 2 log entries
31
31
 
32
32
  Scenario: Count of entries for a time span after and including a specified time
33
- When searching the log for all entries 31 minutes after and including the time 2000-09-07 14:17:43
34
- Then I expect to have found 3 log entries
33
+ When searching the log for all entries 30 minutes after and including the time 2000-09-07 14:17:43
34
+ Then I expect to have found 4 log entries
@@ -1,6 +1,6 @@
1
1
  require File.dirname(__FILE__) + "/lib/indy"
2
2
 
3
- module Indy
3
+ class Indy
4
4
 
5
5
  def self.show_version_changes(version)
6
6
  date = ""
@@ -32,7 +32,7 @@ Gem::Specification.new do |s|
32
32
  s.authors = ["Franklin Webber","Brandon Faloona"]
33
33
  s.description = %{ Indy is a log archelogy library that treats logs like data structures. Search fixed format or custom logs by field and/or time. }
34
34
  s.summary = "Log Search Library"
35
- s.email = 'franklin.webber@gmail.com'
35
+ s.email = 'brandon@faloona.net'
36
36
  s.homepage = "http://github.com/burtlo/Indy"
37
37
  s.license = 'MIT'
38
38
 
@@ -40,9 +40,9 @@ Gem::Specification.new do |s|
40
40
  s.required_ruby_version = '>= 1.8.5'
41
41
  s.add_dependency('activesupport', '>= 2.3.5')
42
42
 
43
- s.add_development_dependency('cucumber', '>= 0.9.2')
43
+ s.add_development_dependency('cucumber', '>= 0.10.0')
44
44
  s.add_development_dependency('yard', '>= 0.6.4')
45
- s.add_development_dependency('cucumber-in-the-yard', '>= 1.7.7')
45
+ s.add_development_dependency('yard-cucumber', '>= 2.0.0')
46
46
  s.add_development_dependency('rspec', '>= 2.4.0')
47
47
  s.add_development_dependency('rspec-mocks', '>= 2.4.0')
48
48
  s.add_development_dependency('rspec-prof', '>= 0.0.3')
@@ -62,11 +62,12 @@ Gem::Specification.new do |s|
62
62
 
63
63
  }
64
64
 
65
- # exclusions = [File.join("performance", "large.log")]
66
- # s.files = `git ls-files`.split("\n") - exclusions
67
-
65
+
68
66
  s.rubygems_version = "1.3.7"
69
- s.files = `git ls-files`.split("\n")
67
+
68
+ exclusions = [File.join("performance", "large.log")]
69
+ s.files = `git ls-files`.split("\n") - exclusions
70
+
70
71
  s.extra_rdoc_files = ["README.md", "History.txt"]
71
72
  s.rdoc_options = ["--charset=UTF-8"]
72
73
  s.require_path = "lib"
@@ -4,7 +4,7 @@ class Indy
4
4
 
5
5
  class InvalidSource < Exception; end
6
6
 
7
- VERSION = "0.1.5"
7
+ VERSION = "0.2.0"
8
8
 
9
9
  #
10
10
  # hash with one key (:string, :file, or :cmd) set to the string that defines the log
@@ -22,6 +22,15 @@ class Indy
22
22
  #
23
23
  attr_accessor :time_format
24
24
 
25
+ #
26
+ # initialization flag required if multiline log entries are allowed
27
+ #
28
+ # @example
29
+ #
30
+ # Indy.new(:source => MY_LOG, :pattern => [MY_REGEXP, FIELD1, FIELD2, FIELD3], :multiline => true)
31
+ #
32
+ attr_accessor :multiline
33
+
25
34
  #
26
35
  # Initialize Indy.
27
36
  #
@@ -34,14 +43,14 @@ class Indy
34
43
  # Indy.new(:time_format => '%m-%d-%Y',:pattern => [LOG_REGEX_PATTERN,:time,:application,:message],:source => LOG_FILENAME)
35
44
  #
36
45
  def initialize(args)
37
- @source = @pattern = @time_format = @log_regexp = @log_fields = nil
46
+ @source = @pattern = @time_format = @log_regexp = @log_fields = @multiline = nil
38
47
  @source = Hash.new
39
48
 
40
49
  while (arg = args.shift) do
41
50
  send("#{arg.first}=",arg.last)
42
51
  end
43
52
 
44
- update_log_pattern(@pattern)
53
+ update_log_pattern( @pattern )
45
54
 
46
55
  end
47
56
 
@@ -71,7 +80,7 @@ class Indy
71
80
  #
72
81
  def search(params=nil)
73
82
 
74
- raise Indy::InvalidSource if params.nil?
83
+ raise Indy::InvalidSource if params.nil? || params.is_a?(Fixnum)
75
84
 
76
85
  if params.respond_to?(:keys) && params[:source]
77
86
  Indy.new(params)
@@ -111,8 +120,6 @@ class Indy
111
120
  def for(search_criteria)
112
121
  results = ResultSet.new
113
122
 
114
- define_struct
115
-
116
123
  case search_criteria
117
124
  when Enumerable
118
125
  results += _search do |result|
@@ -139,7 +146,6 @@ class Indy
139
146
  #
140
147
  def like(search_criteria)
141
148
  results = ResultSet.new
142
- define_struct
143
149
 
144
150
  results += _search do |result|
145
151
  create_struct(result) if search_criteria.reject {|criteria,value| result[criteria] =~ /#{value}/ }.empty?
@@ -152,7 +158,74 @@ class Indy
152
158
 
153
159
 
154
160
  #
155
- # After scopes the eventual search to all entries after to this point.
161
+ # Last() scopes the eventual search to the last N minutes worth of entries.
162
+ #
163
+ # @param [Hash] scope_criteria hash describing the amount of time at
164
+ # the last portion of the source
165
+ #
166
+ # @example For last 10 minutes worth of entries
167
+ #
168
+ # Indy.search(LOG_FILE).last(:span => 100).for(:all)
169
+ #
170
+ def last(scope_criteria)
171
+ case scope_criteria
172
+ when Enumerable
173
+ raise ArgumentError unless scope_criteria[:span] || scope_criteria[:rows]
174
+
175
+ if scope_criteria[:span]
176
+ span = (scope_criteria[:span].to_i * 60).seconds
177
+ starttime = parse_date(last_entry[:_time]) - span
178
+
179
+ within(:time => [starttime, forever])
180
+ end
181
+ else
182
+ raise ArgumentError, "Invalid parameter: #{scope_criteria.inspect}"
183
+ end
184
+
185
+ self
186
+ end
187
+
188
+ #
189
+ # Return a Struct::Line for the last valid entry from the source
190
+ #
191
+ def last_entry
192
+ last_entries(1)
193
+ end
194
+
195
+ #
196
+ # Return an array of Struct::Line entries for the last N valid entries from the source
197
+ #
198
+ # @param [Fixnum] num the number of rows to retrieve
199
+ #
200
+ def last_entries(num)
201
+
202
+ num_entries = 0
203
+ result = []
204
+
205
+ source_io = open_source
206
+ source_io.reverse_each do |line|
207
+
208
+ hash = parse_line(line)
209
+
210
+ set_time(hash) if @time_field
211
+
212
+ if hash
213
+ num_entries += 1
214
+ result << hash
215
+ break if num_entries >= num
216
+ end
217
+ end
218
+
219
+ warn "No matching lines found in source: #{source_io.class}" if result.empty?
220
+
221
+ source_io.close if @source[:file] || @source[:cmd]
222
+
223
+ num == 1 ? create_struct(result.first) : result.collect{|e| create_struct(e)}
224
+ end
225
+
226
+
227
+ #
228
+ # After() scopes the eventual search to all entries after to this point.
156
229
  #
157
230
  # @param [Hash] scope_criteria the field to scope for as the key and the
158
231
  # value to compare against the other log messages
@@ -164,7 +237,7 @@ class Indy
164
237
  def after(scope_criteria)
165
238
  if scope_criteria[:time]
166
239
  time = parse_date(scope_criteria[:time])
167
- @inclusive = scope_criteria[:inclusive] || false
240
+ @inclusive = @inclusive || scope_criteria[:inclusive] || nil
168
241
 
169
242
  if scope_criteria[:span]
170
243
  span = (scope_criteria[:span].to_i * 60).seconds
@@ -178,7 +251,15 @@ class Indy
178
251
  end
179
252
 
180
253
  #
181
- # Before scopes the eventual search to all entries prior to this point.
254
+ # reset_time_scope removes any existing start and end times from the instance
255
+ # Otherwise consecutive calls retain state
256
+ #
257
+ def reset_scope
258
+ @inclusive = @start_time = @end_time = nil
259
+ end
260
+
261
+ #
262
+ # Before() scopes the eventual search to all entries prior to this point.
182
263
  #
183
264
  # @param [Hash] scope_criteria the field to scope for as the key and the
184
265
  # value to compare against the other log messages
@@ -191,7 +272,7 @@ class Indy
191
272
  def before(scope_criteria)
192
273
  if scope_criteria[:time]
193
274
  time = parse_date(scope_criteria[:time])
194
- @inclusive = scope_criteria[:inclusive] || false
275
+ @inclusive = @inclusive || scope_criteria[:inclusive] || nil
195
276
 
196
277
  if scope_criteria[:span]
197
278
  span = (scope_criteria[:span].to_i * 60).seconds
@@ -208,8 +289,8 @@ class Indy
208
289
  if scope_criteria[:time]
209
290
  time = parse_date(scope_criteria[:time])
210
291
 
211
- # does @inclusive add any real value to the #around method?
212
- @inclusive = scope_criteria[:inclusive] || false
292
+ @inclusive = nil
293
+ warn "Ignoring inclusive scope_criteria" if scope_criteria[:inclusive]
213
294
 
214
295
  half_span = ((scope_criteria[:span].to_i * 60)/2).seconds rescue 300.seconds
215
296
  within(:time => [time - half_span, time + half_span])
@@ -220,7 +301,7 @@ class Indy
220
301
 
221
302
 
222
303
  #
223
- # Within scopes the eventual search to all entries between two points.
304
+ # Within() scopes the eventual search to all entries between two points.
224
305
  #
225
306
  # @param [Hash] scope_criteria the field to scope for as the key and the
226
307
  # value to compare against the other log messages
@@ -233,7 +314,7 @@ class Indy
233
314
  if scope_criteria[:time]
234
315
  @start_time, @end_time = scope_criteria[:time].collect {|str| parse_date(str) }
235
316
 
236
- @inclusive = scope_criteria[:inclusive] || false
317
+ @inclusive = @inclusive || scope_criteria[:inclusive] || nil
237
318
  end
238
319
 
239
320
  self
@@ -288,6 +369,9 @@ class Indy
288
369
 
289
370
  @time_field = ( @log_fields.include?(:time) ? :time : nil )
290
371
 
372
+ # now that we know the fields
373
+ define_struct
374
+
291
375
  end
292
376
 
293
377
  #
@@ -302,27 +386,43 @@ class Indy
302
386
  time_search = use_time_criteria?
303
387
 
304
388
  source_io = open_source
305
- results = source_io.collect do |line|
306
389
 
307
- hash = parse_line(line)
390
+ if @multiline
391
+ results = source_io.read.scan(Regexp.new(@log_regexp, Regexp::MULTILINE)).collect do |entry|
308
392
 
309
- hash ? (line_matched = true) : next
310
-
311
- if time_search
312
- set_time(hash)
313
- next unless inside_time_window?(hash)
314
- else
315
- hash[:_time] = nil if hash
393
+ hash = parse_line(entry)
394
+ hash ? (line_matched = true) : next
395
+
396
+ if time_search
397
+ set_time(hash)
398
+ next unless inside_time_window?(hash)
399
+ else
400
+ hash[:_time] = nil if hash
401
+ end
402
+
403
+ block_given? ? block.call(hash) : nil
316
404
  end
317
405
 
318
- block_given? ? block.call(hash) : nil
406
+ else
407
+ results = source_io.collect do |line|
408
+ hash = parse_line(line)
409
+ hash ? (line_matched = true) : next
410
+
411
+ if time_search
412
+ set_time(hash)
413
+ next unless inside_time_window?(hash)
414
+ else
415
+ hash[:_time] = nil if hash
416
+ end
417
+
418
+ block_given? ? block.call(hash) : nil
419
+ end
319
420
 
320
421
  end
321
422
 
322
423
  warn "No matching lines found in source: #{source_io.class}" unless line_matched
323
424
 
324
- source_io.close if @source[:file] || @source[:cmd]
325
-
425
+ source_io.close if @source[:file] || @source[:cmd]
326
426
  results.compact
327
427
  end
328
428
 
@@ -365,17 +465,26 @@ class Indy
365
465
  #
366
466
  def parse_line( line )
367
467
 
368
- match_data = /#{@log_regexp}/.match(line)
468
+ if line.kind_of? String
469
+ match_data = /#{@log_regexp}/.match(line)
470
+ return nil unless match_data
369
471
 
370
- if match_data
371
472
  values = match_data.captures
372
- raise "Field mismatch between log pattern and log data. The data is: '#{values.join(':::')}'" unless values.length == @log_fields.length
473
+ entire_line = line.strip
373
474
 
374
- hash = Hash[ *@log_fields.zip( values ).flatten ]
375
- hash[:line] = line.strip
475
+ elsif line.kind_of? Enumerable
376
476
 
377
- hash
477
+ entire_line = line.shift
478
+ values = line
378
479
  end
480
+
481
+ raise "Field mismatch between log pattern and log data. The data is: '#{values.join(':::')}'" unless values.length == @log_fields.length
482
+
483
+ hash = Hash[ *@log_fields.zip( values ).flatten ]
484
+ hash[:line] = entire_line.strip
485
+
486
+
487
+ hash
379
488
  end
380
489
 
381
490
  #
@@ -426,8 +535,8 @@ class Indy
426
535
  def parse_date(param)
427
536
  return nil unless @time_field
428
537
  return param if param.kind_of? Time or param.kind_of? DateTime
429
-
430
- time_string = param[@time_field] ? param[@time_field] : param
538
+
539
+ time_string = param.is_a?(Hash) ? param[@time_field] : param.to_s
431
540
 
432
541
  if @time_format
433
542
  begin