bracken 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc ADDED
@@ -0,0 +1,11 @@
1
+ = Bracken
2
+
3
+ Bracken, at present, is a glorified logfile tailer.
4
+
5
+ == Behold
6
+
7
+ For now, you're best off looking at the cucumber features.
8
+
9
+ == Install
10
+
11
+ gem install bracken
data/Rakefile ADDED
@@ -0,0 +1,15 @@
1
+ begin
2
+ require 'shoe'
3
+ rescue LoadError
4
+ abort 'Please `gem install shoe` to get started.'
5
+ end
6
+
7
+ Shoe.tie('bracken', '0.1.0', 'Bracken, at present, is a glorified logfile tailer.') do |spec|
8
+ spec.add_runtime_dependency 'open4'
9
+
10
+ spec.add_development_dependency 'cucumber'
11
+ spec.add_development_dependency 'fakefs'
12
+ spec.add_development_dependency 'jeremymcanally-matchy'
13
+ spec.add_development_dependency 'redgreen'
14
+ spec.add_development_dependency 'thoughtbot-shoulda'
15
+ end
data/bin/bracken ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Add our lib directory to the load path as necessary
4
+ lib = File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib'))
5
+ $:.unshift(lib) unless $:.include?(lib)
6
+
7
+ # And fire off the application
8
+ require 'bracken'
9
+ Bracken::Application.execute(ARGV)
@@ -0,0 +1,6 @@
1
+ # To run this example:
2
+ # RUBYOPT=rubygems ./bin/bracken -c examples/console.rb
3
+
4
+ file '/Library/Logs/Console/501/console.log' do
5
+ on 'TextMate'
6
+ end
@@ -0,0 +1,15 @@
1
+ Given /^these are the contents of "([^\"]*)":$/ do |path, contents|
2
+ write_to_file(path, contents)
3
+ end
4
+
5
+ When /^another process appends "([^\"]*)" to "([^\"]*)"$/ do |contents, path|
6
+ write_to_file(path, contents, 'a')
7
+ end
8
+
9
+ When /^I run (.*)$/ do |command|
10
+ run(command)
11
+ end
12
+
13
+ Then /^I should see "([^\"]*)" on standard out$/ do |expected|
14
+ standard_out.gets.chomp.should == expected
15
+ end
@@ -0,0 +1,49 @@
1
+ require 'open4'
2
+ require 'pathname'
3
+ require 'tmpdir'
4
+
5
+ class WorkingDirectory
6
+ PROJECT_ROOT = Pathname.new(File.expand_path(File.join(File.dirname(__FILE__), '..', '..')))
7
+
8
+ attr_reader :working_directory
9
+ attr_reader :standard_out
10
+
11
+ def initialize
12
+ @working_directory ||= Pathname.new(Dir.mktmpdir)
13
+ end
14
+
15
+ def write_to_file(path, contents, mode='w')
16
+ working_directory.join(path).open(mode) { |file| file.puts(contents) }
17
+ end
18
+
19
+ def run(command)
20
+ Dir.chdir(working_directory) do
21
+ @pid, _, @standard_out, _ = Open4.popen4(rejigger_the_path(command))
22
+ end
23
+ end
24
+
25
+ def terminate_last_run
26
+ Process.kill('TERM', @pid) if @pid
27
+ @pid = nil
28
+ @standard_out = nil
29
+ end
30
+
31
+ private
32
+
33
+ def rejigger_the_path(command)
34
+ "/usr/bin/env -i PATH='#{PROJECT_ROOT.join('bin')}:#{ENV['PATH']}' RUBYLIB='#{PROJECT_ROOT.join('lib')}' RUBYOPT=rubygems #{command}"
35
+ end
36
+ end
37
+
38
+ World do
39
+ WorkingDirectory.new
40
+ end
41
+
42
+ Before do
43
+ working_directory.mkpath
44
+ end
45
+
46
+ After do
47
+ terminate_last_run
48
+ working_directory.rmtree
49
+ end
@@ -0,0 +1,33 @@
1
+ Feature: Tailing
2
+ In order to keep track of what's going on with the system
3
+ As a system operator
4
+ I want to simultaneously tail (and filter) a set of logfiles
5
+
6
+ Scenario: Tailing an existing file with no filters or transformations
7
+ Given these are the contents of "config.rb":
8
+ """
9
+ file 'syslog'
10
+ """
11
+ And these are the contents of "syslog":
12
+ """
13
+ Line One
14
+ """
15
+ When I run bracken -c config.rb
16
+ Then I should see "Line One" on standard out
17
+ When another process appends "Line Two" to "syslog"
18
+ Then I should see "Line Two" on standard out
19
+
20
+ Scenario: Tailing and filtering an existing file
21
+ Given these are the contents of "config.rb":
22
+ """
23
+ file 'syslog' do
24
+ on 'fetchmail', /reading message/
25
+ end
26
+ """
27
+ And these are the contents of "syslog":
28
+ """
29
+ Sep 30 13:11:39 frodo fetchmail[19159]: skipping message bob@example.org@pop.example.org:1 (51945 octets) (oversized) flushed
30
+ Sep 30 13:12:08 frodo fetchmail[19159]: reading message bob@example.org@pop.example.org:1 of 1 (4697 octets) flushed
31
+ """
32
+ When I run bracken -c config.rb
33
+ Then I should see "Sep 30 13:12:08 frodo fetchmail[19159]: reading message bob@example.org@pop.example.org:1 of 1 (4697 octets) flushed" on standard out
data/lib/bracken.rb ADDED
@@ -0,0 +1,3 @@
1
+ require 'bracken/application'
2
+ require 'bracken/configuration'
3
+ require 'bracken/logfile'
@@ -0,0 +1,30 @@
1
+ module Bracken
2
+ class Application
3
+ def self.execute(arguments)
4
+ new.execute(arguments)
5
+ end
6
+
7
+ attr_reader :configuration
8
+
9
+ def initialize
10
+ @configuration = Configuration.new
11
+ end
12
+
13
+ def execute(arguments)
14
+ configuration.parse(arguments)
15
+
16
+ while activity = IO.select(configuration.streams)
17
+ streams = activity.first
18
+
19
+ streams.each do |stream|
20
+ line = stream.filtered_gets
21
+
22
+ if line
23
+ STDOUT.puts line
24
+ STDOUT.flush
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,26 @@
1
+ require 'bracken/configuration/builder'
2
+ require 'bracken/configuration/options'
3
+
4
+ module Bracken
5
+ class Configuration
6
+ attr_reader :options
7
+ attr_reader :builder
8
+ attr_reader :files
9
+
10
+ def initialize
11
+ @options = Options.new
12
+ @builder = Builder.new(self)
13
+ @files = []
14
+ end
15
+
16
+ def parse(arguments)
17
+ options.parse(arguments)
18
+ builder.parse(options.configuration_file)
19
+ self
20
+ end
21
+
22
+ def streams
23
+ files.map { |file| file.stream }
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,39 @@
1
+ require 'optparse'
2
+
3
+ module Bracken
4
+ class Configuration
5
+
6
+ class Builder
7
+ attr_reader :configuration
8
+ attr_reader :output_stream
9
+ attr_reader :error_stream
10
+
11
+ def initialize(configuration, output_stream = STDOUT, error_stream = STDERR)
12
+ @configuration = configuration
13
+ @output_stream = output_stream
14
+ @error_stream = error_stream
15
+ end
16
+
17
+ def parse(pathname)
18
+ if File.exist?(pathname)
19
+ instance_eval(File.read(pathname), pathname)
20
+ else
21
+ error_stream.puts("WARNING file #{pathname} does not exist!")
22
+ end
23
+ end
24
+
25
+ def file(path, &block)
26
+ configuration.files << Logfile.new(path)
27
+
28
+ if block_given?
29
+ instance_eval(&block)
30
+ end
31
+ end
32
+
33
+ def on(*args)
34
+ configuration.files.last.filters << Logfile::Filter.new(*args)
35
+ end
36
+ end
37
+
38
+ end
39
+ end
@@ -0,0 +1,25 @@
1
+ require 'optparse'
2
+
3
+ module Bracken
4
+ class Configuration
5
+
6
+ class Options
7
+ attr_reader :configuration_file
8
+
9
+ def initialize
10
+ @configuration_file = '/etc/bracken.rb'
11
+ end
12
+
13
+ def parse(arguments)
14
+ arguments.extend(::OptionParser::Arguable)
15
+
16
+ arguments.options do |opts|
17
+ opts.on('-c', '--config-file PATH', 'Read configuration from PATH.', '[/etc/bracken.rb]') do |path|
18
+ @configuration_file = path
19
+ end
20
+ end.parse!
21
+ end
22
+ end
23
+
24
+ end
25
+ end
@@ -0,0 +1,27 @@
1
+ require 'bracken/logfile/filter'
2
+ require 'bracken/logfile/stream'
3
+ require 'open4'
4
+
5
+ module Bracken
6
+ class Logfile
7
+ attr_reader :filters
8
+ attr_reader :path
9
+
10
+ def initialize(path)
11
+ @filters = []
12
+ @path = path
13
+ end
14
+
15
+ def stream
16
+ @stream ||= begin
17
+ pid, _, out, _ = Open4.popen4("tail -f #{path}")
18
+
19
+ out.extend(Stream)
20
+ out.logfile = self
21
+ out.pid = pid
22
+
23
+ out
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,24 @@
1
+ module Bracken
2
+ class Logfile
3
+
4
+ class Filter < Struct.new(:service, :pattern)
5
+ # FIXME this is heinous
6
+ def match?(line)
7
+ if line.include?(service)
8
+ if pattern
9
+ if line.match(pattern)
10
+ true
11
+ else
12
+ false
13
+ end
14
+ else
15
+ true
16
+ end
17
+ else
18
+ false
19
+ end
20
+ end
21
+ end
22
+
23
+ end
24
+ end
@@ -0,0 +1,21 @@
1
+ module Bracken
2
+ class Logfile
3
+
4
+ module Stream
5
+ attr_accessor :logfile
6
+ attr_accessor :pid
7
+
8
+ # FIXME this could use work
9
+ def filtered_gets
10
+ line = gets
11
+ line if logfile.filters.empty? || logfile.filters.any? { |filter| filter.match?(line) }
12
+ end
13
+
14
+ def kill
15
+ Process.kill('TERM', pid)
16
+ Process.wait(pid)
17
+ end
18
+ end
19
+
20
+ end
21
+ end
@@ -0,0 +1,38 @@
1
+ require File.join(File.dirname(__FILE__), 'test_helper')
2
+
3
+ class ConfigurationBuilderTest < Test::Unit::TestCase
4
+ context '#parse' do
5
+ should "warn on standard error when configuration file doesn't exist" do
6
+ error_stream = StringIO.new('')
7
+
8
+ builder = Configuration::Builder.new(nil, nil, error_stream)
9
+ builder.parse('/does/not/exist')
10
+ error_stream.rewind
11
+ error_stream.read.should == "WARNING file /does/not/exist does not exist!\n"
12
+ end
13
+ end
14
+
15
+ context '#file' do
16
+ should 'add a new file into the configuration' do
17
+ configuration = Configuration.new
18
+
19
+ builder = Configuration::Builder.new(configuration)
20
+ builder.file('/foo/bar')
21
+
22
+ configuration.files.map { |file| file.path }.should == ['/foo/bar']
23
+ end
24
+ end
25
+
26
+ context '#on' do
27
+ should 'add a filter to the current file' do
28
+ configuration = Configuration.new
29
+
30
+ builder = Configuration::Builder.new(configuration)
31
+ builder.file('/foo/bar') do
32
+ on 'foo'
33
+ end
34
+
35
+ configuration.files.map { |file| file.filters }.should == [[Logfile::Filter.new('foo')]]
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,22 @@
1
+ require File.join(File.dirname(__FILE__), 'test_helper')
2
+
3
+ class ConfigurationOptionsTest < Test::Unit::TestCase
4
+ context 'options' do
5
+ setup { @options = Configuration::Options.new }
6
+
7
+ should 'default the configuration file to /etc/bracken.rb' do
8
+ @options.parse(%w())
9
+ @options.configuration_file.should == '/etc/bracken.rb'
10
+ end
11
+
12
+ should 'use the configuration file from -c' do
13
+ @options.parse(%w(-c config.rb))
14
+ @options.configuration_file.should == 'config.rb'
15
+ end
16
+
17
+ should 'use the configuration file from --config-file' do
18
+ @options.parse(%w(--config-file config.rb))
19
+ @options.configuration_file.should == 'config.rb'
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,29 @@
1
+ require File.join(File.dirname(__FILE__), 'test_helper')
2
+
3
+ class ConfigurationTest < Test::Unit::TestCase
4
+ context 'faking the filesystem' do
5
+ setup { FakeFS.activate! }
6
+ teardown { FakeFS.deactivate! }
7
+
8
+ context '#files' do
9
+ should 'be empty when the configuration file specifies no files' do
10
+ File.open('/etc/bracken.rb', 'w') { |file| file.write('') }
11
+ Configuration.new.parse([]).files.should == []
12
+ end
13
+
14
+ should 'have the files specified in the configuration file' do
15
+ File.open('/etc/bracken.rb', 'w') { |file| file.write('file "/var/log/syslog"') }
16
+ Configuration.new.parse([]).files.map { |file| file.path }.should == ['/var/log/syslog']
17
+ end
18
+ end
19
+
20
+ context '#streams' do
21
+ should 'return the IO stream for each file' do
22
+ File.open('/etc/bracken.rb', 'w') { |file| file.write('file "/var/log/syslog"') }
23
+
24
+ configuration = Configuration.new.parse([])
25
+ configuration.streams.should == configuration.files.map { |file| file.stream }
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,19 @@
1
+ require File.join(File.dirname(__FILE__), 'test_helper')
2
+
3
+ class LogfileFilterTest < Test::Unit::TestCase
4
+ context '#match?' do
5
+ setup do
6
+ @line = 'Sep 30 13:11:39 frodo fetchmail[19159]: skipping message bob@example.org@pop.example.org:1 (51945 octets) (oversized) flushed'
7
+ end
8
+
9
+ should 'match a syslog-style service name' do
10
+ Logfile::Filter.new('postfix').match?(@line).should == false
11
+ Logfile::Filter.new('fetchmail').match?(@line).should == true
12
+ end
13
+
14
+ should 'match a syslog-style service name and filter by pattern' do
15
+ Logfile::Filter.new('fetchmail', /reading message/).match?(@line).should == false
16
+ Logfile::Filter.new('fetchmail', /skipping message/).match?(@line).should == true
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,21 @@
1
+ require File.join(File.dirname(__FILE__), 'test_helper')
2
+
3
+ class LogfileTest < Test::Unit::TestCase
4
+ context '#stream' do
5
+ setup do
6
+ @logfile = Logfile.new(__FILE__)
7
+ end
8
+
9
+ should 'return a selectable, killable IO object tailing the file' do
10
+ reads, _, _ = IO.select([@logfile.stream], nil, nil, 0.2)
11
+
12
+ stream = reads.first
13
+
14
+ begin
15
+ stream.gets.should == File.readlines(__FILE__)[-10]
16
+ ensure
17
+ stream.kill
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,15 @@
1
+ require 'test/unit'
2
+
3
+ require 'rubygems'
4
+ require 'fakefs/safe'
5
+ require 'matchy'
6
+ require 'redgreen' if STDIN.tty?
7
+ require 'shoulda'
8
+
9
+ lib = File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib'))
10
+ $:.unshift(lib) unless $:.include?(lib)
11
+ require 'bracken'
12
+
13
+ class Test::Unit::TestCase
14
+ include Bracken
15
+ end
metadata ADDED
@@ -0,0 +1,148 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: bracken
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Matthew Todd
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-10-28 00:00:00 +03:00
13
+ default_executable: bracken
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: shoe
17
+ type: :development
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: "0"
24
+ version:
25
+ - !ruby/object:Gem::Dependency
26
+ name: open4
27
+ type: :runtime
28
+ version_requirement:
29
+ version_requirements: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: "0"
34
+ version:
35
+ - !ruby/object:Gem::Dependency
36
+ name: cucumber
37
+ type: :development
38
+ version_requirement:
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: "0"
44
+ version:
45
+ - !ruby/object:Gem::Dependency
46
+ name: fakefs
47
+ type: :development
48
+ version_requirement:
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: "0"
54
+ version:
55
+ - !ruby/object:Gem::Dependency
56
+ name: jeremymcanally-matchy
57
+ type: :development
58
+ version_requirement:
59
+ version_requirements: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: "0"
64
+ version:
65
+ - !ruby/object:Gem::Dependency
66
+ name: redgreen
67
+ type: :development
68
+ version_requirement:
69
+ version_requirements: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: "0"
74
+ version:
75
+ - !ruby/object:Gem::Dependency
76
+ name: thoughtbot-shoulda
77
+ type: :development
78
+ version_requirement:
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: "0"
84
+ version:
85
+ description:
86
+ email: matthew.todd@gmail.com
87
+ executables:
88
+ - bracken
89
+ extensions: []
90
+
91
+ extra_rdoc_files:
92
+ - README.rdoc
93
+ files:
94
+ - Rakefile
95
+ - README.rdoc
96
+ - bin/bracken
97
+ - examples/console.rb
98
+ - features/step_definitions/bracken_steps.rb
99
+ - features/support/env.rb
100
+ - features/tailing.feature
101
+ - lib/bracken/application.rb
102
+ - lib/bracken/configuration/builder.rb
103
+ - lib/bracken/configuration/options.rb
104
+ - lib/bracken/configuration.rb
105
+ - lib/bracken/logfile/filter.rb
106
+ - lib/bracken/logfile/stream.rb
107
+ - lib/bracken/logfile.rb
108
+ - lib/bracken.rb
109
+ - test/configuration_builder_test.rb
110
+ - test/configuration_options_test.rb
111
+ - test/configuration_test.rb
112
+ - test/logfile_filter_test.rb
113
+ - test/logfile_test.rb
114
+ - test/test_helper.rb
115
+ has_rdoc: true
116
+ homepage:
117
+ licenses: []
118
+
119
+ post_install_message:
120
+ rdoc_options:
121
+ - --main
122
+ - README.rdoc
123
+ - --title
124
+ - bracken-0.1.0
125
+ - --inline-source
126
+ require_paths:
127
+ - lib
128
+ required_ruby_version: !ruby/object:Gem::Requirement
129
+ requirements:
130
+ - - ">="
131
+ - !ruby/object:Gem::Version
132
+ version: "0"
133
+ version:
134
+ required_rubygems_version: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: "0"
139
+ version:
140
+ requirements: []
141
+
142
+ rubyforge_project:
143
+ rubygems_version: 1.3.5
144
+ signing_key:
145
+ specification_version: 3
146
+ summary: Bracken, at present, is a glorified logfile tailer.
147
+ test_files: []
148
+