fluke 0.0.5
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +47 -0
- data/bin/fluke +7 -0
- data/lib/fluke.rb +97 -0
- data/lib/fluke/cli.rb +56 -0
- data/lib/fluke/monkey_patch.rb +35 -0
- data/lib/fluke/result.rb +67 -0
- data/lib/fluke/watcher.rb +111 -0
- metadata +99 -0
data/README.rdoc
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
= fluke
|
2
|
+
|
3
|
+
* http://github.com/maxaf/fluke
|
4
|
+
|
5
|
+
== DESCRIPTION:
|
6
|
+
|
7
|
+
A simple resource observer designed to detect change over time.
|
8
|
+
|
9
|
+
== SYNOPSIS:
|
10
|
+
|
11
|
+
$ fluke --help
|
12
|
+
|
13
|
+
== REQUIREMENTS:
|
14
|
+
|
15
|
+
* Ruby 1.8.6+ || JRuby 1.3.1+
|
16
|
+
* DataMapper 0.10.0+
|
17
|
+
* Some database
|
18
|
+
* github.com/marcel/aws-s3
|
19
|
+
|
20
|
+
== INSTALL:
|
21
|
+
|
22
|
+
# gem install fluke
|
23
|
+
|
24
|
+
== LICENSE:
|
25
|
+
|
26
|
+
(The MIT License)
|
27
|
+
|
28
|
+
Copyright (c) 2009 Max Afonov
|
29
|
+
|
30
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
31
|
+
a copy of this software and associated documentation files (the
|
32
|
+
'Software'), to deal in the Software without restriction, including
|
33
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
34
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
35
|
+
permit persons to whom the Software is furnished to do so, subject to
|
36
|
+
the following conditions:
|
37
|
+
|
38
|
+
The above copyright notice and this permission notice shall be
|
39
|
+
included in all copies or substantial portions of the Software.
|
40
|
+
|
41
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
42
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
43
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
44
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
45
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
46
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
47
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/bin/fluke
ADDED
data/lib/fluke.rb
ADDED
@@ -0,0 +1,97 @@
|
|
1
|
+
$:.unshift(File.dirname(__FILE__)) unless
|
2
|
+
$:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
|
3
|
+
|
4
|
+
require 'rubygems'
|
5
|
+
require 'yaml'
|
6
|
+
require 'fluke/watcher'
|
7
|
+
require 'fluke/monkey_patch'
|
8
|
+
require 'aws/s3'
|
9
|
+
require 'thread'
|
10
|
+
require 'dm-core'
|
11
|
+
|
12
|
+
module Fluke
|
13
|
+
VERSION = '0.0.4'
|
14
|
+
ROOT = Dir.new(File.expand_path(File.dirname(__FILE__) + "/../"))
|
15
|
+
DEFAULT_CONF_PATH = "~/.fluke.yml"
|
16
|
+
@@verbose = false
|
17
|
+
@@watchers = {}
|
18
|
+
@@mutex = { :s3 => Mutex.new }
|
19
|
+
|
20
|
+
def self.verbose?
|
21
|
+
@@verbose
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.verbose=(v = false)
|
25
|
+
@@verbose = v ? true : false
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.watchers
|
29
|
+
@@watchers
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.conf
|
33
|
+
@@conf.dup
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.mutex
|
37
|
+
@@mutex
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.log
|
41
|
+
return unless verbose? and block_given?
|
42
|
+
@@log_here.puts yield
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.watch(watcher, &init)
|
46
|
+
unless watcher.is_a?(Watcher)
|
47
|
+
watcher = Watcher.first(:name => watcher.to_s)
|
48
|
+
end
|
49
|
+
watchers[watcher.name] = watcher
|
50
|
+
if init
|
51
|
+
watcher.instance_eval &init
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.configure!(opts = {})
|
56
|
+
p = {
|
57
|
+
:conf_path => File.expand_path(DEFAULT_CONF_PATH),
|
58
|
+
:stdout => STDOUT,
|
59
|
+
:stderr => STDERR,
|
60
|
+
:log_here => :stderr
|
61
|
+
}
|
62
|
+
p.merge! opts
|
63
|
+
@@log_here = p[p[:log_here]] || p[:stderr]
|
64
|
+
|
65
|
+
begin
|
66
|
+
@@conf = YAML::load(File.open(p[:conf_path]))
|
67
|
+
if !@@conf[:proxy].nil?
|
68
|
+
@@conf[:s3][:connection][:proxy] = @@conf[:proxy]
|
69
|
+
@@conf[:proxy_string] = "http://#{@@conf[:proxy][:host]}:#{@@conf[:proxy][:port]}"
|
70
|
+
end
|
71
|
+
if verbose?
|
72
|
+
DataMapper::Logger.new(@@log_here, :debug)
|
73
|
+
end
|
74
|
+
DataMapper.setup(:default, @@conf[:db])
|
75
|
+
AWS::S3::Base.establish_connection!(@@conf[:s3][:connection])
|
76
|
+
Result::Body.set_current_bucket_to @@conf[:s3][:bucket][:result]
|
77
|
+
rescue => e
|
78
|
+
STDERR.puts e.inspect
|
79
|
+
return false
|
80
|
+
end
|
81
|
+
return true
|
82
|
+
end
|
83
|
+
|
84
|
+
def self.run!
|
85
|
+
watchers.each do |n,watcher|
|
86
|
+
watcher.thread = Thread.new do
|
87
|
+
while true
|
88
|
+
watcher.run
|
89
|
+
watcher.wait
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
sleep while true
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|
data/lib/fluke/cli.rb
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
|
3
|
+
module Fluke
|
4
|
+
class CLI
|
5
|
+
def self.execute(stdout, stderr, arguments=[])
|
6
|
+
p = {
|
7
|
+
:stdout => stdout, :stderr => stderr
|
8
|
+
}
|
9
|
+
|
10
|
+
parser = OptionParser.new do |opts|
|
11
|
+
opts.banner = <<-BANNER.gsub(/^ /,'')
|
12
|
+
A simple resource observer designed to detect change over time.
|
13
|
+
|
14
|
+
Usage: #{File.basename($0)} [options]
|
15
|
+
# run all watchers
|
16
|
+
|
17
|
+
-OR-
|
18
|
+
|
19
|
+
#{File.basename($0)} [options] arg1..argN
|
20
|
+
# args are either files, in which case watcher info is
|
21
|
+
# inferred based on file content, or watcher names
|
22
|
+
Options are:
|
23
|
+
BANNER
|
24
|
+
opts.separator ""
|
25
|
+
opts.on("-c", "--conf", String,
|
26
|
+
"Config file location",
|
27
|
+
"Default: #{Fluke::DEFAULT_CONF_PATH}") { |arg| p[:conf_path] = arg || nil }
|
28
|
+
opts.on("-v", "--verbose",
|
29
|
+
"Be chatty.",
|
30
|
+
"Default: false") { |arg| Fluke.verbose = arg }
|
31
|
+
opts.on("-h", "--help",
|
32
|
+
"Show this help message.") { stderr.puts opts; exit }
|
33
|
+
opts.parse!(arguments)
|
34
|
+
end
|
35
|
+
|
36
|
+
Fluke.configure! p
|
37
|
+
|
38
|
+
if arguments.size > 0 then
|
39
|
+
arguments.uniq.each do |name|
|
40
|
+
if File.exists?(name) and !File.directory?(name) then
|
41
|
+
path = File.expand_path(name)
|
42
|
+
Fluke.class_eval(File.read(path), path)
|
43
|
+
else
|
44
|
+
Fluke.watch name
|
45
|
+
end
|
46
|
+
end
|
47
|
+
else
|
48
|
+
Watcher.all.each do |watcher|
|
49
|
+
Fluke.watch watcher
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
Fluke.run!
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Fluke
|
2
|
+
module Delays
|
3
|
+
def seconds
|
4
|
+
self*1000
|
5
|
+
end
|
6
|
+
|
7
|
+
def minutes
|
8
|
+
self.seconds*60
|
9
|
+
end
|
10
|
+
|
11
|
+
def hours
|
12
|
+
self.minutes*60
|
13
|
+
end
|
14
|
+
|
15
|
+
def days
|
16
|
+
self.hours*24
|
17
|
+
end
|
18
|
+
|
19
|
+
def weeks
|
20
|
+
self.days*7
|
21
|
+
end
|
22
|
+
|
23
|
+
def months
|
24
|
+
self.weeks*4
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
class Fixnum
|
30
|
+
include Fluke::Delays
|
31
|
+
end
|
32
|
+
|
33
|
+
class Float
|
34
|
+
include Fluke::Delays
|
35
|
+
end
|
data/lib/fluke/result.rb
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'digest/sha1'
|
2
|
+
require 'aws/s3'
|
3
|
+
require 'differ'
|
4
|
+
|
5
|
+
module Fluke
|
6
|
+
class Result
|
7
|
+
include DataMapper::Resource
|
8
|
+
storage_names[:default] = 'results'
|
9
|
+
property :id, Serial, :key => true
|
10
|
+
property :checksum, String
|
11
|
+
property :url, String
|
12
|
+
property :created_at, DateTime
|
13
|
+
property :updated_at, DateTime
|
14
|
+
belongs_to :watcher
|
15
|
+
|
16
|
+
class Body < AWS::S3::S3Object
|
17
|
+
end
|
18
|
+
|
19
|
+
def body_key
|
20
|
+
"#{self.watcher.name}/#{self.checksum}"
|
21
|
+
end
|
22
|
+
|
23
|
+
def body
|
24
|
+
begin
|
25
|
+
return Fluke.mutex[:s3].synchronize do
|
26
|
+
Body.find self.body_key
|
27
|
+
end
|
28
|
+
rescue AWS::S3::NoSuchKey => nsk
|
29
|
+
return nil
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def body?
|
34
|
+
Fluke.mutex[:s3].synchronize { Body.exists? self.body_key }
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.from_response(args)
|
38
|
+
opts = {
|
39
|
+
:mime_type => 'binary/octet-stream',
|
40
|
+
:generated_url => nil
|
41
|
+
}.merge(args)
|
42
|
+
|
43
|
+
checksum = Digest::SHA1.hexdigest(opts[:content])
|
44
|
+
result = Result.new :checksum => checksum
|
45
|
+
result.watcher = opts[:watcher]
|
46
|
+
if opts[:generated_url] and opts[:generated_url] != opts[:watcher].url
|
47
|
+
result.url = opts[:generated_url]
|
48
|
+
end
|
49
|
+
result.save
|
50
|
+
|
51
|
+
if result.body?
|
52
|
+
Fluke.log { "#{result.watcher} exists: #{result.body_key}" }
|
53
|
+
else
|
54
|
+
Fluke.log { "#{result.watcher} store: #{result.body_key}" }
|
55
|
+
Fluke.mutex[:s3].synchronize do
|
56
|
+
Body.store(result.body_key, opts[:content].dup, :content_type => opts[:mime_type])
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
result
|
61
|
+
end
|
62
|
+
|
63
|
+
def diff_against(right)
|
64
|
+
differ = Differ.diff_by_line(self.body.value, right.body.value)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'dm-core'
|
3
|
+
require 'fluke/result'
|
4
|
+
require 'httpclient'
|
5
|
+
|
6
|
+
module Fluke
|
7
|
+
class Watcher
|
8
|
+
include DataMapper::Resource
|
9
|
+
storage_names[:default] = 'watchers'
|
10
|
+
property :id, Serial, :key => true
|
11
|
+
property :name, String
|
12
|
+
property :url, String
|
13
|
+
property :delay, Float
|
14
|
+
property :meth, String
|
15
|
+
property :created_at, DateTime
|
16
|
+
property :updated_at, DateTime
|
17
|
+
has n, :results
|
18
|
+
|
19
|
+
attr_accessor :thread
|
20
|
+
|
21
|
+
def run
|
22
|
+
Thread.new do
|
23
|
+
begin
|
24
|
+
generated_url = self.generate_url
|
25
|
+
Fluke::log { "#{self}: generated URL: #{generated_url}" }
|
26
|
+
|
27
|
+
Thread.current[:result] = \
|
28
|
+
if generated_url =~ /^file\:\/\/(.+)$/
|
29
|
+
run_file File.expand_path($1)
|
30
|
+
else
|
31
|
+
run_http generated_url
|
32
|
+
end
|
33
|
+
rescue => e
|
34
|
+
Fluke.log { "#{self}: failed to run: #{e.inspect}; backtrace: #{e.backtrace.join("; ")}" }
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def run_file(path)
|
40
|
+
result = Result.from_response \
|
41
|
+
:watcher => self, :content => extract_content(File.read(path)), :generated_url => "file://#{path}"
|
42
|
+
end
|
43
|
+
|
44
|
+
def run_http(generated_url)
|
45
|
+
hc = HTTPClient.new :proxy => Fluke.conf[:proxy_string], :user_agent => "Fluke/#{Fluke::VERSION}"
|
46
|
+
http_method = self.meth.downcase.to_sym
|
47
|
+
unless hc.respond_to?(http_method)
|
48
|
+
Fluke::log { "#{self}: invalid method '#{http_method}', converting to :get" }
|
49
|
+
http_method = :get
|
50
|
+
end
|
51
|
+
res = hc.send(http_method, generated_url, { 'Cache-Control' => 'no-cache', 'Pragma' => 'no-cache' })
|
52
|
+
result = Result.from_response \
|
53
|
+
:watcher => self, :content => extract_content(res.body.dump.dup),
|
54
|
+
:generated_url => generated_url, :mime_type => res.header['Content-Type']
|
55
|
+
end
|
56
|
+
|
57
|
+
def url_generator(&block)
|
58
|
+
@generate_url = true
|
59
|
+
@url_generator = block
|
60
|
+
end
|
61
|
+
|
62
|
+
def url_strftime(offset = 0)
|
63
|
+
self.url_generator do |raw|
|
64
|
+
(Time.now + offset/1000.000).strftime(raw)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def generate_url
|
69
|
+
if @generate_url
|
70
|
+
if @url_generator.arity then
|
71
|
+
return @url_generator.call(self.url)
|
72
|
+
else
|
73
|
+
return @url_generator.call
|
74
|
+
end
|
75
|
+
else
|
76
|
+
return self.url
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def content_extractor(&block)
|
81
|
+
@extract_content = true
|
82
|
+
@content_extractor = block
|
83
|
+
end
|
84
|
+
|
85
|
+
def extract_content(content)
|
86
|
+
if @extract_content
|
87
|
+
return @content_extractor.call(content)
|
88
|
+
else
|
89
|
+
return content
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def wait
|
94
|
+
dur = self.delay/1000.000
|
95
|
+
Fluke::log { "#{self}: sleeping #{dur} seconds" }
|
96
|
+
sleep dur
|
97
|
+
end
|
98
|
+
|
99
|
+
def to_s
|
100
|
+
"Watcher<#{name}>"
|
101
|
+
end
|
102
|
+
|
103
|
+
def last_result(checksum = nil)
|
104
|
+
if checksum
|
105
|
+
results.last(:checksum => checksum, :order => [ :created_at.desc ])
|
106
|
+
else
|
107
|
+
results.last(:order => [ :created_at.desc ])
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
metadata
ADDED
@@ -0,0 +1,99 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: fluke
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.5
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Max Afonov
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-12-16 00:00:00 -05:00
|
13
|
+
default_executable: fluke
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: aws-s3
|
17
|
+
type: :runtime
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 0.6.2
|
24
|
+
version:
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: httpclient
|
27
|
+
type: :runtime
|
28
|
+
version_requirement:
|
29
|
+
version_requirements: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 2.1.5.2
|
34
|
+
version:
|
35
|
+
- !ruby/object:Gem::Dependency
|
36
|
+
name: differ
|
37
|
+
type: :runtime
|
38
|
+
version_requirement:
|
39
|
+
version_requirements: !ruby/object:Gem::Requirement
|
40
|
+
requirements:
|
41
|
+
- - ">="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: 0.1.1
|
44
|
+
version:
|
45
|
+
- !ruby/object:Gem::Dependency
|
46
|
+
name: dm-core
|
47
|
+
type: :runtime
|
48
|
+
version_requirement:
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: 0.10.0
|
54
|
+
version:
|
55
|
+
description: Watch resources and determine how they change over time. Useful for post-mortem investigations.
|
56
|
+
email: max@bumnetworks.com
|
57
|
+
executables:
|
58
|
+
- fluke
|
59
|
+
extensions: []
|
60
|
+
|
61
|
+
extra_rdoc_files:
|
62
|
+
- README.rdoc
|
63
|
+
files:
|
64
|
+
- lib/fluke.rb
|
65
|
+
- lib/fluke/cli.rb
|
66
|
+
- lib/fluke/monkey_patch.rb
|
67
|
+
- lib/fluke/result.rb
|
68
|
+
- lib/fluke/watcher.rb
|
69
|
+
- README.rdoc
|
70
|
+
has_rdoc: true
|
71
|
+
homepage: http://github.com/maxaf/fluke
|
72
|
+
licenses: []
|
73
|
+
|
74
|
+
post_install_message:
|
75
|
+
rdoc_options:
|
76
|
+
- --charset=UTF-8
|
77
|
+
require_paths:
|
78
|
+
- lib
|
79
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
80
|
+
requirements:
|
81
|
+
- - ">="
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
version: "0"
|
84
|
+
version:
|
85
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: "0"
|
90
|
+
version:
|
91
|
+
requirements: []
|
92
|
+
|
93
|
+
rubyforge_project:
|
94
|
+
rubygems_version: 1.3.5
|
95
|
+
signing_key:
|
96
|
+
specification_version: 3
|
97
|
+
summary: Track change over time
|
98
|
+
test_files: []
|
99
|
+
|