right_support 1.1.2 → 1.2.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.
@@ -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