opinionated_http 0.0.1
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/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
|