right_support 1.1.2 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,5 +1,22 @@
1
1
  module RightSupport::Ruby
2
2
  module ObjectExtensions
3
+ # Attempt to require one or more source files.
4
+ #
5
+ # This method is useful to conditionally define code depending on the availability
6
+ # of gems or standard-library source files.
7
+ #
8
+ # === Parameters
9
+ # Forwards all parameters transparently through to Kernel#require.
10
+ #
11
+ # === Return
12
+ # Returns true or false
13
+ def require_succeeds?(*args)
14
+ require(*args)
15
+ return true
16
+ rescue LoadError => e
17
+ return false
18
+ end
19
+
3
20
  # Attempt to require one or more source files; if the require succeeds (or
4
21
  # if the files have already been successfully required), yield to the block.
5
22
  #
@@ -7,8 +24,7 @@ module RightSupport::Ruby
7
24
  # of gems or standard-library source files.
8
25
  #
9
26
  # === Parameters
10
- # Uses a parameters glob to pass all of its parameters transparently through to
11
- # Kernel#require.
27
+ # Forwards all parameters transparently through to Kernel#require.
12
28
  #
13
29
  # === Block
14
30
  # The block will be called if the require succeeds (if it does not raise LoadError).
@@ -0,0 +1,120 @@
1
+ #
2
+ # Copyright (c) 2012 RightScale Inc
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+
23
+ require 'rbconfig'
24
+
25
+ module RightSupport::Ruby
26
+ module StringExtensions
27
+ if require_succeeds?('active_support')
28
+ ACTIVE_SUPPORT_WORKALIKES = false
29
+ else
30
+ ACTIVE_SUPPORT_WORKALIKES = true
31
+ end
32
+
33
+ # Convert to snake case.
34
+ #
35
+ # "FooBar".snake_case #=> "foo_bar"
36
+ # "HeadlineCNNNews".snake_case #=> "headline_cnn_news"
37
+ # "CNN".snake_case #=> "cnn"
38
+ #
39
+ # @return [String] Receiver converted to snake case.
40
+ #
41
+ # @api public
42
+ def snake_case
43
+ return downcase if match(/\A[A-Z]+\z/)
44
+ gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2').
45
+ gsub(/([a-z])([A-Z])/, '\1_\2').
46
+ downcase
47
+ end
48
+
49
+ # Convert a constant name to a path, assuming a conventional structure.
50
+ #
51
+ # "FooBar::Baz".to_const_path # => "foo_bar/baz"
52
+ #
53
+ # @return [String] Path to the file containing the constant named by receiver
54
+ # (constantized string), assuming a conventional structure.
55
+ #
56
+ # @api public
57
+ def to_const_path
58
+ snake_case.gsub(/::/, "/")
59
+ end
60
+
61
+ # Convert constant name to constant
62
+ #
63
+ # "FooBar::Baz".to_const => FooBar::Baz
64
+ #
65
+ # @return [Constant] Constant corresponding to given name or nil if no
66
+ # constant with that name exists
67
+ #
68
+ # @api public
69
+ def to_const
70
+ names = split('::')
71
+ names.shift if names.empty? || names.first.empty?
72
+
73
+ constant = Object
74
+ names.each do |name|
75
+ # modified to return nil instead of raising an const_missing error
76
+ constant = constant && constant.const_defined?(name) ? constant.const_get(name) : nil
77
+ end
78
+ constant
79
+ end
80
+
81
+ # Reverse operation of snake case:
82
+ #
83
+ # "some_string/some_other_string" => "SomeString::SomeOtherString"
84
+ #
85
+ # @return [String] Camelized string
86
+ #
87
+ # @api public
88
+ if !String.public_method_defined?(:camelize) && ACTIVE_SUPPORT_WORKALIKES
89
+ def camelize(first_letter = :upper)
90
+ case first_letter
91
+ when :upper then gsub(/\/(.?)/) { "::" + $1.upcase }.gsub(/(^|_)(.)/) { $2.upcase }
92
+ when :lower then first + camelize(self)[1..-1]
93
+ end
94
+ end
95
+ end
96
+
97
+ # Add ability to output colored text to console
98
+ # e.g.: puts "Hello".red
99
+ def bold; colorize("\e[1m\e[29m"); end
100
+ def grey; colorize("\e[30m"); end
101
+ def red; colorize("\e[1m\e[31m"); end
102
+ def dark_red; colorize("\e[31m"); end
103
+ def green; colorize("\e[1m\e[32m"); end
104
+ def dark_green; colorize("\e[32m"); end
105
+ def yellow; colorize("\e[1m\e[33m"); end
106
+ def blue; colorize("\e[1m\e[34m"); end
107
+ def dark_blue; colorize("\e[34m"); end
108
+ def pur; colorize("\e[1m\e[35m"); end
109
+ def colorize(color_code)
110
+ # Doesn't work with the Windows prompt...
111
+ @windows ||= RbConfig::CONFIG['host_os'] =~ /mswin|win32|dos|mingw|cygwin/i
112
+ (@windows || !$stdout.isatty) ? to_s : "#{color_code}#{to_s}\e[0m"
113
+ end
114
+
115
+ end
116
+ end
117
+
118
+ class String
119
+ include RightSupport::Ruby::StringExtensions
120
+ end
@@ -0,0 +1,34 @@
1
+ #
2
+ # Copyright (c) 2012 RightScale Inc
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+
23
+ module RightSupport
24
+ #
25
+ # A namespace for statistics tracking functionality.
26
+ #
27
+ module Stats
28
+
29
+ end
30
+ end
31
+
32
+ require 'right_support/stats/exceptions'
33
+ require 'right_support/stats/helpers'
34
+ require 'right_support/stats/activity'
@@ -0,0 +1,206 @@
1
+ # Copyright (c) 2009-2012 RightScale Inc
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining
4
+ # a copy of this software and associated documentation files (the
5
+ # "Software"), to deal in the Software without restriction, including
6
+ # without limitation the rights to use, copy, modify, merge, publish,
7
+ # distribute, sublicense, and/or sell copies of the Software, and to
8
+ # permit persons to whom the Software is furnished to do so, subject to
9
+ # the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be
12
+ # included in all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
+
22
+ module RightSupport::Stats
23
+
24
+ # Track statistics for a given kind of activity
25
+ class Activity
26
+
27
+ # Number of samples included when calculating average recent activity
28
+ # with the smoothing formula A = ((A * (RECENT_SIZE - 1)) + V) / RECENT_SIZE,
29
+ # where A is the current recent average and V is the new activity value
30
+ # As a rough guide, it takes approximately 2 * RECENT_SIZE activity values
31
+ # at value V for average A to reach 90% of the original difference between A and V
32
+ # For example, for A = 0, V = 1, RECENT_SIZE = 3 the progression for A is
33
+ # 0, 0.3, 0.5, 0.7, 0.8, 0.86, 0.91, 0.94, 0.96, 0.97, 0.98, 0.99, ...
34
+ RECENT_SIZE = 3
35
+
36
+ # Maximum string length for activity type
37
+ MAX_TYPE_SIZE = 60
38
+
39
+ # (Integer) Total activity count
40
+ attr_reader :total
41
+
42
+ # (Hash) Count of activity per type
43
+ attr_reader :count_per_type
44
+
45
+ # Initialize activity data
46
+ #
47
+ # === Parameters
48
+ # measure_rate(Boolean):: Whether to measure activity rate
49
+ def initialize(measure_rate = true)
50
+ @measure_rate = measure_rate
51
+ reset
52
+ end
53
+
54
+ # Reset statistics
55
+ #
56
+ # === Return
57
+ # true:: Always return true
58
+ def reset
59
+ @interval = 0.0
60
+ @last_start_time = Time.now
61
+ @avg_duration = nil
62
+ @total = 0
63
+ @count_per_type = {}
64
+ @last_type = nil
65
+ @last_id = nil
66
+ true
67
+ end
68
+
69
+ # Mark the start of an activity and update counts and average rate
70
+ # with weighting toward recent activity
71
+ # Ignore the update if its type contains "stats"
72
+ #
73
+ # === Parameters
74
+ # type(String|Symbol):: Type of activity, with anything that is not a symbol, true, or false
75
+ # automatically converted to a String and truncated to MAX_TYPE_SIZE characters,
76
+ # defaults to nil
77
+ # id(String):: Unique identifier associated with this activity
78
+ #
79
+ # === Return
80
+ # now(Time):: Update time
81
+ def update(type = nil, id = nil)
82
+ now = Time.now
83
+ if type.nil? || !(type =~ /stats/)
84
+ @interval = average(@interval, now - @last_start_time) if @measure_rate
85
+ @last_start_time = now
86
+ @total += 1
87
+ unless type.nil?
88
+ unless [Symbol, TrueClass, FalseClass].include?(type.class)
89
+ type = type.inspect unless type.is_a?(String)
90
+ type = type[0, MAX_TYPE_SIZE - 3] + "..." if type.size > (MAX_TYPE_SIZE - 3)
91
+ end
92
+ @count_per_type[type] = (@count_per_type[type] || 0) + 1
93
+ end
94
+ @last_type = type
95
+ @last_id = id
96
+ end
97
+ now
98
+ end
99
+
100
+ # Mark the finish of an activity and update the average duration
101
+ #
102
+ # === Parameters
103
+ # start_time(Time):: Time when activity started, defaults to last time update was called
104
+ # id(String):: Unique identifier associated with this activity
105
+ #
106
+ # === Return
107
+ # duration(Float):: Activity duration in seconds
108
+ def finish(start_time = nil, id = nil)
109
+ now = Time.now
110
+ start_time ||= @last_start_time
111
+ duration = now - start_time
112
+ @avg_duration = average(@avg_duration || 0.0, duration)
113
+ @last_id = 0 if id && id == @last_id
114
+ duration
115
+ end
116
+
117
+ # Convert average interval to average rate
118
+ #
119
+ # === Return
120
+ # (Float|nil):: Recent average rate, or nil if total is 0
121
+ def avg_rate
122
+ if @total > 0
123
+ if @interval == 0.0 then 0.0 else 1.0 / @interval end
124
+ end
125
+ end
126
+
127
+
128
+ # Get average duration of activity
129
+ #
130
+ # === Return
131
+ # (Float|nil) Average duration in seconds of activity weighted toward recent activity, or nil if total is 0
132
+ def avg_duration
133
+ @avg_duration if @total > 0
134
+ end
135
+
136
+ # Get stats about last activity
137
+ #
138
+ # === Return
139
+ # (Hash|nil):: Information about last activity, or nil if the total is 0
140
+ # "elapsed"(Integer):: Seconds since last activity started
141
+ # "type"(String):: Type of activity if specified, otherwise omitted
142
+ # "active"(Boolean):: Whether activity still active
143
+ def last
144
+ if @total > 0
145
+ result = {"elapsed" => (Time.now - @last_start_time).to_i}
146
+ result["type"] = @last_type if @last_type
147
+ result["active"] = @last_id != 0 if !@last_id.nil?
148
+ result
149
+ end
150
+ end
151
+
152
+ # Convert count per type into percentage by type
153
+ #
154
+ # === Return
155
+ # (Hash|nil):: Converted counts, or nil if total is 0
156
+ # "total"(Integer):: Total activity count
157
+ # "percent"(Hash):: Percentage for each type of activity if tracking type, otherwise omitted
158
+ def percentage
159
+ if @total > 0
160
+ percent = {}
161
+ @count_per_type.each { |k, v| percent[k] = (v / @total.to_f) * 100.0 }
162
+ {"percent" => percent, "total" => @total}
163
+ end
164
+ end
165
+
166
+ # Get stat summary including all aspects of activity that were measured except duration
167
+ #
168
+ # === Return
169
+ # (Hash|nil):: Information about activity, or nil if the total is 0
170
+ # "total"(Integer):: Total activity count
171
+ # "percent"(Hash):: Percentage for each type of activity if tracking type, otherwise omitted
172
+ # "last"(Hash):: Information about last activity
173
+ # "elapsed"(Integer):: Seconds since last activity started
174
+ # "type"(String):: Type of activity if tracking type, otherwise omitted
175
+ # "active"(Boolean):: Whether activity still active if tracking whether active, otherwise omitted
176
+ # "rate"(Float):: Recent average rate if measuring rate, otherwise omitted
177
+ def all
178
+ if @total > 0
179
+ result = if @count_per_type.empty?
180
+ {"total" => @total}
181
+ else
182
+ percentage
183
+ end
184
+ result.merge!("last" => last)
185
+ result.merge!("rate" => avg_rate) if @measure_rate
186
+ result
187
+ end
188
+ end
189
+
190
+ protected
191
+
192
+ # Calculate smoothed average with weighting toward recent activity
193
+ #
194
+ # === Parameters
195
+ # current(Float|Integer):: Current average value
196
+ # value(Float|Integer):: New value
197
+ #
198
+ # === Return
199
+ # (Float):: New average
200
+ def average(current, value)
201
+ ((current * (RECENT_SIZE - 1)) + value) / RECENT_SIZE.to_f
202
+ end
203
+
204
+ end # Activity
205
+
206
+ end # RightScale::Stats
@@ -0,0 +1,96 @@
1
+ # Copyright (c) 2009-2012 RightScale Inc
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining
4
+ # a copy of this software and associated documentation files (the
5
+ # "Software"), to deal in the Software without restriction, including
6
+ # without limitation the rights to use, copy, modify, merge, publish,
7
+ # distribute, sublicense, and/or sell copies of the Software, and to
8
+ # permit persons to whom the Software is furnished to do so, subject to
9
+ # the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be
12
+ # included in all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
+
22
+ module RightSupport::Stats
23
+
24
+ # Track statistics for exceptions
25
+ class Exceptions
26
+
27
+ include RightSupport::Log::Mixin
28
+
29
+ # Maximum number of recent exceptions to track per category
30
+ MAX_RECENT_EXCEPTIONS = 10
31
+
32
+ # (Hash) Exceptions raised per category with keys
33
+ # "total"(Integer):: Total exceptions for this category
34
+ # "recent"(Array):: Most recent as a hash of "count", "type", "message", "when", and "where"
35
+ attr_reader :stats
36
+ alias :all :stats
37
+
38
+ # Initialize exception data
39
+ #
40
+ # === Parameters
41
+ # server(Object):: Server where exceptions are originating, must be defined for callbacks
42
+ # callback(Proc):: Block with following parameters to be activated when an exception occurs
43
+ # exception(Exception):: Exception
44
+ # message(Packet):: Message being processed
45
+ # server(Server):: Server where exception occurred
46
+ def initialize(server = nil, callback = nil)
47
+ @server = server
48
+ @callback = callback
49
+ reset
50
+ end
51
+
52
+ # Reset statistics
53
+ #
54
+ # === Return
55
+ # true:: Always return true
56
+ def reset
57
+ @stats = nil
58
+ true
59
+ end
60
+
61
+ # Track exception statistics and optionally make callback to report exception
62
+ # Catch any exceptions since this function may be called from within an EM block
63
+ # and an exception here would then derail EM
64
+ #
65
+ # === Parameters
66
+ # category(String):: Exception category
67
+ # exception(Exception):: Exception
68
+ #
69
+ # === Return
70
+ # true:: Always return true
71
+ def track(category, exception, message = nil)
72
+ begin
73
+ @callback.call(exception, message, @server) if @server && @callback && message
74
+ @stats ||= {}
75
+ exceptions = (@stats[category] ||= {"total" => 0, "recent" => []})
76
+ exceptions["total"] += 1
77
+ recent = exceptions["recent"]
78
+ last = recent.last
79
+ if last && last["type"] == exception.class.name && last["message"] == exception.message && last["where"] == exception.backtrace.first
80
+ last["count"] += 1
81
+ last["when"] = Time.now.to_i
82
+ else
83
+ backtrace = exception.backtrace.first if exception.backtrace
84
+ recent.shift if recent.size >= MAX_RECENT_EXCEPTIONS
85
+ recent.push({"count" => 1, "when" => Time.now.to_i, "type" => exception.class.name,
86
+ "message" => exception.message, "where" => backtrace})
87
+ end
88
+ rescue Exception => e
89
+ logger.exception("Failed to track exception '#{exception}'", e, :trace) rescue nil
90
+ end
91
+ true
92
+ end
93
+
94
+ end # Exceptions
95
+
96
+ end # RightSupport::Stats