opinionated_http 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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