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 +4 -4
- data/README.md +62 -10
- data/bin/console +14 -0
- data/lib/restful_resource/base.rb +4 -2
- data/lib/restful_resource/http_client.rb +36 -4
- data/lib/restful_resource/instrumentation.rb +93 -0
- data/lib/restful_resource/version.rb +1 -1
- data/lib/restful_resource.rb +1 -0
- data/spec/restful_resource/http_client_configuration_spec.rb +72 -0
- metadata +6 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7ec89f5aff66263aa662e83535625118f92faad2
|
4
|
+
data.tar.gz: f8653d759998fb91a5ac0fdb52d340edb55beafd
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1f7050817b74a710234c0374aa388b4f3f3758ca9694ef49d89430894ae62a7edf0e375579556dc9978fac8d19075c204bc303cc82cb6a22d9c348343ab7ca57
|
7
|
+
data.tar.gz: 12bd9dc5305fc0b362dc94420ec6d7f55172203e775caf0dbeecdf242fb463275dab5f29d2c6656eb5e5d8c80b2eb3b1a221364918d50f3b604f92dad967a0d4
|
data/README.md
CHANGED
@@ -1,24 +1,76 @@
|
|
1
1
|
# RestfulResource 
|
2
2
|
|
3
|
-
|
3
|
+
Provides an ActiveResource like interface to JSON API's
|
4
4
|
|
5
|
-
##
|
5
|
+
## Metrics
|
6
6
|
|
7
|
-
|
7
|
+
### HTTP Metrics
|
8
8
|
|
9
|
-
|
9
|
+
Http requests are automatically instrumented using ActiveSupport::Notifications
|
10
10
|
|
11
|
-
|
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
|
-
|
13
|
+
eg
|
14
14
|
|
15
|
-
|
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
|
-
|
26
|
+
Where the `Metrics` class has in interface like:
|
18
27
|
|
19
|
-
|
28
|
+
```
|
29
|
+
class Metrics
|
30
|
+
module_function
|
20
31
|
|
21
|
-
|
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,
|
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,
|
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,
|
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
|
+
|
data/lib/restful_resource.rb
CHANGED
@@ -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.
|
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-
|
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
|