restful_resource 1.4.1 → 1.4.2

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 709712fad9b584a11db8734c6de7c26ad6ae9060
4
- data.tar.gz: 117d4d8d92144cf017c6e0bf5aa9e9f2ad96b31a
3
+ metadata.gz: 7ec89f5aff66263aa662e83535625118f92faad2
4
+ data.tar.gz: f8653d759998fb91a5ac0fdb52d340edb55beafd
5
5
  SHA512:
6
- metadata.gz: c82a341ee0ba285b130399f5792b30f065555f3a7f9c1e94fa03b83675f36a06d6089bd6ebebeae5d54a6b0739f98a5cb041e1efca4abcea8b6ce145b04ee16a
7
- data.tar.gz: f950b007bbc8087e31b9e5cebde0620d4f7967f0316cdc9fd4b55dad957d1142400580f37bfe5f3efa34745ae7066b4da6780784223f22f7068dbb3cb7407682
6
+ metadata.gz: 1f7050817b74a710234c0374aa388b4f3f3758ca9694ef49d89430894ae62a7edf0e375579556dc9978fac8d19075c204bc303cc82cb6a22d9c348343ab7ca57
7
+ data.tar.gz: 12bd9dc5305fc0b362dc94420ec6d7f55172203e775caf0dbeecdf242fb463275dab5f29d2c6656eb5e5d8c80b2eb3b1a221364918d50f3b604f92dad967a0d4
data/README.md CHANGED
@@ -1,24 +1,76 @@
1
1
  # RestfulResource ![build status](https://circleci.com/gh/carwow/restful_resource.svg?style=shield&circle-token=0558310359000e8786d1fe42774b0e30b2b0e12c)
2
2
 
3
- TODO: Write a gem description
3
+ Provides an ActiveResource like interface to JSON API's
4
4
 
5
- ## Installation
5
+ ## Metrics
6
6
 
7
- Add this line to your application's Gemfile:
7
+ ### HTTP Metrics
8
8
 
9
- gem 'restful_resource'
9
+ Http requests are automatically instrumented using ActiveSupport::Notifications
10
10
 
11
- And then execute:
11
+ A Metrics class can be provided that RestfulResource will use to emit metrics. This class needs to respond to `count, sample, measure` methods.
12
12
 
13
- $ bundle
13
+ eg
14
14
 
15
- Or install it yourself as:
15
+ ```
16
+ RestfulResource::Base.configure(
17
+ base_url: "http://my.api.com/",
18
+ instrumentation: {
19
+ metric_class: Metrics, # Required
20
+ app_name: 'rails_site', # Required
21
+ api_name: 'api' # Optional, defaults to 'api'
22
+ }
23
+ )
24
+ ```
16
25
 
17
- $ gem install restful_resource
26
+ Where the `Metrics` class has in interface like:
18
27
 
19
- ## Usage
28
+ ```
29
+ class Metrics
30
+ module_function
20
31
 
21
- TODO: Write usage instructions here
32
+ def count(name, i)
33
+ end
34
+
35
+ def sample(name, i)
36
+ end
37
+
38
+ def measure(name, i)
39
+ end
40
+ end
41
+ ```
42
+
43
+ This will call the methods on the Metrics class with:
44
+ ```
45
+ Metrics.measure('rails_site.api.time', 215.161237) # Time taken
46
+ Metrics.sample('rails_site.api.status', 200) # HTTP status code
47
+ Metrics.count('rails_site.api.called, 1)
48
+ ```
49
+
50
+ Note: To customize the names we can specify `:app_name` and `:api_name` options to `RestfulResource::Base.configure`
51
+
52
+ ### HTTP Cache Metrics
53
+
54
+ Enable Http caching:
55
+
56
+ ```
57
+ RestfulResource::Base.configure(
58
+ base_url: "http://my.api.com/",
59
+ cache_store: Rails.cache,
60
+ instrumentation: {
61
+ metric_class: Metrics,
62
+ app_name: 'rails_site',
63
+ api_name: 'api'
64
+ }
65
+ )
66
+ ```
67
+
68
+ This will call the methods on the Metrics class with:
69
+ ```
70
+ Metrics.sample('rails_site.api.cache_hit', 1) # When a request is served from the cache
71
+ Metrics.sample('rails_site.api.cache_miss', 1) # When a request is fetched from the remote API
72
+ Metrics.sample('rails_site.api.cache_bypass', 1) # When a request did not go through the cache at all
73
+ ```
22
74
 
23
75
  ## Contributing
24
76
 
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'restful_resource'
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require 'pry'
11
+ # Pry.start
12
+
13
+ require 'irb'
14
+ IRB.start
@@ -6,14 +6,16 @@ module RestfulResource
6
6
  username: nil,
7
7
  password: nil,
8
8
  logger: nil,
9
- cache_store: nil)
9
+ cache_store: nil,
10
+ instrumentation: {})
10
11
 
11
12
  @base_url = URI.parse(base_url)
12
13
 
13
14
  @http = RestfulResource::HttpClient.new(username: username,
14
15
  password: password,
15
16
  logger: logger,
16
- cache_store: cache_store)
17
+ cache_store: cache_store,
18
+ instrumentation: instrumentation)
17
19
  end
18
20
 
19
21
  def self.resource_path(url)
@@ -52,9 +52,29 @@ module RestfulResource
52
52
  end
53
53
  end
54
54
 
55
- def initialize(username: nil, password: nil, logger: nil, cache_store: nil, connection: nil)
55
+ def initialize(username: nil,
56
+ password: nil,
57
+ logger: nil,
58
+ cache_store: nil,
59
+ connection: nil,
60
+ instrumentation: {})
61
+
62
+
63
+ api_name = instrumentation[:api_name] ||= 'api'
64
+ instrumentation[:request_instrument_name] ||= "http.#{api_name}"
65
+ instrumentation[:cache_instrument_name] ||= "http_cache.#{api_name}"
66
+
67
+ if instrumentation[:metric_class]
68
+ @metrics = Instrumentation.new(instrumentation.slice(:app_name, :api_name, :request_instrument_name, :cache_instrument_name, :metric_class))
69
+ @metrics.subscribe_to_notifications
70
+ end
71
+
56
72
  # Use a provided faraday client or initalize a new one
57
- @connection = connection || initialize_connection(logger: logger, cache_store: cache_store)
73
+ @connection = connection || initialize_connection(logger: logger,
74
+ cache_store: cache_store,
75
+ instrumenter: ActiveSupport::Notifications,
76
+ request_instrument_name: instrumentation.fetch(:request_instrument_name, nil),
77
+ cache_instrument_name: instrumentation.fetch(:cache_instrument_name, nil))
58
78
 
59
79
  if username && password
60
80
  @connection.basic_auth username, password
@@ -79,7 +99,12 @@ module RestfulResource
79
99
 
80
100
  private
81
101
 
82
- def initialize_connection(logger: nil, cache_store: nil)
102
+ def initialize_connection(logger: nil,
103
+ cache_store: nil,
104
+ instrumenter: nil,
105
+ request_instrument_name: nil,
106
+ cache_instrument_name: nil)
107
+
83
108
  @connection = Faraday.new do |b|
84
109
  b.request :url_encoded
85
110
  b.response :raise_error
@@ -89,7 +114,14 @@ module RestfulResource
89
114
  end
90
115
 
91
116
  if cache_store
92
- b.use :http_cache, store: cache_store
117
+ b.use :http_cache, store: cache_store,
118
+ logger: logger,
119
+ instrumenter: instrumenter,
120
+ instrument_name: cache_instrument_name
121
+ end
122
+
123
+ if instrumenter && request_instrument_name
124
+ b.use :instrumentation, name: request_instrument_name
93
125
  end
94
126
 
95
127
  b.response :encoding
@@ -0,0 +1,93 @@
1
+ require 'active_support/notifications'
2
+
3
+ module RestfulResource
4
+ class Instrumentation
5
+
6
+ def initialize(app_name:, api_name:, request_instrument_name:, cache_instrument_name:, metric_class:)
7
+ @app_name = app_name
8
+ @api_name = api_name
9
+ @request_instrument_name = request_instrument_name
10
+ @cache_instrument_name = cache_instrument_name
11
+ @metric_class = metric_class
12
+ end
13
+
14
+ attr_reader :app_name, :api_name, :request_instrument_name, :cache_instrument_name, :metric_class
15
+
16
+ def subscribe_to_notifications
17
+ validate_metric_class!
18
+
19
+ # Subscribes to events from Faraday
20
+ ActiveSupport::Notifications.subscribe request_instrument_name do |*args|
21
+ event = ActiveSupport::Notifications::Event.new(*args)
22
+
23
+ status = status_from_event(event)
24
+
25
+ # Outputs per API log lines like:
26
+ # measure#quotes_site.research_site_api.time=215.161237
27
+ # sample#quotes_site.research_site_api.status=200
28
+ # count#quotes_site.research_site_api.called=1
29
+ metric_class.measure cache_notifier_namespace(metric: 'time'), event.duration
30
+ metric_class.sample cache_notifier_namespace(metric: 'status'), status
31
+ metric_class.count cache_notifier_namespace(metric: 'called'), 1
32
+
33
+ # Outputs per resource log lines like:
34
+ # measure#quotes_site.research_site_api.api_v2_cap_derivatives.time=215.161237
35
+ # sample#quotes_site.research_site_api.api_v2_cap_derivatives.status=200
36
+ # count#quotes_site.research_site_api.api_v2_cap_derivatives.called=1
37
+ metric_class.measure cache_notifier_namespace(metric: 'time', event: event), event.duration
38
+ metric_class.sample cache_notifier_namespace(metric: 'status', event: event), status
39
+ metric_class.count cache_notifier_namespace(metric: 'called', event: event), 1
40
+ end
41
+
42
+ # Subscribes to events from Faraday::HttpCache
43
+ ActiveSupport::Notifications.subscribe cache_instrument_name do |*args|
44
+ event = ActiveSupport::Notifications::Event.new(*args)
45
+ cache_status = event.payload[:cache_status]
46
+
47
+ # Outputs log lines like:
48
+ # sample#quotes_site.research_site_api.cache_hit=1
49
+ # sample#quotes_site.research_site_api.api_v2_cap_derivatives.cache_hit=1
50
+ case cache_status
51
+ when :fresh, :valid
52
+ metric_class.sample cache_notifier_namespace(metric: 'cache_hit'), 1
53
+ metric_class.sample cache_notifier_namespace(metric: 'cache_hit', event: event), 1
54
+ when :invalid, :miss
55
+ metric_class.sample cache_notifier_namespace(metric: 'cache_miss'), 1
56
+ metric_class.sample cache_notifier_namespace(metric: 'cache_miss', event: event), 1
57
+ when :unacceptable
58
+ metric_class.sample cache_notifier_namespace(metric: 'cache_bypass'), 1
59
+ metric_class.sample cache_notifier_namespace(metric: 'cache_bypass', event: event), 1
60
+ end
61
+ end
62
+ end
63
+
64
+ def validate_metric_class!
65
+ metric_methods = [:count, :sample, :measure]
66
+ if metric_methods.any? {|m| !metric_class.respond_to?(m) }
67
+ raise ArgumentError.new "Metric class '#{metric_class}' does not respond to #{metric_methods.join ','}"
68
+ end
69
+ end
70
+
71
+ def cache_notifier_namespace(event: nil, metric:)
72
+ [app_name, api_name, base_request_path(event), metric].compact.join('.')
73
+ end
74
+
75
+ # Converts a path like "/api/v2/cap_derivatives/75423" to "api_v2_cap_derivatives"
76
+ def base_request_path(event)
77
+ path_from_event(event).split('/').drop(1).take(3).join('_') if event
78
+ end
79
+
80
+ def path_from_event(event)
81
+ url_from_event(event)&.path.to_s
82
+ end
83
+
84
+ def url_from_event(event)
85
+ event.payload[:env]&.url || event.payload&.url
86
+ end
87
+
88
+ def status_from_event(event)
89
+ event.payload[:env]&.status || event.payload&.status
90
+ end
91
+ end
92
+ end
93
+
@@ -1,3 +1,3 @@
1
1
  module RestfulResource
2
- VERSION = '1.4.1'
2
+ VERSION = '1.4.2'
3
3
  end
@@ -20,4 +20,5 @@ require_relative 'restful_resource/http_client'
20
20
  require_relative 'restful_resource/associations'
21
21
  require_relative 'restful_resource/rails_validations'
22
22
  require_relative 'restful_resource/redirections'
23
+ require_relative 'restful_resource/instrumentation'
23
24
  require_relative 'restful_resource/base'
@@ -40,6 +40,62 @@ describe RestfulResource::HttpClient do
40
40
  expect(middleware).to include FaradayMiddleware::Gzip
41
41
  end
42
42
 
43
+ describe 'instrumentation' do
44
+ context 'with the default key' do
45
+ it 'uses default instrumenter and key' do
46
+ expect(find_middleware_args(connection, 'FaradayMiddleware::Instrumentation')).to include(name: 'http.api')
47
+ end
48
+ end
49
+
50
+ context 'with an api_name' do
51
+ let(:connection) { described_class.new(instrumentation: { api_name: 'my_api_name'}).instance_variable_get("@connection") }
52
+
53
+ it 'uses default instrumenter with the api_name' do
54
+ expect(find_middleware_args(connection, 'FaradayMiddleware::Instrumentation')).to include(name: 'http.my_api_name')
55
+ end
56
+ end
57
+
58
+ context 'with a custom instrumentation key' do
59
+ let(:connection) { described_class.new(instrumentation: { request_instrument_name: 'foo.bar'}).instance_variable_get("@connection") }
60
+
61
+ it 'uses default instrumenter with the custom key' do
62
+ expect(find_middleware_args(connection, 'FaradayMiddleware::Instrumentation')).to include(name: 'foo.bar')
63
+ end
64
+ end
65
+
66
+ context 'with a given Metrics class' do
67
+ class FakeMetrics
68
+ def count(name, value); end
69
+ def sample(name, value); end
70
+ def measure(name, value); end
71
+ end
72
+
73
+ let(:mock_instrumention) { instance_double(RestfulResource::Instrumentation) }
74
+
75
+ before do
76
+ allow(RestfulResource::Instrumentation).to receive(:new).and_return mock_instrumention
77
+ allow(mock_instrumention).to receive(:subscribe_to_notifications)
78
+ end
79
+
80
+ it 'initializes the Instrumentation' do
81
+ described_class.new(instrumentation: { app_name: 'rails', api_name: 'api', metric_class: FakeMetrics})
82
+
83
+ expect(RestfulResource::Instrumentation).to have_received(:new)
84
+ .with(app_name: 'rails',
85
+ api_name: 'api',
86
+ request_instrument_name: 'http.api',
87
+ cache_instrument_name: 'http_cache.api',
88
+ metric_class: FakeMetrics)
89
+ end
90
+
91
+ it 'subscribes to the notifications' do
92
+ described_class.new(instrumentation: { app_name: 'rails', api_name: 'api', metric_class: FakeMetrics})
93
+
94
+ expect(mock_instrumention).to have_received(:subscribe_to_notifications)
95
+ end
96
+ end
97
+ end
98
+
43
99
  describe 'when provided a logger' do
44
100
  let(:connection) { described_class.new(logger: logger).instance_variable_get("@connection") }
45
101
  let(:logger) { Logger.new('/dev/null') }
@@ -63,6 +119,22 @@ describe RestfulResource::HttpClient do
63
119
  it 'uses that cache_store' do
64
120
  expect(find_middleware_args(connection, 'Faraday::HttpCache')).to include(store: 'redis')
65
121
  end
122
+
123
+ context 'and an api_name is provided' do
124
+ let(:connection) { described_class.new(cache_store: 'redis', instrumentation: { api_name: 'my_api_name'}).instance_variable_get("@connection") }
125
+
126
+ it 'passes the instrumenter and the api_name' do
127
+ expect(find_middleware_args(connection, 'Faraday::HttpCache')).to include(instrumenter: ActiveSupport::Notifications, instrument_name: 'http_cache.my_api_name')
128
+ end
129
+ end
130
+
131
+ context 'and a custom instrument name is provided' do
132
+ let(:connection) { described_class.new(cache_store: 'redis', instrumentation: { cache_instrument_name: 'foo.bar'}).instance_variable_get("@connection") }
133
+
134
+ it 'passes the instrumenter to the http cache middleware' do
135
+ expect(find_middleware_args(connection, 'Faraday::HttpCache')).to include(instrumenter: ActiveSupport::Notifications, instrument_name: 'foo.bar')
136
+ end
137
+ end
66
138
  end
67
139
  end
68
140
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: restful_resource
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.1
4
+ version: 1.4.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Santoro
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2017-01-13 00:00:00.000000000 Z
12
+ date: 2017-01-17 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: bundler
@@ -169,7 +169,8 @@ description: A simple activerecord inspired rest resource base class implemented
169
169
  rest-client
170
170
  email:
171
171
  - developers@carwow.co.uk
172
- executables: []
172
+ executables:
173
+ - console
173
174
  extensions: []
174
175
  extra_rdoc_files: []
175
176
  files:
@@ -178,12 +179,14 @@ files:
178
179
  - LICENSE.txt
179
180
  - README.md
180
181
  - Rakefile
182
+ - bin/console
181
183
  - circle.yml
182
184
  - lib/restful_resource.rb
183
185
  - lib/restful_resource/associations.rb
184
186
  - lib/restful_resource/authorization.rb
185
187
  - lib/restful_resource/base.rb
186
188
  - lib/restful_resource/http_client.rb
189
+ - lib/restful_resource/instrumentation.rb
187
190
  - lib/restful_resource/null_logger.rb
188
191
  - lib/restful_resource/open_object.rb
189
192
  - lib/restful_resource/paginated_array.rb