fiveruns-dash-ruby 0.7.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 +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
|