grifter 0.2.0 → 0.3.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: 272206b2927fd805a4d0eebfaeb12aa9bb5544b8
4
- data.tar.gz: 811ddf5f1872f2c8a82a0ccf7fb5082d7d5537ac
3
+ metadata.gz: b3c5acde96e3f1e8b052b55aa73c4c26e955d50e
4
+ data.tar.gz: 855578ed7e193ba868a043e3cc8c99b93451bbb4
5
5
  SHA512:
6
- metadata.gz: e687565405a56497e50a449267895dc594006df2ae3679034003fc24132937fab025a76f3204e1707af89a8456c661b50a9766bec55d50b02343221262874759
7
- data.tar.gz: 4aaf0eda705b509714ffbcb3b02d2d06df44dbf3645ebb412ccc6bb20dcef047bfac5693815bedfa4b26421689480cc432080e150931896031bf9a146418914f
6
+ metadata.gz: 49c8a5c1f5529e7f144921987a7032b26dff13e6b482c3a5095b458e940e7c6eb1085fd0ee120ff04de6a7479e93528edb760c635bc91cbcf2f036adbc9dba80
7
+ data.tar.gz: 1aad905c89ba51705b0aa203e0e5ad7fe3e93f324cea6c79bbdeb15604ee7e9e9dea8aa0dcba7dec1e6f11211f41d0eb66312fcc0b969db102db31cdb2e85583
data/Gemfile CHANGED
@@ -2,7 +2,9 @@ source :rubygems
2
2
 
3
3
  #gemspec
4
4
 
5
+ gem 'faraday'
5
6
  gem 'json'
7
+ gem 'typhoeus'
6
8
 
7
9
  group :development do
8
10
  gem 'rspec'
data/Gemfile.lock CHANGED
@@ -6,8 +6,12 @@ GEM
6
6
  builder (3.2.2)
7
7
  coderay (1.0.9)
8
8
  diff-lcs (1.2.4)
9
+ ethon (0.6.1)
10
+ ffi (>= 1.3.0)
11
+ mime-types (~> 1.18)
9
12
  faraday (0.8.8)
10
13
  multipart-post (~> 1.2.0)
14
+ ffi (1.9.3)
11
15
  git (1.2.5)
12
16
  github_api (0.10.1)
13
17
  addressable
@@ -32,6 +36,7 @@ GEM
32
36
  jwt (0.1.8)
33
37
  multi_json (>= 1.5)
34
38
  method_source (0.8.2)
39
+ mime-types (1.25.1)
35
40
  multi_json (1.7.8)
36
41
  multi_xml (0.5.4)
37
42
  multipart-post (1.2.0)
@@ -60,13 +65,17 @@ GEM
60
65
  diff-lcs (>= 1.1.3, < 2.0)
61
66
  rspec-mocks (2.14.2)
62
67
  slop (3.4.6)
68
+ typhoeus (0.6.6)
69
+ ethon (~> 0.6.1)
63
70
 
64
71
  PLATFORMS
65
72
  ruby
66
73
 
67
74
  DEPENDENCIES
68
75
  awesome_print
76
+ faraday
69
77
  jeweler
70
78
  json
71
79
  pry
72
80
  rspec
81
+ typhoeus
data/grifter.gemspec CHANGED
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = "grifter"
8
- s.version = "0.2.0"
8
+ s.version = "0.3.0"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Robert Schultheis"]
12
- s.date = "2013-11-05"
12
+ s.date = "2013-12-04"
13
13
  s.description = "convention based approach to interfacing with an HTTP JSON API."
14
14
  s.email = "rob@knewton.com"
15
15
  s.executables = ["grift"]
@@ -28,6 +28,7 @@ Gem::Specification.new do |s|
28
28
  "lib/grifter/configuration.rb",
29
29
  "lib/grifter/helpers.rb",
30
30
  "lib/grifter/http_service.rb",
31
+ "lib/grifter/instrumentation.rb",
31
32
  "lib/grifter/json_helpers.rb",
32
33
  "lib/grifter/log.rb"
33
34
  ]
@@ -41,20 +42,26 @@ Gem::Specification.new do |s|
41
42
  s.specification_version = 4
42
43
 
43
44
  if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
45
+ s.add_runtime_dependency(%q<faraday>, [">= 0"])
44
46
  s.add_runtime_dependency(%q<json>, [">= 0"])
47
+ s.add_runtime_dependency(%q<typhoeus>, [">= 0"])
45
48
  s.add_development_dependency(%q<rspec>, [">= 0"])
46
49
  s.add_development_dependency(%q<pry>, [">= 0"])
47
50
  s.add_development_dependency(%q<awesome_print>, [">= 0"])
48
51
  s.add_development_dependency(%q<jeweler>, [">= 0"])
49
52
  else
53
+ s.add_dependency(%q<faraday>, [">= 0"])
50
54
  s.add_dependency(%q<json>, [">= 0"])
55
+ s.add_dependency(%q<typhoeus>, [">= 0"])
51
56
  s.add_dependency(%q<rspec>, [">= 0"])
52
57
  s.add_dependency(%q<pry>, [">= 0"])
53
58
  s.add_dependency(%q<awesome_print>, [">= 0"])
54
59
  s.add_dependency(%q<jeweler>, [">= 0"])
55
60
  end
56
61
  else
62
+ s.add_dependency(%q<faraday>, [">= 0"])
57
63
  s.add_dependency(%q<json>, [">= 0"])
64
+ s.add_dependency(%q<typhoeus>, [">= 0"])
58
65
  s.add_dependency(%q<rspec>, [">= 0"])
59
66
  s.add_dependency(%q<pry>, [">= 0"])
60
67
  s.add_dependency(%q<awesome_print>, [">= 0"])
data/lib/grifter.rb CHANGED
@@ -2,9 +2,11 @@ require_relative 'grifter/http_service'
2
2
  require_relative 'grifter/configuration'
3
3
  require_relative 'grifter/log'
4
4
  require_relative 'grifter/blankslate'
5
+ require_relative 'grifter/instrumentation'
5
6
 
6
7
  class Grifter
7
8
  include Grifter::Configuration
9
+ include Grifter::Instrumentation
8
10
 
9
11
  DefaultConfigOptions = {
10
12
  #TODO: service_config: nil,
@@ -43,6 +45,8 @@ class Grifter
43
45
  if @config[:authenticate]
44
46
  self.grifter_authenticate_do
45
47
  end
48
+
49
+ start_instrumentation
46
50
  end
47
51
 
48
52
  attr_reader :services
@@ -23,6 +23,7 @@ class Grifter
23
23
  raise GrifterConfigurationError.new "url is not a proper aboslute URL: #{url}"
24
24
  end
25
25
  parsed = URI.parse url
26
+ #make the url faraday is configured with
26
27
  {
27
28
  :hostname => parsed.host,
28
29
  :port => parsed.port,
@@ -36,7 +37,7 @@ class Grifter
36
37
  config_file: ENV['GRIFTER_CONFIG_FILE'] ? ENV['GRIFTER_CONFIG_FILE'] : 'grifter.yml',
37
38
  environment: ENV['GRIFTER_ENVIRONMENT'],
38
39
  }.merge(options)
39
- Log.debug "Loading config file '#{options[:config_file]}'"
40
+ Grifter::Log.debug "Loading config file '#{options[:config_file]}'"
40
41
  unless File.exist?(options[:config_file])
41
42
  raise GrifterConfigFileMissing.new "No such config file: '#{options[:config_file]}'"
42
43
  end
@@ -68,7 +69,6 @@ class Grifter
68
69
  base_uri: '',
69
70
  port: (service_config[:ssl] == true ? 443 : 80),
70
71
  }.merge(service_config))
71
-
72
72
  end
73
73
 
74
74
  #merge any environment overrides into the service block
@@ -95,6 +95,12 @@ class Grifter
95
95
  end
96
96
  end
97
97
 
98
+ #add in the faraday url as the final thing after figuring everything else out
99
+ config[:services].each_pair do |service_name, service_config|
100
+ #set the url we'll use to start faraday
101
+ service_config[:faraday_url] = "#{service_config[:ssl] ? 'https':'http'}://#{service_config[:hostname]}:#{service_config[:port].to_s}"
102
+ end
103
+
98
104
  #join the grift globs with the relative path to config file
99
105
  if config[:grift_globs] && options[:config_file]
100
106
  config_file_dir = File.dirname options[:config_file]
@@ -1,5 +1,6 @@
1
- require 'net/http'
2
- require 'openssl'
1
+ require 'faraday'
2
+ require 'typhoeus'
3
+ require 'typhoeus/adapters/faraday' #https://github.com/typhoeus/typhoeus/issues/226#issuecomment-9919517
3
4
 
4
5
  require_relative 'json_helpers'
5
6
  require_relative 'log'
@@ -7,6 +8,7 @@ require_relative 'log'
7
8
  class Grifter
8
9
  class HTTPService
9
10
 
11
+ include Grifter::Log
10
12
  include Grifter::JsonHelpers
11
13
 
12
14
  def initialize config
@@ -14,30 +16,50 @@ class Grifter
14
16
  @config = config
15
17
  @name = config[:name]
16
18
  @base_uri = config[:base_uri]
17
-
18
- Log.debug "Configuring service '#{@name}' with:\n\t#{@config.inspect}"
19
-
20
- @http = Net::HTTP.new(@config[:hostname], @config[:port])
21
- @http.use_ssl = @config[:ssl]
22
- @http.verify_mode = OpenSSL::SSL::VERIFY_NONE if @config[:ignore_ssl_cert]
23
- @http.read_timeout = @config[:timeout] if @config[:timeout]
19
+ @log_headers = config.fetch(:log_headers, true)
20
+ @log_bodies = config.fetch(:log_bodies, true)
21
+
22
+ logger.debug "Configuring service '#{@name}' with:\n\t#{@config.inspect}"
23
+
24
+ #@conn = Net::HTTP.new(@config[:hostname], @config[:port])
25
+ #@conn.use_ssl = @config[:ssl]
26
+ @conn = Faraday.new @config[:faraday_url] do |conn_builder|
27
+ #do our own logging
28
+ #conn_builder.response logger: logger
29
+ #conn_builder.adapter Faraday.default_adapter # make requests with Net::HTTP
30
+ conn_builder.adapter :typhoeus
31
+ conn_builder.ssl[:verify] = false if @config[:ignore_ssl_cert]
32
+
33
+ #this nonsense dont work?! https://github.com/lostisland/faraday_middleware/issues/76
34
+ #conn_builder.use :instrumentation
35
+ end
24
36
 
25
37
  @headers = {
26
38
  'accept' => 'application/json',
27
39
  'content-type' => 'application/json',
28
40
  }
29
41
  if @config[:default_headers]
30
- Log.debug "Default headers configured: " + @config[:default_headers].inspect
42
+ logger.debug "Default headers configured: " + @config[:default_headers].inspect
31
43
  @config[:default_headers].each_pair do |k, v|
32
44
  @headers[k.to_s] = v.to_s
33
45
  end
34
46
  end
47
+ @default_timeout = @config.fetch(:timeout, 10)
48
+ logger.info "Initialized grifter service '#{@name}'"
49
+ end
50
+
51
+ def stubs &blk
52
+ stubs = Faraday::Adapter::Test::Stubs.new
53
+ @conn = Faraday.new @config[:faraday_url] do |conn_builder|
54
+ conn_builder.adapter :test, stubs
55
+ end
56
+ stubs
35
57
  end
36
58
 
37
59
  #allow stubbing http if we are testing
38
60
  attr_reader :http if defined?(RSpec)
39
61
 
40
- attr_reader :headers, :name, :config
62
+ attr_reader :headers, :name, :config, :conn
41
63
 
42
64
  #this is useful for testing apis, and other times
43
65
  #you want to interrogate the http details of a response
@@ -49,34 +71,74 @@ class Grifter
49
71
  # options can include:
50
72
  # - :timeout, which specifies num secs the request should timeout in
51
73
  # (this turns out to be kind of annoying to implement)
52
- def do_request req, options={}
53
- Log.debug RequestLogSeperator
54
- Log.debug "#{req.class} #{req.path}"
55
- Log.debug "HEADERS: #{req.to_hash}"
56
- Log.debug "BODY:\n#{req.body}" if req.request_body_permitted?
57
-
58
- if options.has_key? :timeout
59
- cur_timeout = @http.read_timeout
60
- Log.debug "Overriding timeout to: #{options[:timeout]}"
61
- @http.read_timeout = options[:timeout]
74
+ def do_request method, path, obj=nil, options={}
75
+
76
+ #grifter clients pass in path possibly including query params.
77
+ #Faraday needs the query and path seperately.
78
+ parsed = URI.parse make_path(path)
79
+ #faraday needs the request params as a hash.
80
+ #this turns out to be non-trivial
81
+ query_hash = if parsed.query
82
+ cgi_hash = CGI.parse(parsed.query)
83
+ #make to account for one param having multiple values
84
+ cgi_hash.inject({}) { |h,(k,v)| h[k] = v[1] ? v : v.first; h }
85
+ else
86
+ nil
87
+ end
88
+
89
+ req_headers = make_headers(options)
90
+
91
+ body = if options[:form]
92
+ URI.encode_www_form obj
93
+ else
94
+ jsonify(obj)
62
95
  end
63
96
 
64
- response = @http.request(req)
65
-
66
- if cur_timeout
67
- @http.read_timeout = cur_timeout
97
+ #log the request
98
+ logger.debug [
99
+ "Doing request: #{@name}: #{method.to_s.upcase} #{path}",
100
+ @log_headers ? ["Request Headers:",
101
+ req_headers.map{ |k, v| "#{k}: #{v.inspect}" }] : nil,
102
+ @log_bodies ? ["Request Body:", body] : nil,
103
+ ].flatten.compact.join("\n")
104
+
105
+ #doing it this way avoids problem with OPTIONS method: https://github.com/lostisland/faraday/issues/305
106
+ response = nil
107
+ metrics_obj = { method: method, service: @name, path: path }
108
+ ActiveSupport::Notifications.instrument('request.grifter', metrics_obj) do
109
+ response = @conn.run_request(method, nil, nil, nil) do |req|
110
+ req.path = parsed.path
111
+ req.params = metrics_obj[:params] = query_hash if query_hash
112
+
113
+ req.headers = req_headers
114
+ req.body = body
115
+ req.options[:timeout] = options.fetch(:timeout, @default_timeout)
116
+ end
117
+ metrics_obj[:response] = response
68
118
  end
69
119
 
70
- Log.debug "RESPONSE CODE: #{response.code}"
71
- Log.debug "RESPONSE HEADERS: #{response.to_hash}"
72
- Log.debug "RESPONSE BODY:\n#{jsonify response.body}\n"
73
-
74
- @last_request = req
120
+ logger.info "Request status: (#{response.status}) #{@name}: #{method.to_s.upcase} #{path}"
121
+ #@last_request = req
75
122
  @last_response = response
76
123
 
77
- raise RequestException.new(req, response) unless response.kind_of? Net::HTTPSuccess
124
+ raise RequestException.new(nil, response) unless response.status >= 200 and response.status < 300
125
+
126
+ response_obj = objectify response.body
127
+ if response.headers['content-type'] =~ /json/
128
+ logger.debug [
129
+ "Response Details:",
130
+ @log_headers ? ["Response Headers:",
131
+ response.headers.map { |k, v| "#{k}: #{v.inspect}" }] : nil,
132
+ @log_bodies ? [ "Response Body:", jsonify(response_obj)] : nil,
133
+ ''
134
+ ].flatten.compact.join("\n")
135
+ end
136
+ return response_obj
137
+ end
78
138
 
79
- objectify response.body
139
+ def in_parallel &blk
140
+ @conn.headers = @headers
141
+ @conn.in_parallel &blk
80
142
  end
81
143
 
82
144
  #add base uri to request
@@ -90,13 +152,15 @@ class Grifter
90
152
  end
91
153
 
92
154
  def make_headers options
93
- if options[:additional_headers]
155
+ headers = if options[:additional_headers]
94
156
  @headers.merge options[:additional_headers]
95
157
  elsif options[:headers]
96
158
  options[:headers]
97
159
  else
98
- @headers
160
+ @headers.clone
99
161
  end
162
+ headers['content-type'] = 'application/x-www-form-urlencoded' if options[:form]
163
+ headers
100
164
  end
101
165
 
102
166
  def req_args path, options
@@ -104,47 +168,38 @@ class Grifter
104
168
  end
105
169
 
106
170
  def get path, options={}
107
- req = Net::HTTP::Get.new(*req_args(path, options))
108
- do_request req, options
171
+ do_request :get, path, nil, options
109
172
  end
110
173
 
111
174
  def head path, options={}
112
- req = Net::HTTP::Head.new(*req_args(path, options))
113
- do_request req, options
175
+ do_request :head, path, nil, options
114
176
  end
115
177
 
116
178
  def options path, options={}
117
- req = Net::HTTP::Options.new(*req_args(path, options))
118
- do_request req, options
179
+ do_request :options, path, nil, options
119
180
  end
120
181
 
121
182
  def delete path, options={}
122
- req = Net::HTTP::Delete.new(*req_args(path, options))
123
- do_request req, options
183
+ do_request :delete, path, nil, options
124
184
  end
125
185
 
126
186
  def post path, obj, options={}
127
- req = Net::HTTP::Post.new(*req_args(path, options))
128
- req.body = jsonify(obj)
129
- do_request req, options
187
+ do_request :post, path, obj, options
130
188
  end
131
189
 
132
190
  def put path, obj, options={}
133
- req = Net::HTTP::Put.new(*req_args(path, options))
134
- req.body = jsonify(obj)
135
- do_request req, options
191
+ do_request :put, path, obj, options
136
192
  end
137
193
 
138
194
  def patch path, obj, options={}
139
- req = Net::HTTP::Patch.new(*req_args(path, options))
140
- req.body = jsonify(obj)
141
- do_request req, options
195
+ do_request :patch, path, obj, options
142
196
  end
143
197
 
144
198
  def post_form path, params, options={}
145
- request_obj = Net::HTTP::Post.new(*req_args(path, options))
146
- request_obj.set_form_data params
147
- do_request request_obj, options
199
+ do_request :post, path, params, options.merge(form: true)
200
+ #request_obj = Net::HTTP::Post.new(*req_args(path, options))
201
+ #request_obj.set_form_data params
202
+ #do_request request_obj, options
148
203
  end
149
204
  end
150
205
 
@@ -155,12 +210,12 @@ class Grifter
155
210
 
156
211
  #this makes good info show up in rspec reports
157
212
  def to_s
158
- "#{self.class}\nResponseCode: #{@response.code}\nResponseBody:\n#{@response.body}"
213
+ "#{self.class}\nResponseCode: #{self.code}\nResponseBody:\n#{self.body}"
159
214
  end
160
215
 
161
216
  #shortcut methods
162
217
  def code
163
- @response.code
218
+ @response.status
164
219
  end
165
220
 
166
221
  def body
@@ -0,0 +1,30 @@
1
+ require 'active_support/notifications'
2
+ class Grifter
3
+ module Instrumentation
4
+
5
+ Sample = Struct.new(:service_name, :method, :path, :status, :duration_ms, :end_time)
6
+
7
+ def start_instrumentation
8
+ @all_requests ||= []
9
+ ActiveSupport::Notifications.subscribe('request.grifter') do |name, start_time, end_time, _, data|
10
+ #do nothing if exception happened, else we might interfere with exception handling
11
+ unless data[:exception]
12
+ duration_ms = ((end_time.to_f - start_time.to_f) * 1000).to_i
13
+ #$stderr.puts '[%s] %s %s (%.3f s)' % [url.host, http_method, url.request_uri, duration]
14
+ @all_requests << Sample.new(
15
+ data[:service],
16
+ data[:method].intern,
17
+ data[:path],
18
+ data[:response].status.to_s.intern,
19
+ duration_ms,
20
+ end_time
21
+ )
22
+ end
23
+ end
24
+ end
25
+
26
+ def metrics_all_requests
27
+ @all_requests
28
+ end
29
+ end
30
+ end
@@ -21,14 +21,14 @@ class Grifter
21
21
 
22
22
  #attempts to parse json strings into native ruby objects
23
23
  def objectify json_string
24
+ return nil if json_string.nil? or json_string==''
24
25
  case json_string
25
26
  when Hash, Array
26
27
  return json_string
27
28
  else
28
29
  JSON.parse(json_string.to_s)
29
30
  end
30
- rescue Exception => e
31
- Log.debug "Unable to parse non-json object: #{e.to_s}"
31
+ rescue Exception
32
32
  json_string
33
33
  end
34
34
 
data/lib/grifter/log.rb CHANGED
@@ -1,34 +1,44 @@
1
1
  require 'logger'
2
2
 
3
3
  class Grifter
4
- class Log
4
+ module Log
5
+ extend self
6
+
5
7
  GrifterFormatter = proc do |severity, datetime, progname, msg|
6
8
  "#{severity[0]}: [#{datetime.strftime('%m/%d/%y %H:%M:%S')}][#{progname}] - #{msg}\n"
7
9
  end
8
10
 
9
11
  @@loggers = []
10
- def self.add_logger handle
12
+ def loggers
13
+ @@loggers
14
+ end
15
+
16
+ def add_logger handle
11
17
  new_logger = Logger.new handle
12
18
  new_logger.progname = 'grifter'
13
19
  new_logger.formatter = GrifterFormatter
14
20
  @@loggers << new_logger
15
21
  end
16
22
 
17
- self.add_logger(STDOUT)
23
+ add_logger(STDOUT)
18
24
 
19
- def self.level= log_level
25
+ def level= log_level
20
26
  @@loggers.each { |logger| logger.level = log_level}
21
27
  end
22
28
 
23
- def self.log level, msg
29
+ def log level, msg
24
30
  @@loggers.each {|logger| logger.send(level, msg)}
25
31
  end
26
32
 
27
33
  [:fatal, :error, :warn, :info,:debug].each do |log_method|
28
- define_singleton_method log_method do |msg|
29
- log(log_method, msg)
34
+ define_method log_method do |msg|
35
+ self.log(log_method, msg)
30
36
  end
31
37
  end
38
+
39
+ def logger
40
+ self
41
+ end
32
42
  end
33
43
  end
34
44
 
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: grifter
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Robert Schultheis
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2013-11-05 00:00:00.000000000 Z
11
+ date: 2013-12-04 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: faraday
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '>='
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '>='
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: json
15
29
  requirement: !ruby/object:Gem::Requirement
@@ -24,6 +38,20 @@ dependencies:
24
38
  - - '>='
25
39
  - !ruby/object:Gem::Version
26
40
  version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: typhoeus
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
27
55
  - !ruby/object:Gem::Dependency
28
56
  name: rspec
29
57
  requirement: !ruby/object:Gem::Requirement
@@ -99,6 +127,7 @@ files:
99
127
  - lib/grifter/configuration.rb
100
128
  - lib/grifter/helpers.rb
101
129
  - lib/grifter/http_service.rb
130
+ - lib/grifter/instrumentation.rb
102
131
  - lib/grifter/json_helpers.rb
103
132
  - lib/grifter/log.rb
104
133
  homepage: http://github.com/knewton/grifter