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,361 @@
1
+ module Appoptics
2
+ module Metrics
3
+
4
+ class Client
5
+ extend Forwardable
6
+
7
+ def_delegator :annotator, :add, :annotate
8
+
9
+ attr_accessor :api_key, :proxy
10
+
11
+ # @example Have the gem build your identifier string
12
+ # Appoptics::Metrics.agent_identifier 'flintstone', '0.5', 'fred'
13
+ #
14
+ # @example Provide your own identifier string
15
+ # Appoptics::Metrics.agent_identifier 'flintstone/0.5 (dev_id:fred)'
16
+ #
17
+ # @example Remove identifier string
18
+ # Appoptics::Metrics.agent_identifier ''
19
+ def agent_identifier(*args)
20
+ if args.length == 1
21
+ @agent_identifier = args.first
22
+ elsif args.length == 3
23
+ @agent_identifier = "#{args[0]}/#{args[1]} (dev_id:#{args[2]})"
24
+ elsif ![0,1,3].include?(args.length)
25
+ raise ArgumentError, 'invalid arguments, see method documentation'
26
+ end
27
+ @agent_identifier ||= ''
28
+ end
29
+
30
+ def annotator
31
+ @annotator ||= Annotator.new(client: self)
32
+ end
33
+
34
+ # API endpoint to use for queries and direct
35
+ # persistence.
36
+ #
37
+ # @return [String] api_endpoint
38
+ def api_endpoint
39
+ @api_endpoint ||= 'https://api.appoptics.com'
40
+ end
41
+
42
+ # Set API endpoint for use with queries and direct
43
+ # persistence. Generally you should not need to set this
44
+ # as it will default to the current Appoptics endpoint.
45
+ #
46
+ def api_endpoint=(endpoint)
47
+ @api_endpoint = endpoint
48
+ end
49
+
50
+ # Authenticate for direct persistence
51
+ #
52
+ # @param [String] email
53
+ # @param [String] api_key
54
+ def authenticate(api_key)
55
+ flush_authentication
56
+ self.api_key = api_key
57
+ end
58
+
59
+ # Current connection object
60
+ #
61
+ def connection
62
+ # prevent successful creation if no credentials set
63
+ raise CredentialsMissing unless (self.api_key)
64
+ @connection ||= Connection.new(client: self, api_endpoint: api_endpoint,
65
+ adapter: faraday_adapter, proxy: self.proxy)
66
+ end
67
+
68
+ # Overrride user agent for this client's connections. If you
69
+ # are trying to specify an agent identifier for developer
70
+ # program, see #agent_identifier.
71
+ #
72
+ def custom_user_agent=(agent)
73
+ @user_agent = agent
74
+ @connection = nil
75
+ end
76
+
77
+ def custom_user_agent
78
+ @user_agent
79
+ end
80
+
81
+ # Completely delete metrics with the given names. Be
82
+ # careful with this, this is instant and permanent.
83
+ #
84
+ # @example Delete metric 'temperature'
85
+ # Appoptics::Metrics.delete_metrics :temperature
86
+ #
87
+ # @example Delete metrics 'foo' and 'bar'
88
+ # Appoptics::Metrics.delete_metrics :foo, :bar
89
+ #
90
+ # @example Delete metrics that start with 'foo' except 'foobar'
91
+ # Appoptics::Metrics.delete_metrics names: 'foo*', exclude: ['foobar']
92
+ #
93
+ def delete_metrics(*metric_names)
94
+ raise(NoMetricsProvided, 'Metric name missing.') if metric_names.empty?
95
+ if metric_names[0].respond_to?(:keys) # hash form
96
+ params = metric_names[0]
97
+ else
98
+ params = { names: metric_names.map(&:to_s) }
99
+ end
100
+ connection.delete do |request|
101
+ request.url connection.build_url("metrics")
102
+ request.body = SmartJSON.write(params)
103
+ end
104
+ # expects 204, middleware will raise exception otherwise.
105
+ true
106
+ end
107
+
108
+ # Return current adapter this client will use.
109
+ # Defaults to Metrics.faraday_adapter if set, otherwise
110
+ # Faraday.default_adapter
111
+ def faraday_adapter
112
+ @faraday_adapter ||= default_faraday_adapter
113
+ end
114
+
115
+ # Set faraday adapter this client will use
116
+ def faraday_adapter=(adapter)
117
+ @faraday_adapter = adapter
118
+ end
119
+
120
+ # Retrieve measurements for a given composite metric definition.
121
+ # :start_time and :resolution are required options, :end_time is
122
+ # optional.
123
+ #
124
+ # @example Get 5m moving average of 'foo'
125
+ # measurements = Appoptics::Metrics.get_composite
126
+ # 'moving_average(mean(series("foo", "*"), {size: "5"}))',
127
+ # start_time: Time.now.to_i - 60*60, resolution: 300
128
+ #
129
+ # @param [String] definition Composite definition
130
+ # @param [hash] options Query options
131
+ def get_composite(definition, options={})
132
+ unless options[:start_time] && options[:resolution]
133
+ raise "You must provide a :start_time and :resolution"
134
+ end
135
+ query = options.dup
136
+ query[:compose] = definition
137
+ url = connection.build_url("metrics", query)
138
+ response = connection.get(url)
139
+ parsed = SmartJSON.read(response.body)
140
+ # TODO: pagination support
141
+ parsed
142
+ end
143
+
144
+ # Retrieve a specific metric by name, optionally including data points
145
+ #
146
+ # @example Get attributes for a metric
147
+ # metric = Appoptics::Metrics.get_metric :temperature
148
+ #
149
+ # @example Get a metric and its 20 most recent data points
150
+ # metric = Appoptics::Metrics.get_metric :temperature, count: 20
151
+ # metric['measurements'] # => {...}
152
+ #
153
+ # A full list of query parameters can be found in the API
154
+ # documentation: {http://docs.appoptics.com/api/#retrieve-a-metric}
155
+ #
156
+ # @param [Symbol|String] name Metric name
157
+ # @param [Hash] options Query options
158
+ def get_metric(name, options = {})
159
+ query = options.dup
160
+ if query[:start_time].respond_to?(:year)
161
+ query[:start_time] = query[:start_time].to_i
162
+ end
163
+ if query[:end_time].respond_to?(:year)
164
+ query[:end_time] = query[:end_time].to_i
165
+ end
166
+ unless query.empty?
167
+ query[:resolution] ||= 1
168
+ end
169
+ # expects 200
170
+ url = connection.build_url("metrics/#{name}", query)
171
+ response = connection.get(url)
172
+ parsed = SmartJSON.read(response.body)
173
+ # TODO: pagination support
174
+ parsed
175
+ end
176
+
177
+ # Retrieve series of measurements for a given metric
178
+ #
179
+ # @example Get series for metric
180
+ # series = Appoptics::Metrics.get_series :requests, resolution: 1, duration: 3600
181
+ #
182
+ # @example Get series for metric grouped by tag
183
+ # query = { duration: 3600, resolution: 1, group_by: "environment", group_by_function: "sum" }
184
+ # series = Appoptics::Metrics.get_series :requests, query
185
+ #
186
+ # @example Get series for metric grouped by tag and negated by tag filter
187
+ # query = { duration: 3600, resolution: 1, group_by: "environment", group_by_function: "sum", tags_search: "environment=!staging" }
188
+ # series = Appoptics::Metrics.get_series :requests, query
189
+ #
190
+ # @param [Symbol|String] metric_name Metric name
191
+ # @param [Hash] options Query options
192
+ def get_series(metric_name, options={})
193
+ raise ArgumentError, ":resolution and :duration or :start_time must be set" if options.empty?
194
+ query = options.dup
195
+ if query[:start_time].respond_to?(:year)
196
+ query[:start_time] = query[:start_time].to_i
197
+ end
198
+ if query[:end_time].respond_to?(:year)
199
+ query[:end_time] = query[:end_time].to_i
200
+ end
201
+ query[:resolution] ||= 1
202
+ unless query[:start_time] || query[:end_time]
203
+ query[:duration] ||= 3600
204
+ end
205
+ url = connection.build_url("measurements/#{metric_name}", query)
206
+ response = connection.get(url)
207
+ parsed = SmartJSON.read(response.body)
208
+ parsed["series"]
209
+ end
210
+
211
+ # Retrieve data points for a specific metric
212
+ #
213
+ # @example Get 20 most recent data points for metric
214
+ # data = Appoptics::Metrics.get_measurements :temperature, count: 20
215
+ #
216
+ # @example Get the 20 most recent 15 minute data point rollups
217
+ # data = Appoptics::Metrics.get_measurements :temperature, count: 20,
218
+ # resolution: 900
219
+ #
220
+ # @example Get data points for the last hour
221
+ # data = Appoptics::Metrics.get_measurements start_time: Time.now-3600
222
+ #
223
+ # @example Get 15 min data points from two hours to an hour ago
224
+ # data = Appoptics::Metrics.get_measurements start_time: Time.now-7200,
225
+ # end_time: Time.now-3600,
226
+ # resolution: 900
227
+ #
228
+ # A full list of query parameters can be found in the API
229
+ # documentation: {http://docs.appoptics.com/api/#retrieve-a-metric}
230
+ #
231
+ # @param [Symbol|String] metric_name Metric name
232
+ # @param [Hash] options Query options
233
+ def get_measurements(metric_name, options = {})
234
+ raise ArgumentError, "you must provide at least a :start_time or :count" if options.empty?
235
+ get_metric(metric_name, options)["measurements"]
236
+ end
237
+
238
+ # Purge current credentials and connection.
239
+ #
240
+ def flush_authentication
241
+ self.api_key = nil
242
+ @connection = nil
243
+ end
244
+
245
+ # List currently existing metrics
246
+ #
247
+ # @example List all metrics
248
+ # Appoptics::Metrics.metrics
249
+ #
250
+ # @example List metrics with 'foo' in the name
251
+ # Appoptics::Metrics.metrics name: 'foo'
252
+ #
253
+ # @param [Hash] options
254
+ def metrics(options={})
255
+ query = {}
256
+ query[:name] = options[:name] if options[:name]
257
+ offset = 0
258
+ path = "metrics"
259
+ Collection.paginated_metrics(connection, path, query)
260
+ end
261
+
262
+ # Create a new queue which uses this client.
263
+ #
264
+ # @return [Queue]
265
+ def new_queue(options={})
266
+ options[:client] = self
267
+ Queue.new(options)
268
+ end
269
+
270
+ # Persistence type to use when saving metrics.
271
+ # Default is :direct.
272
+ #
273
+ # @return [Symbol]
274
+ def persistence
275
+ @persistence ||= :direct
276
+ end
277
+
278
+ # Set persistence type to use when saving metrics.
279
+ #
280
+ # @param [Symbol] persist_method
281
+ def persistence=(persist_method)
282
+ @persistence = persist_method
283
+ end
284
+
285
+ # Current persister object.
286
+ def persister
287
+ @queue ? @queue.persister : nil
288
+ end
289
+
290
+ # Submit all queued metrics.
291
+ #
292
+ def submit(args)
293
+ @queue ||= Queue.new(client: self,
294
+ skip_measurement_times: true,
295
+ clear_failures: true)
296
+ @queue.add args
297
+ @queue.submit
298
+ end
299
+
300
+ # Update a single metric with new attributes.
301
+ #
302
+ # @example Update metric 'temperature'
303
+ # Appoptics::Metrics.update_metric :temperature, period: 15, attributes: { color: 'F00' }
304
+ #
305
+ # @example Update metric 'humidity', creating it if it doesn't exist
306
+ # Appoptics::Metrics.update_metric 'humidity', type: :gauge, period: 60, display_name: 'Humidity'
307
+ #
308
+ def update_metric(metric, options = {})
309
+ url = "metrics/#{metric}"
310
+ connection.put do |request|
311
+ request.url connection.build_url(url)
312
+ request.body = SmartJSON.write(options)
313
+ end
314
+ end
315
+
316
+ # Update multiple metrics.
317
+ #
318
+ # @example Update multiple metrics by name
319
+ # Appoptics::Metrics.update_metrics names: ["foo", "bar"], period: 60
320
+ #
321
+ # @example Update all metrics that start with 'foo' that aren't 'foobar'
322
+ # Appoptics::Metrics.update_metrics names: 'foo*', exclude: ['foobar'], display_min: 0
323
+ #
324
+ def update_metrics(metrics)
325
+ url = "metrics" # update multiple metrics
326
+ connection.put do |request|
327
+ request.url connection.build_url(url)
328
+ request.body = SmartJSON.write(metrics)
329
+ end
330
+ end
331
+
332
+ # Retrive a snapshot, to check its progress or find its image_href
333
+ #
334
+ # @example Get a snapshot identified by 42
335
+ # Appoptics::Metrics.get_snapshot 42
336
+ #
337
+ # @param [Integer|String] id
338
+ def get_snapshot(id)
339
+ url = "snapshots/#{id}"
340
+ response = connection.get(url)
341
+ parsed = SmartJSON.read(response.body)
342
+ end
343
+
344
+ private
345
+
346
+ def default_faraday_adapter
347
+ if Metrics.client == self
348
+ Faraday.default_adapter
349
+ else
350
+ Metrics.faraday_adapter
351
+ end
352
+ end
353
+
354
+ def flush_persistence
355
+ @persistence = nil
356
+ end
357
+
358
+ end
359
+
360
+ end
361
+ end
@@ -0,0 +1,43 @@
1
+ module Appoptics
2
+ module Metrics
3
+
4
+ # An internal class used for extracting pagination logic
5
+ #
6
+ # @api private
7
+ class Collection
8
+
9
+ MAX_RESULTS = 100
10
+
11
+ # Aggregates all results of paginated elements, requesting more collections as needed
12
+ #
13
+ # @param [Excon] connection Connection to Metrics service
14
+ # @param [String] path API uri
15
+ # @param [Hash] query Query options
16
+ def self.paginated_metrics(connection, path, query)
17
+ paginated_collection("metrics", connection, path, query)
18
+ end
19
+
20
+ def self.paginated_collection(name, connection, path, query)
21
+ results = []
22
+ # expects 200
23
+ url = connection.build_url(path, query)
24
+ response = connection.get(url)
25
+ parsed = SmartJSON.read(response.body)
26
+ results = parsed[name]
27
+ return results if parsed["query"]["found"] <= MAX_RESULTS
28
+ query[:offset] = MAX_RESULTS
29
+ begin
30
+ # expects 200
31
+ url = connection.build_url(path, query)
32
+ response = connection.get(url)
33
+ parsed = SmartJSON.read(response.body)
34
+ results.push(*parsed[name])
35
+ query[:offset] += MAX_RESULTS
36
+ end while query[:offset] < parsed["query"]["found"]
37
+ results
38
+
39
+ end
40
+
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,101 @@
1
+ require 'faraday'
2
+ require 'metrics/middleware/count_requests'
3
+ require 'metrics/middleware/expects_status'
4
+ require 'metrics/middleware/request_body'
5
+ require 'metrics/middleware/retry'
6
+
7
+ module Appoptics
8
+ module Metrics
9
+
10
+ class Connection
11
+ extend Forwardable
12
+
13
+ DEFAULT_API_ENDPOINT = 'https://api.appoptics.com'
14
+
15
+ def_delegators :transport, :get, :post, :head, :put, :delete,
16
+ :build_url
17
+
18
+ def initialize(options={})
19
+ @client = options[:client]
20
+ @api_endpoint = options[:api_endpoint]
21
+ @adapter = options[:adapter]
22
+ @proxy = options[:proxy]
23
+ end
24
+
25
+ # API endpoint that will be used for requests.
26
+ #
27
+ def api_endpoint
28
+ @api_endpoint || DEFAULT_API_ENDPOINT
29
+ end
30
+
31
+ def transport
32
+ raise(NoClientProvided, "No client provided.") unless @client
33
+ @transport ||= Faraday::Connection.new(
34
+ url: api_endpoint + "/v1/",
35
+ request: {open_timeout: 20, timeout: 30}) do |f|
36
+
37
+ f.use Appoptics::Metrics::Middleware::RequestBody
38
+ f.use Appoptics::Metrics::Middleware::Retry
39
+ f.use Appoptics::Metrics::Middleware::CountRequests
40
+ f.use Appoptics::Metrics::Middleware::ExpectsStatus
41
+
42
+ f.adapter @adapter || Metrics.faraday_adapter
43
+ f.proxy @proxy if @proxy
44
+ end.tap do |transport|
45
+ transport.headers[:user_agent] = user_agent
46
+ transport.headers[:content_type] = 'application/json'
47
+ transport.basic_auth @client.api_key, nil
48
+ end
49
+ end
50
+
51
+ # User-agent used when making requests.
52
+ #
53
+ def user_agent
54
+ return @client.custom_user_agent if @client.custom_user_agent
55
+ ua_chunks = []
56
+ agent_identifier = @client.agent_identifier
57
+ if agent_identifier && !agent_identifier.empty?
58
+ ua_chunks << agent_identifier
59
+ end
60
+ ua_chunks << "appoptics-api-ruby/#{Metrics::VERSION}"
61
+ ua_chunks << "(#{ruby_engine}; #{RUBY_VERSION}p#{RUBY_PATCHLEVEL}; #{RUBY_PLATFORM})"
62
+ ua_chunks << "direct-faraday/#{Faraday::VERSION}"
63
+ # TODO: include adapter information
64
+ #ua_chunks << "(#{transport_adapter})"
65
+ ua_chunks.join(' ')
66
+ end
67
+
68
+ private
69
+
70
+ def adapter_version
71
+ adapter = transport_adapter
72
+ case adapter
73
+ when "NetHttp"
74
+ "Net::HTTP/#{Net::HTTP::Revision}"
75
+ when "Typhoeus"
76
+ "typhoeus/#{Typhoeus::VERSION}"
77
+ when "Excon"
78
+ "excon/#{Excon::VERSION}"
79
+ else
80
+ adapter
81
+ end
82
+ end
83
+
84
+ def ruby_engine
85
+ return RUBY_ENGINE if Object.constants.include?(:RUBY_ENGINE)
86
+ RUBY_DESCRIPTION.split[0]
87
+ end
88
+
89
+ # figure out which adapter faraday is using
90
+ def transport_adapter
91
+ transport.builder.handlers.each do |handler|
92
+ if handler.name[0,16] == "Faraday::Adapter"
93
+ return handler.name[18..-1]
94
+ end
95
+ end
96
+ end
97
+
98
+ end
99
+
100
+ end
101
+ end