restful_resource 1.4.1 → 1.4.2

Sign up to get free protection for your applications and to get access to all the features.
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