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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 363d11105aac72ce511d7f71cf94423173c34ef2
4
- data.tar.gz: bb50281d1b1b9c7f621b6421e3683c61ab712fa9
3
+ metadata.gz: 4b1535085159dedb0d551659b5fafcc840abd490
4
+ data.tar.gz: 3348c5d75c8a626302f4e1dae864d00edca3b593
5
5
  SHA512:
6
- metadata.gz: edfc237f243425064e3658e536ebb24194067726d534ba92f79e2334ce61a44da898b68e054aca0c1f0f507786c4447fe7b0bdb1ae6d07be41ff977d74f6768e
7
- data.tar.gz: 7861aa8565ba63612cf22ffdd744d5a4ab53bea29af78da05d610cad3d82fc412e58a51fb5c9f82857dab15c8b99afe8be94ee742df9d3ec2038538c0dd75a98
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 timeouts
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 repoting system lets you collect information about
286
- all the backend requests that happened while executing a block. For example you
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
- +Songkick::Transport::Service+ is a class to make writing service clients more convenient.
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 +Songkick::Transport::Service+
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-2013 Songkick
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
- HTTP_VERBS.each do |verb|
8
- class_eval %{
9
- def #{verb}(path, params = {}, head = {}, timeout = nil)
10
- do_verb("#{verb}", path, params, head, timeout)
11
- end
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
- req = Request.new(endpoint, verb, path, params, headers.merge(head), timeout)
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
- begin
20
- req.response = execute_request(req)
21
- rescue => error
22
- req.error = error
23
- Reporting.record(req)
24
- raise error
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 with_headers(headers = {})
33
- HeaderDecorator.new(self, headers)
95
+ def instrumentation_payload_extras
96
+ Thread.current[:transport_base_payload_extras] ||= {}
34
97
  end
35
98
 
36
- def with_timeout(timeout = DEFAULT_TIMEOUT)
37
- TimeoutDecorator.new(self, timeout)
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
- Thread.current[:transport_curb_easy] = nil
15
+ Thread.current[:transport_curb_easy] = nil
16
16
  end
17
-
17
+
18
18
  def initialize(host, options = {})
19
- @host = host
20
- @timeout = options[:timeout] || DEFAULT_TIMEOUT
21
- @user_agent = options[:user_agent]
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 endpoint
33
- @host
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
-