jersey 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/Gemfile +11 -0
- data/LICENSE.txt +22 -0
- data/README.md +233 -0
- data/Rakefile +10 -0
- data/examples/readme.ru +15 -0
- data/jersey.gemspec +27 -0
- data/lib/jersey.rb +10 -0
- data/lib/jersey/api.rb +12 -0
- data/lib/jersey/base.rb +31 -0
- data/lib/jersey/extensions/error_handler.rb +30 -0
- data/lib/jersey/extensions/route_signature.rb +16 -0
- data/lib/jersey/helpers/log.rb +7 -0
- data/lib/jersey/http_errors.rb +62 -0
- data/lib/jersey/log.rb +25 -0
- data/lib/jersey/logging/base_logger.rb +58 -0
- data/lib/jersey/logging/json_logger.rb +8 -0
- data/lib/jersey/logging/logfmt_logger.rb +37 -0
- data/lib/jersey/logging/mixins.rb +31 -0
- data/lib/jersey/middleware/request_id.rb +46 -0
- data/lib/jersey/middleware/request_logger.rb +38 -0
- data/lib/jersey/setup.rb +40 -0
- data/lib/jersey/time.rb +29 -0
- data/lib/jersey/version.rb +3 -0
- data/test/errors_test.rb +48 -0
- data/test/helper.rb +39 -0
- data/test/log_test.rb +55 -0
- data/test/request_logger_test.rb +161 -0
- data/test/setup_test.rb +13 -0
- metadata +162 -0
@@ -0,0 +1,62 @@
|
|
1
|
+
module Jersey
|
2
|
+
module HTTP
|
3
|
+
module Errors
|
4
|
+
HTTPError = Class.new(StandardError)
|
5
|
+
ClientError = Class.new(HTTPError)
|
6
|
+
ServerError = Class.new(HTTPError)
|
7
|
+
|
8
|
+
# Define ALL the HTTP errors as constants
|
9
|
+
FOUR_HUNDRED = [
|
10
|
+
[400, 'BadRequest', :bad_request],
|
11
|
+
[401, 'Unauthorized', :unauthorized],
|
12
|
+
[402, 'PaymentRequired', :payment_required],
|
13
|
+
[403, 'Forbidden', :forbidden],
|
14
|
+
[404, 'NotFound', :not_found],
|
15
|
+
[405, 'MethodNotAllowed', :method_not_allowed],
|
16
|
+
[406, 'NotAcceptable', :not_acceptable],
|
17
|
+
[407, 'ProxyAuthenticationRequired', :proxy_authentication_required],
|
18
|
+
[408, 'RequestTimeout', :request_timeout],
|
19
|
+
[409, 'Conflict', :conflict],
|
20
|
+
[410, 'Gone', :gone],
|
21
|
+
[411, 'LengthRequired', :length_required],
|
22
|
+
[412, 'PreconditionFailed', :precondition_failed],
|
23
|
+
[413, 'RequestEntityTooLarge', :request_entity_too_large],
|
24
|
+
[414, 'RequestURITooLong', :request_uri_too_long],
|
25
|
+
[415, 'UnsupportedMediaType', :unsupported_media_type],
|
26
|
+
[416, 'RequestedRangeNotSatisfiable', :requested_range_not_satisfiable],
|
27
|
+
[417, 'ExpectationFailed', :expectation_failed],
|
28
|
+
[422, 'UnprocessableEntity', :unprocessable_entity],
|
29
|
+
[423, 'Locked', :locked],
|
30
|
+
[424, 'FailedDependency', :failed_dependency],
|
31
|
+
[426, 'UpgradeRequired', :upgrade_required],
|
32
|
+
]
|
33
|
+
|
34
|
+
FIVE_HUNDRED = [
|
35
|
+
[500, 'InternalServerError', :internal_server_error],
|
36
|
+
[501, 'NotImplemented', :not_implemented],
|
37
|
+
[502, 'BadGateway', :bad_gateway],
|
38
|
+
[503, 'ServiceUnavailable', :service_unavailable],
|
39
|
+
[504, 'GatewayTimeout', :gateway_timeout],
|
40
|
+
[505, 'HTTPVersionNotSupported', :http_version_not_supported],
|
41
|
+
[507, 'InsufficientStorage', :insufficient_storage],
|
42
|
+
[510, 'NotExtended', :not_extended],
|
43
|
+
]
|
44
|
+
|
45
|
+
(FOUR_HUNDRED + FIVE_HUNDRED).each do |s|
|
46
|
+
code, const_name, symbol = s[0],s[1],s[2]
|
47
|
+
if code >= 500
|
48
|
+
klass = Class.new(ServerError)
|
49
|
+
else
|
50
|
+
klass = Class.new(ClientError)
|
51
|
+
end
|
52
|
+
klass.const_set(:STATUS_CODE, code)
|
53
|
+
const_set(const_name,klass)
|
54
|
+
end
|
55
|
+
|
56
|
+
# Take over Sinatra's NotFound so we don't have to deal with the
|
57
|
+
# not_found block and can raise our own NotFound error
|
58
|
+
::Sinatra.send(:remove_const, :NotFound)
|
59
|
+
::Sinatra.const_set(:NotFound, ::Jersey::HTTP::Errors::NotFound)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
data/lib/jersey/log.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'jersey/logging/base_logger'
|
2
|
+
require 'jersey/logging/logfmt_logger'
|
3
|
+
require 'jersey/logging/json_logger'
|
4
|
+
require 'jersey/logging/mixins'
|
5
|
+
|
6
|
+
module Jersey
|
7
|
+
class Logger < LogfmtLogger
|
8
|
+
include Logging::LogTime
|
9
|
+
include Logging::LogError
|
10
|
+
end
|
11
|
+
|
12
|
+
module LoggingSingleton
|
13
|
+
attr_writer :logger
|
14
|
+
|
15
|
+
def logger
|
16
|
+
@logger ||= Jersey::Logger.new
|
17
|
+
end
|
18
|
+
|
19
|
+
def log(loggable = {})
|
20
|
+
logger.log(loggable)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
extend LoggingSingleton
|
25
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module Jersey
|
2
|
+
class BaseLogger
|
3
|
+
attr_accessor :stream, :defaults
|
4
|
+
|
5
|
+
def initialize(opts = {})
|
6
|
+
@stream = opts.fetch(:stream, $stdout)
|
7
|
+
@defaults = opts.fetch(:defaults, {})
|
8
|
+
end
|
9
|
+
|
10
|
+
def <<(data)
|
11
|
+
@defaults.merge!(data)
|
12
|
+
end
|
13
|
+
|
14
|
+
def reset!(key = nil)
|
15
|
+
if key
|
16
|
+
@defaults.delete(key)
|
17
|
+
@defaults.delete(key.to_sym)
|
18
|
+
else
|
19
|
+
@defaults.clear
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def log(data, &block)
|
24
|
+
log_to_stream(stream, @defaults.merge(data), &block)
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
def log_to_stream(stream, data, &block)
|
29
|
+
data.merge!(request_data || {})
|
30
|
+
unless block
|
31
|
+
str = unparse(data.merge(now: Time.now))
|
32
|
+
stream.print(str + "\n")
|
33
|
+
else
|
34
|
+
data = data.dup
|
35
|
+
start = Time.now
|
36
|
+
log_to_stream(stream, data.merge(at: "start"))
|
37
|
+
begin
|
38
|
+
res = yield
|
39
|
+
log_to_stream(stream, data.merge(
|
40
|
+
at: "finish", elapsed: (Time.now - start).to_f))
|
41
|
+
res
|
42
|
+
rescue
|
43
|
+
log_to_stream(stream, data.merge(
|
44
|
+
at: "exception", elapsed: (Time.now - start).to_f))
|
45
|
+
raise
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def unparse
|
51
|
+
raise "Need to subclass and implement #unparse"
|
52
|
+
end
|
53
|
+
|
54
|
+
def request_data
|
55
|
+
RequestStore[:log] if defined? RequestStore
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Jersey
|
2
|
+
class LogfmtLogger < BaseLogger
|
3
|
+
private
|
4
|
+
def unparse(attrs)
|
5
|
+
attrs.map { |k, v| unparse_pair(k, v) }.compact.join(" ")
|
6
|
+
end
|
7
|
+
|
8
|
+
def quote_string(k, v)
|
9
|
+
# try to find a quote style that fits
|
10
|
+
if !v.include?('"')
|
11
|
+
%{#{k}="#{v}"}
|
12
|
+
elsif !v.include?("'")
|
13
|
+
%{#{k}='#{v}'}
|
14
|
+
else
|
15
|
+
%{#{k}="#{v.gsub(/"/, '\\"')}"}
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def unparse_pair(k, v)
|
20
|
+
v = v.call if v.is_a?(Proc)
|
21
|
+
# only quote strings if they include whitespace
|
22
|
+
if v == nil
|
23
|
+
nil
|
24
|
+
elsif v == true
|
25
|
+
k
|
26
|
+
elsif v.is_a?(Float)
|
27
|
+
"#{k}=#{format("%.3f", v)}"
|
28
|
+
elsif v.is_a?(String) && v =~ /\s/
|
29
|
+
quote_string(k, v)
|
30
|
+
elsif v.is_a?(Time)
|
31
|
+
"#{k}=#{v.iso8601}"
|
32
|
+
else
|
33
|
+
"#{k}=#{v}"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Jersey
|
2
|
+
module Logging
|
3
|
+
# always log the time
|
4
|
+
module LogTime
|
5
|
+
def log(hash = {})
|
6
|
+
super(hash.merge(now: Time.now))
|
7
|
+
end
|
8
|
+
end
|
9
|
+
# only log the first ten lines of a backtrace so error logs
|
10
|
+
# are digestible
|
11
|
+
module LogError
|
12
|
+
def log(loggable = {})
|
13
|
+
case loggable
|
14
|
+
when Hash
|
15
|
+
super(loggable)
|
16
|
+
when Exception
|
17
|
+
e = loggable
|
18
|
+
super(error: true, id: e.object_id, message: e.message)
|
19
|
+
lineno = 0
|
20
|
+
e.backtrace[0,10].each do |line|
|
21
|
+
lineno += 1
|
22
|
+
super(error: true,
|
23
|
+
id: e.object_id,
|
24
|
+
backtrace: line,
|
25
|
+
line_number: lineno)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'securerandom'
|
2
|
+
|
3
|
+
module Jersey::Middleware
|
4
|
+
class RequestID
|
5
|
+
UUID_PATTERN =
|
6
|
+
/\A[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}\Z/
|
7
|
+
|
8
|
+
def initialize(app)
|
9
|
+
@app = app
|
10
|
+
end
|
11
|
+
|
12
|
+
def call(env)
|
13
|
+
request_ids = [SecureRandom.uuid] + extract_request_ids(env)
|
14
|
+
|
15
|
+
# make ID of the request accessible to consumers down the stack
|
16
|
+
env["REQUEST_ID"] = request_ids[0]
|
17
|
+
RequestStore[:log] ||= {}
|
18
|
+
RequestStore[:log][:request_id] = request_ids[0]
|
19
|
+
|
20
|
+
# Extract request IDs from incoming headers as well. Can be used for
|
21
|
+
# identifying a request across a number of components in SOA.
|
22
|
+
env["REQUEST_IDS"] = request_ids
|
23
|
+
|
24
|
+
status, headers, response = @app.call(env)
|
25
|
+
|
26
|
+
# tag all responses with a request ID
|
27
|
+
headers["Request-Id"] = request_ids[0]
|
28
|
+
|
29
|
+
[status, headers, response]
|
30
|
+
ensure
|
31
|
+
RequestStore[:log].delete(:request_id)
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def extract_request_ids(env)
|
37
|
+
request_ids = []
|
38
|
+
if env["HTTP_REQUEST_ID"]
|
39
|
+
request_ids = env["HTTP_REQUEST_ID"].split(",")
|
40
|
+
request_ids.map! { |id| id.strip }
|
41
|
+
request_ids.select! { |id| id =~ UUID_PATTERN }
|
42
|
+
end
|
43
|
+
request_ids
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Jersey::Middleware
|
2
|
+
# Logs request info using the configured logger or the Jersey singleton
|
3
|
+
#
|
4
|
+
# Adds request_id to the default logger params
|
5
|
+
class RequestLogger
|
6
|
+
def initialize(app, options={})
|
7
|
+
@app = app
|
8
|
+
@logger = options[:logger] || Jersey.logger
|
9
|
+
end
|
10
|
+
|
11
|
+
def call(env)
|
12
|
+
@request_start = Time.now
|
13
|
+
request = Rack::Request.new(env)
|
14
|
+
start_data = {
|
15
|
+
at: "start",
|
16
|
+
request_id: env['REQUEST_ID'],
|
17
|
+
method: request.request_method,
|
18
|
+
path: request.path_info,
|
19
|
+
content_type: request.content_type,
|
20
|
+
content_length: request.content_length
|
21
|
+
}
|
22
|
+
@logger.log(start_data)
|
23
|
+
status, headers, response = @app.call(env)
|
24
|
+
@logger.log(
|
25
|
+
at: "finish",
|
26
|
+
method: request.request_method,
|
27
|
+
path: request.path_info,
|
28
|
+
status: status,
|
29
|
+
content_length: headers['Content-Length'],
|
30
|
+
route_signature: env['ROUTE_SIGNATURE'],
|
31
|
+
elapsed: (Time.now - @request_start).to_f,
|
32
|
+
request_id: env['REQUEST_ID']
|
33
|
+
)
|
34
|
+
@logger.reset!(:request_id)
|
35
|
+
[status, headers, response]
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
data/lib/jersey/setup.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'env-conf'
|
2
|
+
|
3
|
+
module Jersey
|
4
|
+
#require bundler and the proper gems for the ENV
|
5
|
+
def self.require
|
6
|
+
Kernel.require 'bundler'
|
7
|
+
Config.dotenv!
|
8
|
+
$stderr.puts "Loading #{Config.app_env} environment..."
|
9
|
+
Bundler.require(:default, Config.app_env)
|
10
|
+
end
|
11
|
+
|
12
|
+
# adds ./lib dir to the load path
|
13
|
+
def self.load_path
|
14
|
+
$stderr.puts "Adding './lib' to path..."
|
15
|
+
$LOAD_PATH.unshift(File.expand_path('./lib'))
|
16
|
+
end
|
17
|
+
|
18
|
+
# sets TZ to UTC and Sequel timezone to :utc
|
19
|
+
def self.set_timezones
|
20
|
+
$stderr.puts "Setting timezones to UTC..."
|
21
|
+
Sequel.default_timezone = :utc if defined? Sequel
|
22
|
+
ENV['TZ'] = 'UTC'
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.hack_time_class
|
26
|
+
$stderr.puts "Modifying Time#to_s to use #iso8601..." if ENV['DEBUG']
|
27
|
+
# use send to call private method
|
28
|
+
Time.send(:define_method, :to_s) do
|
29
|
+
self.iso8601
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# all in one go
|
34
|
+
def self.setup
|
35
|
+
self.require
|
36
|
+
self.load_path
|
37
|
+
self.set_timezones
|
38
|
+
self.hack_time_class
|
39
|
+
end
|
40
|
+
end
|
data/lib/jersey/time.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
class Time
|
2
|
+
def last_month
|
3
|
+
(self.to_datetime << 1).to_time
|
4
|
+
end
|
5
|
+
|
6
|
+
def next_month
|
7
|
+
(self.to_datetime >> 1).to_time
|
8
|
+
end
|
9
|
+
|
10
|
+
def tomorrow
|
11
|
+
(self.to_datetime + 1).to_time
|
12
|
+
end
|
13
|
+
|
14
|
+
def yesterday
|
15
|
+
(self.to_datetime - 1).to_time
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class Fixnum
|
20
|
+
def weeks
|
21
|
+
self.days * 7
|
22
|
+
end
|
23
|
+
alias :week :weeks
|
24
|
+
|
25
|
+
def days
|
26
|
+
self * 24 * 60 * 60
|
27
|
+
end
|
28
|
+
alias :day :days
|
29
|
+
end
|
data/test/errors_test.rb
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
require_relative 'helper'
|
2
|
+
|
3
|
+
class ErrorsTest < ApiTest
|
4
|
+
class SimpleApi < Jersey::API::Base
|
5
|
+
get '/test-409' do
|
6
|
+
raise Conflict, "bad"
|
7
|
+
end
|
8
|
+
|
9
|
+
get '/test-500' do
|
10
|
+
raise InternalServerError, "bad"
|
11
|
+
end
|
12
|
+
|
13
|
+
get '/test-runtime-error' do
|
14
|
+
raise "boom!"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def app
|
19
|
+
SimpleApi
|
20
|
+
end
|
21
|
+
|
22
|
+
def test_not_found
|
23
|
+
get '/not-found'
|
24
|
+
assert_equal(404, last_response.status)
|
25
|
+
assert_equal('NotFound', json['error']['type'])
|
26
|
+
end
|
27
|
+
|
28
|
+
def test_http_errors_409
|
29
|
+
get '/test-409'
|
30
|
+
assert_equal(409, last_response.status)
|
31
|
+
assert_equal('Conflict', json['error']['type'])
|
32
|
+
assert_equal('bad', json['error']['message'])
|
33
|
+
end
|
34
|
+
|
35
|
+
def test_http_errors_500
|
36
|
+
get '/test-500'
|
37
|
+
assert_equal(500, last_response.status)
|
38
|
+
assert_equal('InternalServerError', json['error']['type'])
|
39
|
+
assert_equal('bad', json['error']['message'])
|
40
|
+
end
|
41
|
+
|
42
|
+
def test_http_errors_Undefined
|
43
|
+
get '/test-runtime-error'
|
44
|
+
assert_equal(500, last_response.status)
|
45
|
+
assert_equal('RuntimeError', json['error']['type'])
|
46
|
+
assert_equal('boom!', json['error']['message'])
|
47
|
+
end
|
48
|
+
end
|