opinionated_http 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +19 -0
- data/Rakefile +23 -0
- data/lib/opinionated_http/client.rb +98 -0
- data/lib/opinionated_http/logger.rb +36 -0
- data/lib/opinionated_http/version.rb +3 -0
- data/lib/opinionated_http.rb +77 -0
- data/test/client_test.rb +33 -0
- data/test/config/application.yml +4 -0
- data/test/test_helper.rb +14 -0
- metadata +97 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 21c5b626468b387d5b10f055e3f45b399d1b7c0e91a3962a31de1914f582848e
|
4
|
+
data.tar.gz: 4190403fda9fd6bb3853f8d552aa53f22e1ddf89ead008ee4fde7d9f184c7eaa
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: ff4c0ec5fadca2ac6a6cc6bbabb1e0893aa7064da3adbbfb33ef1c3561df302e2408b983ad1598360fab213ca5c35f5cc14765bcde59da95c8684d4f7f32894a
|
7
|
+
data.tar.gz: 274ff50c9ab8770f292ac37f2341480e01384ceddf8ef08a2ae644c139b5ecb965e0621309dd381f64e2e0a5d44bc2ee06b8037ada96c8731b8cdf645d8ab035
|
data/README.md
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
# Opinionated HTTP
|
2
|
+
|
3
|
+
An opinionated HTTP Client library using convention over configuration.
|
4
|
+
|
5
|
+
Uses
|
6
|
+
* PersistentHTTP for http connection pooling.
|
7
|
+
* Semantic Logger for logging and metrics.
|
8
|
+
* Secret Config for its configuration.
|
9
|
+
|
10
|
+
By convention the following metrics are measured and logged:
|
11
|
+
*
|
12
|
+
|
13
|
+
PersistentHTTP with the following enhancements:
|
14
|
+
* Read config from Secret Config, just supply the `secret_config_path`.
|
15
|
+
* Redirect logging into standard Semantic Logger.
|
16
|
+
* Implements metrics and measure call durations.
|
17
|
+
* Standardized Service Exception.
|
18
|
+
* Retries on HTTP 5XX errors
|
19
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# Setup bundler to avoid having to run bundle exec all the time.
|
2
|
+
require 'rubygems'
|
3
|
+
require 'bundler/setup'
|
4
|
+
|
5
|
+
require 'rake/testtask'
|
6
|
+
require_relative 'lib/opinionated_http/version'
|
7
|
+
|
8
|
+
task :gem do
|
9
|
+
system 'gem build opinionated_http.gemspec'
|
10
|
+
end
|
11
|
+
|
12
|
+
task publish: :gem do
|
13
|
+
system "git tag -a v#{OpinionatedHTTP::VERSION} -m 'Tagging #{OpinionatedHTTP::VERSION}'"
|
14
|
+
system 'git push --tags'
|
15
|
+
system "gem push opinionated_http-#{OpinionatedHTTP::VERSION}.gem"
|
16
|
+
system "rm opinionated_http-#{OpinionatedHTTP::VERSION}.gem"
|
17
|
+
end
|
18
|
+
|
19
|
+
Rake::TestTask.new(:test) do |t|
|
20
|
+
t.pattern = 'test/**/*_test.rb'
|
21
|
+
t.verbose = true
|
22
|
+
t.warning = false
|
23
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
require 'persistent_http'
|
2
|
+
require 'secret_config'
|
3
|
+
require 'semantic_logger'
|
4
|
+
#
|
5
|
+
# Client http implementation
|
6
|
+
#
|
7
|
+
module OpinionatedHTTP
|
8
|
+
class Client
|
9
|
+
attr_reader :secret_config_prefix, :logger, :metric_prefix, :error_class, :driver
|
10
|
+
|
11
|
+
def initialize(secret_config_prefix:, logger: nil, metric_prefix:, error_class:, **options)
|
12
|
+
@metric_prefix = metric_prefix
|
13
|
+
@logger = logger || SemanticLogger[self]
|
14
|
+
@error_class = error_class
|
15
|
+
|
16
|
+
internal_logger = OpinionatedHTTP::Logger.new(@logger)
|
17
|
+
new_options = {
|
18
|
+
logger: internal_logger,
|
19
|
+
debug_output: internal_logger,
|
20
|
+
name: "",
|
21
|
+
pool_size: SecretConfig.fetch("#{secret_config_prefix}/pool_size", type: :integer, default: 100),
|
22
|
+
open_timeout: SecretConfig.fetch("#{secret_config_prefix}/open_timeout", type: :float, default: 10),
|
23
|
+
read_timeout: SecretConfig.fetch("#{secret_config_prefix}/read_timeout", type: :float, default: 10),
|
24
|
+
idle_timeout: SecretConfig.fetch("#{secret_config_prefix}/idle_timeout", type: :float, default: 300),
|
25
|
+
keep_alive: SecretConfig.fetch("#{secret_config_prefix}/keep_alive", type: :float, default: 300),
|
26
|
+
pool_timeout: SecretConfig.fetch("#{secret_config_prefix}/pool_timeout", type: :float, default: 5),
|
27
|
+
warn_timeout: SecretConfig.fetch("#{secret_config_prefix}/warn_timeout", type: :float, default: 0.25),
|
28
|
+
proxy: SecretConfig.fetch("#{secret_config_prefix}/proxy", type: :symbol, default: :ENV),
|
29
|
+
force_retry: SecretConfig.fetch("#{secret_config_prefix}/force_retry", type: :boolean, default: true),
|
30
|
+
}
|
31
|
+
|
32
|
+
url = SecretConfig["#{secret_config_prefix}/url"]
|
33
|
+
new_options[:url] = url if url
|
34
|
+
@driver = PersistentHTTP.new(new_options.merge(options))
|
35
|
+
end
|
36
|
+
|
37
|
+
# Perform an HTTP Get against the supplied path
|
38
|
+
def get(action:, path: "/#{action}", parameters: nil)
|
39
|
+
path = "/#{path}" unless path.start_with?("/")
|
40
|
+
path = "#{path}?#{URI.encode_www_form(parameters)}" if parameters
|
41
|
+
|
42
|
+
request = Net::HTTP::Get.new(path)
|
43
|
+
response =
|
44
|
+
begin
|
45
|
+
payload = {}
|
46
|
+
if logger.trace?
|
47
|
+
payload[:parameters] = parameters
|
48
|
+
payload[:path] = path
|
49
|
+
end
|
50
|
+
message = "HTTP GET: #{action}" if logger.debug?
|
51
|
+
|
52
|
+
logger.benchmark_info(message: message, metric: "#{metric_prefix}/#{action}", payload: payload) { driver.request(request) }
|
53
|
+
rescue StandardError => exc
|
54
|
+
message = "HTTP GET: #{action} Failure: #{exc.class.name}: #{exc.message}"
|
55
|
+
logger.error(message: message, metric: "#{metric_prefix}/exception", exception: exc)
|
56
|
+
raise(error_class, message)
|
57
|
+
end
|
58
|
+
|
59
|
+
unless response.is_a?(Net::HTTPSuccess)
|
60
|
+
message = "HTTP GET: #{action} Failure: (#{response.code}) #{response.message}"
|
61
|
+
logger.error(message: message, metric: "#{metric_prefix}/exception")
|
62
|
+
raise(error_class, message)
|
63
|
+
end
|
64
|
+
|
65
|
+
response.body
|
66
|
+
end
|
67
|
+
|
68
|
+
def post(action:, path: "/#{action}", parameters: nil)
|
69
|
+
path = "/#{path}" unless path.start_with?("/")
|
70
|
+
request = Net::HTTP::Post.new(path)
|
71
|
+
request.set_form_data(*parameters) if parameters
|
72
|
+
|
73
|
+
response =
|
74
|
+
begin
|
75
|
+
payload = {}
|
76
|
+
if logger.trace?
|
77
|
+
payload[:parameters] = parameters
|
78
|
+
payload[:path] = path
|
79
|
+
end
|
80
|
+
message = "HTTP POST: #{action}" if logger.debug?
|
81
|
+
|
82
|
+
logger.benchmark_info(message: message, metric: "#{metric_prefix}/#{action}", payload: payload) { driver.request(request) }
|
83
|
+
rescue StandardError => exc
|
84
|
+
message = "HTTP POST: #{action} Failure: #{exc.class.name}: #{exc.message}"
|
85
|
+
logger.error(message: message, metric: "#{metric_prefix}/exception", exception: exc)
|
86
|
+
raise(error_class, message)
|
87
|
+
end
|
88
|
+
|
89
|
+
unless response.is_a?(Net::HTTPSuccess)
|
90
|
+
message = "HTTP POST: #{action} Failure: (#{response.code}) #{response.message}"
|
91
|
+
logger.error(message: message, metric: "#{metric_prefix}/exception")
|
92
|
+
raise(error_class, message)
|
93
|
+
end
|
94
|
+
|
95
|
+
response.body
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# Hack to make PersistentHTTP log to the standard logger
|
2
|
+
# and to make it and GenePool log trace info as trace.
|
3
|
+
module OpinionatedHTTP
|
4
|
+
class Logger
|
5
|
+
attr_reader :logger
|
6
|
+
|
7
|
+
def initialize(logger)
|
8
|
+
@logger = logger
|
9
|
+
end
|
10
|
+
|
11
|
+
def <<(message)
|
12
|
+
return unless logger.trace?
|
13
|
+
|
14
|
+
message = message.strip
|
15
|
+
return if message.blank?
|
16
|
+
|
17
|
+
logger.trace(message)
|
18
|
+
end
|
19
|
+
|
20
|
+
def debug(*args, &block)
|
21
|
+
logger.trace(*args, &block)
|
22
|
+
end
|
23
|
+
|
24
|
+
def info(*args, &block)
|
25
|
+
logger.info(*args, &block)
|
26
|
+
end
|
27
|
+
|
28
|
+
def warn(*args, &block)
|
29
|
+
logger.warn(*args, &block)
|
30
|
+
end
|
31
|
+
|
32
|
+
def error(*args, &block)
|
33
|
+
logger.error(*args, &block)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
#
|
2
|
+
# Opinionated HTTP
|
3
|
+
#
|
4
|
+
# An opinionated HTTP Client library using convention over configuration.
|
5
|
+
#
|
6
|
+
# Uses
|
7
|
+
# * PersistentHTTP for http connection pooling.
|
8
|
+
# * Semantic Logger for logging and metrics.
|
9
|
+
# * Secret Config for its configuration.
|
10
|
+
#
|
11
|
+
# By convention the following metrics are measured and logged:
|
12
|
+
# *
|
13
|
+
#
|
14
|
+
# PersistentHTTP with the following enhancements:
|
15
|
+
# * Read config from Secret Config, just supply the `secret_config_path`.
|
16
|
+
# * Redirect logging into standard Semantic Logger.
|
17
|
+
# * Implements metrics and measure call durations.
|
18
|
+
# * Standardized Service Exception.
|
19
|
+
# * Retries on HTTP 5XX errors
|
20
|
+
#
|
21
|
+
|
22
|
+
require 'opinionated_http/version'
|
23
|
+
module OpinionatedHTTP
|
24
|
+
autoload :Client, 'opinionated_http/client'
|
25
|
+
autoload :Logger, 'opinionated_http/logger'
|
26
|
+
|
27
|
+
# Create a new Opinionated HTTP instance.
|
28
|
+
#
|
29
|
+
# Parameters:
|
30
|
+
# secret_config_prefix:
|
31
|
+
# Required
|
32
|
+
# metric_prefix:
|
33
|
+
# Required
|
34
|
+
# error_class:
|
35
|
+
# Whenever exceptions are raised it is important that every client gets its own exception / error class
|
36
|
+
# so that failures to specific http servers can be easily identified.
|
37
|
+
# Required.
|
38
|
+
# logger:
|
39
|
+
# Default: SemanticLogger[OpinionatedHTTP]
|
40
|
+
# Other options as supported by PersistentHTTP
|
41
|
+
# #TODO: Expand PersistentHTTP options here
|
42
|
+
#
|
43
|
+
# Configuration:
|
44
|
+
# Off of the `secret_config_path` path above, Opinionated HTTP uses specific configuration entry names
|
45
|
+
# to configure the underlying HTTP setup:
|
46
|
+
# url: [String]
|
47
|
+
# The host url to the site to connect to.
|
48
|
+
# Exclude any path, since that will be supplied when `#get` or `#post` is called.
|
49
|
+
# Required.
|
50
|
+
# Examples:
|
51
|
+
# "https://example.com"
|
52
|
+
# "https://example.com:8443/"
|
53
|
+
# pool_size: [Integer]
|
54
|
+
# default: 100
|
55
|
+
# open_timeout: [Float]
|
56
|
+
# default: 10
|
57
|
+
# read_timeout: [Float]
|
58
|
+
# default: 10
|
59
|
+
# idle_timeout: [Float]
|
60
|
+
# default: 300
|
61
|
+
# keep_alive: [Float]
|
62
|
+
# default: 300
|
63
|
+
# pool_timeout: [Float]
|
64
|
+
# default: 5
|
65
|
+
# warn_timeout: [Float]
|
66
|
+
# default: 0.25
|
67
|
+
# proxy: [Symbol]
|
68
|
+
# default: :ENV
|
69
|
+
# force_retry: [true|false]
|
70
|
+
# default: true
|
71
|
+
#
|
72
|
+
# Metrics:
|
73
|
+
# During each call to `#get` or `#put`, the following metrics are logged using the
|
74
|
+
def self.new(**args)
|
75
|
+
Client.new(**args)
|
76
|
+
end
|
77
|
+
end
|
data/test/client_test.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'json'
|
3
|
+
require_relative 'test_helper'
|
4
|
+
|
5
|
+
module OpinionatedHTTP
|
6
|
+
class ClientTest < Minitest::Test
|
7
|
+
describe OpinionatedHTTP::Client do
|
8
|
+
class ServiceError < StandardError
|
9
|
+
end
|
10
|
+
|
11
|
+
let :http do
|
12
|
+
OpinionatedHTTP.new(
|
13
|
+
secret_config_prefix: 'fake_service',
|
14
|
+
metric_prefix: 'FakeService',
|
15
|
+
logger: SemanticLogger["FakeService"],
|
16
|
+
error_class: ServiceError,
|
17
|
+
header: {'Content-Type' => 'application/json'}
|
18
|
+
)
|
19
|
+
end
|
20
|
+
|
21
|
+
describe "get" do
|
22
|
+
it 'success' do
|
23
|
+
output = {zip: '12345', population: 54321}
|
24
|
+
body = output.to_json
|
25
|
+
response = Net::HTTPSuccess.new(200, 'OK', body)
|
26
|
+
http.driver.stub(:request, response) do
|
27
|
+
http.get(action: 'lookup', parameters: {zip: '12345'})
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
$LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
|
2
|
+
ENV['TZ'] = 'America/New_York'
|
3
|
+
|
4
|
+
require 'yaml'
|
5
|
+
require 'minitest/autorun'
|
6
|
+
require 'awesome_print'
|
7
|
+
require 'secret_config'
|
8
|
+
require 'semantic_logger'
|
9
|
+
require 'opinionated_http'
|
10
|
+
|
11
|
+
SemanticLogger.add_appender(file_name: 'test.log', formatter: :color)
|
12
|
+
SemanticLogger.default_level = :debug
|
13
|
+
|
14
|
+
SecretConfig.use :file, path: 'test', file_name: File.expand_path('config/application.yml', __dir__)
|
metadata
ADDED
@@ -0,0 +1,97 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: opinionated_http
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Reid Morrison
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2020-02-20 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: persistent_http
|
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'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: secret_config
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: semantic_logger
|
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'
|
55
|
+
description: HTTP Client with retries. Uses PersistentHTTP for http connection pooling,
|
56
|
+
Semantic Logger for logging and metrics, and uses Secret Config for its configuration.
|
57
|
+
email:
|
58
|
+
- reidmo@gmail.com
|
59
|
+
executables: []
|
60
|
+
extensions: []
|
61
|
+
extra_rdoc_files: []
|
62
|
+
files:
|
63
|
+
- README.md
|
64
|
+
- Rakefile
|
65
|
+
- lib/opinionated_http.rb
|
66
|
+
- lib/opinionated_http/client.rb
|
67
|
+
- lib/opinionated_http/logger.rb
|
68
|
+
- lib/opinionated_http/version.rb
|
69
|
+
- test/client_test.rb
|
70
|
+
- test/config/application.yml
|
71
|
+
- test/test_helper.rb
|
72
|
+
homepage:
|
73
|
+
licenses: []
|
74
|
+
metadata: {}
|
75
|
+
post_install_message:
|
76
|
+
rdoc_options: []
|
77
|
+
require_paths:
|
78
|
+
- lib
|
79
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
80
|
+
requirements:
|
81
|
+
- - ">="
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
version: '0'
|
84
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - ">="
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '0'
|
89
|
+
requirements: []
|
90
|
+
rubygems_version: 3.0.6
|
91
|
+
signing_key:
|
92
|
+
specification_version: 4
|
93
|
+
summary: Opinionated HTTP Client
|
94
|
+
test_files:
|
95
|
+
- test/client_test.rb
|
96
|
+
- test/config/application.yml
|
97
|
+
- test/test_helper.rb
|