appoptics-api-ruby 2.1.3

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.
Files changed (49) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +23 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +25 -0
  5. data/CHANGELOG.md +184 -0
  6. data/Gemfile +36 -0
  7. data/LICENSE +24 -0
  8. data/README.md +271 -0
  9. data/Rakefile +63 -0
  10. data/appoptics-api-ruby.gemspec +31 -0
  11. data/benchmarks/array_vs_set.rb +29 -0
  12. data/certs/librato-public.pem +20 -0
  13. data/examples/simple.rb +24 -0
  14. data/examples/submit_every.rb +27 -0
  15. data/lib/appoptics/metrics.rb +95 -0
  16. data/lib/appoptics/metrics/aggregator.rb +138 -0
  17. data/lib/appoptics/metrics/annotator.rb +145 -0
  18. data/lib/appoptics/metrics/client.rb +361 -0
  19. data/lib/appoptics/metrics/collection.rb +43 -0
  20. data/lib/appoptics/metrics/connection.rb +101 -0
  21. data/lib/appoptics/metrics/errors.rb +32 -0
  22. data/lib/appoptics/metrics/middleware/count_requests.rb +28 -0
  23. data/lib/appoptics/metrics/middleware/expects_status.rb +38 -0
  24. data/lib/appoptics/metrics/middleware/request_body.rb +18 -0
  25. data/lib/appoptics/metrics/middleware/retry.rb +31 -0
  26. data/lib/appoptics/metrics/persistence.rb +2 -0
  27. data/lib/appoptics/metrics/persistence/direct.rb +73 -0
  28. data/lib/appoptics/metrics/persistence/test.rb +27 -0
  29. data/lib/appoptics/metrics/processor.rb +130 -0
  30. data/lib/appoptics/metrics/queue.rb +191 -0
  31. data/lib/appoptics/metrics/smart_json.rb +43 -0
  32. data/lib/appoptics/metrics/util.rb +25 -0
  33. data/lib/appoptics/metrics/version.rb +5 -0
  34. data/spec/integration/metrics/annotator_spec.rb +190 -0
  35. data/spec/integration/metrics/connection_spec.rb +14 -0
  36. data/spec/integration/metrics/middleware/count_requests_spec.rb +28 -0
  37. data/spec/integration/metrics/queue_spec.rb +96 -0
  38. data/spec/integration/metrics_spec.rb +375 -0
  39. data/spec/rackups/status.ru +30 -0
  40. data/spec/spec_helper.rb +88 -0
  41. data/spec/unit/metrics/aggregator_spec.rb +417 -0
  42. data/spec/unit/metrics/client_spec.rb +127 -0
  43. data/spec/unit/metrics/connection_spec.rb +113 -0
  44. data/spec/unit/metrics/queue/autosubmission_spec.rb +57 -0
  45. data/spec/unit/metrics/queue_spec.rb +593 -0
  46. data/spec/unit/metrics/smart_json_spec.rb +79 -0
  47. data/spec/unit/metrics/util_spec.rb +23 -0
  48. data/spec/unit/metrics_spec.rb +63 -0
  49. metadata +135 -0
@@ -0,0 +1,32 @@
1
+
2
+ module Appoptics
3
+ module Metrics
4
+
5
+ class MetricsError < StandardError; end
6
+
7
+ class CredentialsMissing < MetricsError; end
8
+ class NoMetricsProvided < MetricsError; end
9
+ class NoClientProvided < MetricsError; end
10
+ class InvalidMeasureTime < MetricsError; end
11
+ class NotMergeable < MetricsError; end
12
+ class InvalidParameters < MetricsError; end
13
+
14
+ class NetworkError < StandardError
15
+ attr_reader :response
16
+
17
+ def initialize(msg, response = nil)
18
+ super(msg)
19
+ @response = response
20
+ end
21
+ end
22
+
23
+ class ClientError < NetworkError; end
24
+ class Unauthorized < ClientError; end
25
+ class Forbidden < ClientError; end
26
+ class NotFound < ClientError; end
27
+ class EntityAlreadyExists < ClientError; end
28
+
29
+ class ServerError < NetworkError; end
30
+
31
+ end
32
+ end
@@ -0,0 +1,28 @@
1
+ module Appoptics
2
+ module Metrics
3
+ module Middleware
4
+
5
+ class CountRequests < Faraday::Response::Middleware
6
+ @total_requests = 0
7
+
8
+ class << self
9
+ attr_reader :total_requests
10
+
11
+ def increment
12
+ @total_requests += 1
13
+ end
14
+
15
+ def reset
16
+ @total_requests = 0
17
+ end
18
+ end
19
+
20
+ def call(env)
21
+ self.class.increment
22
+ @app.call(env)
23
+ end
24
+ end
25
+
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,38 @@
1
+ module Appoptics
2
+ module Metrics
3
+ module Middleware
4
+
5
+ class ExpectsStatus < Faraday::Response::Middleware
6
+
7
+ def on_complete(env)
8
+ sanitized = sanitize_request(env)
9
+ case env[:status]
10
+ when 401
11
+ raise Unauthorized.new(sanitized.to_s, sanitized)
12
+ when 403
13
+ raise Forbidden.new(sanitized.to_s, sanitized)
14
+ when 404
15
+ raise NotFound.new(sanitized.to_s, sanitized)
16
+ when 422
17
+ raise EntityAlreadyExists.new(sanitized.to_s, sanitized)
18
+ when 400..499
19
+ raise ClientError.new(sanitized.to_s, sanitized)
20
+ when 500..599
21
+ raise ServerError.new(sanitized.to_s, sanitized)
22
+ end
23
+ end
24
+
25
+ def sanitize_request(env)
26
+ {
27
+ status: env[:status],
28
+ url: env[:url].to_s,
29
+ user_agent: (env[:request_headers] || {})["User-Agent"],
30
+ request_body: env[:request_body],
31
+ response_headers: env[:response_headers],
32
+ response_body: env[:body]
33
+ }
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,18 @@
1
+ module Appoptics
2
+ module Metrics
3
+ module Middleware
4
+
5
+ class RequestBody < Faraday::Response::Middleware
6
+
7
+ def call(env)
8
+ # duplicate request body so it is preserved through request
9
+ # in case we need it for exception output
10
+ env[:request_body] = env[:body]
11
+ @app.call(env)
12
+ end
13
+
14
+ end
15
+
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,31 @@
1
+ module Appoptics
2
+ module Metrics
3
+ module Middleware
4
+
5
+ class Retry < Faraday::Middleware
6
+
7
+ def initialize(app, retries = 3)
8
+ @retries = retries
9
+ super(app)
10
+ end
11
+
12
+ def call(env)
13
+ retries = @retries
14
+ request_body = env[:body]
15
+ begin
16
+ env[:body] = request_body # after failure is set to response body
17
+ @app.call(env)
18
+ rescue Appoptics::Metrics::ServerError, Timeout::Error,
19
+ Faraday::Error::ConnectionFailed
20
+ if retries > 0
21
+ retries -= 1 and retry
22
+ end
23
+ raise
24
+ end
25
+ end
26
+
27
+ end
28
+
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,2 @@
1
+ require 'metrics/persistence/direct'
2
+ require 'metrics/persistence/test'
@@ -0,0 +1,73 @@
1
+
2
+ # Manages direct persistence with the Appoptics API
3
+
4
+ module Appoptics
5
+ module Metrics
6
+ module Persistence
7
+ class Direct
8
+ # Persist the queued metrics directly to the
9
+ # Metrics web API.
10
+ #
11
+ def persist(client, queued, options={})
12
+ per_request = options[:per_request]
13
+ if per_request
14
+ requests = chunk_queued(queued, per_request)
15
+ else
16
+ requests = [queued]
17
+ end
18
+ requests.each do |request|
19
+ resource =
20
+ if queued[:gauges] || queued[:counters]
21
+ "metrics"
22
+ else
23
+ "measurements"
24
+ end
25
+ payload = SmartJSON.write(request)
26
+ # expects 200
27
+ client.connection.post(resource, payload)
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def chunk_queued(queued, per_request)
34
+ return [queued] if queue_count(queued) <= per_request
35
+ reqs = []
36
+ # separate metric-containing values from global values
37
+ globals = fetch_globals(queued)
38
+ top_level_keys.each do |key|
39
+ metrics = queued[key]
40
+ next unless metrics
41
+ if metrics.size <= per_request
42
+ # we can fit all of this metric type in a single request
43
+ reqs << build_request(key, metrics, globals)
44
+ else
45
+ # going to have to split things up
46
+ metrics.each_slice(per_request) do |elements|
47
+ reqs << build_request(key, elements, globals)
48
+ end
49
+ end
50
+ end
51
+ reqs
52
+ end
53
+
54
+ def build_request(type, metrics, globals)
55
+ {type => metrics}.merge(globals)
56
+ end
57
+
58
+ def top_level_keys
59
+ [Appoptics::Metrics::PLURAL_TYPES, :measurements].flatten
60
+ end
61
+
62
+ def fetch_globals(queued)
63
+ queued.reject { |k, v| top_level_keys.include?(k) }
64
+ end
65
+
66
+ def queue_count(queued)
67
+ queued.inject(0) { |result, data| result + data.last.size }
68
+ end
69
+
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,27 @@
1
+ # Use for testing the interface with persistence methods
2
+
3
+ module Appoptics
4
+ module Metrics
5
+ module Persistence
6
+ class Test
7
+
8
+ # persist the given metrics
9
+ def persist(client, metrics, options={})
10
+ @persisted = metrics
11
+ return !@return_value.nil? ? @return_value : true
12
+ end
13
+
14
+ # return what was persisted
15
+ def persisted
16
+ @persisted
17
+ end
18
+
19
+ # force a return value from persistence
20
+ def return_value(value)
21
+ @return_value = value
22
+ end
23
+
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,130 @@
1
+ require "set"
2
+
3
+ module Appoptics
4
+ module Metrics
5
+
6
+ # Mixin which provides common logic between {Queue} and {Aggregator}
7
+ # objects.
8
+ module Processor
9
+ MEASUREMENTS_PER_REQUEST = 500
10
+
11
+ attr_reader :per_request, :last_submit_time
12
+ attr_accessor :prefix, :tags
13
+
14
+ def tags
15
+ @tags ||= {}
16
+ end
17
+
18
+ # The current Client instance this queue is using to authenticate
19
+ # and connect to Appoptics. This will default to the primary
20
+ # client used by the Appoptics::Metrics module unless it has been
21
+ # set to something else.
22
+ #
23
+ # @return [Appoptics::Metrics::Client]
24
+ def client
25
+ @client ||= Appoptics::Metrics.client
26
+ end
27
+
28
+ def has_tags?
29
+ !@tags.empty?
30
+ end
31
+ alias :tags? :has_tags?
32
+
33
+ # The object this MetricSet will use to persist
34
+ #
35
+ def persister
36
+ @persister ||= create_persister
37
+ end
38
+
39
+ # Persist currently queued metrics
40
+ #
41
+ # @return Boolean
42
+ def submit
43
+ return true if self.empty?
44
+ options = {per_request: @per_request}
45
+ if persister.persist(self.client, self.queued, options)
46
+ @last_submit_time = Time.now
47
+ clear and return true
48
+ end
49
+ false
50
+ rescue ClientError
51
+ # clean up if we hit exceptions if asked to
52
+ clear if @clear_on_failure
53
+ raise
54
+ end
55
+
56
+ # Capture execution time for a block and queue
57
+ # it as the value for a metric. Times are recorded
58
+ # in milliseconds.
59
+ #
60
+ # Options are the same as for #add.
61
+ #
62
+ # @example Queue API request response time
63
+ # queue.time :api_request_time do
64
+ # # API request..
65
+ # end
66
+ #
67
+ # @example Queue API request response time w/ source
68
+ # queue.time :api_request_time, source: 'app1' do
69
+ # # API request..
70
+ # end
71
+ #
72
+ # @param [Symbol|String] name Metric name
73
+ # @param [Hash] options Metric options
74
+ def time(name, options={})
75
+ start = Time.now
76
+ yield.tap do
77
+ duration = (Time.now - start) * 1000.0 # milliseconds
78
+ metric = {name => options.merge({value: duration})}
79
+ add metric
80
+ end
81
+ end
82
+ alias :benchmark :time
83
+
84
+ private
85
+
86
+ def create_persister
87
+ type = self.client.persistence.to_s.capitalize
88
+ Appoptics::Metrics::Persistence.const_get(type).new
89
+ end
90
+
91
+ def epoch_time
92
+ Time.now.to_i
93
+ end
94
+
95
+ def setup_common_options(options)
96
+ validate_parameters(options)
97
+ @autosubmit_interval = options[:autosubmit_interval]
98
+ @client = options[:client] || Appoptics::Metrics.client
99
+ @per_request = options[:per_request] || MEASUREMENTS_PER_REQUEST
100
+ @source = options[:source]
101
+ @tags = options.fetch(:tags, {})
102
+ @time = (options[:time] && options[:time].to_i || options[:measure_time] && options[:measure_time].to_i)
103
+ @create_time = Time.now
104
+ @clear_on_failure = options[:clear_failures] || false
105
+ @prefix = options[:prefix]
106
+ end
107
+
108
+ def autosubmit_check
109
+ if @autosubmit_interval
110
+ last = @last_submit_time || @create_time
111
+ self.submit if (Time.now - last).to_i >= @autosubmit_interval
112
+ end
113
+ end
114
+
115
+ def validate_parameters(options)
116
+ invalid_combinations = [
117
+ [:source, :tags],
118
+ ]
119
+ opts = options.keys.to_set
120
+ invalid_combinations.each do |combo|
121
+ if combo.to_set.subset?(opts)
122
+ raise InvalidParameters, "#{combo} cannot be simultaneously set"
123
+ end
124
+ end
125
+ end
126
+
127
+ end
128
+
129
+ end
130
+ end
@@ -0,0 +1,191 @@
1
+ require 'metrics/processor'
2
+
3
+ module Appoptics
4
+ module Metrics
5
+ class Queue
6
+ include Processor
7
+
8
+ attr_accessor :skip_measurement_times
9
+
10
+ # @option opts [Integer] :autosubmit_count If set the queue will auto-submit any time it hits this number of measurements.
11
+ # @option opts [Integer] :autosubmit_interval If set the queue will auto-submit if the given number of seconds has passed when a new metric is added.
12
+ # @option opts [Boolean] :clear_failures Should the queue remove any queued measurements from its queue if it runs into problems with a request? (default: false)
13
+ # @option opts [Client] :client The client object to use to connect to Metrics. (default: Appoptics::Metrics.client)
14
+ # @option opts [Time|Integer] :measure_time A default measure_time to use for measurements added.
15
+ # @option opts [String] :prefix If set will apply the given prefix to all metric names of measurements added.
16
+ # @option opts [Boolean] :skip_measurement_times If true will not assign measurement_time to each measure as they are added.
17
+ # @option opts [String] :source The default source to use for measurements added.
18
+ def initialize(opts={})
19
+ @queued = {}
20
+ @autosubmit_count = opts[:autosubmit_count]
21
+ @skip_measurement_times = opts[:skip_measurement_times]
22
+ setup_common_options(opts)
23
+ end
24
+
25
+ # Add a metric entry to the metric set:
26
+ #
27
+ # @param [Hash] measurements measurements to add
28
+ # @return [Queue] returns self
29
+ def add(measurements)
30
+ measurements.each do |key, value|
31
+ multidimensional = has_tags?
32
+ if value.respond_to?(:each)
33
+ validate_parameters(value)
34
+ metric = value
35
+ metric[:name] = key.to_s
36
+ type = metric.delete(:type) || metric.delete('type') || 'gauge'
37
+ else
38
+ metric = {name: key.to_s, value: value}
39
+ type = :gauge
40
+ end
41
+ if @prefix
42
+ metric[:name] = "#{@prefix}.#{metric[:name]}"
43
+ end
44
+ multidimensional = true if metric[:tags] || metric[:time]
45
+ type = ("#{type}s").to_sym
46
+ time_key = multidimensional ? :time : :measure_time
47
+ metric[:time] = metric.delete(:measure_time) if multidimensional && metric[:measure_time]
48
+
49
+ if metric[time_key]
50
+ metric[time_key] = metric[time_key].to_i
51
+ check_measure_time(metric)
52
+ elsif !skip_measurement_times
53
+ metric[time_key] = epoch_time
54
+ end
55
+ if multidimensional
56
+ @queued[:measurements] ||= []
57
+ @queued[:measurements] << metric
58
+ else
59
+ @queued[type] ||= []
60
+ @queued[type] << metric
61
+ end
62
+ end
63
+ submit_check
64
+ self
65
+ end
66
+
67
+ # Currently queued counters
68
+ #
69
+ # @return [Array]
70
+ def counters
71
+ @queued[:counters] || []
72
+ end
73
+
74
+ # Are any metrics currently queued?
75
+ #
76
+ # @return Boolean
77
+ def empty?
78
+ gauges.empty? && counters.empty? && measurements.empty?
79
+ end
80
+
81
+ # Remove all queued metrics
82
+ #
83
+ def clear
84
+ @queued = {}
85
+ end
86
+ alias :flush :clear
87
+
88
+ # Currently queued gauges
89
+ #
90
+ # @return Array
91
+ def gauges
92
+ @queued[:gauges] || []
93
+ end
94
+
95
+ def measurements
96
+ @queued[:measurements] || []
97
+ end
98
+
99
+ # Combines queueable measures from the given object
100
+ # into this queue.
101
+ #
102
+ # @example Merging queues for more performant submission
103
+ # queue1.merge!(queue2)
104
+ # queue1.submit # submits combined contents
105
+ #
106
+ # @return self
107
+ def merge!(mergeable)
108
+ if mergeable.respond_to?(:queued)
109
+ to_merge = mergeable.queued
110
+ elsif mergeable.respond_to?(:has_key?)
111
+ to_merge = mergeable
112
+ else
113
+ raise NotMergeable
114
+ end
115
+ Metrics::PLURAL_TYPES.each do |type|
116
+ if to_merge[type]
117
+ payload = reconcile(to_merge[type], to_merge[:source])
118
+ if @queued[type]
119
+ @queued[type] += payload
120
+ else
121
+ @queued[type] = payload
122
+ end
123
+ end
124
+ end
125
+
126
+ if to_merge[:measurements]
127
+ payload = reconcile(to_merge[:measurements], to_merge[:tags])
128
+ if @queued[:measurements]
129
+ @queued[:measurements] += payload
130
+ else
131
+ @queued[:measurements] = payload
132
+ end
133
+ end
134
+
135
+ submit_check
136
+ self
137
+ end
138
+
139
+ # All currently queued metrics
140
+ #
141
+ # @return Hash
142
+ def queued
143
+ return {} if @queued.empty?
144
+ globals = {}
145
+ time = has_tags? ? :time : :measure_time
146
+ globals[time] = @time if @time
147
+ globals[:source] = @source if @source
148
+ globals[:tags] = @tags if has_tags?
149
+ @queued.merge(globals)
150
+ end
151
+
152
+ # Count of metrics currently queued
153
+ #
154
+ # @return Integer
155
+ def size
156
+ self.queued.inject(0) { |result, data| result + data.last.size }
157
+ end
158
+ alias :length :size
159
+
160
+ private
161
+
162
+ def check_measure_time(data)
163
+ time_keys = [:measure_time, :time]
164
+
165
+ if time_keys.any? { |key| data[key] && data[key] < Metrics::MIN_MEASURE_TIME }
166
+ raise InvalidMeasureTime, "Measure time for submitted metric (#{data}) is invalid."
167
+ end
168
+ end
169
+
170
+ def reconcile(measurements, val)
171
+ arr = val.is_a?(Hash) ? [@tags, :tags] : [@source, :source]
172
+ return measurements if !val || val == arr.first
173
+ measurements.map! do |measurement|
174
+ unless measurement[arr.last]
175
+ measurement[arr.last] = val
176
+ end
177
+ measurement
178
+ end
179
+ measurements
180
+ end
181
+
182
+ def submit_check
183
+ autosubmit_check # in Processor
184
+ if @autosubmit_count && self.length >= @autosubmit_count
185
+ self.submit
186
+ end
187
+ end
188
+
189
+ end
190
+ end
191
+ end