dyndrop 0.0.1 → 0.0.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.
- data/LICENSE +1277 -0
- data/lib/dyndrop.rb +2 -0
- data/lib/dyndrop/auth_token.rb +63 -0
- data/lib/dyndrop/baseclient.rb +178 -0
- data/lib/dyndrop/client.rb +18 -0
- data/lib/dyndrop/errors.rb +152 -0
- data/lib/dyndrop/rest_client.rb +299 -0
- data/lib/dyndrop/trace_helpers.rb +40 -0
- data/lib/dyndrop/v1/base.rb +28 -0
- data/lib/dyndrop/v1/client.rb +114 -0
- data/lib/dyndrop/v1/model_magic.rb +129 -0
- data/lib/dyndrop/validator.rb +39 -0
- data/lib/dyndrop/version.rb +1 -1
- metadata +13 -1
data/lib/dyndrop.rb
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
module Dyndrop
|
2
|
+
class AuthToken
|
3
|
+
class << self
|
4
|
+
def from_token_info(token_info)
|
5
|
+
new(
|
6
|
+
token_info[:token],
|
7
|
+
token_info[:refresh_token]
|
8
|
+
)
|
9
|
+
end
|
10
|
+
|
11
|
+
def from_hash(hash)
|
12
|
+
new(
|
13
|
+
hash[:token],
|
14
|
+
hash[:refresh_token]
|
15
|
+
)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def initialize(auth_header, refresh_token = nil)
|
20
|
+
@auth_header = auth_header
|
21
|
+
@refresh_token = refresh_token
|
22
|
+
end
|
23
|
+
|
24
|
+
attr_accessor :auth_header
|
25
|
+
attr_reader :refresh_token
|
26
|
+
|
27
|
+
def to_hash
|
28
|
+
{
|
29
|
+
:token => auth_header,
|
30
|
+
:refresh_token => @refresh_token
|
31
|
+
}
|
32
|
+
end
|
33
|
+
|
34
|
+
JSON_HASH = /\{.*?\}/.freeze
|
35
|
+
|
36
|
+
# TODO: rename to #data
|
37
|
+
def token_data
|
38
|
+
return @token_data if @token_data
|
39
|
+
return {} unless @auth_header
|
40
|
+
|
41
|
+
json_hashes = Base64.decode64(@auth_header.split(" ", 2).last)
|
42
|
+
data_json = json_hashes.sub(JSON_HASH, "")[JSON_HASH]
|
43
|
+
return {} unless data_json
|
44
|
+
|
45
|
+
@token_data = MultiJson.load data_json, :symbolize_keys => true
|
46
|
+
rescue MultiJson::DecodeError
|
47
|
+
{}
|
48
|
+
end
|
49
|
+
|
50
|
+
def auth_header=(auth_header)
|
51
|
+
@token_data = nil
|
52
|
+
@auth_header = auth_header
|
53
|
+
end
|
54
|
+
|
55
|
+
def expiration
|
56
|
+
Time.at(token_data[:exp])
|
57
|
+
end
|
58
|
+
|
59
|
+
def expires_soon?
|
60
|
+
(expiration.to_i - Time.now.to_i) < 60
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,178 @@
|
|
1
|
+
require "dyndrop/trace_helpers"
|
2
|
+
require "net/https"
|
3
|
+
require "net/http/post/multipart"
|
4
|
+
require "multi_json"
|
5
|
+
require "fileutils"
|
6
|
+
require "forwardable"
|
7
|
+
|
8
|
+
module Dyndrop
|
9
|
+
class BaseClient # :nodoc:
|
10
|
+
extend Forwardable
|
11
|
+
|
12
|
+
attr_reader :rest_client
|
13
|
+
|
14
|
+
def_delegators :rest_client, :target, :target=, :token,
|
15
|
+
:proxy, :proxy=, :trace, :backtrace, :backtrace=,
|
16
|
+
:log, :log=
|
17
|
+
|
18
|
+
def initialize(target = "https://api.dyndrop.com", token = nil)
|
19
|
+
@rest_client = Dyndrop::RestClient.new(target, token)
|
20
|
+
self.trace = false
|
21
|
+
self.backtrace = false
|
22
|
+
self.log = false
|
23
|
+
end
|
24
|
+
|
25
|
+
def token=(token)
|
26
|
+
if token.is_a?(String)
|
27
|
+
token = Dyndrop::AuthToken.new(token)
|
28
|
+
end
|
29
|
+
|
30
|
+
@rest_client.token = token
|
31
|
+
end
|
32
|
+
|
33
|
+
def trace=(trace)
|
34
|
+
@rest_client.trace = trace
|
35
|
+
end
|
36
|
+
|
37
|
+
# Cloud metadata
|
38
|
+
def info
|
39
|
+
get("info", :accept => :json)
|
40
|
+
end
|
41
|
+
|
42
|
+
def get(*args)
|
43
|
+
request("GET", *args)
|
44
|
+
end
|
45
|
+
|
46
|
+
def delete(*args)
|
47
|
+
request("DELETE", *args)
|
48
|
+
end
|
49
|
+
|
50
|
+
def post(*args)
|
51
|
+
request("POST", *args)
|
52
|
+
end
|
53
|
+
|
54
|
+
def put(*args)
|
55
|
+
request("PUT", *args)
|
56
|
+
end
|
57
|
+
|
58
|
+
def request(method, *args)
|
59
|
+
if needs_token_refresh?
|
60
|
+
token.auth_header = nil
|
61
|
+
refresh_token!
|
62
|
+
end
|
63
|
+
|
64
|
+
path, options = normalize_arguments(args)
|
65
|
+
request, response = request_raw(method, path, options)
|
66
|
+
handle_response(response, options, request)
|
67
|
+
end
|
68
|
+
|
69
|
+
def request_raw(method, path, options)
|
70
|
+
@rest_client.request(method, path, options)
|
71
|
+
end
|
72
|
+
|
73
|
+
def stream_url(url, &blk)
|
74
|
+
uri = URI.parse(url)
|
75
|
+
|
76
|
+
Net::HTTP.start(uri.host, uri.port) do |http|
|
77
|
+
http.read_timeout = 5
|
78
|
+
|
79
|
+
req = Net::HTTP::Get.new(uri.request_uri)
|
80
|
+
req["Authorization"] = token.auth_header if token
|
81
|
+
|
82
|
+
http.request(req) do |response|
|
83
|
+
case response
|
84
|
+
when Net::HTTPOK
|
85
|
+
response.read_body(&blk)
|
86
|
+
when Net::HTTPNotFound
|
87
|
+
raise Dyndrop::NotFound.new(response.body, 404)
|
88
|
+
when Net::HTTPForbidden
|
89
|
+
raise Dyndrop::Denied.new(response.body, 403)
|
90
|
+
else
|
91
|
+
raise Dyndrop::BadResponse.new(response.body, response.code)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
private
|
98
|
+
|
99
|
+
def needs_token_refresh?
|
100
|
+
token && token.auth_header && token.refresh_token && \
|
101
|
+
token.expires_soon?
|
102
|
+
end
|
103
|
+
|
104
|
+
def status_is_successful?(code)
|
105
|
+
(code >= 200) && (code < 400)
|
106
|
+
end
|
107
|
+
|
108
|
+
def handle_response(response, options, request)
|
109
|
+
if status_is_successful?(response[:status].to_i)
|
110
|
+
handle_successful_response(response, options)
|
111
|
+
else
|
112
|
+
handle_error_response(response, request)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def handle_successful_response(response, options)
|
117
|
+
if options[:return_response]
|
118
|
+
response
|
119
|
+
elsif options[:accept] == :json
|
120
|
+
parse_json(response[:body])
|
121
|
+
else
|
122
|
+
response[:body]
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def handle_error_response(response, request)
|
127
|
+
body_json = parse_json(response[:body])
|
128
|
+
body_code = body_json && body_json[:code]
|
129
|
+
code = body_code || response[:status].to_i
|
130
|
+
|
131
|
+
if body_code
|
132
|
+
error_class = Dyndrop::APIError.error_classes[body_code] || Dyndrop::APIError
|
133
|
+
raise error_class.new(body_json[:description], body_code, request, response)
|
134
|
+
end
|
135
|
+
|
136
|
+
case code
|
137
|
+
when 404
|
138
|
+
raise Dyndrop::NotFound.new(nil, code, request, response)
|
139
|
+
when 403
|
140
|
+
raise Dyndrop::Denied.new(nil, code, request, response)
|
141
|
+
else
|
142
|
+
raise Dyndrop::BadResponse.new(nil, code, request, response)
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
def normalize_arguments(args)
|
147
|
+
if args.last.is_a?(Hash)
|
148
|
+
options = args.pop
|
149
|
+
else
|
150
|
+
options = {}
|
151
|
+
end
|
152
|
+
|
153
|
+
[normalize_path(args), options]
|
154
|
+
end
|
155
|
+
|
156
|
+
URI_ENCODING_PATTERN = Regexp.new("[^#{URI::PATTERN::UNRESERVED}]")
|
157
|
+
|
158
|
+
def normalize_path(segments)
|
159
|
+
if segments.size == 1 && segments.first =~ /^\//
|
160
|
+
segments.first
|
161
|
+
else
|
162
|
+
segments.flatten.collect { |x|
|
163
|
+
URI.encode(x.to_s, URI_ENCODING_PATTERN)
|
164
|
+
}.join("/")
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
def parse_json(x)
|
169
|
+
if x.empty?
|
170
|
+
raise MultiJson::DecodeError.new("Empty JSON string", [], "")
|
171
|
+
else
|
172
|
+
MultiJson.load(x, :symbolize_keys => true)
|
173
|
+
end
|
174
|
+
rescue MultiJson::DecodeError
|
175
|
+
nil
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require "dyndrop/baseclient"
|
2
|
+
require "dyndrop/rest_client"
|
3
|
+
require "dyndrop/auth_token"
|
4
|
+
|
5
|
+
require "dyndrop/v1/base"
|
6
|
+
require "dyndrop/v1/client"
|
7
|
+
|
8
|
+
module Dyndrop
|
9
|
+
class Client < BaseClient
|
10
|
+
def self.new(*args)
|
11
|
+
target, _ = args
|
12
|
+
|
13
|
+
base = super(target)
|
14
|
+
|
15
|
+
Dyndrop::V1::Client.new(*args)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,152 @@
|
|
1
|
+
require "net/https"
|
2
|
+
require "multi_json"
|
3
|
+
require "yaml"
|
4
|
+
|
5
|
+
module Dyndrop
|
6
|
+
# Base class for Dyndrop errors (not from the server).
|
7
|
+
class Error < RuntimeError
|
8
|
+
end
|
9
|
+
|
10
|
+
class Deprecated < Error
|
11
|
+
end
|
12
|
+
|
13
|
+
class Mismatch < Error
|
14
|
+
def initialize(expected, got)
|
15
|
+
@expected = expected
|
16
|
+
@got = got
|
17
|
+
end
|
18
|
+
|
19
|
+
def to_s
|
20
|
+
"Invalid value type; expected #{@expected.inspect}, got #{@got.inspect}"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class InvalidTarget < Error
|
25
|
+
attr_reader :target
|
26
|
+
|
27
|
+
def initialize(target)
|
28
|
+
@target = target
|
29
|
+
end
|
30
|
+
|
31
|
+
def to_s
|
32
|
+
"Invalid target URI: #{@target}"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
class TargetRefused < Error
|
37
|
+
# Error message.
|
38
|
+
attr_reader :message
|
39
|
+
|
40
|
+
# Message varies as this represents various network errors.
|
41
|
+
def initialize(message)
|
42
|
+
@message = message
|
43
|
+
end
|
44
|
+
|
45
|
+
# Exception message.
|
46
|
+
def to_s
|
47
|
+
"target refused connection (#@message)"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
class Timeout < Timeout::Error
|
52
|
+
attr_reader :method, :uri, :parent
|
53
|
+
|
54
|
+
def initialize(method, uri, parent = nil)
|
55
|
+
@method = method
|
56
|
+
@uri = uri
|
57
|
+
@parent = parent
|
58
|
+
super(to_s)
|
59
|
+
end
|
60
|
+
|
61
|
+
def to_s
|
62
|
+
"#{method} #{uri} timed out"
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# Exception representing errors returned by the API.
|
67
|
+
class APIError < RuntimeError
|
68
|
+
include TraceHelpers
|
69
|
+
|
70
|
+
class << self
|
71
|
+
def error_classes
|
72
|
+
@error_classes ||= {}
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
attr_reader :error_code, :description, :request, :response
|
77
|
+
|
78
|
+
# Create an APIError with a given request and response.
|
79
|
+
def initialize(description = nil, error_code = nil, request = nil, response = nil)
|
80
|
+
@response = response
|
81
|
+
@request = request
|
82
|
+
@error_code = error_code || (response ? response[:status] : nil)
|
83
|
+
@description = description || parse_description
|
84
|
+
end
|
85
|
+
|
86
|
+
# Exception message.
|
87
|
+
def to_s
|
88
|
+
"#{error_code}: #{description}"
|
89
|
+
end
|
90
|
+
|
91
|
+
def request_trace
|
92
|
+
super(request)
|
93
|
+
end
|
94
|
+
|
95
|
+
def response_trace
|
96
|
+
super(response)
|
97
|
+
end
|
98
|
+
|
99
|
+
private
|
100
|
+
|
101
|
+
def parse_description
|
102
|
+
return unless response
|
103
|
+
|
104
|
+
parse_json(response[:body])[:description]
|
105
|
+
rescue MultiJson::DecodeError
|
106
|
+
response[:body]
|
107
|
+
end
|
108
|
+
|
109
|
+
def parse_json(x)
|
110
|
+
if x.empty?
|
111
|
+
raise MultiJson::DecodeError.new("Empty JSON string", [], "")
|
112
|
+
else
|
113
|
+
MultiJson.load(x, :symbolize_keys => true)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
class NotFound < APIError
|
119
|
+
end
|
120
|
+
|
121
|
+
class Denied < APIError
|
122
|
+
end
|
123
|
+
|
124
|
+
class BadResponse < APIError
|
125
|
+
end
|
126
|
+
|
127
|
+
class UAAError < APIError
|
128
|
+
end
|
129
|
+
|
130
|
+
def self.define_error(class_name, code)
|
131
|
+
base =
|
132
|
+
case class_name
|
133
|
+
when /NotFound$/
|
134
|
+
NotFound
|
135
|
+
else
|
136
|
+
APIError
|
137
|
+
end
|
138
|
+
|
139
|
+
klass =
|
140
|
+
if const_defined?(class_name)
|
141
|
+
const_get(class_name)
|
142
|
+
else
|
143
|
+
Class.new(base)
|
144
|
+
end
|
145
|
+
|
146
|
+
APIError.error_classes[code] = klass
|
147
|
+
|
148
|
+
unless const_defined?(class_name)
|
149
|
+
const_set(class_name, klass)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
@@ -0,0 +1,299 @@
|
|
1
|
+
require "dyndrop/trace_helpers"
|
2
|
+
require "net/https"
|
3
|
+
require "net/http/post/multipart"
|
4
|
+
require "multi_json"
|
5
|
+
require "fileutils"
|
6
|
+
|
7
|
+
module Dyndrop
|
8
|
+
class RestClient
|
9
|
+
include Dyndrop::TraceHelpers
|
10
|
+
|
11
|
+
LOG_LENGTH = 10
|
12
|
+
|
13
|
+
HTTP_METHODS = {
|
14
|
+
"GET" => Net::HTTP::Get,
|
15
|
+
"PUT" => Net::HTTP::Put,
|
16
|
+
"POST" => Net::HTTP::Post,
|
17
|
+
"DELETE" => Net::HTTP::Delete,
|
18
|
+
"HEAD" => Net::HTTP::Head,
|
19
|
+
}
|
20
|
+
|
21
|
+
DEFAULT_OPTIONS = {
|
22
|
+
:follow_redirects => true
|
23
|
+
}
|
24
|
+
|
25
|
+
attr_reader :target
|
26
|
+
|
27
|
+
attr_accessor :trace, :backtrace, :log, :request_id, :token, :target, :proxy
|
28
|
+
|
29
|
+
def initialize(target, token = nil)
|
30
|
+
@target = target
|
31
|
+
@token = token
|
32
|
+
@trace = false
|
33
|
+
@backtrace = false
|
34
|
+
@log = false
|
35
|
+
end
|
36
|
+
|
37
|
+
def request(method, path, options = {})
|
38
|
+
request_uri(method, construct_url(path), DEFAULT_OPTIONS.merge(options))
|
39
|
+
end
|
40
|
+
|
41
|
+
def generate_headers(payload, options)
|
42
|
+
headers = {}
|
43
|
+
|
44
|
+
if payload.is_a?(String)
|
45
|
+
headers["Content-Length"] = payload.size
|
46
|
+
elsif !payload
|
47
|
+
headers["Content-Length"] = 0
|
48
|
+
end
|
49
|
+
|
50
|
+
headers["X-Request-Id"] = @request_id if @request_id
|
51
|
+
headers["Authorization"] = @token.auth_header if @token
|
52
|
+
headers["Proxy-User"] = @proxy if @proxy
|
53
|
+
|
54
|
+
if accept_type = mimetype(options[:accept])
|
55
|
+
headers["Accept"] = accept_type
|
56
|
+
end
|
57
|
+
|
58
|
+
if content_type = mimetype(options[:content])
|
59
|
+
headers["Content-Type"] = content_type
|
60
|
+
end
|
61
|
+
|
62
|
+
headers.merge!(options[:headers]) if options[:headers]
|
63
|
+
headers
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
def request_uri(method, uri, options = {})
|
69
|
+
uri = URI.parse(uri)
|
70
|
+
|
71
|
+
unless uri.is_a?(URI::HTTP)
|
72
|
+
raise InvalidTarget.new(@target)
|
73
|
+
end
|
74
|
+
|
75
|
+
# keep original options in case there's a redirect to follow
|
76
|
+
original_options = options.dup
|
77
|
+
payload = options[:payload]
|
78
|
+
|
79
|
+
if params = options[:params]
|
80
|
+
if uri.query
|
81
|
+
uri.query += "&" + encode_params(params)
|
82
|
+
else
|
83
|
+
uri.query = encode_params(params)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
unless payload.is_a?(String)
|
88
|
+
case options[:content]
|
89
|
+
when :json
|
90
|
+
payload = MultiJson.dump(payload)
|
91
|
+
when :form
|
92
|
+
payload = encode_params(payload)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
method_class = get_method_class(method)
|
97
|
+
if payload.is_a?(Hash)
|
98
|
+
multipart = method_class.const_get(:Multipart)
|
99
|
+
request = multipart.new(uri.request_uri, payload)
|
100
|
+
else
|
101
|
+
request = method_class.new(uri.request_uri)
|
102
|
+
request.body = payload if payload
|
103
|
+
end
|
104
|
+
|
105
|
+
headers = generate_headers(payload, options)
|
106
|
+
|
107
|
+
request_hash = {
|
108
|
+
:url => uri.to_s,
|
109
|
+
:method => method,
|
110
|
+
:headers => headers,
|
111
|
+
:body => payload
|
112
|
+
}
|
113
|
+
|
114
|
+
print_request(request_hash) if @trace
|
115
|
+
|
116
|
+
add_headers(request, headers)
|
117
|
+
|
118
|
+
# TODO: test http proxies
|
119
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
120
|
+
|
121
|
+
# TODO remove this when staging returns streaming responses
|
122
|
+
http.read_timeout = 300
|
123
|
+
|
124
|
+
if uri.is_a?(URI::HTTPS)
|
125
|
+
http.use_ssl = true
|
126
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
127
|
+
end
|
128
|
+
|
129
|
+
before = Time.now
|
130
|
+
http.start do
|
131
|
+
response = http.request(request)
|
132
|
+
time = Time.now - before
|
133
|
+
|
134
|
+
response_hash = {
|
135
|
+
:headers => sane_headers(response),
|
136
|
+
:status => response.code,
|
137
|
+
:body => response.body
|
138
|
+
}
|
139
|
+
|
140
|
+
print_response(response_hash) if @trace
|
141
|
+
print_backtrace(caller) if @trace
|
142
|
+
|
143
|
+
log_request(time, request, response)
|
144
|
+
|
145
|
+
if response.is_a?(Net::HTTPRedirection) && options[:follow_redirects]
|
146
|
+
request_uri("GET", response["location"], original_options)
|
147
|
+
else
|
148
|
+
return request_hash, response_hash
|
149
|
+
end
|
150
|
+
end
|
151
|
+
rescue ::Timeout::Error => e
|
152
|
+
raise Timeout.new(method, uri, e)
|
153
|
+
rescue SocketError, Errno::ECONNREFUSED => e
|
154
|
+
raise TargetRefused, e.message
|
155
|
+
rescue URI::InvalidURIError
|
156
|
+
raise InvalidTarget.new(@target)
|
157
|
+
end
|
158
|
+
|
159
|
+
def construct_url(path)
|
160
|
+
uri = URI.parse(path)
|
161
|
+
return path if uri.scheme
|
162
|
+
|
163
|
+
path = "/#{path}" unless path[0] == ?\/
|
164
|
+
target + path
|
165
|
+
end
|
166
|
+
|
167
|
+
def get_method_class(method_string)
|
168
|
+
HTTP_METHODS[method_string.upcase]
|
169
|
+
end
|
170
|
+
|
171
|
+
def add_headers(request, headers)
|
172
|
+
headers.each { |key, value| request[key] = value }
|
173
|
+
end
|
174
|
+
|
175
|
+
def mimetype(content)
|
176
|
+
case content
|
177
|
+
when String
|
178
|
+
content
|
179
|
+
when :json
|
180
|
+
"application/json"
|
181
|
+
when :form
|
182
|
+
"application/x-www-form-urlencoded"
|
183
|
+
when nil
|
184
|
+
nil
|
185
|
+
# return request headers (not really Accept)
|
186
|
+
else
|
187
|
+
raise Dyndrop::Error, "Unknown mimetype '#{content.inspect}'"
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
def encode_params(hash, escape = true)
|
192
|
+
hash.keys.map do |k|
|
193
|
+
v = hash[k]
|
194
|
+
v = MultiJson.dump(v) if v.is_a?(Hash)
|
195
|
+
v = URI.escape(v.to_s, /[^#{URI::PATTERN::UNRESERVED}]/) if escape
|
196
|
+
"#{k}=#{v}"
|
197
|
+
end.join("&")
|
198
|
+
end
|
199
|
+
|
200
|
+
def log_data(time, request, response)
|
201
|
+
{ :time => time,
|
202
|
+
:request => {
|
203
|
+
:method => request.method,
|
204
|
+
:url => request.path,
|
205
|
+
:headers => sane_headers(request)
|
206
|
+
},
|
207
|
+
:response => {
|
208
|
+
:code => response.code,
|
209
|
+
:headers => sane_headers(response)
|
210
|
+
}
|
211
|
+
}
|
212
|
+
end
|
213
|
+
|
214
|
+
def log_line(io, data)
|
215
|
+
io.printf(
|
216
|
+
"[%s] %0.3fs %6s -> %d %s\n",
|
217
|
+
Time.now.strftime("%F %T"),
|
218
|
+
data[:time],
|
219
|
+
data[:request][:method].to_s.upcase,
|
220
|
+
data[:response][:code],
|
221
|
+
data[:request][:url])
|
222
|
+
end
|
223
|
+
|
224
|
+
def log_request(time, request, response)
|
225
|
+
return unless @log
|
226
|
+
|
227
|
+
data = log_data(time, request, response)
|
228
|
+
|
229
|
+
case @log
|
230
|
+
when IO
|
231
|
+
log_line(@log, data)
|
232
|
+
return
|
233
|
+
when String
|
234
|
+
if File.exists?(@log)
|
235
|
+
log = File.readlines(@log).last(LOG_LENGTH - 1)
|
236
|
+
elsif !File.exists?(File.dirname(@log))
|
237
|
+
FileUtils.mkdir_p(File.dirname(@log))
|
238
|
+
end
|
239
|
+
|
240
|
+
File.open(@log, "w") do |io|
|
241
|
+
log.each { |l| io.print l } if log
|
242
|
+
log_line(io, data)
|
243
|
+
end
|
244
|
+
|
245
|
+
return
|
246
|
+
end
|
247
|
+
|
248
|
+
if @log.respond_to?(:call)
|
249
|
+
@log.call(data)
|
250
|
+
return
|
251
|
+
end
|
252
|
+
|
253
|
+
if @log.respond_to?(:<<)
|
254
|
+
@log << data
|
255
|
+
return
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
def print_request(request)
|
260
|
+
$stderr.puts ">>>"
|
261
|
+
$stderr.puts request_trace(request)
|
262
|
+
end
|
263
|
+
|
264
|
+
def print_response(response)
|
265
|
+
$stderr.puts response_trace(response)
|
266
|
+
$stderr.puts "<<<"
|
267
|
+
end
|
268
|
+
|
269
|
+
def print_backtrace(locs)
|
270
|
+
return unless @backtrace
|
271
|
+
|
272
|
+
interesting_locs = locs.drop_while { |loc|
|
273
|
+
loc =~ /\/(dyndrop\/|restclient\/|net\/http)/
|
274
|
+
}
|
275
|
+
|
276
|
+
$stderr.puts "--- backtrace:"
|
277
|
+
|
278
|
+
$stderr.puts "... (boring)" unless locs == interesting_locs
|
279
|
+
|
280
|
+
trimmed_locs = interesting_locs[0..5]
|
281
|
+
|
282
|
+
trimmed_locs.each do |loc|
|
283
|
+
$stderr.puts "=== #{loc}"
|
284
|
+
end
|
285
|
+
|
286
|
+
$stderr.puts "... (trimmed)" unless trimmed_locs == interesting_locs
|
287
|
+
end
|
288
|
+
|
289
|
+
def sane_headers(obj)
|
290
|
+
hds = {}
|
291
|
+
|
292
|
+
obj.each_header do |k, v|
|
293
|
+
hds[k] = v
|
294
|
+
end
|
295
|
+
|
296
|
+
hds
|
297
|
+
end
|
298
|
+
end
|
299
|
+
end
|