winsome_wolverine 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,84 @@
1
+ h1. Wolverine
2
+
3
+ An appetite for logs.
4
+
5
+
6
+ h2. Summary
7
+
8
+ Wolverine is a library for processing event streams. Processing starts with a source of events, such as a log file. Filters wrap a source and themselves look like a source. The DSL supplies shortcut methods to easily build a pipeline of filters in this way. Filters are processed incrementally as events are required; one can quickly get the first few events from a very long stream.
9
+
10
+ Events may be stored in a database for easy indexing. See the @ActiveRecordSource@, which provides an optimized @where@ filter.
11
+
12
+ For a list of convenience methods for building filter pipelines, see @FilterDSL@.
13
+
14
+ When processing a Rails log with multiple lines per requests and multiple web processes writing to the log concurrently, Wolverine can filter requests matching a particular criteria in a particular context. For example, it can tell you the number of exceptions thrown on POST requests, even though those bits of information are contained in different log messages.
15
+
16
+
17
+ h2. Examples
18
+
19
+ <pre>
20
+ <code>
21
+ include Wolverine
22
+
23
+ # Read events from a file, one per line
24
+ requests = FileSource.new("log/production.log").
25
+
26
+ # In this log, subsequent lines of a message may appear indented,
27
+ # so coalesce them into one event.
28
+ append_indented.
29
+
30
+ # Parse text out into fields using regex captures. Imagine a log
31
+ # line that looks like this:
32
+ # Nov 19 19:08:27.931605 dhcp-172-25-211-77 45471: Hello World
33
+ field(/\A(\w+ \d+ \d+:\d+:\d+\.\d{6}) (\w+) (\w+):/m,
34
+ :timestamp, :host, :pid).
35
+
36
+ # Group messages from a single request together.
37
+ # Since the logs are from multiple web server processes, messages
38
+ # might be interleaved with those from other processes. Use the
39
+ # :pid and :host fields to separate them. Whenever a processes
40
+ # logs "Processing," that's a new request.
41
+ group(:following => [:pid, :host], :from => /: Processing/).
42
+
43
+ # Pull some per-request fields out of the requests.
44
+ fields(/Processing (\w+)#(\w+)/, :controller, :action).
45
+ fields(/Session ID: (\w+)/, :session_id)
46
+
47
+ # Filter by a field. Note this returns a new filter immediately
48
+ # but does not consume any input yet.
49
+ accounts_requests = requests.where(:controller => "AccountsController")
50
+
51
+ # Consume the entire stream and count the number of events
52
+ accounts_requests.count
53
+
54
+ # View the first five matches with "less"
55
+ accounts_requests.head(5).less
56
+
57
+ # Group requests into session traces
58
+ sessions = requests.group(:following => :session_id)
59
+
60
+ # Turn the timestamp into a real ruby object
61
+ ts_requests = requests.parse_time(:timestamp)
62
+
63
+ # Shuffle two streams into one, with events in order of timestamp
64
+ ts_requests.interleave(other_host_requests)
65
+ </code>
66
+ </pre>
67
+
68
+ Wolverine works well interactively:
69
+
70
+ bc. $ irb -r wolverine
71
+
72
+ You may also wish to store some helper functions specific to your log format and/or task at hand to pre-load into your session:
73
+
74
+ bc. $ irb -r my-app
75
+
76
+
77
+ h2. Road Map
78
+
79
+ * Sources to read files over SSH
80
+ * Sources that can tail a file
81
+ * Histograms and tables
82
+ * Faster field extraction (regex matching is the bottleneck)
83
+ * Processing multiple pipelines in one pass over the underlying data
84
+ * Sliding window filters that emit when a 5-minute average exceeds a threshold
@@ -0,0 +1 @@
1
+ require 'wolverine'
@@ -0,0 +1,23 @@
1
+ module Wolverine
2
+ end
3
+
4
+ require 'wolverine/filter_dsl'
5
+ require 'wolverine/enumerable'
6
+ require 'wolverine/source'
7
+ require 'wolverine/file_source'
8
+ require 'wolverine/active_record_source'
9
+ require 'wolverine/array_source'
10
+ require 'wolverine/filter'
11
+ require 'wolverine/append_indented_filter'
12
+ require 'wolverine/field_filter'
13
+ require 'wolverine/parse_time_filter'
14
+ require 'wolverine/head_filter'
15
+ require 'wolverine/where_filter'
16
+ require 'wolverine/group_filter'
17
+ require 'wolverine/interleave_filter'
18
+ require 'wolverine/sink'
19
+ require 'wolverine/count_sink'
20
+ require 'wolverine/less_sink'
21
+ require 'wolverine/file_sink'
22
+ require 'wolverine/active_record_sink'
23
+ require 'wolverine/event'
@@ -0,0 +1,11 @@
1
+ module Wolverine
2
+ class Filter < Wolverine::Enumerable
3
+ def initialize(source)
4
+ @source = source
5
+ end
6
+ # The base filter implements the identity transformation.
7
+ def each(&block)
8
+ @source.each(&block)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,32 @@
1
+ module Wolverine
2
+ class ActiveRecordSink < Sink
3
+ attr_reader :columns, :klass
4
+ def initialize(source, klass)
5
+ super(source)
6
+ @klass = klass
7
+ @columns = klass.columns.map(&:name) - %w{id created_at updated_at}
8
+ end
9
+ def self.create(source, table_name, columns, connection_params)
10
+ require 'active_record'
11
+ klass = Class.new(ActiveRecord::Base)
12
+ klass.class_eval { self.table_name = table_name }
13
+ klass.establish_connection(connection_params)
14
+ klass.connection.create_table table_name do |t|
15
+ t.column :text, :text
16
+ columns.each do |col|
17
+ t.column col, :string
18
+ end
19
+ #t.timestamps
20
+ end
21
+ self.new(source, klass)
22
+ end
23
+ def run
24
+ @source.each do |evt|
25
+ hsh = {}
26
+ columns.each {|col| hsh[col] = evt.send(col) }
27
+ rec = @klass.new(hsh)
28
+ rec.save!
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,71 @@
1
+ module Wolverine
2
+ class ActiveRecordSource < Source
3
+ attr_reader :klass
4
+ def initialize(active_record_klass, conditions={}, search=nil)
5
+ @klass = active_record_klass
6
+ @conds = conditions
7
+ @search = search
8
+ require 'logger'
9
+ ActiveRecord::Base.logger = Logger.new(STDOUT)
10
+ end
11
+ def self.open(table_name, connection_params)
12
+ require 'active_record'
13
+ klass = Class.new(ActiveRecord::Base)
14
+ klass.class_eval do
15
+ self.table_name = table_name
16
+ def to_s
17
+ text
18
+ end
19
+ end
20
+ klass.establish_connection(connection_params)
21
+ self.new(klass)
22
+ end
23
+ def each
24
+ if can_search_with_conditions
25
+ evts = @klass.where({})
26
+ evts = evts.where(@conds) if !@conds.empty?
27
+ if @search
28
+ evts = evts.where(["text like ?", "%#{@search}%"])
29
+ # Add a fulltext search if we're on MySQL. Boolean fulltext search
30
+ # is slightly less strict than "like", so it will only be a prefilter.
31
+ # Fixme: table could be Innodb...
32
+ if !@search.include? '"' and
33
+ evts.connection.class.name.include? "Mysql"
34
+ evts = evts.where(["MATCH (text) AGAINST (? IN BOOLEAN MODE)",
35
+ "\"#{@search}\""])
36
+ end
37
+ end
38
+ else
39
+ if @search
40
+ evts = @klass.find(:all, :conditions =>
41
+ ["text like ?", "%#{@search}%"])
42
+ else
43
+ evts = @klass.find(:all, :conditions => @conds)
44
+ end
45
+ end
46
+ evts.each {|rec| yield rec }
47
+ end
48
+ def where(conditions=nil)
49
+ if @search && !can_search_with_conditions
50
+ raise Exception, "Can't do both where and search"
51
+ end
52
+
53
+ if conditions
54
+ conds = @conds.merge(conditions)
55
+ return self.class.new(@klass, conds, @search)
56
+ else
57
+ # Get ready for where.not(...), which should happen in-memory
58
+ super(conditions)
59
+ end
60
+ end
61
+ def search(word)
62
+ if @conds && !can_search_with_conditions
63
+ raise Exception, "Can't do both search and where"
64
+ end
65
+ self.class.new(@klass, @conds, word)
66
+ end
67
+ def can_search_with_conditions
68
+ @klass.respond_to? :where
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,31 @@
1
+ module Wolverine
2
+ class AppendIndentedFilter < Filter
3
+ def each
4
+ merged_evt = nil
5
+ prefix = self.prefix
6
+ @source.each do |evt|
7
+ if evt.to_s.start_with? prefix
8
+ if merged_evt
9
+ merged_evt = merge_events(merged_evt, evt)
10
+ # else drop the partial event
11
+ end
12
+ else
13
+ if merged_evt
14
+ yield merged_evt
15
+ end
16
+ merged_evt = evt
17
+ end
18
+ end
19
+ if merged_evt
20
+ yield merged_evt
21
+ end
22
+ end
23
+ def merge_events(ev1, ev2)
24
+ str = ev2.to_s
25
+ return ev1.to_s + str.slice(prefix.length..str.length)
26
+ end
27
+ def prefix
28
+ " "
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,10 @@
1
+ module Wolverine
2
+ class ArraySource < Source
3
+ def initialize(array)
4
+ @array = array
5
+ end
6
+ def each
7
+ @array.each {|str| yield Event.new(str) }
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,9 @@
1
+ module Wolverine::CoreExt::Symbol
2
+ def to_proc
3
+ proc { |obj, *args| obj.send(self, *args) }
4
+ end
5
+ end
6
+
7
+ unless :foo.respond_to? :to_proc
8
+ Symbol.send(:include, Wolverine::CoreExt::Symbol)
9
+ end
@@ -0,0 +1,12 @@
1
+ module Wolverine
2
+ class CountSink < Sink
3
+ def initialize(source)
4
+ @source = source
5
+ end
6
+ def run
7
+ cnt = 0
8
+ @source.each {|evt| cnt += 1 }
9
+ cnt
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,27 @@
1
+ module Wolverine
2
+ class Enumerable
3
+ include ::Enumerable
4
+ include FilterDSL
5
+ def to_strings
6
+ map {|el| el.to_s}
7
+ end
8
+ def to_a
9
+ filterize super
10
+ end
11
+ def map(*args)
12
+ filterize super
13
+ end
14
+ alias collect map
15
+
16
+ protected
17
+
18
+ def filterize(arr)
19
+ arr.extend(FilterDSL)
20
+ def arr.inspect
21
+ super if size < 1
22
+ "[#{first.inspect}, (size: #{size})...]"
23
+ end
24
+ arr
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,12 @@
1
+ class Wolverine::Event
2
+ attr_reader :text
3
+ def initialize(text)
4
+ @text = text
5
+ end
6
+ def <=>(other)
7
+ to_s <=> other.to_s
8
+ end
9
+ def to_s
10
+ @text
11
+ end
12
+ end
@@ -0,0 +1,34 @@
1
+ module Wolverine
2
+ class FieldFilter < Filter
3
+ def initialize(source, regex, *fields)
4
+ super(source)
5
+ @regex = regex
6
+ @fields = fields
7
+ @class = Class.new(Event)
8
+ @class.class_eval <<-EOF, __FILE__, __LINE__
9
+ def initialize(text, evt, #{@fields.join(", ")})
10
+ super(text)
11
+ @evt = evt
12
+ #{@fields.map {|f| "@#{f}" }.join(", ")} = #{@fields.join(", ")}
13
+ end
14
+ def method_missing(name, *args)
15
+ # Danger: bypassing method access modifier (private/protected)
16
+ @evt.send(name) if args.empty?
17
+ end
18
+ EOF
19
+ @class.send(:attr_reader, *@fields)
20
+ # Explicitly enumerating the variables passed is faster than splat
21
+ self.instance_eval <<-EOF, __FILE__, __LINE__
22
+ def self.each
23
+ @source.each do |evt|
24
+ md = @regex.match(evt.to_s) || []
25
+ #md = ["pepper.bp", "12345"]
26
+ #{@fields.join(", ")} =
27
+ #{@fields.length > 1 ? "md[1..#{@fields.length}]" : "md[1]"}
28
+ yield @class.new(evt.to_s, evt, #{@fields.join(", ")})
29
+ end
30
+ end
31
+ EOF
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,13 @@
1
+ module Wolverine
2
+ class FileSink < Sink
3
+ def initialize(source, filename)
4
+ super(source)
5
+ @filename = filename
6
+ end
7
+ def run
8
+ File.open(@filename, "w") do |file|
9
+ @source.each {|evt| file.write(evt) }
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,38 @@
1
+ require 'progressbar'
2
+ module Wolverine
3
+ class FileSource < Source
4
+ attr_accessor :progress, :gzip
5
+ def initialize(filename, opts={})
6
+ @filename = filename
7
+ self.progress = opts[:progress]
8
+ self.gzip = opts.key?(:gzip) ? opts[:gzip] : filename.match(/\.gz$/i)
9
+ require 'zlib' if self.gzip
10
+ end
11
+ def each
12
+ progress = self.progress
13
+ if progress
14
+ size = File.size(@filename)
15
+ bar = ProgressBar.new(@filename, size)
16
+ bar.file_transfer_mode
17
+ end
18
+ File.open(@filename, "r") do |file|
19
+ realfile = file
20
+ file = Zlib::GzipReader.new(file) if self.gzip
21
+ bar_update = 10_000
22
+ cnt = 0
23
+ file.each do |line|
24
+ if progress
25
+ if cnt < bar_update
26
+ cnt += 1
27
+ else
28
+ bar.set(realfile.tell)
29
+ cnt = 0
30
+ end
31
+ end
32
+ yield line
33
+ end
34
+ end
35
+ bar.finish if progress
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,37 @@
1
+ module Wolverine
2
+ module FilterDSL
3
+ def append_indented
4
+ AppendIndentedFilter.new(self)
5
+ end
6
+ def field(regex, *fields)
7
+ FieldFilter.new(self, regex, *fields)
8
+ end
9
+ def parse_time(source_field, dest_field=source_field)
10
+ ParseTimeFilter.new(self, source_field, dest_field)
11
+ end
12
+ def head(count, opts={})
13
+ HeadFilter.new(self, count, opts)
14
+ end
15
+ def where(conditions=nil)
16
+ WhereFilter.where(self, conditions)
17
+ end
18
+ def count
19
+ CountSink.new(self).run
20
+ end
21
+ def less
22
+ LessSink.new(self).run
23
+ end
24
+ def save_file(filename)
25
+ FileSink.new(self, filename).run
26
+ end
27
+ def save_records(active_record_klass)
28
+ ActiveRecordSink.new(self, active_record_klass).run
29
+ end
30
+ def group(opts)
31
+ GroupFilter.new(self, opts)
32
+ end
33
+ def interleave(other_source)
34
+ InterleaveFilter.new(self, other_source)
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,45 @@
1
+ module Wolverine
2
+ class GroupFilter < Filter
3
+ def initialize(source, opts)
4
+ super(source)
5
+ @following = arg_to_a(opts[:following])
6
+ @from = opts[:from]
7
+ end
8
+ def each
9
+ groups = Hash.new { |hash,k| hash[k] = [] }
10
+ @source.each do |evt|
11
+ key = @following.map {|meth| evt.send(meth) }
12
+ group = groups[key]
13
+ if @from
14
+ if @from.match(evt.to_s)
15
+ if group.any?
16
+ yield merge_events(group)
17
+ group.clear
18
+ end
19
+ group.push(evt)
20
+ else
21
+ group.push(evt) unless group.empty?
22
+ end
23
+ else
24
+ group.push(evt)
25
+ end
26
+ end
27
+ # ordering?
28
+ groups.each do |k,group|
29
+ yield merge_events(group)
30
+ end
31
+ end
32
+ private
33
+ def arg_to_a(arg)
34
+ if Array === arg
35
+ arg
36
+ else
37
+ [arg]
38
+ end
39
+ end
40
+ def merge_events(group)
41
+ Event.new(group.map {|evt| evt.to_s }.join(""))
42
+ end
43
+ end
44
+ end
45
+
@@ -0,0 +1,34 @@
1
+ require 'progressbar'
2
+ module Wolverine
3
+ class GzipFileSource < Source
4
+ attr_accessor :progress
5
+ def initialize(filename, opts={})
6
+ @filename = filename
7
+ self.progress = opts[:progress]
8
+ end
9
+ def each
10
+ progress = self.progress
11
+ if progress
12
+ size = File.size(@filename)
13
+ bar = ProgressBar.new(@filename, size)
14
+ bar.file_transfer_mode
15
+ end
16
+ File.open(@filename, "r") do |file|
17
+ bar_update = 10_000
18
+ cnt = 0
19
+ file.each do |line|
20
+ if progress
21
+ if cnt < bar_update
22
+ cnt += 1
23
+ else
24
+ bar.set(file.tell)
25
+ cnt = 0
26
+ end
27
+ end
28
+ yield Event.new(line)
29
+ end
30
+ end
31
+ bar.finish if progress
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,29 @@
1
+ module Wolverine
2
+ class HeadFilter < Filter
3
+ attr_accessor :progress
4
+ def initialize(source, limit, opts={})
5
+ super(source)
6
+ @limit = limit
7
+ self.progress = opts[:progress]
8
+ end
9
+ def each
10
+ progress = self.progress
11
+ bar = ProgressBar.new("head(#{number_with_delimiter @limit})",
12
+ @limit) if progress
13
+ cnt = 0
14
+ @source.each do |evt|
15
+ bar.set(cnt) if progress
16
+ if cnt >= @limit
17
+ bar.finish if progress
18
+ return
19
+ end
20
+ yield evt
21
+ cnt += 1
22
+ end
23
+ end
24
+ # From Rails.
25
+ def number_with_delimiter(number, delimiter=",")
26
+ number.to_s.gsub(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1#{delimiter}")
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,20 @@
1
+ require 'generator'
2
+
3
+ module Wolverine
4
+ class InterleaveFilter < Filter
5
+ def initialize(source, other_source)
6
+ super(source)
7
+ @other_source = other_source
8
+ end
9
+ def each(&block)
10
+ # The Generator docs warn these are slow b/c they use continuations
11
+ generators = [Generator.new(@source), Generator.new(@other_source)]
12
+ generators.reject!(&:end?)
13
+ while generators.any?
14
+ generators = generators.sort_by(&:current)
15
+ yield generators.first.next
16
+ generators.shift if generators.first.end?
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,14 @@
1
+ module Wolverine
2
+ class LessSink < Sink
3
+ def run
4
+ IO.popen(pager_command, "a") do |pipe|
5
+ @source.each {|evt| pipe.puts evt }
6
+ nil
7
+ end
8
+ end
9
+ def pager_command
10
+ "less"
11
+ end
12
+ end
13
+ end
14
+
@@ -0,0 +1,41 @@
1
+ module Wolverine
2
+ class ParseTimeFilter < FieldFilter
3
+ def initialize(source, source_field, dest_field=source_field)
4
+ @source_field = source_field
5
+ @dest_field = dest_field
6
+ super(source, nil, dest_field)
7
+ # each is defined by eval in superclass
8
+ #self.send(:alias_method, :each, :_each)
9
+ def self.each(*args, &block)
10
+ _each(*args, &block)
11
+ end
12
+ @class.class_eval <<-EOF, __FILE__, __LINE__
13
+ def <=>(other)
14
+ mine = self.#@dest_field
15
+ yours = other.#@dest_field
16
+
17
+ # Handle nils; sort them first
18
+ if mine.nil?
19
+ if yours.nil?
20
+ return 0
21
+ end
22
+ return -1
23
+ end
24
+ if yours.nil?
25
+ return 1
26
+ end
27
+
28
+ mine <=> yours
29
+ end
30
+ EOF
31
+ end
32
+
33
+ def _each
34
+ @source.each do |evt|
35
+ ts_text = evt.send(@source_field)
36
+ ts = Time.parse(ts_text) if ts_text
37
+ yield @class.new(evt.to_s, evt, ts)
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,11 @@
1
+ module Wolverine
2
+ class Sink
3
+ def initialize(source)
4
+ @source = source
5
+ end
6
+ def run
7
+ @source.each { }
8
+ nil
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,8 @@
1
+ module Wolverine
2
+ class Source < Wolverine::Enumerable
3
+ def each(&block)
4
+ raise Exception, "Override me"
5
+ yield event
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,50 @@
1
+ module Wolverine
2
+ class WhereFilter < Filter
3
+ attr_reader :conditions
4
+ def initialize(source, conditions)
5
+ super(source)
6
+ @conditions = Conditions.new(self, conditions)
7
+ end
8
+ def each
9
+ @source.each do |evt|
10
+ next unless @conditions.match?(evt)
11
+ yield evt
12
+ end
13
+ end
14
+ def self.where(source, conditions=nil)
15
+ where = WhereFilter.new(source, conditions)
16
+ if conditions
17
+ where
18
+ else
19
+ where.conditions
20
+ end
21
+ end
22
+ end
23
+
24
+ class Conditions
25
+ def initialize(where, conds=nil)
26
+ @where = where
27
+ set_conds(conds)
28
+ end
29
+ def match?(evt)
30
+ @conditions_hash.each do |k,v|
31
+ return @negate unless v === evt.send(k)
32
+ end
33
+ return !@negate
34
+ end
35
+ def not(conds)
36
+ set_conds(conds)
37
+ @negate = true
38
+ @where
39
+ end
40
+ private
41
+ def set_conds(conds)
42
+ return unless conds
43
+ @conditions_hash = case conds
44
+ when Regexp then {:to_s => conds}
45
+ when Hash then conds
46
+ else raise ArgumentError, "Unknown parameter type: #{conds.class}"
47
+ end
48
+ end
49
+ end
50
+ end
metadata ADDED
@@ -0,0 +1,91 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: winsome_wolverine
3
+ version: !ruby/object:Gem::Version
4
+ hash: 29
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 0
9
+ - 1
10
+ version: 0.0.1
11
+ platform: ruby
12
+ authors:
13
+ - Marcel M. Cary
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2012-11-28 00:00:00 -08:00
19
+ default_executable:
20
+ dependencies: []
21
+
22
+ description: Library and DSL to process log files with a pipe-and-filter architecture
23
+ email: marcel@oak.homeunix.org
24
+ executables: []
25
+
26
+ extensions: []
27
+
28
+ extra_rdoc_files: []
29
+
30
+ files:
31
+ - README.textile
32
+ - lib/winsome_wolverine.rb
33
+ - lib/wolverine/active_record_sink.rb
34
+ - lib/wolverine/active_record_source.rb
35
+ - lib/wolverine/append_indented_filter.rb
36
+ - lib/wolverine/array_source.rb
37
+ - lib/wolverine/core_ext.rb
38
+ - lib/wolverine/count_sink.rb
39
+ - lib/wolverine/enumerable.rb
40
+ - lib/wolverine/event.rb
41
+ - lib/wolverine/field_filter.rb
42
+ - lib/wolverine/file_sink.rb
43
+ - lib/wolverine/file_source.rb
44
+ - lib/wolverine/Filter.rb
45
+ - lib/wolverine/filter_dsl.rb
46
+ - lib/wolverine/group_filter.rb
47
+ - lib/wolverine/gzip_file_source.rb
48
+ - lib/wolverine/head_filter.rb
49
+ - lib/wolverine/interleave_filter.rb
50
+ - lib/wolverine/less_sink.rb
51
+ - lib/wolverine/parse_time_filter.rb
52
+ - lib/wolverine/sink.rb
53
+ - lib/wolverine/source.rb
54
+ - lib/wolverine/where_filter.rb
55
+ - lib/wolverine.rb
56
+ has_rdoc: true
57
+ homepage: http://github.com/mcary/wolverine
58
+ licenses: []
59
+
60
+ post_install_message:
61
+ rdoc_options: []
62
+
63
+ require_paths:
64
+ - lib
65
+ required_ruby_version: !ruby/object:Gem::Requirement
66
+ none: false
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ hash: 3
71
+ segments:
72
+ - 0
73
+ version: "0"
74
+ required_rubygems_version: !ruby/object:Gem::Requirement
75
+ none: false
76
+ requirements:
77
+ - - ">="
78
+ - !ruby/object:Gem::Version
79
+ hash: 3
80
+ segments:
81
+ - 0
82
+ version: "0"
83
+ requirements: []
84
+
85
+ rubyforge_project:
86
+ rubygems_version: 1.4.2
87
+ signing_key:
88
+ specification_version: 3
89
+ summary: Log file processor
90
+ test_files: []
91
+