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 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,3 @@
1
+ module OpinionatedHTTP
2
+ VERSION = "0.0.1".freeze
3
+ 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
@@ -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
@@ -0,0 +1,4 @@
1
+ test:
2
+ fake_supplier:
3
+ url: https://example.org
4
+
@@ -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