bracken 0.1.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/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
+