jersey 0.0.3
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.
- 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
|