indy 0.1.1

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