sbmt-strangler 0.9.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +54 -0
  4. data/.rubocop_todo.yml +0 -0
  5. data/Appraisals +20 -0
  6. data/CHANGELOG.md +106 -0
  7. data/Gemfile +5 -0
  8. data/LICENSE +21 -0
  9. data/README.md +86 -0
  10. data/Rakefile +12 -0
  11. data/config/initializers/strangler.rb +5 -0
  12. data/config/initializers/yabeda.rb +40 -0
  13. data/dip.yml +67 -0
  14. data/docker-compose.yml +19 -0
  15. data/docs/img/01-proxy_mode.png +0 -0
  16. data/docs/img/02-mirror_mode.png +0 -0
  17. data/docs/img/03-replace_mode.png +0 -0
  18. data/lefthook-local.dip_example.yml +4 -0
  19. data/lefthook.yml +6 -0
  20. data/lib/sbmt/strangler/action.rb +37 -0
  21. data/lib/sbmt/strangler/action_invoker.rb +43 -0
  22. data/lib/sbmt/strangler/builder.rb +66 -0
  23. data/lib/sbmt/strangler/configurable.rb +32 -0
  24. data/lib/sbmt/strangler/configuration.rb +25 -0
  25. data/lib/sbmt/strangler/const_definer.rb +36 -0
  26. data/lib/sbmt/strangler/controller.rb +30 -0
  27. data/lib/sbmt/strangler/engine.rb +11 -0
  28. data/lib/sbmt/strangler/error_tracker.rb +34 -0
  29. data/lib/sbmt/strangler/errors.rb +7 -0
  30. data/lib/sbmt/strangler/feature_flags.rb +59 -0
  31. data/lib/sbmt/strangler/flipper.rb +38 -0
  32. data/lib/sbmt/strangler/http/client.rb +41 -0
  33. data/lib/sbmt/strangler/http/transport.rb +89 -0
  34. data/lib/sbmt/strangler/http.rb +89 -0
  35. data/lib/sbmt/strangler/logger.rb +48 -0
  36. data/lib/sbmt/strangler/metric_tracker.rb +69 -0
  37. data/lib/sbmt/strangler/mixin.rb +56 -0
  38. data/lib/sbmt/strangler/version.rb +7 -0
  39. data/lib/sbmt/strangler/work_modes/base.rb +22 -0
  40. data/lib/sbmt/strangler/work_modes/mirror.rb +73 -0
  41. data/lib/sbmt/strangler/work_modes/proxy.rb +20 -0
  42. data/lib/sbmt/strangler/work_modes/replace.rb +65 -0
  43. data/lib/sbmt/strangler.rb +84 -0
  44. data/sbmt-strangler.gemspec +59 -0
  45. metadata +473 -0
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sbmt
4
+ module Strangler
5
+ class Configuration
6
+ extend Sbmt::Strangler::Configurable
7
+
8
+ option :params_tracking_allowlist, :headers_allowlist, default: []
9
+ option :action_controller_base_class, default: "ActionController::Base"
10
+ option :error_tracker, default: "Sbmt::Strangler::ErrorTracker"
11
+ option :flipper_actor, default: ->(_http_params, _headers) {}
12
+
13
+ attr_reader :controllers, :http
14
+
15
+ def initialize(options = {})
16
+ @controllers = []
17
+ @http = ActiveSupport::InheritableOptions.new(Sbmt::Strangler::Http::DEFAULT_HTTP_OPTIONS)
18
+ end
19
+
20
+ def controller(name, &)
21
+ controllers.push(Sbmt::Strangler::Controller.new(name, self, &))
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sbmt
4
+ module Strangler
5
+ class ConstDefiner
6
+ class << self
7
+ def call!(name, klass)
8
+ const_names = name.split("::")
9
+ class_name = const_names.pop
10
+ module_name = if const_names.any?
11
+ define_modules(const_names)
12
+ else
13
+ Object
14
+ end
15
+
16
+ module_name.const_set(class_name, klass)
17
+ end
18
+
19
+ private
20
+
21
+ def define_modules(module_names)
22
+ module_names.reduce(Object) do |parent_module_name, module_name|
23
+ define_module(module_name, parent_module_name)
24
+ "#{parent_module_name}::#{module_name}".constantize
25
+ end
26
+ end
27
+
28
+ def define_module(module_name, parent_module_name)
29
+ return if parent_module_name.const_defined?(module_name, false)
30
+
31
+ parent_module_name.const_set(module_name, Module.new)
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sbmt
4
+ module Strangler
5
+ class Controller
6
+ extend Sbmt::Strangler::Configurable
7
+
8
+ option :params_tracking_allowlist, :headers_allowlist, :flipper_actor, default_from: :configuration
9
+
10
+ attr_reader :name, :class_name, :actions, :configuration
11
+
12
+ def initialize(name, configuration, &)
13
+ @name = name
14
+ @class_name = "#{name.camelize}Controller"
15
+ @actions = []
16
+ @configuration = configuration
17
+
18
+ yield(self)
19
+ end
20
+
21
+ def action(name, &)
22
+ @actions.push(Sbmt::Strangler::Action.new(name, self, &))
23
+ end
24
+
25
+ def http
26
+ @http ||= ActiveSupport::InheritableOptions.new(configuration.http)
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/engine"
4
+
5
+ module Sbmt
6
+ module Strangler
7
+ class Engine < Rails::Engine
8
+ isolate_namespace Sbmt::Strangler
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sbmt
4
+ module Strangler
5
+ class ErrorTracker
6
+ class << self
7
+ def error(message, params = {})
8
+ unless defined?(Sentry)
9
+ Sbmt::Strangler.logger.log_error(message, params)
10
+ return
11
+ end
12
+
13
+ logging(:error, message, params)
14
+ end
15
+
16
+ private
17
+
18
+ def logging(level, message, params)
19
+ params = {message: params} if params.is_a?(String)
20
+
21
+ Sentry.with_scope do |scope|
22
+ scope.set_contexts(contexts: params)
23
+
24
+ if message.is_a?(Exception)
25
+ Sentry.capture_exception(message, level: level)
26
+ else
27
+ Sentry.capture_message(message, level: level)
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sbmt
4
+ module Strangler
5
+ class ConfigurationError < StandardError; end
6
+ end
7
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sbmt
4
+ module Strangler
5
+ class FeatureFlags
6
+ FLAGS = %i[
7
+ mirror
8
+ replace
9
+ ]
10
+ FEATURES_HEADER_NAME = "HTTP_STRANGLER_FEATURES"
11
+
12
+ attr_reader :strangler_action, :rails_controller
13
+
14
+ def initialize(strangler_action:, rails_controller: nil)
15
+ @strangler_action = strangler_action
16
+ @rails_controller = rails_controller
17
+ end
18
+
19
+ FLAGS.each do |flag_name|
20
+ define_method(:"#{flag_name}?") { feature_enabled?(feature_name(flag_name)) }
21
+ end
22
+
23
+ def add_all!
24
+ FLAGS.each { |flag_name| add(feature_name(flag_name)) }
25
+ end
26
+
27
+ private
28
+
29
+ delegate :add, :enabled?, :enabled_on_time?, to: "Sbmt::Strangler::Flipper"
30
+
31
+ FEATURE_NAME_SANITIZER = -> { _1.to_s.gsub(/[^A-Za-z0-9]+/, "-") }
32
+
33
+ def feature_name(flag_name)
34
+ sanitized_controller_name = FEATURE_NAME_SANITIZER.call(strangler_action.controller.name)
35
+ sanitized_action_name = FEATURE_NAME_SANITIZER.call(strangler_action.name)
36
+ sanitized_flag_name = FEATURE_NAME_SANITIZER.call(flag_name)
37
+
38
+ "#{sanitized_controller_name}__#{sanitized_action_name}--#{sanitized_flag_name}"
39
+ end
40
+
41
+ def feature_enabled?(feature_name)
42
+ enabled?(feature_name, flipper_actor) ||
43
+ enabled_on_time?(feature_name) ||
44
+ enabled_by_header?(feature_name)
45
+ end
46
+
47
+ def flipper_actor
48
+ @flipper_actor ||= strangler_action.flipper_actor.call(rails_controller.http_params, rails_controller.request.headers)
49
+ end
50
+
51
+ def enabled_by_header?(feature_name)
52
+ features = rails_controller.request.headers[FEATURES_HEADER_NAME]
53
+ return false unless features.present? && features.is_a?(String)
54
+
55
+ features.split(",").map(&:strip).include?(feature_name)
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sbmt
4
+ module Strangler
5
+ module Flipper
6
+ FLIPPER_ID_STRUCT = Struct.new(:flipper_id)
7
+ ONTIME_ACTOR_REGEXP = /^ONTIME:(\d{2})-(\d{2})$/
8
+
9
+ class << self
10
+ delegate :add, to: ::Flipper
11
+
12
+ def enabled?(feature_name, *actors)
13
+ raise "feature name is blank" if feature_name.blank?
14
+
15
+ actors = Array(actors).flatten.compact
16
+ ::Flipper.enabled?(feature_name, *actors.map { FLIPPER_ID_STRUCT.new(_1) })
17
+ end
18
+
19
+ def enabled_on_time?(feature_name)
20
+ raise "feature name is blank" if feature_name.blank?
21
+
22
+ hours_ranges =
23
+ ::Flipper[feature_name]
24
+ .actors_value
25
+ .filter_map { |e|
26
+ e.match(ONTIME_ACTOR_REGEXP) {
27
+ $LAST_MATCH_INFO.captures.map(&:to_i)
28
+ }
29
+ }
30
+ .compact
31
+
32
+ hour_now = DateTime.now.in_time_zone.hour
33
+ hours_ranges.any? { |range| (range.first..range.last).cover?(hour_now) }
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sbmt
4
+ module Strangler
5
+ module Http
6
+ class Client
7
+ include Dry::Monads::Result::Mixin
8
+
9
+ def initialize(http_options: nil)
10
+ @http_options = http_options
11
+ end
12
+
13
+ def call(url, http_verb, payload: {}, headers: {})
14
+ case http_verb.downcase
15
+ when :get
16
+ transport.get_request(url, params: payload, headers: prepare_headers(headers))
17
+ when :post
18
+ transport.post_request(url, body: payload, headers: prepare_headers(headers))
19
+ when :put
20
+ transport.put_request(url, body: payload, headers: prepare_headers(headers))
21
+
22
+ else
23
+ raise "unsupported http verb - #{http_verb}"
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ attr_reader :http_options
30
+
31
+ def transport
32
+ @transport ||= Sbmt::Strangler::Http::Transport.new(http_options: http_options)
33
+ end
34
+
35
+ def prepare_headers(headers)
36
+ headers&.transform_keys { |key| key.sub("HTTP_", "").tr("_", "-") }
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sbmt
4
+ module Strangler
5
+ module Http
6
+ class Transport
7
+ include Dry::Monads::Do
8
+ include Dry::Monads::Result::Mixin
9
+
10
+ def initialize(http_options: nil)
11
+ @http_options = http_options
12
+ end
13
+
14
+ def get_request(url, params: {}, headers: {})
15
+ with_error_handling(url) do
16
+ response = connection.get(url, params, headers)
17
+ Success(body: response.body, status: response.status, headers: response.headers)
18
+ end
19
+ end
20
+
21
+ def post_request(url, body: {}, headers: {})
22
+ with_error_handling(url) do
23
+ response = connection.post(url, body, headers)
24
+ Success(body: response.body, status: response.status, headers: response.headers)
25
+ end
26
+ end
27
+
28
+ def put_request(url, body: {}, headers: {})
29
+ with_error_handling(url) do
30
+ response = connection.put(url, body, headers)
31
+ Success(body: response.body, status: response.status, headers: response.headers)
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ attr_reader :http_options
38
+
39
+ def connection
40
+ @connection ||= Faraday.new do |conn|
41
+ conn.response :raise_error
42
+ Sbmt::Strangler::Http.configure_faraday(conn, name: "strangler_http_client", http_options: http_options)
43
+ # Skip JSON parsing because
44
+ # 1. it speeds up proxy mode and
45
+ # 2. allows us to duplicate proxy response easily.
46
+ # conn.response :json
47
+ conn.request :json
48
+ end
49
+ end
50
+
51
+ def with_error_handling(url)
52
+ retry_count ||= 0
53
+
54
+ yield
55
+ rescue Faraday::ConnectionFailed => error
56
+ Sbmt::Strangler.logger.error(
57
+ message: "Sbmt::Strangler::Http::Transport ConnectionFailed",
58
+ url: url,
59
+ attempt: retry_count + 1,
60
+ retries_count: http_options.retries_count
61
+ )
62
+
63
+ retry if (retry_count += 1) && retry_count <= http_options.retries_count
64
+
65
+ Failure(status: :bad_gateway)
66
+ rescue Faraday::UnprocessableEntityError, Faraday::ForbiddenError => error
67
+ Failure(body: error.response_body, status: error.response_status, headers: error.response_headers)
68
+ rescue Faraday::TimeoutError
69
+ Sbmt::Strangler.logger.error(
70
+ message: "Sbmt::Strangler::Http::Transport TimeoutError",
71
+ url: url
72
+ )
73
+ Failure(status: :gateway_timeout)
74
+ rescue Faraday::Error => error
75
+ response = error.response
76
+ Sbmt::Strangler.logger.error(error.message)
77
+ Sbmt::Strangler.error_tracker.error(error)
78
+ return Failure(status: :internal_server_error) unless response
79
+
80
+ Failure(body: response[:body], status: response[:status], headers: response[:headers])
81
+ rescue => error
82
+ Sbmt::Strangler.logger.error(error.message)
83
+ Sbmt::Strangler.error_tracker.error(error)
84
+ Failure(status: :internal_server_error)
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "http/client"
4
+ require_relative "http/transport"
5
+
6
+ module Sbmt
7
+ module Strangler
8
+ module Http
9
+ DEFAULT_HTTP_OPTIONS = {
10
+ keepalive_pool_size: 256,
11
+ keepalive_idle_timeout: 30,
12
+ timeout: 5,
13
+ read_timeout: 5,
14
+ write_timeout: 5,
15
+ open_timeout: 1,
16
+ retries_count: 1
17
+ }.freeze
18
+ REQUEST_PATH_FILTER_REGEX = %r{(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})|(/\d+)|(/[A-Z]\d{9,11}(-\d{1})?)}
19
+
20
+ # Configures Faraday connection. Sets default options and adds default middlewares into chain.
21
+ # Accepts an optional block to configure net-http-persistent-adapter
22
+ #
23
+ # @example
24
+ #
25
+ # @conn ||= Faraday.new(@base_url) do |f|
26
+ # Sbmt::Strangler::Http.configure_faraday(f, name: "http-client") do |http|
27
+ # http.idle_timeout = 42
28
+ # end
29
+ # f.timeout = 5
30
+ # f.response :json
31
+ # end
32
+ #
33
+ # @param [Faraday::Connection] conn
34
+ # @param [Hash] opts faraday & middlewares options
35
+ # @option opts [String] :name client name for tracing and instrumentation. Required.
36
+ # @option opts [Hash] :adapter_opts net_http_persistent adapter options
37
+ # @option opts [ActiveSupport::InheritableOptions] :http_options timeout options
38
+ # @option opts [Regexp] :request_path_filter_regex (REQUEST_PATH_FILTER_REGEX) regex for filtering out
39
+ # variables from http request metric `path` tag. Set to false to add empty value instead.
40
+ #
41
+ # @return [Faraday::Connection]
42
+ def self.configure_faraday(conn, opts = {})
43
+ raise ConfigurationError, "Faraday client :name must be set" unless opts[:name]
44
+
45
+ http_options = opts[:http_options] || Sbmt::Strangler.configuration.http
46
+
47
+ conn.options.timeout = http_options.timeout
48
+ conn.options.read_timeout = http_options.read_timeout
49
+ conn.options.open_timeout = http_options.open_timeout
50
+ conn.options.write_timeout = http_options.write_timeout
51
+
52
+ configure_faraday_metrics(conn, opts.slice(:name, :request_path_filter_regex))
53
+
54
+ adapter_opts = {pool_size: http_options.keepalive_pool_size}.merge(opts[:adapter_opts] || {})
55
+ conn.adapter :net_http_persistent, adapter_opts do |http|
56
+ http.idle_timeout = http_options.keepalive_idle_timeout
57
+ yield http if block_given?
58
+ end
59
+
60
+ conn
61
+ end
62
+
63
+ def self.configure_faraday_metrics(conn, opts = {})
64
+ @subscribers ||= {}
65
+ name = opts.fetch(:name)
66
+ instrument_full_name = ["request.faraday", name].compact.join(".")
67
+ filter = opts.fetch(:request_path_filter_regex, REQUEST_PATH_FILTER_REGEX)
68
+
69
+ conn.request :instrumentation, name: instrument_full_name
70
+ return if @subscribers[instrument_full_name]
71
+
72
+ @subscribers[instrument_full_name] = ActiveSupport::Notifications.subscribe(instrument_full_name) do |*args|
73
+ event = ActiveSupport::Notifications::Event.new(*args)
74
+ env = event.payload
75
+
76
+ tags = {
77
+ name: name,
78
+ method: env.method,
79
+ status: env.status || :error,
80
+ host: env.url.host,
81
+ path: filter ? env.url.path.gsub(filter, "/:id") : ""
82
+ }
83
+
84
+ Yabeda.sbmt_strangler.http_request_duration.measure(tags, event.duration.fdiv(1000))
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sbmt
4
+ module Strangler
5
+ class Logger
6
+ delegate :logger, to: :Rails
7
+ delegate_missing_to :logger
8
+
9
+ def log_debug(message, **params)
10
+ with_tags(**params) do
11
+ logger.debug(message)
12
+ end
13
+ end
14
+
15
+ def log_info(message, **params)
16
+ with_tags(**params) do
17
+ logger.info(message)
18
+ end
19
+ end
20
+
21
+ def log_warn(message, **params)
22
+ with_tags(**params) do
23
+ logger.warn(message)
24
+ end
25
+ end
26
+
27
+ def log_error(message, **params)
28
+ with_tags(**params) do
29
+ logger.error(message)
30
+ end
31
+ end
32
+
33
+ def log_success(message, **params)
34
+ log_info(message, status: "success", **params)
35
+ end
36
+
37
+ def log_failure(message, **params)
38
+ log_error(message, status: "failure", **params)
39
+ end
40
+
41
+ def with_tags(**params)
42
+ logger.tagged(**params) do
43
+ yield
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sbmt
4
+ module Strangler
5
+ class MetricTracker
6
+ attr_reader :rails_controller
7
+
8
+ def initialize(rails_controller)
9
+ @rails_controller = rails_controller
10
+ end
11
+
12
+ def track_params_usage
13
+ ::Yabeda.sbmt_strangler.params_usage.increment(common_tags)
14
+ end
15
+
16
+ def log_unallowed_params
17
+ unallowed_params = all_request_params - allowed_request_params
18
+ Sbmt::Strangler.logger.log_warn(<<~WARN.strip) if unallowed_params.any?
19
+ Not allowed parameters in #{controller_path}##{action_name}: #{unallowed_params}
20
+ WARN
21
+ end
22
+
23
+ def track_work_mode(mode)
24
+ yabeda_tags = common_tags.merge(mode: mode.to_s)
25
+ ::Yabeda.sbmt_strangler.work_mode.increment(yabeda_tags)
26
+ end
27
+
28
+ def track_mirror_call(success)
29
+ yabeda_tags = common_tags.merge(success: success.to_s)
30
+ ::Yabeda.sbmt_strangler.mirror_call.increment(yabeda_tags)
31
+ end
32
+
33
+ def track_compare_call(success)
34
+ yabeda_tags = common_tags.merge(success: success.to_s)
35
+ ::Yabeda.sbmt_strangler.compare_call.increment(yabeda_tags)
36
+ end
37
+
38
+ def track_compare_call_result(value)
39
+ yabeda_tags = common_tags.merge(value: value.to_s)
40
+ ::Yabeda.sbmt_strangler.compare_call_result.increment(yabeda_tags)
41
+ end
42
+
43
+ def track_render_call(success)
44
+ yabeda_tags = common_tags.merge(success: success.to_s)
45
+ ::Yabeda.sbmt_strangler.render_call.increment(yabeda_tags)
46
+ end
47
+
48
+ private
49
+
50
+ delegate :http_params, :allowed_params, :controller_path, :action_name, to: :rails_controller
51
+
52
+ def common_tags
53
+ {
54
+ params: allowed_request_params.join(","),
55
+ controller: controller_path,
56
+ action: action_name
57
+ }
58
+ end
59
+
60
+ def allowed_request_params
61
+ @allowed_request_params ||= allowed_params.keys.map(&:to_s).sort.uniq
62
+ end
63
+
64
+ def all_request_params
65
+ @all_request_params ||= http_params.keys.map(&:to_s).sort.uniq
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sbmt
4
+ module Strangler
5
+ module Mixin
6
+ attr_reader :strangler_action
7
+
8
+ def http_params
9
+ params.to_unsafe_h.except(:action, :controller, :format)
10
+ end
11
+
12
+ def allowed_params
13
+ return http_params if strangler_action.params_tracking_allowlist.blank?
14
+
15
+ params.permit(*strangler_action.params_tracking_allowlist).to_h
16
+ end
17
+
18
+ def allowed_headers
19
+ if strangler_action.headers_allowlist.blank?
20
+ return request.headers.select { |name, _| name.starts_with?("HTTP_") }.to_h
21
+ end
22
+
23
+ request.headers.select { |name, _| name.in?(strangler_action.headers_allowlist) }.to_h
24
+ end
25
+
26
+ def http_request(payload)
27
+ strangler_action.http_client.call(
28
+ proxy_url,
29
+ strangler_action.proxy_http_method,
30
+ payload: payload,
31
+ headers: allowed_headers
32
+ )
33
+ end
34
+
35
+ def proxy_url
36
+ case strangler_action.proxy_url
37
+ in String => url
38
+ url
39
+ in Proc => proc
40
+ proc.call(http_params, request.headers)
41
+ end
42
+ end
43
+
44
+ def render_origin_response(response)
45
+ if response.success?
46
+ body, status = response.value!.values_at(:body, :status)
47
+ render json: body, status: status
48
+ return
49
+ end
50
+
51
+ body, status = response.failure.values_at(:body, :status)
52
+ render json: body, status: status
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sbmt
4
+ module Strangler
5
+ VERSION = "0.9.1"
6
+ end
7
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sbmt
4
+ module Strangler
5
+ module WorkModes
6
+ class Base
7
+ attr_reader :rails_controller, :strangler_action, :metric_tracker, :feature_flags
8
+
9
+ def initialize(rails_controller:, strangler_action:, metric_tracker:, feature_flags:)
10
+ @rails_controller = rails_controller
11
+ @strangler_action = strangler_action
12
+ @metric_tracker = metric_tracker
13
+ @feature_flags = feature_flags
14
+ end
15
+
16
+ def call
17
+ raise NotImplementedError
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end