indy 0.1.1

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.
Files changed (42) hide show
  1. data/.autotest +18 -0
  2. data/.gitignore +2 -0
  3. data/.rspec +1 -0
  4. data/History.txt +11 -0
  5. data/README.md +132 -0
  6. data/Rakefile +68 -0
  7. data/autotest/discover.rb +2 -0
  8. data/cucumber.yml +6 -0
  9. data/features/after_time.feature +41 -0
  10. data/features/application.feature +33 -0
  11. data/features/around_time.feature +34 -0
  12. data/features/before_time.feature +34 -0
  13. data/features/custom_pattern.feature +52 -0
  14. data/features/exact_log_level.feature +35 -0
  15. data/features/exact_message.feature +33 -0
  16. data/features/exact_mulitple_fields.feature +38 -0
  17. data/features/exact_time.feature +28 -0
  18. data/features/file.feature +30 -0
  19. data/features/log_levels.feature +40 -0
  20. data/features/message.feature +39 -0
  21. data/features/multiple_fields.feature +38 -0
  22. data/features/step_definitions/find_by.steps.rb +55 -0
  23. data/features/step_definitions/log_file.steps.rb +8 -0
  24. data/features/step_definitions/support/env.rb +1 -0
  25. data/features/step_definitions/support/transforms.rb +28 -0
  26. data/features/step_definitions/test_setup.steps.rb +4 -0
  27. data/features/step_definitions/test_teardown.steps.rb +0 -0
  28. data/features/step_definitions/time.steps.rb +29 -0
  29. data/features/within_time.feature +41 -0
  30. data/indy.gemspec +61 -0
  31. data/lib/indy.rb +5 -0
  32. data/lib/indy/indy.rb +463 -0
  33. data/lib/indy/result_set.rb +8 -0
  34. data/performance/helper.rb +5 -0
  35. data/performance/profile_spec.rb +35 -0
  36. data/spec/data.log +2 -0
  37. data/spec/helper.rb +4 -0
  38. data/spec/indy_spec.rb +212 -0
  39. data/spec/result_set_spec.rb +9 -0
  40. data/spec/search_spec.rb +97 -0
  41. data/spec/time_spec.rb +80 -0
  42. metadata +126 -0
@@ -0,0 +1,61 @@
1
+ require File.dirname(__FILE__) + "/lib/indy"
2
+
3
+ module Indy
4
+
5
+ def self.show_version_changes(version)
6
+ date = ""
7
+ changes = []
8
+ grab_changes = false
9
+
10
+ File.open("#{File.dirname(__FILE__)}/History.txt",'r') do |file|
11
+ while (line = file.gets) do
12
+
13
+ if line =~ /^===\s*#{version.gsub('.','\.')}\s*\/\s*(.+)\s*$/
14
+ grab_changes = true
15
+ date = $1.strip
16
+ elsif line =~ /^===\s*.+$/
17
+ grab_changes = false
18
+ elsif grab_changes
19
+ changes = changes << line
20
+ end
21
+
22
+ end
23
+ end
24
+
25
+ { :date => date, :changes => changes }
26
+ end
27
+ end
28
+
29
+ Gem::Specification.new do |s|
30
+ s.name = 'indy'
31
+ s.version = ::Indy::VERSION
32
+ s.authors = ["Franklin Webber","Brandon Faloona"]
33
+ s.description = %{ Indy is a log archelogy tool that allows you to search through log files. }
34
+ s.summary = "Log Search Tool"
35
+ s.email = 'franklin.webber@gmail.com'
36
+ s.homepage = "http://github.com/burtlo/Indy"
37
+ s.license = 'MIT'
38
+
39
+ s.platform = Gem::Platform::RUBY
40
+ s.required_ruby_version = '>= 1.8.7'
41
+ s.add_dependency('activesupport', '>= 2.3.5')
42
+
43
+ changes = Indy.show_version_changes(::Indy::VERSION)
44
+
45
+ s.post_install_message = %{
46
+ [<>] [<>] [<>] [<>] [<>] [<>] [<>] [<>] [<>] [<>] [<>] [<>] [<>] [<>] [<>]
47
+
48
+ Thank you for installing Indy #{::Indy::VERSION} / #{changes[:date]}.
49
+
50
+ Changes:
51
+ #{changes[:changes].collect{|change| " #{change}"}.join("")}
52
+ [<>] [<>] [<>] [<>] [<>] [<>] [<>] [<>] [<>] [<>] [<>] [<>] [<>] [<>] [<>]
53
+
54
+ }
55
+
56
+ s.rubygems_version = "1.3.7"
57
+ s.files = `git ls-files`.split("\n")
58
+ s.extra_rdoc_files = ["README.md", "History.txt"]
59
+ s.rdoc_options = ["--charset=UTF-8"]
60
+ s.require_path = "lib"
61
+ end
@@ -0,0 +1,5 @@
1
+ $:.unshift(File.dirname(__FILE__)) unless
2
+ $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
3
+
4
+ require 'indy/indy'
5
+ require 'indy/result_set'
@@ -0,0 +1,463 @@
1
+ require 'ostruct'
2
+ require 'active_support/core_ext'
3
+
4
+ class Indy
5
+
6
+ VERSION = "0.1.1"
7
+
8
+ #
9
+ # string, file, or command that provides the log
10
+ #
11
+ attr_accessor :source
12
+
13
+ #
14
+ # array with regexp string and capture groups followed by log field
15
+ # name symbols. :time field is required to use time scoping
16
+ #
17
+ attr_accessor :pattern
18
+
19
+ #
20
+ # format string for explicit date/time format (optional)
21
+ #
22
+ attr_accessor :time_format
23
+
24
+ DATE_TIME = "\\d{4}.\\d{2}.\\d{2}\s+\\d{2}.\\d{2}.\\d{2}" #"%Y-%m-%d %H:%M:%S"
25
+ SEVERITY = [:trace,:debug,:info,:warn,:error,:fatal]
26
+ SEVERITY_PATTERN = "(?:#{SEVERITY.map{|s| s.to_s.upcase}.join("|")})"
27
+ APPLICATION = "\\w+"
28
+ MESSAGE = ".+"
29
+
30
+ DEFAULT_LOG_PATTERN = "^(#{DATE_TIME})\\s+(#{SEVERITY_PATTERN})\\s+(#{APPLICATION})\\s+-\\s+(#{MESSAGE})$"
31
+ DEFAULT_LOG_FIELDS = [:time,:severity,:application,:message]
32
+
33
+ FOREVER_AGO = DateTime.now - 200_000
34
+ FOREVER = DateTime.now + 200_000
35
+
36
+
37
+ #
38
+ # Initialize Indy.
39
+ #
40
+ # @example
41
+ #
42
+ # Indy.new(:source => LOG_FILENAME)
43
+ # Indy.new(:source => LOG_CONTENTS_STRING)
44
+ # Indy.new(:source => {:cmd => LOG_COMMAND_STRING})
45
+ # Indy.new(:pattern => [LOG_REGEX_PATTERN,:time,:application,:message],:source => LOG_FILENAME)
46
+ # Indy.new(:time_format => '%m-%d-%Y',:pattern => [LOG_REGEX_PATTERN,:time,:application,:message],:source => LOG_FILENAME)
47
+ #
48
+ def initialize(args)
49
+ @source = @pattern = nil
50
+ @source_info = Hash.new
51
+
52
+ while (arg = args.shift) do
53
+ send("#{arg.first}=",arg.last)
54
+ end
55
+
56
+ @pattern = @pattern || [DEFAULT_LOG_PATTERN,DEFAULT_LOG_FIELDS].flatten
57
+ @time_field = ( @pattern[1..-1].include?(:time) ? :time : nil )
58
+
59
+ end
60
+
61
+ class << self
62
+
63
+ #
64
+ # Create a new instance of Indy with @source, or multiple, parameters
65
+ # specified. This allows for a more fluent creation that moves
66
+ # into the execution.
67
+ #
68
+ # @param [String,Hash] params To specify @source, provide a filename or
69
+ # log contents as a string. To specify a command, use a :cmd => STRING hash.
70
+ # Alternately, a Hash with a :source key (amoung others) can be used to
71
+ # provide multiple initialization parameters.
72
+ #
73
+ # @example
74
+ # Indy.search("apache.log").for(:severity => "INFO")
75
+ #
76
+ # @example
77
+ # Indy.search("INFO 2000-09-07 MyApp - Entering APPLICATION.\nINFO 2000-09-07 MyApp - Entering APPLICATION.").for(:all)
78
+ #
79
+ # @example
80
+ # Indy.search(:cmd => "cat apache.log").for(:severity => "INFO")
81
+ #
82
+ # @example
83
+ # Indy.search(:source => {:cmd => "cat apache.log"}, :pattern => LOG_PATTERN, :time_format => MY_TIME_FORMAT).for(:all)
84
+ #
85
+ def search(params)
86
+ if params.respond_to?(:keys) && params[:source]
87
+ Indy.new(params)
88
+ else
89
+ Indy.new(:source => params, :pattern => [DEFAULT_LOG_PATTERN,DEFAULT_LOG_FIELDS].flatten)
90
+ end
91
+ end
92
+
93
+ end
94
+
95
+
96
+
97
+ #
98
+ # Specify the log pattern to use as the comparison against each line within
99
+ # the log file that has been specified.
100
+ #
101
+ # @param [Array] pattern_array an Array with the regular expression as the first element
102
+ # followed by list of fields (Symbols) in the log entry
103
+ # to use for comparison against each log line.
104
+ #
105
+ # @example Log formatted as - HH:MM:SS Message
106
+ #
107
+ # Indy.search(LOG_FILE).with("^(\\d{2}.\\d{2}.\\d{2})\s*(.+)$",:time,:message)
108
+ #
109
+ def with(pattern_array = :default)
110
+ @pattern = pattern_array == :default ? [DEFAULT_LOG_PATTERN,DEFAULT_LOG_FIELDS].flatten : pattern_array
111
+ @time_field = @pattern[1..-1].include?(:time) ? :time : nil
112
+ self
113
+ end
114
+
115
+ #
116
+ # Search the source and make an == comparison
117
+ #
118
+ # @param [Hash,Symbol] search_criteria the field to search for as the key and the
119
+ # value to compare against the other log messages. This function also
120
+ # supports symbol :all to return all messages
121
+ #
122
+ def search(search_criteria)
123
+ results = ResultSet.new
124
+
125
+ case
126
+ when search_criteria.is_a?(Enumerable)
127
+ results += _search do |result|
128
+ OpenStruct.new(result) if search_criteria.reject {|criteria,value| result[criteria] == value }.empty?
129
+ end
130
+ when search_criteria == :all
131
+ results += _search {|result| OpenStruct.new(result) }
132
+ end
133
+
134
+ results
135
+ end
136
+
137
+ alias_method :for, :search
138
+
139
+ #
140
+ # Search the source and make a regular expression comparison
141
+ #
142
+ # @param [Hash] search_criteria the field to search for as the key and the
143
+ # value to compare against the other log messages
144
+ #
145
+ # @example For all applications that end with Service
146
+ #
147
+ # Indy.search(LOG_FILE).like(:application => '(.+)Service')
148
+ #
149
+ def like(search_criteria)
150
+ results = ResultSet.new
151
+
152
+ results += _search do |result|
153
+ OpenStruct.new(result) if search_criteria.reject {|criteria,value| result[criteria] =~ /#{value}/ }.empty?
154
+ end
155
+
156
+ results
157
+ end
158
+
159
+ alias_method :matching, :like
160
+
161
+
162
+ #
163
+ # After scopes the eventual search to all entries after to this point.
164
+ #
165
+ # @param [Hash] scope_criteria the field to scope for as the key and the
166
+ # value to compare against the other log messages
167
+ #
168
+ # @example For all messages after specified date
169
+ #
170
+ # Indy.search(LOG_FILE).after(:time => time).for(:all)
171
+ #
172
+ def after(scope_criteria)
173
+ if scope_criteria[:time]
174
+ time = parse_date(scope_criteria[:time])
175
+ @inclusive = scope_criteria[:inclusive] || false
176
+
177
+ if scope_criteria[:span]
178
+ span = (scope_criteria[:span].to_i * 60).seconds
179
+ within(:time => [time, time + span])
180
+ else
181
+ @start_time = time
182
+ end
183
+ end
184
+
185
+ self
186
+ end
187
+
188
+ #
189
+ # Before scopes the eventual search to all entries prior to this point.
190
+ #
191
+ # @param [Hash] scope_criteria the field to scope for as the key and the
192
+ # value to compare against the other log messages
193
+ #
194
+ # @example For all messages before specified date
195
+ #
196
+ # Indy.search(LOG_FILE).before(:time => time).for(:all)
197
+ # Indy.search(LOG_FILE).before(:time => time, :span => 10).for(:all)
198
+ #
199
+ def before(scope_criteria)
200
+ if scope_criteria[:time]
201
+ time = parse_date(scope_criteria[:time])
202
+ @inclusive = scope_criteria[:inclusive] || false
203
+
204
+ if scope_criteria[:span]
205
+ span = (scope_criteria[:span].to_i * 60).seconds
206
+ within(:time => [time - span, time], :inclusive => scope_criteria[:inclusive])
207
+ else
208
+ @end_time = time
209
+ end
210
+ end
211
+
212
+ self
213
+ end
214
+
215
+ def around(scope_criteria)
216
+ if scope_criteria[:time]
217
+ time = parse_date(scope_criteria[:time])
218
+
219
+ # does @inclusive add any real value to the #around method?
220
+ @inclusive = scope_criteria[:inclusive] || false
221
+
222
+ half_span = ((scope_criteria[:span].to_i * 60)/2).seconds rescue 300.seconds
223
+ within(:time => [time - half_span, time + half_span])
224
+ end
225
+
226
+ self
227
+ end
228
+
229
+
230
+ #
231
+ # Within scopes the eventual search to all entries between two points.
232
+ #
233
+ # @param [Hash] scope_criteria the field to scope for as the key and the
234
+ # value to compare against the other log messages
235
+ #
236
+ # @example For all messages within the specified dates
237
+ #
238
+ # Indy.search(LOG_FILE).within(:time => [start_time,stop_time]).for(:all)
239
+ #
240
+ def within(scope_criteria)
241
+ if scope_criteria[:time]
242
+ @start_time, @end_time = scope_criteria[:time]
243
+ @inclusive = scope_criteria[:inclusive] || false
244
+ end
245
+
246
+ self
247
+ end
248
+
249
+
250
+ #
251
+ # Search the source for the specific severity
252
+ #
253
+ # @param [String,Symbol] severity the severity of the log messages to search
254
+ # for within the source
255
+ # @param [Symbol] direction by default search at the severity level, but you
256
+ # can specify :equal, :equal_and_above, and :equal_and_below
257
+ #
258
+ # @example INFO and more severe
259
+ #
260
+ # Indy.search(LOG_FILE).severity('INFO',:equal_and_above)
261
+ #
262
+ # @example Custom Level and Below
263
+ #
264
+ # Indy.search(LOG_FILE).with([CUSTOM_PATTERN,time,severity,message]).severity(:yellow,:equal_and_below,[:green,:yellow,:orange,:red])
265
+ # Indy.search(LOG_FILE).with([CUSTOM_PATTERN,time,severity,message]).matching(:severity => '(GREEN|YELLOW)')
266
+ #
267
+ def severity(severity,direction = :equal,scale = SEVERITY)
268
+ severity = severity.to_s.downcase.to_sym
269
+
270
+ case direction
271
+ when :equal
272
+ severity = [severity]
273
+ when :equal_and_above
274
+ severity = scale[scale.index(severity)..-1]
275
+ when :equal_and_below
276
+ severity = scale[0..scale.index(severity)]
277
+ end
278
+
279
+ ResultSet.new + _search {|result| OpenStruct.new(result) if severity.include?(result[:severity].downcase.to_sym) }
280
+
281
+ end
282
+
283
+ private
284
+
285
+ #
286
+ # Sets the source for the Indy instance.
287
+ #
288
+ # @param [String,Hash] source A filename or string. Use a Hash to specify a command string.
289
+ #
290
+ # @example
291
+ #
292
+ # source("apache.log")
293
+ # source(:cmd => "cat apache.log")
294
+ # source("INFO 2000-09-07 MyApp - Entering APPLICATION.\nINFO 2000-09-07 MyApp - Entering APPLICATION.")
295
+ #
296
+ def source=(specified_source)
297
+
298
+ cmd = specified_source[:cmd] rescue nil
299
+
300
+ if cmd
301
+ possible_source = try_as_command(cmd)
302
+ @source_info[:cmd] = specified_source[:cmd]
303
+ else
304
+
305
+ possible_source = try_as_file(specified_source) unless possible_source
306
+
307
+ if possible_source
308
+ @source_info[:file] = specified_source
309
+ else
310
+ possible_source = StringIO.new(specified_source.to_s)
311
+ @source_info[:string] = specified_source
312
+ end
313
+ end
314
+
315
+ @source = possible_source
316
+ end
317
+
318
+ #
319
+ # Search the specified source and yield to the block the line that was found
320
+ # with the given log pattern
321
+ #
322
+ # This method is supposed to be used internally.
323
+ # @param [IO] source is a Ruby IO object
324
+ #
325
+ def _search(source = @source,pattern_array = @pattern,&block)
326
+
327
+ if @start_time || @end_time
328
+ @start_time = @start_time || FOREVER_AGO
329
+ @end_time = @end_time || FOREVER
330
+ end
331
+
332
+ if @source_info[:cmd]
333
+ actual_source = try_as_command(@source_info[:cmd])
334
+ else
335
+ source.rewind
336
+ actual_source = source.dup
337
+ end
338
+
339
+ results = actual_source.each.collect do |line|
340
+
341
+ hash = parse_line(line, pattern_array)
342
+
343
+ if @time_field && @start_time
344
+ set_time(hash)
345
+ next unless inside_time_window?(hash)
346
+ end
347
+
348
+ next unless hash
349
+
350
+ block_given? ? block.call(hash) : nil
351
+ end
352
+
353
+
354
+ results.compact
355
+ end
356
+
357
+ #
358
+ # Return a hash of field=>value pairs for the log line
359
+ #
360
+ # @param [String] line The log line
361
+ # @param [Array] pattern_array The match regexp string, followed by log fields
362
+ # see Class method search
363
+ #
364
+ def parse_line( line, pattern_array = @pattern)
365
+ regexp, *fields = pattern_array
366
+
367
+ if /#{regexp}/.match(line)
368
+ values = /#{regexp}/.match(line).captures
369
+ raise "Field mismatch between log pattern and log data. The data is: '#{values.join(':::')}'" unless values.length == fields.length
370
+
371
+ hash = Hash[ *fields.zip( values ).flatten ]
372
+ hash[:line] = line.strip
373
+
374
+ hash
375
+ end
376
+ end
377
+
378
+ #
379
+ # Set the :_time value in the hash
380
+ #
381
+ # @param [Hash] hash The log line hash to modify
382
+ #
383
+ def set_time(hash)
384
+ hash[:_time] = parse_date( hash ) if hash
385
+ end
386
+
387
+ #
388
+ # Evaluate if a log line satisfies the configured time conditions
389
+ #
390
+ # @param [Hash] line_hash The log line hash to be evaluated
391
+ #
392
+ def inside_time_window?( line_hash )
393
+
394
+ if line_hash && line_hash[:_time]
395
+ if @inclusive
396
+ true unless line_hash[:_time] > @end_time or line_hash[:_time] < @start_time
397
+ else
398
+ true unless line_hash[:_time] >= @end_time or line_hash[:_time] <= @start_time
399
+ end
400
+ end
401
+
402
+ end
403
+
404
+ #
405
+ # Return a valid DateTime object for the log line or string
406
+ #
407
+ # @param [String, Hash] param The log line hash, or string to be evaluated
408
+ #
409
+ def parse_date(param)
410
+ return nil unless @time_field
411
+
412
+ time_string = param[@time_field] ? param[@time_field] : param
413
+
414
+ begin
415
+ # Attempt the appropriate parse method
416
+ date = @time_format ? DateTime.strptime(time_string, @time_format) : DateTime.parse(time_string)
417
+ rescue
418
+ begin
419
+ # If appropriate, fall back to simple parse method
420
+ if @time_format
421
+ date = DateTime.parse(time_string)
422
+ else
423
+ date = @time_field = nil
424
+ end
425
+ rescue ArgumentError
426
+ date = @time_field = nil
427
+ end
428
+ end
429
+ date
430
+ end
431
+
432
+ #
433
+ # Try opening the string as a command string, returning an IO object
434
+ #
435
+ # @param [String] command_string string of command that will return log contents
436
+ #
437
+ def try_as_command(command_string)
438
+
439
+ begin
440
+ io = IO.popen(command_string)
441
+ return nil if io.eof?
442
+ rescue
443
+ nil
444
+ end
445
+ io
446
+ end
447
+
448
+ #
449
+ # Try opening the string as a file, returning an File IO Object
450
+ #
451
+ # @param [String] filename path to log file
452
+ #
453
+ def try_as_file(filename)
454
+
455
+ begin
456
+ File.open(filename)
457
+ rescue
458
+ nil
459
+ end
460
+
461
+ end
462
+
463
+ end