indy 0.1.5 → 0.2.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.
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