songkick-transport 1.2.0 → 1.9.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.rdoc +52 -15
- data/lib/songkick/transport/authentication.rb +27 -0
- data/lib/songkick/transport/base.rb +146 -19
- data/lib/songkick/transport/curb.rb +31 -19
- data/lib/songkick/transport/headers.rb +15 -10
- data/lib/songkick/transport/html_report.html.erb +6 -2
- data/lib/songkick/transport/httparty.rb +15 -21
- data/lib/songkick/transport/rack_test.rb +20 -35
- data/lib/songkick/transport/reporting.rb +1 -2
- data/lib/songkick/transport/request.rb +17 -18
- data/lib/songkick/transport/response.rb +6 -7
- data/lib/songkick/transport/service.rb +51 -14
- data/lib/songkick/transport.rb +27 -25
- data/spec/songkick/transport/authentication_spec.rb +28 -0
- data/spec/songkick/transport/base_spec.rb +146 -0
- data/spec/songkick/transport/curb_spec.rb +16 -38
- data/spec/songkick/transport/httparty_spec.rb +8 -8
- data/spec/songkick/transport/request_spec.rb +9 -9
- data/spec/songkick/transport/response_spec.rb +12 -12
- data/spec/songkick/transport/service_spec.rb +70 -0
- data/spec/songkick/transport_spec.rb +168 -58
- data/spec/spec_helper.rb +21 -12
- metadata +79 -43
- data/lib/songkick/transport/header_decorator.rb +0 -27
- data/lib/songkick/transport/timeout_decorator.rb +0 -27
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4b1535085159dedb0d551659b5fafcc840abd490
|
4
|
+
data.tar.gz: 3348c5d75c8a626302f4e1dae864d00edca3b593
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4134a141cb12aeab651d930938b2fa77361db813fc127c9344477619c748abc57b9ab84deacce2063eafd65cd1cf6e40728cf5af0a50cb0c3770630777fb37ac
|
7
|
+
data.tar.gz: b0f7e3b1acda2080a5bb44aab9890739290c191287480d79b8a3bd08ab54c3df81c2a931ec088bf363b8813d8f8f223a6bf5ac88a7f6db5d5e6f2f895a323e66
|
data/README.rdoc
CHANGED
@@ -1,7 +1,5 @@
|
|
1
1
|
= Songkick::Transport {<img src="https://secure.travis-ci.org/songkick/transport.png?branch=master" />}[http://travis-ci.org/songkick/transport]
|
2
2
|
|
3
|
-
http://songkickontour.appspot.com/lego_tourbus.png
|
4
|
-
|
5
3
|
This is a transport layer abstraction for talking to our service APIs. It
|
6
4
|
provides an abstract HTTP-like interface while hiding the underlying transport
|
7
5
|
and serialization details. It transparently deals with parameter serialization,
|
@@ -61,7 +59,8 @@ takes a reference to a Rack application, for example:
|
|
61
59
|
|
62
60
|
client = Transport.new(Sinatra::Application,
|
63
61
|
:user_agent => 'Test Agent',
|
64
|
-
:timeout => 5
|
62
|
+
:timeout => 5,
|
63
|
+
:basic_auth => {:username => "foo", :password => "bar"})
|
65
64
|
|
66
65
|
All transports expose exactly the same instance methods.
|
67
66
|
|
@@ -148,6 +147,10 @@ parsers for other content-types like so:
|
|
148
147
|
|
149
148
|
The parser object you register must respond to <tt>parse(string)</tt>.
|
150
149
|
|
150
|
+
You can also register a default parser, to handle all content types that don't have a specified parser.
|
151
|
+
|
152
|
+
Songkick::Transport.register_default_parser(DefaultParser)
|
153
|
+
|
151
154
|
|
152
155
|
=== Nested parameters
|
153
156
|
|
@@ -173,7 +176,7 @@ Rails and Sinatra will parse this back into the original data structure for you
|
|
173
176
|
on the server side.
|
174
177
|
|
175
178
|
|
176
|
-
=== Request headers and
|
179
|
+
=== Request headers, timeouts and basic auth
|
177
180
|
|
178
181
|
You can make requests with custom headers using +with_headers+. The return value
|
179
182
|
of +with_headers+ works just like a client object, so you can use it for
|
@@ -191,6 +194,10 @@ Similarly, the request timeout can be adjusted per-request:
|
|
191
194
|
|
192
195
|
client.with_timeout(10).get('/slow_resource')
|
193
196
|
|
197
|
+
Likewise basic auth credentials:
|
198
|
+
|
199
|
+
client.with_basic_auth({:username => "foo", :password => "bar"}).get('/')
|
200
|
+
|
194
201
|
|
195
202
|
=== File uploads
|
196
203
|
|
@@ -253,8 +260,7 @@ types into an <tt>IO</tt> for you:
|
|
253
260
|
You can then use this to forward uploaded files to another service from your
|
254
261
|
Rails or Sinatra application.
|
255
262
|
|
256
|
-
|
257
|
-
=== Logging and reporting
|
263
|
+
=== Logging, instrumentation and reporting
|
258
264
|
|
259
265
|
You can enable basic logging by supplying a logger and switching logging on.
|
260
266
|
|
@@ -282,15 +288,15 @@ exclude headers used for authentication:
|
|
282
288
|
|
283
289
|
There is also a more advanced reporting system that lets you aggregate request
|
284
290
|
statistics. During a request to a web application, many requests to backend
|
285
|
-
services may be involved. The
|
286
|
-
all the backend requests that happened while executing a block. For
|
287
|
-
can use it to create a logging middleware:
|
291
|
+
services may be involved. The reporting system lets you collect information
|
292
|
+
about all the backend requests that happened while executing a block. For
|
293
|
+
example you can use it to create a logging middleware:
|
288
294
|
|
289
295
|
class Reporter
|
290
296
|
def initialize(app)
|
291
297
|
@app = app
|
292
298
|
end
|
293
|
-
|
299
|
+
|
294
300
|
def call(env)
|
295
301
|
report = Songkick::Transport.report
|
296
302
|
response = report.execute { @app.call(env) }
|
@@ -314,9 +320,14 @@ following API:
|
|
314
320
|
The +report+ object itself also responds to +total_duration+, which gives you
|
315
321
|
the total time spent calling backend services during the block.
|
316
322
|
|
323
|
+
To instrument transports using the `ActiveSupport::Notifications` API, pass
|
324
|
+
`{:instrumenter => ActiveSupport::Notifications}` in the options. You can also
|
325
|
+
override the default event label of `http.songkick_transport` by passing
|
326
|
+
`:instrumentation_label`.
|
327
|
+
|
317
328
|
== Writing Service classes
|
318
329
|
|
319
|
-
|
330
|
+
`Songkick::Transport::Service` is a class to make writing service clients more convenient.
|
320
331
|
|
321
332
|
Set up config globally (perhaps in a Rails initializer):
|
322
333
|
|
@@ -339,17 +350,44 @@ Subclass to create service clients:
|
|
339
350
|
end
|
340
351
|
end
|
341
352
|
|
342
|
-
The default transport layer for clients inheriting from
|
343
|
-
is Curb, if you want to use something else you can override it globally or in a class
|
353
|
+
The default transport layer for clients inheriting from `Songkick::Transport::Service`
|
354
|
+
is Curb, if you want to use something else you can override it globally or in a class
|
344
355
|
with:
|
345
356
|
|
346
357
|
transport_layer Songkick::Transport::HttParty
|
347
358
|
|
359
|
+
You can specify extra headers to be sent with every request from a service class and
|
360
|
+
from the root class, and they are merged together:
|
361
|
+
|
362
|
+
Songkick::Transport::Service.with_headers "rlah" => "1"
|
363
|
+
|
364
|
+
class BlahService < Songkick::Transport::Service
|
365
|
+
with_headers "blah" => "1"
|
366
|
+
end
|
367
|
+
|
368
|
+
class FlahService < BlahService
|
369
|
+
with_headers "flah" => "1"
|
370
|
+
|
371
|
+
def get_data
|
372
|
+
http.get("/stuff") # will have headers "rlah", "blah" and "flah"
|
373
|
+
end
|
374
|
+
end
|
375
|
+
|
376
|
+
To pass extra, perhaps transport-specific, options hashes to the transport
|
377
|
+
layer on initialize, specify them with:
|
378
|
+
|
379
|
+
class FooService < Songkick::Transport::Service
|
380
|
+
transport_layer Songkick::Transport::Curb
|
381
|
+
transport_layer_options :no_signal => true
|
382
|
+
end
|
383
|
+
|
384
|
+
These are also inheritable, and merge down like extra headers do.
|
385
|
+
|
348
386
|
== License
|
349
387
|
|
350
388
|
The MIT License
|
351
389
|
|
352
|
-
Copyright (c) 2012-
|
390
|
+
Copyright (c) 2012-2015 Songkick
|
353
391
|
|
354
392
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
355
393
|
this software and associated documentation files (the "Software"), to deal in
|
@@ -367,4 +405,3 @@ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
367
405
|
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
368
406
|
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
369
407
|
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
370
|
-
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require "base64"
|
2
|
+
|
3
|
+
module Songkick
|
4
|
+
module Transport
|
5
|
+
module Authentication
|
6
|
+
|
7
|
+
extend self
|
8
|
+
|
9
|
+
def basic_auth_headers(credentials)
|
10
|
+
username = credentials.fetch(:username)
|
11
|
+
password = credentials.fetch(:password)
|
12
|
+
encoded_creds = strict_encode64("#{username}:#{password}")
|
13
|
+
Headers.new({"Authorization" => "Basic #{encoded_creds}"})
|
14
|
+
end
|
15
|
+
|
16
|
+
# Base64.strict_encode64 is not available on Ruby 1.8.7
|
17
|
+
def strict_encode64(str)
|
18
|
+
if Base64.respond_to?(:strict_encode64)
|
19
|
+
Base64.strict_encode64(str)
|
20
|
+
else
|
21
|
+
Base64.encode64(str).gsub("\n", '')
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -2,39 +2,102 @@ module Songkick
|
|
2
2
|
module Transport
|
3
3
|
|
4
4
|
class Base
|
5
|
+
module API
|
6
|
+
def get(path, params = {}, head = {}, timeout = nil)
|
7
|
+
do_verb("get", path, params, head, timeout)
|
8
|
+
end
|
9
|
+
|
10
|
+
def post(path, params = {}, head = {}, timeout = nil)
|
11
|
+
do_verb("post", path, params, head, timeout)
|
12
|
+
end
|
13
|
+
|
14
|
+
def put(path, params = {}, head = {}, timeout = nil)
|
15
|
+
do_verb("put", path, params, head, timeout)
|
16
|
+
end
|
17
|
+
|
18
|
+
def patch(path, params = {}, head = {}, timeout = nil)
|
19
|
+
do_verb("patch", path, params, head, timeout)
|
20
|
+
end
|
21
|
+
|
22
|
+
def delete(path, params = {}, head = {}, timeout = nil)
|
23
|
+
do_verb("delete", path, params, head, timeout)
|
24
|
+
end
|
25
|
+
|
26
|
+
def options(path, params = {}, head = {}, timeout = nil)
|
27
|
+
do_verb("options", path, params, head, timeout)
|
28
|
+
end
|
29
|
+
|
30
|
+
def head(path, params = {}, head = {}, timeout = nil)
|
31
|
+
do_verb("head", path, params, head, timeout)
|
32
|
+
end
|
33
|
+
|
34
|
+
def with_headers(headers = {})
|
35
|
+
HeaderDecorator.new(self, headers)
|
36
|
+
end
|
37
|
+
|
38
|
+
def with_timeout(timeout)
|
39
|
+
TimeoutDecorator.new(self, timeout)
|
40
|
+
end
|
41
|
+
|
42
|
+
def with_params(params)
|
43
|
+
ParamsDecorator.new(self, params)
|
44
|
+
end
|
45
|
+
|
46
|
+
def with_basic_auth(credentials)
|
47
|
+
BasicAuthDecorator.new(self, credentials)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
include API
|
52
|
+
|
5
53
|
attr_accessor :user_agent, :user_error_codes
|
54
|
+
attr_reader :host, :timeout, :instrumenter, :basic_auth
|
55
|
+
alias_method :endpoint, :host
|
56
|
+
DEFAULT_INSTRUMENTATION_LABEL = 'http.songkick_transport'
|
6
57
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
58
|
+
def initialize(host, options = {})
|
59
|
+
@host = host
|
60
|
+
@timeout = options[:timeout] || DEFAULT_TIMEOUT
|
61
|
+
@user_agent = options[:user_agent]
|
62
|
+
@user_error_codes = options[:user_error_codes] || DEFAULT_USER_ERROR_CODES
|
63
|
+
@instrumenter ||= options[:instrumenter]
|
64
|
+
@instrumentation_label = options[:instrumentation_label] || DEFAULT_INSTRUMENTATION_LABEL
|
65
|
+
@basic_auth = options[:basic_auth]
|
13
66
|
end
|
14
67
|
|
15
68
|
def do_verb(verb, path, params = {}, head = {}, timeout = nil)
|
16
|
-
|
69
|
+
auth_headers = basic_auth ? Authentication.basic_auth_headers(basic_auth) : {}
|
70
|
+
req = Request.new(endpoint, verb, path, params, headers.merge(auth_headers).merge(head), timeout)
|
17
71
|
Reporting.log_request(req)
|
18
72
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
73
|
+
instrument(req) do |payload|
|
74
|
+
begin
|
75
|
+
req.response = execute_request(req)
|
76
|
+
payload.merge!({ :status => req.response.status,
|
77
|
+
:response_headers => req.response.headers.to_hash }) if req.response
|
78
|
+
rescue => error
|
79
|
+
req.error = error
|
80
|
+
payload.merge!({ :status => error.status,
|
81
|
+
:response_headers => error.headers.to_hash }) if error.is_a?(HttpError)
|
82
|
+
Reporting.record(req)
|
83
|
+
raise error
|
84
|
+
ensure
|
85
|
+
payload.merge!(self.instrumentation_payload_extras)
|
86
|
+
end
|
25
87
|
end
|
26
88
|
|
27
89
|
Reporting.log_response(req)
|
28
90
|
Reporting.record(req)
|
91
|
+
|
29
92
|
req.response
|
30
93
|
end
|
31
94
|
|
32
|
-
def
|
33
|
-
|
95
|
+
def instrumentation_payload_extras
|
96
|
+
Thread.current[:transport_base_payload_extras] ||= {}
|
34
97
|
end
|
35
98
|
|
36
|
-
def
|
37
|
-
|
99
|
+
def instrumentation_payload_extras=(extras)
|
100
|
+
Thread.current[:transport_base_payload_extras] = {}
|
38
101
|
end
|
39
102
|
|
40
103
|
private
|
@@ -43,6 +106,23 @@ module Songkick
|
|
43
106
|
Response.process(url, status, headers, body, @user_error_codes)
|
44
107
|
end
|
45
108
|
|
109
|
+
def instrument(request)
|
110
|
+
if self.instrumenter
|
111
|
+
payload = { :adapter => self.class.name,
|
112
|
+
:endpoint => request.endpoint,
|
113
|
+
:verb => request.verb,
|
114
|
+
:path => request.path,
|
115
|
+
:params => request.params,
|
116
|
+
:request_headers => request.headers.to_hash }
|
117
|
+
|
118
|
+
self.instrumenter.instrument(@instrumentation_label, payload) do
|
119
|
+
yield(payload)
|
120
|
+
end
|
121
|
+
else
|
122
|
+
yield({})
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
46
126
|
def headers
|
47
127
|
Headers.new(
|
48
128
|
'Connection' => 'close',
|
@@ -53,8 +133,55 @@ module Songkick
|
|
53
133
|
def logger
|
54
134
|
Transport.logger
|
55
135
|
end
|
56
|
-
end
|
57
136
|
|
137
|
+
class HeaderDecorator < Struct.new(:client, :headers)
|
138
|
+
include API
|
139
|
+
|
140
|
+
def do_verb(verb, path, params = {}, new_headers = {}, timeout = nil)
|
141
|
+
client.do_verb(verb, path, params, Headers.new(headers).merge(new_headers), timeout)
|
142
|
+
end
|
143
|
+
|
144
|
+
def method_missing(*args, &block)
|
145
|
+
client.__send__(*args, &block)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
class TimeoutDecorator < Struct.new(:client, :timeout)
|
150
|
+
include API
|
151
|
+
|
152
|
+
def do_verb(verb, path, params = {}, headers = {}, new_timeout = nil)
|
153
|
+
client.do_verb(verb, path, params, headers, new_timeout || timeout)
|
154
|
+
end
|
155
|
+
|
156
|
+
def method_missing(*args, &block)
|
157
|
+
client.__send__(*args, &block)
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
class ParamsDecorator < Struct.new(:client, :params)
|
162
|
+
include API
|
163
|
+
|
164
|
+
def do_verb(verb, path, new_params = {}, headers = {}, timeout = nil)
|
165
|
+
client.do_verb(verb, path, params.merge(new_params), headers, timeout)
|
166
|
+
end
|
167
|
+
|
168
|
+
def method_missing(*args, &block)
|
169
|
+
client.__send__(*args, &block)
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
class BasicAuthDecorator < Struct.new(:client, :credentials)
|
174
|
+
include API
|
175
|
+
|
176
|
+
def do_verb(verb, path, params = {}, headers = {}, timeout = nil)
|
177
|
+
auth_headers = Authentication.basic_auth_headers(credentials)
|
178
|
+
client.do_verb(verb, path, params, auth_headers.merge(headers), timeout)
|
179
|
+
end
|
180
|
+
|
181
|
+
def method_missing(*args, &block)
|
182
|
+
client.__send__(*args, &block)
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
58
186
|
end
|
59
187
|
end
|
60
|
-
|
@@ -12,37 +12,40 @@ module Songkick
|
|
12
12
|
DEFAULT_HEADERS = {"Expect" => ""}
|
13
13
|
|
14
14
|
def self.clear_thread_connection
|
15
|
-
|
15
|
+
Thread.current[:transport_curb_easy] = nil
|
16
16
|
end
|
17
|
-
|
17
|
+
|
18
18
|
def initialize(host, options = {})
|
19
|
-
|
20
|
-
@
|
21
|
-
|
22
|
-
@user_error_codes = options[:user_error_codes] || DEFAULT_USER_ERROR_CODES
|
23
|
-
if c = options[:connection]
|
24
|
-
Thread.current[:transport_curb_easy] = c
|
25
|
-
end
|
19
|
+
super(host, options)
|
20
|
+
@no_signal = !!options[:no_signal]
|
21
|
+
Thread.current[:transport_curb_easy] ||= options[:connection]
|
26
22
|
end
|
27
|
-
|
23
|
+
|
28
24
|
def connection
|
29
25
|
Thread.current[:transport_curb_easy] ||= Curl::Easy.new
|
30
26
|
end
|
31
|
-
|
32
|
-
def
|
33
|
-
|
27
|
+
|
28
|
+
def instrumentation_payload_extras
|
29
|
+
Thread.current[:transport_curb_payload_extras] ||= {}
|
34
30
|
end
|
35
|
-
|
31
|
+
|
32
|
+
def instrumentation_payload_extras=(extras)
|
33
|
+
Thread.current[:transport_curb_payload_extras] = {}
|
34
|
+
end
|
35
|
+
|
36
36
|
def execute_request(req)
|
37
|
+
self.instrumentation_payload_extras = {} if self.instrumenter
|
37
38
|
connection.reset
|
38
|
-
|
39
|
+
|
39
40
|
connection.url = req.url
|
40
41
|
timeout = req.timeout || @timeout
|
41
42
|
connection.timeout = timeout
|
43
|
+
connection.encoding = ''
|
42
44
|
connection.headers.update(DEFAULT_HEADERS.merge(req.headers))
|
43
|
-
|
45
|
+
connection.nosignal = true if @no_signal
|
46
|
+
|
44
47
|
response_headers = {}
|
45
|
-
|
48
|
+
|
46
49
|
connection.on_header do |header_line|
|
47
50
|
line = header_line.sub(/\r\n$/, '')
|
48
51
|
parts = line.split(/:\s*/)
|
@@ -51,13 +54,18 @@ module Songkick
|
|
51
54
|
end
|
52
55
|
header_line.bytesize
|
53
56
|
end
|
54
|
-
|
57
|
+
|
55
58
|
if req.use_body?
|
56
59
|
connection.__send__("http_#{req.verb}", req.body)
|
57
60
|
else
|
58
61
|
connection.http(req.verb.upcase)
|
59
62
|
end
|
60
63
|
|
64
|
+
if self.instrumenter
|
65
|
+
self.instrumentation_payload_extras[:connect_time] = connection.connect_time
|
66
|
+
self.instrumentation_payload_extras[:name_lookup_time] = connection.name_lookup_time
|
67
|
+
end
|
68
|
+
|
61
69
|
process(req, connection.response_code, response_headers, connection.body_str)
|
62
70
|
|
63
71
|
rescue Curl::Err::HostResolutionError => error
|
@@ -75,9 +83,13 @@ module Songkick
|
|
75
83
|
rescue Curl::Err::GotNothingError => error
|
76
84
|
logger.warn "Got nothing: #{req}"
|
77
85
|
raise Transport::UpstreamError, req
|
86
|
+
|
87
|
+
rescue Curl::Err::RecvError => error
|
88
|
+
logger.warn "Failure receiving network data: #{error.message} : #{req}"
|
89
|
+
raise Transport::UpstreamError, req
|
90
|
+
|
78
91
|
end
|
79
92
|
end
|
80
93
|
|
81
94
|
end
|
82
95
|
end
|
83
|
-
|