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,173 @@
1
+ module Fiveruns::Dash
2
+
3
+ class Host
4
+
5
+ UNIXES = [:osx, :linux, :solaris]
6
+
7
+ def initialize
8
+ configure_host
9
+ end
10
+
11
+ def architecture
12
+ @architecture
13
+ end
14
+
15
+ def os_name
16
+ @os_name
17
+ end
18
+
19
+ def os_version
20
+ @os_version
21
+ end
22
+
23
+ def big_endian?
24
+ @big_endian
25
+ end
26
+
27
+ def little_endian?
28
+ !@big_endian
29
+ end
30
+
31
+ def ip_addresses
32
+ @ip_addresses
33
+ end
34
+
35
+ def os_name_match?(name)
36
+ platform == name
37
+ end
38
+
39
+ def platform
40
+ execute_on_osx { return :osx }
41
+ execute_on_windows { return :windows }
42
+ execute_on_linux { return :linux }
43
+ execute_on_solaris { return :solaris }
44
+ :unknown
45
+ end
46
+
47
+ def execute_on_unix(&block)
48
+ UNIXES.each do |unix|
49
+ send "execute_on_#{unix}", &block
50
+ end
51
+ end
52
+
53
+ def execute_on_osx(&block)
54
+ block.call if RUBY_PLATFORM =~ /darwin/
55
+ end
56
+
57
+ def execute_on_linux(&block)
58
+ block.call if RUBY_PLATFORM =~ /linux/
59
+ end
60
+
61
+ def execute_on_solaris(&block)
62
+ block.call if RUBY_PLATFORM =~ /solaris/
63
+ end
64
+
65
+ def execute_on_windows(&block)
66
+ block.call if RUBY_PLATFORM =~ /win32|i386-mingw32/
67
+ end
68
+
69
+ def hostname
70
+ @hostname
71
+ end
72
+
73
+ def ip_address
74
+ address = ip_addresses[0]
75
+ address ? address[1] : "127.0.0.1"
76
+ end
77
+
78
+ def mac_address
79
+ @mac_address
80
+ end
81
+
82
+ def configure_host
83
+ @hostname ||= `hostname`.strip!
84
+ @big_endian = ([123].pack("s") == [123].pack("n"))
85
+ case RUBY_PLATFORM
86
+ when /darwin|linux/
87
+ begin # use Sys::Uname library if present
88
+ require 'sys/uname'
89
+ @os_name = Sys::Uname.sysname
90
+ @architecture = Sys::Uname.machine
91
+ @os_version = Sys::Uname.release
92
+ rescue LoadError # otherwise shell out and scrape out the information
93
+ @os_name = `uname -s`.strip
94
+ @architecture = `uname -p`.strip
95
+ @os_version = `uname -r`.strip
96
+ end
97
+ when /win32|i386-mingw32/
98
+ require "dl/win32"
99
+ getVersionEx = Win32API.new("kernel32", "GetVersionExA", ['P'], 'L')
100
+
101
+ lpVersionInfo = [148, 0, 0, 0, 0].pack("LLLLL") + "�0" * 128
102
+ getVersionEx.Call lpVersionInfo
103
+
104
+ dwOSVersionInfoSize, dwMajorVersion, dwMinorVersion, dwBuildNumber, dwPlatformId, szCSDVersion = lpVersionInfo.unpack("LLLLLC128")
105
+ @os_name = ['Windows 3.1/3.11', 'Windows 95/98', 'Windows NT/XP'][dwPlatformId]
106
+ @os_version = "#{dwMajorVersion}.#{dwMinorVersion}"
107
+ @architecture = ENV['PROCESSOR_ARCHITECTURE']
108
+ end
109
+
110
+ @ip_addresses = []
111
+ begin # use Sys::Host library if present
112
+ require 'sys/host'
113
+ Sys::Host.ip_addr.each do |ip|
114
+ addresses << ip
115
+ end
116
+ rescue LoadError # otherwise shell out and scrape out the information
117
+ execute_on_osx do
118
+ ifconfig = `ifconfig`
119
+ x = 0
120
+ while true
121
+ if ifconfig =~ /en#{x}:/
122
+ x+=1
123
+ else
124
+ break
125
+ end
126
+ end
127
+ x.times do |dev|
128
+ ifconfig = `ifconfig en#{dev}`
129
+ ifconfig.scan(/ether ([0-9a-f\:]*) /) do |mac_address|
130
+ @mac_address ||= mac_address[0]
131
+ end
132
+ ifconfig.scan(/inet ([0-9\.]*) /) { |ip| @ip_addresses << ["en#{dev}", ip[0]] }
133
+ end
134
+ end
135
+ execute_on_solaris do
136
+ arp = `/usr/sbin/arp -an`.split("\n")
137
+ re = /^(\w+).*?(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}).*((([a-f0-9]{2}):){5}[a-f0-9]{2})/
138
+ arp.find do |line|
139
+ line =~ re
140
+ end
141
+ @ip_addresses << $2
142
+ @mac_address = $3
143
+ end
144
+ execute_on_linux do
145
+ ifconfig = `/sbin/ifconfig`
146
+ x = 0
147
+ while true
148
+ if ifconfig =~ /eth#{x} /
149
+ x+=1
150
+ else
151
+ break
152
+ end
153
+ end
154
+ x.times do |dev|
155
+ ifconfig = `/sbin/ifconfig eth#{dev}`
156
+ ifconfig.scan(/HWaddr ([0-9A-Fa-f\:]*) /) do |mac_address|
157
+ @mac_address ||= mac_address[0]
158
+ end
159
+ ifconfig.scan(/inet addr:([0-9\.]*) /) { |ip| @ip_addresses << ["eth#{dev}", ip[0]] }
160
+ end
161
+ @mac_address ||= "#{@hostname}-UNKNOWN-MAC"
162
+ end
163
+ execute_on_windows do
164
+ addrs = Socket.getaddrinfo(Socket.gethostname, 80)
165
+ addrs.each do |addr|
166
+ @ip_addresses << ['eth0', addr[3]]
167
+ end
168
+ end
169
+ end
170
+ end
171
+ end
172
+
173
+ end
@@ -0,0 +1,128 @@
1
+ module Fiveruns::Dash
2
+
3
+ module Instrument
4
+
5
+ class Error < ::NameError; end
6
+
7
+ def self.handlers
8
+ @handlers ||= []
9
+ end
10
+
11
+ # call-seq:
12
+ # Instrument.add("ClassName#instance_method", ...) { |instance, time, *args| ... }
13
+ # Instrument.add("ClassName::class_method", ...) { |klass, time, *args| ... }
14
+ # Instrument.add("ClassName.class_method", ...) { |klass, time, *args| ... }
15
+ #
16
+ # Add a handler to be called every time a method is invoked
17
+ def self.add(*raw_targets, &handler)
18
+ options = raw_targets.last.is_a?(Hash) ? raw_targets.pop : {}
19
+ raw_targets.each do |raw_target|
20
+ begin
21
+ obj, meth = case raw_target
22
+ when /^(.+)#(.+)$/
23
+ [$1.constantize, $2]
24
+ when /^(.+)(?:\.|::)(.+)$/
25
+ [(class << $1.constantize; self; end), $2]
26
+ else
27
+ raise Error, "Bad target format: #{raw_target}"
28
+ end
29
+ instrument(obj, meth, options, &handler)
30
+ rescue Fiveruns::Dash::Instrument::Error => em
31
+ raise em
32
+ rescue => e
33
+ Fiveruns::Dash.logger.error "Unable to instrument '#{raw_target}': #{e.message}"
34
+ Fiveruns::Dash.logger.error e.backtrace.join("\n\t")
35
+ end
36
+ end
37
+ end
38
+
39
+ def self.reentrant_timing(token, offset, this, args)
40
+ # token allows us to handle re-entrant timing, see e.g. ar_time
41
+ Thread.current[token] = 0 if Thread.current[token].nil?
42
+ Thread.current[token] = Thread.current[token] + 1
43
+ begin
44
+ start = Time.now
45
+ result = yield
46
+ ensure
47
+ time = Time.now - start
48
+ Thread.current[token] = Thread.current[token] - 1
49
+ if Thread.current[token] == 0
50
+ ::Fiveruns::Dash::Instrument.handlers[offset].call(this, time, *args)
51
+ end
52
+ end
53
+ result
54
+ end
55
+
56
+ def self.timing(offset, this, args)
57
+ start = Time.now
58
+ begin
59
+ result = yield
60
+ ensure
61
+ time = Time.now - start
62
+ ::Fiveruns::Dash::Instrument.handlers[offset].call(this, time, *args)
63
+ end
64
+ result
65
+ end
66
+
67
+ #######
68
+ private
69
+ #######
70
+
71
+ def self.instrument(obj, meth, options = {}, &handler)
72
+ handlers << handler unless handlers.include?(handler)
73
+ offset = handlers.size - 1
74
+ identifier = "instrument_#{handler.hash}"
75
+ code = wrapping meth, identifier do |without|
76
+ if options[:exceptions]
77
+ <<-EXCEPTIONS
78
+ begin
79
+ #{without}(*args, &block)
80
+ rescue Exception => _e
81
+ _sample = ::Fiveruns::Dash::Instrument.handlers[#{offset}].call(_e, self, *args)
82
+ ::Fiveruns::Dash.session.add_exception(_e, _sample)
83
+ raise
84
+ end
85
+ EXCEPTIONS
86
+ elsif options[:reentrant_token]
87
+ <<-REENTRANT
88
+ ::Fiveruns::Dash::Instrument.reentrant_timing(:id#{options[:reentrant_token]}, #{offset}, self, args) do
89
+ #{without}(*args, &block)
90
+ end
91
+ REENTRANT
92
+ else
93
+ <<-PERFORMANCE
94
+ ::Fiveruns::Dash::Instrument.timing(#{offset}, self, args) do
95
+ #{without}(*args, &block)
96
+ end
97
+ PERFORMANCE
98
+ end
99
+ end
100
+ obj.module_eval code, __FILE__, __LINE__
101
+ identifier
102
+ rescue SyntaxError => e
103
+ puts "Syntax error (#{e.message})\n#{code}"
104
+ raise
105
+ rescue => e
106
+ raise Error, "Could not attach (#{e.message})"
107
+ end
108
+
109
+ def self.wrapping(meth, feature)
110
+ format = meth =~ /^(.*?)(\?|!|=)$/ ? "#{$1}_%s_#{feature}#{$2}" : "#{meth}_%s_#{feature}"
111
+ <<-DYNAMIC
112
+ def #{format % :with}(*args, &block)
113
+ _trace = Thread.current[:trace]
114
+ if _trace
115
+ _trace.step do
116
+ #{yield(format % :without)}
117
+ end
118
+ else
119
+ #{yield(format % :without)}
120
+ end
121
+ end
122
+ alias_method_chain :#{meth}, :#{feature}
123
+ DYNAMIC
124
+ end
125
+
126
+ end
127
+
128
+ end
@@ -0,0 +1,379 @@
1
+ require 'dash/typable'
2
+
3
+ module Fiveruns::Dash
4
+
5
+ class Metric
6
+ include Typable
7
+
8
+ attr_reader :name, :description, :help_text, :options
9
+ attr_accessor :recipe
10
+ def initialize(name, *args, &block)
11
+ @@warned = false
12
+ @name = name.to_s
13
+ @options = args.extract_options!
14
+ @description = args.shift || @name.titleize
15
+ @help_text = args.shift
16
+ @operation = block
17
+ @virtual = !!options[:sources]
18
+ @abstract = options[:abstract]
19
+ validate!
20
+ end
21
+
22
+ # Indicates that this metric is calculated based on the value(s)
23
+ # of other metrics.
24
+ def virtual?
25
+ @virtual
26
+ end
27
+
28
+ # Indicates that this metric is only used for virtual calculations
29
+ # and should not be sent to the server for storage.
30
+ def abstract?
31
+ @abstract
32
+ end
33
+
34
+ def data
35
+ return nil if virtual?
36
+ value_hash.merge(key)
37
+ end
38
+
39
+ def calculate(real_data)
40
+ return nil unless virtual?
41
+
42
+ datas = options[:sources].map {|met_name| real_data.detect { |hash| hash[:name] == met_name } }.compact
43
+
44
+ if datas.size != options[:sources].size && options[:sources].include?('response_time')
45
+ Fiveruns::Dash.logger.warn(<<-LOG
46
+ ActiveRecord utilization metrics require a time metric so Dash can calculate a percentage of time spent in the database.
47
+ Please set the :ar_total_time option when configuring Dash:
48
+
49
+ # Define an application-specific metric cooresponding to the total processing time for this app.
50
+ Fiveruns::Dash.register_recipe :loader, :url => 'http://dash.fiveruns.com' do |recipe|
51
+ recipe.time :total_time, 'Load Time', :method => 'Loader::Engine#load'
52
+ end
53
+
54
+ # Pass the name of this custom metric to Dash so it will be used in the AR metric calculations.
55
+ Fiveruns::Dash.configure :app => token, :ar_total_time => 'total_time' do |config|
56
+ config.add_recipe :activerecord
57
+ config.add_recipe :loader, :url => 'http://dash.fiveruns.com'
58
+ end
59
+ LOG
60
+ ) unless @@warned
61
+ @@warned = true
62
+ return nil
63
+ else
64
+ raise ArgumentError, "Could not find one or more of #{options[:sources].inspect} in #{real_data.map { |h| h[:name] }.inspect}" unless datas.size == options[:sources].size
65
+ end
66
+
67
+ combine(datas.map { |hsh| hsh[:values] }).merge(key)
68
+ end
69
+
70
+ def reset
71
+ # Abstract
72
+ end
73
+
74
+ def info
75
+ key
76
+ end
77
+
78
+ def key
79
+ @key ||= begin
80
+ {
81
+ :name => name,
82
+ :recipe_url => recipe ? recipe.url : nil,
83
+ :recipe_name => recipe ? recipe.name.to_s : nil,
84
+ :data_type => self.class.metric_type,
85
+ :description => description,
86
+ :help_text => help_text,
87
+ }.merge(optional_info)
88
+ end
89
+ end
90
+
91
+ def ==(other)
92
+ key == other.key
93
+ end
94
+
95
+ # Set context finder
96
+ def find_context_with(&block)
97
+ @context_finder = block
98
+ end
99
+
100
+ #######
101
+ private
102
+ #######
103
+
104
+ def validate!
105
+ raise ArgumentError, "#{name} - Virtual metrics should have source metrics" if virtual? && options[:sources].blank?
106
+ raise ArgumentError, "#{name} - metrics should not have source metrics" if !virtual? && options[:sources]
107
+ end
108
+
109
+ def optional_info
110
+ returning({}) do |optional|
111
+ copy = optional.merge(@options[:unit] ? {:unit => @options[:unit].to_s} : {})
112
+ copy = copy.merge(@options[:scope] ? {:scope => @options[:scope].to_s} : {})
113
+ copy = copy.merge(abstract? ? {:abstract => true} : {})
114
+ optional.merge!(copy)
115
+ end
116
+ end
117
+
118
+ def combine(source_values)
119
+ # Get the intersection of contexts for all the source metrics.
120
+ # We combine the values for all shared contexts.
121
+ contexts = source_values.map { |values| values.map { |value| value[:context] }}
122
+ intersection = nil
123
+ contexts.each_with_index do |arr, idx|
124
+ if idx == 0
125
+ intersection = arr
126
+ else
127
+ intersection = intersection & arr
128
+ end
129
+ end
130
+
131
+ values = intersection.map do |context|
132
+ args = source_values.map do |values|
133
+ values.detect { |value| value[:context] == context }[:value]
134
+ end
135
+
136
+ { :value => @operation.call(*args), :context => context }
137
+ end
138
+
139
+ {:values => values}
140
+ end
141
+
142
+ def value_hash
143
+ current_value = ::Fiveruns::Dash.sync { @operation.call }
144
+ {:values => parse_value(current_value)}
145
+ end
146
+
147
+ # Verifies value matches one of the following patterns:
148
+ # * A numeric value (indicates no namespace)
149
+ # * A hash of [namespace_kind, namespace_name, ...] => value pairs, eg:
150
+ # [:controller, 'FooController', :action, 'bar'] => 12
151
+ def parse_value(value)
152
+ case value
153
+ when Numeric
154
+ [{:context => [], :value => value}]
155
+ when Hash
156
+ value.inject([]) do |all, (key, val)|
157
+ case key
158
+ when nil
159
+ all.push :context => [], :value => val
160
+ when Array
161
+ if key.size % 2 == 0
162
+ all.push :context => key, :value => val
163
+ else
164
+ bad_value! "Contexts must have an even number of items"
165
+ end
166
+ else
167
+ bad_value! "Unknown context type"
168
+ end
169
+ all
170
+ end
171
+ else
172
+ bad_value! "Unknown value type"
173
+ end
174
+ end
175
+
176
+ def bad_value!(message)
177
+ raise ArgumentError, "Bad data for `#{@name}' #{self.class.metric_type} metric: #{message}"
178
+ end
179
+
180
+ # Note: only to be used when the +@operation+
181
+ # block is used to set contexts
182
+ def find_containers(*args, &block) #:nodoc:
183
+ contexts = Array(current_context_for(*args))
184
+ if contexts.empty? || contexts == [[]]
185
+ contexts = [[]]
186
+ elsif contexts.all? { |item| !item.is_a?(Array) }
187
+ contexts = [contexts]
188
+ end
189
+ if Thread.current[:trace]
190
+ result = yield blank_data[[]]
191
+ Thread.current[:trace].add_data(self, contexts, result)
192
+ end
193
+ contexts.each do |context|
194
+ with_container_for_context(context, &block)
195
+ end
196
+ end
197
+
198
+ # Get the container for this context, allow modifications to it,
199
+ # and store it
200
+ # * Note: We sync here when looking up the container, while
201
+ # the block is being executed, and when it is stored
202
+ def with_container_for_context(context)
203
+ ctx = (context || []).dup # normalize nil context to empty
204
+ ::Fiveruns::Dash.sync do
205
+ container = @data[ctx]
206
+ new_container = yield container
207
+ #Fiveruns::Dash.logger.info "#{name}/#{context.inspect}/#{new_container.inspect}"
208
+ @data[ctx] = new_container # For hash defaults
209
+ end
210
+ end
211
+
212
+ def context_finder
213
+ @context_finder ||= begin
214
+ context_setting = @options[:context] || @options[:contexts]
215
+ context_setting.is_a?(Proc) ? context_setting : lambda { |*args| Array(context_setting) }
216
+ end
217
+ end
218
+
219
+ # Retrieve the context for the given arguments
220
+ # * Note: We need to sync here (and wherever the context is modified)
221
+ def current_context_for(*args)
222
+ ::Fiveruns::Dash.sync { context_finder.call(*args) }
223
+ end
224
+
225
+ end
226
+
227
+ class TimeMetric < Metric
228
+
229
+ def initialize(*args)
230
+ super(*args)
231
+ reset
232
+ install_hook
233
+ end
234
+
235
+ def reset
236
+ ::Fiveruns::Dash.sync do
237
+ @data = blank_data
238
+ end
239
+ end
240
+
241
+ #######
242
+ private
243
+ #######
244
+
245
+ def blank_data
246
+ Hash.new {{ :invocations => 0, :value => 0 }}
247
+ end
248
+
249
+ def value_hash
250
+ returning(:values => current_value) do
251
+ reset
252
+ end
253
+ end
254
+
255
+ def install_hook
256
+ @operation ||= lambda { nil }
257
+ methods_to_instrument.each do |meth|
258
+ Instrument.add meth, instrument_options do |obj, time, *args|
259
+ find_containers(obj, *args) do |container|
260
+ container[:invocations] += 1
261
+ container[:value] += time
262
+ container
263
+ end
264
+ end
265
+ end
266
+ end
267
+
268
+ def instrument_options
269
+ returning({}) do |options|
270
+ options[:reentrant_token] = self.object_id if @options[:reentrant]
271
+ end
272
+ end
273
+
274
+ def methods_to_instrument
275
+ @methods_to_instrument ||= Array(@options[:method]) + Array(@options[:methods])
276
+ end
277
+
278
+ def validate!
279
+ super
280
+ raise ArgumentError, "Can not set :unit for `#{@name}' time metric" if @options[:unit]
281
+ if methods_to_instrument.blank?
282
+ raise ArgumentError, "Must set :method or :methods option for `#{@name}` time metric"
283
+ end
284
+ end
285
+
286
+ # Get the current value
287
+ # * Note: We sync here (and wherever @data is being written)
288
+ def current_value
289
+ ::Fiveruns::Dash.sync do
290
+ @data.inject([]) do |all, (context, data)|
291
+ all.push(data.merge(:context => context))
292
+ end
293
+ end
294
+ end
295
+
296
+ end
297
+
298
+ class CounterMetric < Metric
299
+
300
+ def initialize(*args)
301
+ super(*args)
302
+ if incrementing_methods.any?
303
+ reset
304
+ install_hook
305
+ end
306
+ end
307
+
308
+ def value_hash
309
+ if incrementing_methods.any?
310
+ returning(:values => current_value) do
311
+ reset
312
+ end
313
+ else
314
+ super
315
+ end
316
+ end
317
+
318
+ def install_hook
319
+ if incrementing_methods.blank?
320
+ raise RuntimeError, "Bad configuration for `#{@name}` counter metric"
321
+ end
322
+ @operation ||= lambda { nil }
323
+ incrementing_methods.each do |meth|
324
+ Instrument.add meth do |obj, time, *args|
325
+ find_containers(obj, *args) do |container|
326
+ container += 1
327
+ container
328
+ end
329
+ end
330
+ end
331
+ end
332
+
333
+ # Reset the current value
334
+ # * Note: We sync here (and wherever @data is being written)
335
+ def reset
336
+ ::Fiveruns::Dash.sync { @data = blank_data }
337
+ end
338
+
339
+ def blank_data
340
+ Hash.new(0)
341
+ end
342
+
343
+ def incrementing_methods
344
+ @incrementing_methods ||= Array(@options[:incremented_by])
345
+ end
346
+
347
+ def validate!
348
+ super
349
+ if !@options[:incremented_by]
350
+ raise ArgumentError, "No block given to capture counter `#{@name}'" unless @operation
351
+ end
352
+ end
353
+
354
+ # Get the current value
355
+ # * Note: We sync here (and wherever @data is being written)
356
+ def current_value
357
+ result = ::Fiveruns::Dash.sync do
358
+ # Ensure the empty context is stored with a default of 0
359
+ @data[[]] = @data.fetch([], 0)
360
+ @data
361
+ end
362
+ parse_value result
363
+ end
364
+
365
+ end
366
+
367
+ class PercentageMetric < Metric
368
+
369
+ def validate!
370
+ super
371
+ raise ArgumentError, "Can not set :unit for `#{@name}' percentage metric" if @options[:unit]
372
+ end
373
+
374
+ end
375
+
376
+ class AbsoluteMetric < Metric
377
+ end
378
+
379
+ end