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.
@@ -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