fiveruns-dash-ruby 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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