fiveruns-dash-ruby 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +68 -0
- data/Rakefile +39 -0
- data/lib/fiveruns/dash.rb +144 -0
- data/lib/fiveruns/dash/configuration.rb +116 -0
- data/lib/fiveruns/dash/exception_recorder.rb +135 -0
- data/lib/fiveruns/dash/host.rb +173 -0
- data/lib/fiveruns/dash/instrument.rb +128 -0
- data/lib/fiveruns/dash/metric.rb +379 -0
- data/lib/fiveruns/dash/recipe.rb +63 -0
- data/lib/fiveruns/dash/reporter.rb +208 -0
- data/lib/fiveruns/dash/scm.rb +126 -0
- data/lib/fiveruns/dash/session.rb +81 -0
- data/lib/fiveruns/dash/store/file.rb +24 -0
- data/lib/fiveruns/dash/store/http.rb +198 -0
- data/lib/fiveruns/dash/threads.rb +24 -0
- data/lib/fiveruns/dash/trace.rb +65 -0
- data/lib/fiveruns/dash/typable.rb +29 -0
- data/lib/fiveruns/dash/update.rb +215 -0
- data/lib/fiveruns/dash/version.rb +86 -0
- data/recipes/jruby.rb +107 -0
- data/recipes/ruby.rb +34 -0
- data/test/collector_communication_test.rb +260 -0
- data/test/configuration_test.rb +97 -0
- data/test/exception_recorder_test.rb +112 -0
- data/test/file_store_test.rb +56 -0
- data/test/fixtures/http_store_test/response.json +6 -0
- data/test/http_store_test.rb +210 -0
- data/test/metric_test.rb +204 -0
- data/test/recipe_test.rb +146 -0
- data/test/reliability_test.rb +60 -0
- data/test/reporter_test.rb +46 -0
- data/test/scm_test.rb +70 -0
- data/test/session_test.rb +49 -0
- data/test/test_helper.rb +96 -0
- data/test/tracing_test.rb +68 -0
- data/test/update_test.rb +42 -0
- data/version.yml +3 -0
- metadata +112 -0
@@ -0,0 +1,63 @@
|
|
1
|
+
module Fiveruns::Dash
|
2
|
+
|
3
|
+
class Recipe
|
4
|
+
|
5
|
+
class ConfigurationError < ::ArgumentError; end
|
6
|
+
|
7
|
+
def self.scope_stack
|
8
|
+
@scope_stack ||= []
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.current
|
12
|
+
scope_stack.last
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.in_scope(recipe)
|
16
|
+
scope_stack << recipe
|
17
|
+
yield
|
18
|
+
scope_stack.pop
|
19
|
+
end
|
20
|
+
|
21
|
+
attr_reader :name, :url, :options
|
22
|
+
def initialize(name, options = {}, &block)
|
23
|
+
@name = name
|
24
|
+
@options = options
|
25
|
+
@url = options[:url]
|
26
|
+
@block = block
|
27
|
+
validate!
|
28
|
+
end
|
29
|
+
|
30
|
+
def add_to(configuration)
|
31
|
+
self.class.in_scope self do
|
32
|
+
@block.call(configuration)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def matches?(criteria)
|
37
|
+
criteria.all? { |k, v| options[k] == v }
|
38
|
+
end
|
39
|
+
|
40
|
+
def ==(other)
|
41
|
+
name == other.name && other.options == options
|
42
|
+
end
|
43
|
+
|
44
|
+
def info
|
45
|
+
{
|
46
|
+
:name => name.to_s,
|
47
|
+
:url => url.to_s
|
48
|
+
}
|
49
|
+
end
|
50
|
+
|
51
|
+
#######
|
52
|
+
private
|
53
|
+
#######
|
54
|
+
|
55
|
+
def validate!
|
56
|
+
unless @url
|
57
|
+
raise ConfigurationError, "Recipe requires :url option"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
@@ -0,0 +1,208 @@
|
|
1
|
+
require 'thread'
|
2
|
+
|
3
|
+
module Fiveruns::Dash
|
4
|
+
|
5
|
+
class ShutdownSignal < ::Exception; end
|
6
|
+
|
7
|
+
class Reporter
|
8
|
+
|
9
|
+
attr_accessor :interval
|
10
|
+
attr_reader :started_at
|
11
|
+
def initialize(session, interval = 60.seconds.to_i)
|
12
|
+
@session = session
|
13
|
+
@interval = interval
|
14
|
+
end
|
15
|
+
|
16
|
+
def revive!
|
17
|
+
return if !started? || foreground?
|
18
|
+
start if !@thread || !@thread.alive?
|
19
|
+
end
|
20
|
+
|
21
|
+
def alive?
|
22
|
+
@thread && @thread.alive? && started?
|
23
|
+
end
|
24
|
+
|
25
|
+
def start(run_in_background = true)
|
26
|
+
restarted = @started_at ? true : false
|
27
|
+
unless defined?(@started_at)
|
28
|
+
@started_at = ::Fiveruns::Dash::START_TIME
|
29
|
+
end
|
30
|
+
setup_for run_in_background
|
31
|
+
if @background
|
32
|
+
@thread = Thread.new { run(restarted) }
|
33
|
+
else
|
34
|
+
# Will it be run in foreground?
|
35
|
+
run(restarted)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def started?
|
40
|
+
@started_at
|
41
|
+
end
|
42
|
+
|
43
|
+
def foreground?
|
44
|
+
started? && !@background
|
45
|
+
end
|
46
|
+
|
47
|
+
def background?
|
48
|
+
started? && @background
|
49
|
+
end
|
50
|
+
|
51
|
+
def send_trace(trace)
|
52
|
+
if trace.data
|
53
|
+
payload = TracePayload.new(trace)
|
54
|
+
Fiveruns::Dash.logger.debug "Sending trace: #{payload.to_json}"
|
55
|
+
Thread.new { Update.new(payload).store(*update_locations) }
|
56
|
+
else
|
57
|
+
Fiveruns::Dash.logger.debug "No trace to send"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def ping
|
62
|
+
payload = PingPayload.new(@session.info, @started_at)
|
63
|
+
Update.new(payload).ping(*update_locations)
|
64
|
+
end
|
65
|
+
|
66
|
+
def stop
|
67
|
+
@thread && @thread.alive? && @thread.raise(ShutdownSignal.new)
|
68
|
+
end
|
69
|
+
|
70
|
+
#######
|
71
|
+
private
|
72
|
+
#######
|
73
|
+
|
74
|
+
TRAPS = {}
|
75
|
+
|
76
|
+
def install_signals
|
77
|
+
%w(INT TERM).each do |sym|
|
78
|
+
TRAPS[sym] = Signal.trap(sym) do
|
79
|
+
stop
|
80
|
+
TRAPS[sym].call if TRAPS[sym]
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def run(restarted)
|
86
|
+
Fiveruns::Dash.logger.info "Starting reporter thread; endpoints are #{update_locations.inspect}"
|
87
|
+
|
88
|
+
install_signals
|
89
|
+
error_barrier do
|
90
|
+
total_time = 0
|
91
|
+
loop do
|
92
|
+
# account for the amount of time it took to upload, and adjust the sleep time accordingly
|
93
|
+
total_time += time_for { send_info_update }
|
94
|
+
rest(@interval - total_time)
|
95
|
+
total_time = 0
|
96
|
+
total_time += time_for do
|
97
|
+
send_data_update
|
98
|
+
send_exceptions_update
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def error_barrier
|
105
|
+
begin
|
106
|
+
yield
|
107
|
+
rescue Fiveruns::Dash::ShutdownSignal => me
|
108
|
+
return
|
109
|
+
rescue Exception => e
|
110
|
+
Fiveruns::Dash.logger.error "#{e.class.name}: #{e.message}"
|
111
|
+
Fiveruns::Dash.logger.error e.backtrace.join("\n\t")
|
112
|
+
retry
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def rest(amount)
|
117
|
+
amount > 0 ? sleep(amount) : nil
|
118
|
+
end
|
119
|
+
|
120
|
+
def time_for(&block)
|
121
|
+
a = Time.now
|
122
|
+
block.call
|
123
|
+
Time.now - a
|
124
|
+
end
|
125
|
+
|
126
|
+
def setup_for(run_in_background = true)
|
127
|
+
@background = run_in_background
|
128
|
+
end
|
129
|
+
|
130
|
+
def send_info_update
|
131
|
+
@info_update_sent ||= begin
|
132
|
+
payload = InfoPayload.new(@session.info, @started_at)
|
133
|
+
Fiveruns::Dash.logger.debug "Sending info: #{payload.to_json}"
|
134
|
+
result = Update.new(payload).store(*update_locations)
|
135
|
+
send_fake_info(payload)
|
136
|
+
result
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def send_exceptions_update
|
141
|
+
if @info_update_sent
|
142
|
+
data = @session.exception_data
|
143
|
+
if data.empty?
|
144
|
+
Fiveruns::Dash.logger.debug "No exceptions for this interval"
|
145
|
+
else
|
146
|
+
payload = ExceptionsPayload.new(data)
|
147
|
+
Fiveruns::Dash.logger.debug "Sending exceptions: #{payload.to_json}"
|
148
|
+
Update.new(payload).store(*update_locations)
|
149
|
+
end
|
150
|
+
else
|
151
|
+
# Discard data
|
152
|
+
@session.reset
|
153
|
+
Fiveruns::Dash.logger.warn "Discarding interval exceptions"
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
def send_data_update
|
158
|
+
if @info_update_sent
|
159
|
+
data = @session.data
|
160
|
+
payload = DataPayload.new(data)
|
161
|
+
Fiveruns::Dash.logger.debug "Sending data: #{payload.to_json}"
|
162
|
+
result = Update.new(payload).store(*update_locations)
|
163
|
+
send_fake_data(payload)
|
164
|
+
result
|
165
|
+
else
|
166
|
+
# Discard data
|
167
|
+
@session.reset
|
168
|
+
Fiveruns::Dash.logger.warn "Discarding interval data"
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
def update_locations
|
173
|
+
@update_locations ||= if ENV['DASH_UPDATE']
|
174
|
+
ENV['DASH_UPDATE'].strip.split(/\s*,\s*/)
|
175
|
+
else
|
176
|
+
default_update_locations
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
def send_fake_data(payload)
|
181
|
+
fake_host_count.times do |idx|
|
182
|
+
payload.params[:process_id] = Fiveruns::Dash.process_ids[idx+1]
|
183
|
+
Fiveruns::Dash.logger.debug "Sending data: #{payload.to_json}"
|
184
|
+
Update.new(payload).store(*update_locations)
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
def send_fake_info(payload)
|
189
|
+
host = payload.params[:hostname]
|
190
|
+
fake_host_count.times do |idx|
|
191
|
+
payload.params[:mac] += idx.to_s
|
192
|
+
payload.params[:hostname] = host + idx.to_s
|
193
|
+
Fiveruns::Dash.logger.debug "Sending info: #{payload.to_json}"
|
194
|
+
Update.new(payload).store(*update_locations)
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
def fake_host_count
|
199
|
+
ENV['DASH_FAKE_HOST_COUNT'].to_i
|
200
|
+
end
|
201
|
+
|
202
|
+
def default_update_locations
|
203
|
+
%w(https://dash-collector.fiveruns.com https://dash-collector02.fiveruns.com)
|
204
|
+
end
|
205
|
+
|
206
|
+
end
|
207
|
+
|
208
|
+
end
|
@@ -0,0 +1,126 @@
|
|
1
|
+
require 'date'
|
2
|
+
module Fiveruns::Dash
|
3
|
+
class SCM
|
4
|
+
include Typable
|
5
|
+
|
6
|
+
def self.matching(startpath)
|
7
|
+
scm_hash = {}
|
8
|
+
types.each do |name, klass|
|
9
|
+
if path = locate_upwards(startpath, ".#{name}")
|
10
|
+
Fiveruns::Dash.logger.info "SCM: Found #{name} in #{path}"
|
11
|
+
scm_hash[path] = klass
|
12
|
+
end
|
13
|
+
end
|
14
|
+
winning_path = best_match(scm_hash.keys)
|
15
|
+
return nil if winning_path.nil?
|
16
|
+
Fiveruns::Dash.logger.info "SCM: Using #{scm_hash[winning_path].name} in #{winning_path}"
|
17
|
+
begin
|
18
|
+
scm_hash[winning_path].new(winning_path)
|
19
|
+
rescue Exception => e
|
20
|
+
Fiveruns::Dash.logger.warn "WARNING: #{e.message}"
|
21
|
+
nil
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.best_match( scm_paths )
|
26
|
+
scm_paths.max{|a,b| a.split("/").length <=> b.split("/").length}
|
27
|
+
end
|
28
|
+
|
29
|
+
def initialize(path)
|
30
|
+
@path = path
|
31
|
+
require_binding
|
32
|
+
end
|
33
|
+
|
34
|
+
def time
|
35
|
+
raise NotImplementedError, 'Abstract'
|
36
|
+
end
|
37
|
+
|
38
|
+
def revision
|
39
|
+
raise NotImplementedError, 'Abstract'
|
40
|
+
end
|
41
|
+
|
42
|
+
#######
|
43
|
+
private
|
44
|
+
#######
|
45
|
+
|
46
|
+
def self.locate_upwards(startpath, target)
|
47
|
+
return nil unless startpath
|
48
|
+
startpath = File.expand_path(startpath)
|
49
|
+
return startpath if File.exist?(File.join( startpath, target ))
|
50
|
+
return locate_upwards( File.dirname(startpath), target) unless File.dirname(startpath) == startpath
|
51
|
+
nil
|
52
|
+
end
|
53
|
+
|
54
|
+
def require_binding
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
|
59
|
+
class GitSCM < SCM
|
60
|
+
|
61
|
+
def revision
|
62
|
+
commit.sha
|
63
|
+
end
|
64
|
+
|
65
|
+
def time
|
66
|
+
commit.date
|
67
|
+
end
|
68
|
+
|
69
|
+
def url
|
70
|
+
@url ||= begin
|
71
|
+
origin = repo.remotes.detect { |r| r.name == 'origin' }
|
72
|
+
origin.url if origin
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
#######
|
77
|
+
private
|
78
|
+
#######
|
79
|
+
|
80
|
+
def commit
|
81
|
+
@commit ||= repo.object('HEAD')
|
82
|
+
end
|
83
|
+
|
84
|
+
def repo
|
85
|
+
@repo ||= Git.open(@path)
|
86
|
+
end
|
87
|
+
|
88
|
+
def require_binding
|
89
|
+
require 'git'
|
90
|
+
rescue LoadError
|
91
|
+
raise LoadError, "Dash deployment tracking for Git apps requires the 'git' gem"
|
92
|
+
end
|
93
|
+
|
94
|
+
end
|
95
|
+
|
96
|
+
class SvnSCM < SCM
|
97
|
+
|
98
|
+
def revision
|
99
|
+
@yaml['Last Changed Rev'] || @yaml['Revision']
|
100
|
+
end
|
101
|
+
|
102
|
+
def time
|
103
|
+
datestring = @yaml['Last Changed Date']
|
104
|
+
datestring.nil? ? nil : DateTime.parse(datestring.split("(").first.strip)
|
105
|
+
end
|
106
|
+
|
107
|
+
def url
|
108
|
+
@url ||= @yaml['URL']
|
109
|
+
end
|
110
|
+
|
111
|
+
#######
|
112
|
+
private
|
113
|
+
#######
|
114
|
+
|
115
|
+
def require_binding
|
116
|
+
@yaml = YAML.load(svn_info)
|
117
|
+
@yaml = {} unless Hash === @yaml
|
118
|
+
end
|
119
|
+
|
120
|
+
def svn_info
|
121
|
+
`svn info #{@path}`
|
122
|
+
end
|
123
|
+
|
124
|
+
end
|
125
|
+
|
126
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
module Fiveruns::Dash
|
2
|
+
|
3
|
+
class Session
|
4
|
+
|
5
|
+
attr_reader :configuration, :reporter
|
6
|
+
def initialize(configuration)
|
7
|
+
@configuration = configuration
|
8
|
+
# eager create the host data in the main thread
|
9
|
+
# as it is dangerous to load in the reporter thread
|
10
|
+
Fiveruns::Dash.host
|
11
|
+
end
|
12
|
+
|
13
|
+
def start(background = true, &block)
|
14
|
+
reporter.start(background, &block)
|
15
|
+
end
|
16
|
+
|
17
|
+
def exceptions
|
18
|
+
@exceptions ||= []
|
19
|
+
end
|
20
|
+
|
21
|
+
# Trace and send metric collection
|
22
|
+
def trace(name)
|
23
|
+
Thread.current[:trace] = ::Fiveruns::Dash::Trace.new(name)
|
24
|
+
result = yield
|
25
|
+
reporter.send_trace(Thread.current[:trace])
|
26
|
+
Thread.current[:trace] = nil
|
27
|
+
result
|
28
|
+
end
|
29
|
+
|
30
|
+
def add_exception(exception, sample=nil)
|
31
|
+
exception_recorder.record(exception, sample)
|
32
|
+
end
|
33
|
+
|
34
|
+
def info
|
35
|
+
{
|
36
|
+
:recipes => recipe_metadata,
|
37
|
+
:metric_infos => metric_metadata
|
38
|
+
}
|
39
|
+
end
|
40
|
+
|
41
|
+
def recipe_metadata
|
42
|
+
configuration.recipes.inject([]) do |recipes, recipe|
|
43
|
+
recipes << recipe.info
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def metric_metadata
|
48
|
+
configuration.metrics.inject([]) do |metrics, metric|
|
49
|
+
metrics << metric.info
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def reset
|
54
|
+
exception_recorder.reset
|
55
|
+
configuration.metrics.each(&:reset)
|
56
|
+
end
|
57
|
+
|
58
|
+
def data
|
59
|
+
real_data = configuration.metrics.map { |metric| metric.data }.compact
|
60
|
+
virtual_data = configuration.metrics.map { |metric| metric.calculate(real_data) }.compact
|
61
|
+
# Return any metrics which are not abstract and should be sent to the server
|
62
|
+
metric_payload = (real_data + virtual_data).find_all { |data| !data[:abstract] }
|
63
|
+
#puts "Sending #{metric_payload.map { |met| [met[:name], met[:values].size] }.inspect} metrics"
|
64
|
+
metric_payload
|
65
|
+
end
|
66
|
+
|
67
|
+
def exception_data
|
68
|
+
exception_recorder.data
|
69
|
+
end
|
70
|
+
|
71
|
+
def exception_recorder
|
72
|
+
@exception_recorder ||= ExceptionRecorder.new(self)
|
73
|
+
end
|
74
|
+
|
75
|
+
def reporter
|
76
|
+
@reporter ||= Reporter.new(self)
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|