songkick-transport 0.2.2 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 363d11105aac72ce511d7f71cf94423173c34ef2
4
+ data.tar.gz: bb50281d1b1b9c7f621b6421e3683c61ab712fa9
5
+ SHA512:
6
+ metadata.gz: edfc237f243425064e3658e536ebb24194067726d534ba92f79e2334ce61a44da898b68e054aca0c1f0f507786c4447fe7b0bdb1ae6d07be41ff977d74f6768e
7
+ data.tar.gz: 7861aa8565ba63612cf22ffdd744d5a4ab53bea29af78da05d610cad3d82fc412e58a51fb5c9f82857dab15c8b99afe8be94ee742df9d3ec2038538c0dd75a98
data/README.rdoc CHANGED
@@ -2,8 +2,6 @@
2
2
 
3
3
  http://songkickontour.appspot.com/lego_tourbus.png
4
4
 
5
- (Image from {Songkick on Tour}[http://songkickontour.appspot.com])
6
-
7
5
  This is a transport layer abstraction for talking to our service APIs. It
8
6
  provides an abstract HTTP-like interface while hiding the underlying transport
9
7
  and serialization details. It transparently deals with parameter serialization,
@@ -131,6 +129,14 @@ If the request raises an exception, it will be of one of the following types:
131
129
  * <tt>Songkick::Transport::HttpError</tt> -- we received a response with a
132
130
  non-successful status code, e.g. 404 or 500
133
131
 
132
+ It is possible to customise the status codes which are treated as UserError
133
+ when initializing the client. Requests responding with any of the provided
134
+ status codes will then yield a response object with error details, rather than
135
+ raising an exception;
136
+
137
+ client = Transport.new('http://localhost:4567',
138
+ :user_error_codes => [409, 422])
139
+
134
140
 
135
141
  === Registering response parsers
136
142
 
@@ -308,6 +314,36 @@ following API:
308
314
  The +report+ object itself also responds to +total_duration+, which gives you
309
315
  the total time spent calling backend services during the block.
310
316
 
317
+ == Writing Service classes
318
+
319
+ +Songkick::Transport::Service+ is a class to make writing service clients more convenient.
320
+
321
+ Set up config globally (perhaps in a Rails initializer):
322
+
323
+ Songkick::Transport::Service.set_endpoints("blah-service" => "of1-dev-services:2347")
324
+ Songkick::Transport::Service.user_agent "myproject"
325
+ Songkick::Transport::Service.timeout 60 # optional, default is 10
326
+
327
+ Subclass to create service clients:
328
+
329
+ class BlahService < Songkick::Transport::Service
330
+ endpoint "blah-service"
331
+
332
+ # these global configs can also be set at the class level, in which case they
333
+ # override the global config
334
+ user_agent "myproject mainservice class"
335
+ timeout 10
336
+
337
+ def get_data
338
+ http.get("/stuff", :search => "name")
339
+ end
340
+ end
341
+
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
344
+ with:
345
+
346
+ transport_layer Songkick::Transport::HttParty
311
347
 
312
348
  == License
313
349
 
@@ -8,6 +8,7 @@ module Songkick
8
8
  module Transport
9
9
  DEFAULT_TIMEOUT = 5
10
10
  DEFAULT_FORMAT = :json
11
+ DEFAULT_USER_ERROR_CODES = [409]
11
12
 
12
13
  HTTP_VERBS = %w[options head get patch post put delete]
13
14
  USE_BODY = %w[post put]
@@ -26,6 +27,7 @@ module Songkick
26
27
  autoload :Request, ROOT + '/transport/request'
27
28
  autoload :Response, ROOT + '/transport/response'
28
29
  autoload :TimeoutDecorator, ROOT + '/transport/timeout_decorator'
30
+ autoload :Service, ROOT + '/transport/service'
29
31
 
30
32
  autoload :UpstreamError, ROOT + '/transport/upstream_error'
31
33
  autoload :HostResolutionError, ROOT + '/transport/upstream_error'
@@ -1,55 +1,60 @@
1
1
  module Songkick
2
2
  module Transport
3
-
3
+
4
4
  class Base
5
- attr_accessor :user_agent
6
-
5
+ attr_accessor :user_agent, :user_error_codes
6
+
7
7
  HTTP_VERBS.each do |verb|
8
8
  class_eval %{
9
9
  def #{verb}(path, params = {}, head = {}, timeout = nil)
10
- req = Request.new(endpoint, '#{verb}', path, params, headers.merge(head), timeout)
11
- Reporting.log_request(req)
12
-
13
- req.response = execute_request(req)
14
-
15
- Reporting.log_response(req)
16
- Reporting.record(req)
17
- req.response
18
-
19
- rescue => error
20
- req.error = error
21
- Reporting.record(req)
22
- raise error
10
+ do_verb("#{verb}", path, params, head, timeout)
23
11
  end
24
12
  }
25
13
  end
26
-
14
+
15
+ def do_verb(verb, path, params = {}, head = {}, timeout = nil)
16
+ req = Request.new(endpoint, verb, path, params, headers.merge(head), timeout)
17
+ Reporting.log_request(req)
18
+
19
+ begin
20
+ req.response = execute_request(req)
21
+ rescue => error
22
+ req.error = error
23
+ Reporting.record(req)
24
+ raise error
25
+ end
26
+
27
+ Reporting.log_response(req)
28
+ Reporting.record(req)
29
+ req.response
30
+ end
31
+
27
32
  def with_headers(headers = {})
28
33
  HeaderDecorator.new(self, headers)
29
34
  end
30
-
35
+
31
36
  def with_timeout(timeout = DEFAULT_TIMEOUT)
32
37
  TimeoutDecorator.new(self, timeout)
33
38
  end
34
-
35
- private
36
-
39
+
40
+ private
41
+
37
42
  def process(url, status, headers, body)
38
- Response.process(url, status, headers, body)
43
+ Response.process(url, status, headers, body, @user_error_codes)
39
44
  end
40
-
45
+
41
46
  def headers
42
47
  Headers.new(
43
48
  'Connection' => 'close',
44
49
  'User-Agent' => user_agent
45
50
  )
46
51
  end
47
-
52
+
48
53
  def logger
49
54
  Transport.logger
50
55
  end
51
56
  end
52
-
57
+
53
58
  end
54
59
  end
55
60
 
@@ -19,6 +19,7 @@ module Songkick
19
19
  @host = host
20
20
  @timeout = options[:timeout] || DEFAULT_TIMEOUT
21
21
  @user_agent = options[:user_agent]
22
+ @user_error_codes = options[:user_error_codes] || DEFAULT_USER_ERROR_CODES
22
23
  if c = options[:connection]
23
24
  Thread.current[:transport_curb_easy] = c
24
25
  end
@@ -35,8 +36,9 @@ module Songkick
35
36
  def execute_request(req)
36
37
  connection.reset
37
38
 
38
- connection.url = req.url
39
- connection.timeout = req.timeout || @timeout
39
+ connection.url = req.url
40
+ timeout = req.timeout || @timeout
41
+ connection.timeout = timeout
40
42
  connection.headers.update(DEFAULT_HEADERS.merge(req.headers))
41
43
 
42
44
  response_headers = {}
@@ -67,7 +69,7 @@ module Songkick
67
69
  raise Transport::ConnectionFailedError, req
68
70
 
69
71
  rescue Curl::Err::TimeoutError => error
70
- logger.warn "Request timed out after #{@timeout}s : #{req}"
72
+ logger.warn "Request timed out after #{timeout}s : #{req}"
71
73
  raise Transport::TimeoutError, req
72
74
 
73
75
  rescue Curl::Err::GotNothingError => error
@@ -0,0 +1,140 @@
1
+ <% # don't copy this style, this is just for development mode %>
2
+ <% begin %>
3
+ <div id="transport-report">
4
+ <script>
5
+ function transportReportToggle() {
6
+ var element = document.getElementById("transport-report").getElementsByClassName("service-metrics")[0];
7
+ if(element.style.display == "none") {
8
+ element.style.display = "block";
9
+ } else {
10
+ element.style.display = "none";
11
+ }
12
+ }
13
+ </script>
14
+
15
+ <style>
16
+ #transport-report {
17
+ position: absolute;
18
+ top: 25px;
19
+ left: 3px;
20
+ z-index: 100000;
21
+ font-size: 11px;
22
+ }
23
+
24
+ #transport-report .switch {
25
+ color: #333;
26
+ border: 1px solid #555;
27
+ background-color: #999;
28
+ padding: 3px;
29
+ }
30
+
31
+ #transport-report .service-metrics {
32
+ color: #333;
33
+ margin-top: 2px;
34
+ border: 1px solid #aaa;
35
+ background-color: #eee;
36
+ padding: 4px;
37
+ padding-left: 7px;
38
+ padding-right: 7px;
39
+ }
40
+
41
+ #transport-report .total {
42
+ color: #333;
43
+ font-weight: bold;
44
+ font-size: 0.9em;
45
+ }
46
+
47
+ #transport-report table {
48
+ width: 100%;
49
+ font-size: 11px;
50
+ }
51
+
52
+ #transport-report td.data-header {
53
+ font-weight: bold;
54
+ }
55
+
56
+ #transport-report td.data-footer {
57
+ font-weight: bold;
58
+ }
59
+
60
+ #transport-report td.data-ms {
61
+ text-align: right;
62
+ }
63
+
64
+ #transport-report td.data-verb {
65
+ font-size: 0.8em;
66
+ width: 40px;
67
+ vertical-align: top;
68
+ }
69
+
70
+ #transport-report td.data-path {
71
+ width: 300px;
72
+ }
73
+
74
+ #transport-report .duplicate-request {
75
+ color: red;
76
+ }
77
+
78
+ #transport-report .params {
79
+ color: gray;
80
+ }
81
+
82
+ #transport-report div.params {
83
+ display: none;
84
+ }
85
+
86
+ #transport-report a.request-link:hover + div.params {
87
+ display: block;
88
+ }
89
+ </style>
90
+
91
+ <% if report = Songkick::Transport::Reporting.report %>
92
+ <% services = Hash.new { |hash, key| hash[key] = {:total_duration => 0, :requests => []} }%>
93
+ <% report.each do |request|
94
+ service_name = endpoints_to_names[request.endpoint]
95
+ services[service_name][:total_duration] += request.duration.to_i
96
+ services[service_name][:requests] << request
97
+ end %>
98
+
99
+ <a class="switch" onclick="transportReportToggle();">SERVICES (<span class="total"><%= services.values.inject(0) {|m,o| m + o[:total_duration]} %> ms</span>)</a>
100
+
101
+ <div id="transport-report-data" class="service-metrics" style="display:none;">
102
+ <% already = {} %>
103
+ <% services.to_a.sort_by {|_, h| h[:total_duration]}.reverse.each do |service_name, hash| %>
104
+
105
+ <table>
106
+ <tr><td class="data-header" colspan="3"><%= service_name %></td></tr>
107
+
108
+ <% hash[:requests].each do |request| %>
109
+ <tr>
110
+ <td class="data-verb"><%= request.verb.upcase %></td>
111
+ <td class="data-path">
112
+ <% if request.verb == "GET" %>
113
+ <% nice_params = (request.params.any? ? "?#{request.params.to_a.sort_by {|x,_| x.to_s}.map {|k,v| "#{k}=#{v}"}.join("&") }": "no params") %>
114
+ <% path = request.path + nice_params %>
115
+ <% uri = request.endpoint + path %>
116
+ <a class="request-link <%= "duplicate-request" if already[uri] %>" href="http://<%= uri %>">
117
+ <%= request.path.split("?").first[0..40] %>
118
+ </a>
119
+ <div class="params">
120
+ <%= nice_params %>
121
+ </div>
122
+ <% else %>
123
+ <%= request.path.split("?").first %>
124
+ <% end %>
125
+ </td>
126
+ <td class="data-ms"><%= request.duration.to_i %> ms</td>
127
+ </tr>
128
+ <% already[uri] = true %>
129
+ <% end %>
130
+ <tr><td></td><td></td><td class="data-footer data-ms"><%= hash[:total_duration] %> ms</td></tr>
131
+ </table>
132
+
133
+ <% end %>
134
+ </div>
135
+ <% end %>
136
+ </div>
137
+
138
+ <% rescue => e %>
139
+ <!-- Error generating transport report -->
140
+ <% end %>
@@ -12,6 +12,8 @@ module Songkick
12
12
 
13
13
  transport = klass.new
14
14
  transport.user_agent = options[:user_agent]
15
+ transport.user_error_codes =
16
+ options[:user_error_codes] || DEFAULT_USER_ERROR_CODES
15
17
  transport
16
18
  end
17
19
 
@@ -17,6 +17,7 @@ module Songkick
17
17
  def initialize(app, options = {})
18
18
  @app = app
19
19
  @timeout = options[:timeout] || DEFAULT_TIMEOUT
20
+ @user_error_codes = options[:user_error_codes] || DEFAULT_USER_ERROR_CODES
20
21
  end
21
22
 
22
23
  HTTP_VERBS.each do |verb|
@@ -1,21 +1,27 @@
1
+ require 'erb'
2
+
1
3
  module Songkick
2
4
  module Transport
3
-
5
+
4
6
  module Reporting
7
+ def self.start
8
+ Thread.current[:songkick_transport_report] = Report.new
9
+ end
10
+
5
11
  def self.report
6
- Report.new
12
+ Thread.current[:songkick_transport_report]
7
13
  end
8
-
14
+
9
15
  def self.record(request)
10
- return unless report = Thread.current[:songkick_transport_report]
16
+ return unless report
11
17
  report << request
12
18
  end
13
-
19
+
14
20
  def self.log_request(request)
15
21
  return unless Transport.verbose?
16
22
  logger.info(request.to_s)
17
23
  end
18
-
24
+
19
25
  def self.log_response(request)
20
26
  return unless Transport.verbose?
21
27
  response = request.response
@@ -23,33 +29,42 @@ module Songkick
23
29
  logger.info "Response status: #{response.status}, duration: #{duration.ceil}ms"
24
30
  logger.debug "Response data: #{response.data.inspect}"
25
31
  end
26
-
32
+
27
33
  def self.logger
28
34
  Transport.logger
29
35
  end
30
-
36
+
31
37
  class Report
32
38
  include Enumerable
33
39
  extend Forwardable
34
40
  def_delegators :@requests, :each, :first, :last, :length, :size, :[], :<<
35
-
41
+
36
42
  def initialize
37
43
  @requests = []
38
44
  end
39
-
45
+
40
46
  def execute
41
47
  Thread.current[:songkick_transport_report] = self
42
48
  yield
43
49
  ensure
44
50
  Thread.current[:songkick_transport_report] = nil
45
51
  end
46
-
52
+
47
53
  def total_duration
48
54
  inject(0) { |s,r| s + r.duration }
49
55
  end
56
+
57
+ # endpoints_to_names is a hash like:
58
+ #
59
+ # {"dc1-live-service1:9324" => "media-service"}
60
+ def to_html(endpoints_to_names)
61
+ source = File.read(File.expand_path("../html_report.html.erb", __FILE__))
62
+ template = ERB.new(source)
63
+ template.result(binding)
64
+ end
50
65
  end
51
66
  end
52
-
67
+
53
68
  end
54
69
  end
55
70
 
@@ -2,14 +2,13 @@ module Songkick
2
2
  module Transport
3
3
 
4
4
  class Response
5
- def self.process(request, status, headers, body)
5
+ def self.process(request, status, headers, body, user_error_codes=409)
6
6
  case status.to_i
7
7
  when 200 then OK.new(status, headers, body)
8
8
  when 201 then Created.new(status, headers, body)
9
9
  when 204 then NoContent.new(status, headers, body)
10
- when 409 then UserError.new(status, headers, body)
10
+ when *user_error_codes then UserError.new(status, headers, body)
11
11
  else
12
- Transport.logger.warn "Received error code: #{status} -- #{request}"
13
12
  raise HttpError.new(request, status, headers, body)
14
13
  end
15
14
  rescue Yajl::ParseError