zappb-exceptions 1.0.0

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.
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ZappbException
4
+ module ClassFactory
5
+ module Namespace; end
6
+
7
+ module_function
8
+
9
+ def call(definition, aliases = [])
10
+ unless definition.external || definition.klass
11
+ keys = definition.payload if definition.declared_payload?
12
+ ensure_constant(definition.name, base_class(definition), keys)
13
+ end
14
+
15
+ parent = parent_for(definition)
16
+ aliases.each do |alias_name|
17
+ next if alias_name.include?("::")
18
+
19
+ ensure_constant(alias_name, parent)
20
+ end
21
+ end
22
+
23
+ def parent_for(definition)
24
+ return base_class(definition) if definition.external || definition.klass
25
+
26
+ ZappbException.const_get(definition.name, false)
27
+ rescue ::NameError
28
+ base_class(definition)
29
+ end
30
+
31
+ def base_class(definition)
32
+ definition.payload && !definition.external && !definition.klass ? PayloadError : BaseError
33
+ end
34
+
35
+ def ensure_constant(name, superclass, payload_keys = nil)
36
+ *path, leaf = name.split("::")
37
+ namespace = path.inject(ZappbException) { |parent, segment| namespace_for(parent, segment) }
38
+
39
+ if namespace.const_defined?(leaf, false)
40
+ existing = namespace.const_get(leaf, false)
41
+ return existing if existing.is_a?(::Class) && existing <= ::Exception
42
+ return promote(namespace, leaf, existing, superclass, payload_keys) if existing.is_a?(Namespace)
43
+
44
+ raise ConfigurationError, "ZappbException::#{name} already exists and is not an error class"
45
+ end
46
+ namespace.const_set(leaf, build(superclass, payload_keys))
47
+ end
48
+
49
+ def namespace_for(parent, segment)
50
+ unless parent.const_defined?(segment, false)
51
+ return parent.const_set(segment, ::Module.new.extend(Namespace))
52
+ end
53
+
54
+ existing = parent.const_get(segment, false)
55
+ return existing if existing.is_a?(::Module)
56
+
57
+ raise ConfigurationError, "#{parent.name}::#{segment} already exists and is neither a module nor a class"
58
+ end
59
+
60
+ def promote(namespace, leaf, old, superclass, payload_keys)
61
+ klass = build(superclass, payload_keys)
62
+ old.constants(false).each { |const| klass.const_set(const, old.const_get(const, false)) }
63
+ namespace.send(:remove_const, leaf)
64
+ namespace.const_set(leaf, klass)
65
+ end
66
+
67
+ def build(superclass, payload_keys)
68
+ klass = ::Class.new(superclass)
69
+ klass.payload_keys = payload_keys if payload_keys
70
+ klass
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "logger"
4
+
5
+ module ZappbException
6
+ class Configuration
7
+ UNSET = Object.new.freeze
8
+ private_constant :UNSET
9
+
10
+ attr_reader :code_prefix, :safe_detail, :meta_key, :locale
11
+ attr_writer :logger
12
+
13
+ def initialize(registry)
14
+ @registry = registry
15
+ @code_prefix = ""
16
+ @service_label = UNSET
17
+ @meta_key = nil
18
+ @safe_detail = false
19
+ @locale = nil
20
+ @logger = nil
21
+ end
22
+
23
+ def code_prefix=(value)
24
+ @code_prefix = value.to_s
25
+ end
26
+
27
+ def meta_key=(value)
28
+ unless value.nil? || ((value.is_a?(::String) || value.is_a?(::Symbol)) && !value.to_s.empty?)
29
+ raise ConfigurationError, "meta_key must be nil, or a non-empty String or Symbol"
30
+ end
31
+
32
+ @meta_key = value&.to_sym
33
+ end
34
+
35
+ def service_label
36
+ return @service_label unless @service_label.equal?(UNSET)
37
+
38
+ default_service_label
39
+ end
40
+
41
+ def service_label=(value)
42
+ @service_label = value
43
+ end
44
+
45
+ def safe_detail=(value)
46
+ unless [true, false].include?(value) || value.respond_to?(:call)
47
+ raise ConfigurationError, "safe_detail must be true, false or a callable"
48
+ end
49
+
50
+ @safe_detail = value
51
+ end
52
+
53
+ def locale=(value)
54
+ unless value.nil? || ((value.is_a?(::String) || value.is_a?(::Symbol)) && !value.to_s.empty?)
55
+ raise ConfigurationError, "locale must be nil, or a non-empty String or Symbol"
56
+ end
57
+
58
+ @locale = value&.to_sym
59
+ end
60
+
61
+ def logger
62
+ @logger || default_logger
63
+ end
64
+
65
+ def register(name, **options)
66
+ @registry.register(name, **options)
67
+ end
68
+
69
+ def alias_external(class_name, to:)
70
+ @registry.alias_external(class_name, to: to)
71
+ end
72
+
73
+ private
74
+
75
+ def default_service_label
76
+ return unless defined?(::Rails) && ::Rails.respond_to?(:application) && ::Rails.application
77
+
78
+ ::Rails.application.class.module_parent_name
79
+ end
80
+
81
+ def default_logger
82
+ rails_logger = ::Rails.logger if defined?(::Rails) && ::Rails.respond_to?(:logger)
83
+ rails_logger || (@fallback_logger ||= ::Logger.new($stdout))
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ZappbException
4
+ NAME_SEGMENT = /\A[A-Z]\w*\z/
5
+ PAYLOAD_KEY = /\A[a-z_]\w*\z/
6
+ RESERVED_PAYLOAD_KEYS = %i[meta exception detail].freeze
7
+
8
+ Definition = Data.define(:name, :status, :number, :message, :code, :external, :payload, :klass, :base, :fallback,
9
+ :safe_detail) do
10
+ def initialize(name:, status:, message:, number: nil, code: nil, external: false, payload: false,
11
+ klass: nil, base: false, fallback: false, safe_detail: nil)
12
+ name = name.to_s
13
+ raise ConfigurationError, "The error name cannot be empty" if name.empty?
14
+ if name.start_with?("ZappbException::")
15
+ raise ConfigurationError, "The name '#{name}' must not have the 'ZappbException::' prefix"
16
+ end
17
+ unless status.is_a?(::Integer) && (400..599).cover?(status)
18
+ raise ConfigurationError, "#{name}: status must be an integer between 400 and 599 (got #{status.inspect})"
19
+ end
20
+ unless message.is_a?(::String) && !message.empty?
21
+ raise ConfigurationError, "#{name}: message is required"
22
+ end
23
+ unless code.nil? || (code.is_a?(::String) && !code.empty?)
24
+ raise ConfigurationError, "#{name}: code must be a non-empty String"
25
+ end
26
+ if klass && !(klass.is_a?(::Class) || klass.respond_to?(:call))
27
+ raise ConfigurationError, "#{name}: klass must be a class or a callable returning one"
28
+ end
29
+ unless fallback || code
30
+ unless number.is_a?(::Integer) && number >= 1
31
+ raise ConfigurationError, "#{name}: number must be an integer >= 1 (got #{number.inspect})"
32
+ end
33
+ end
34
+ unless number.nil? || (number.is_a?(::Integer) && number >= 0)
35
+ raise ConfigurationError, "#{name}: number must be an integer >= 1 (got #{number.inspect})"
36
+ end
37
+
38
+ external = external || (klass && name.include?("::")) ? true : false
39
+ if !external && !name.split("::", -1).all? { |segment| segment.match?(NAME_SEGMENT) }
40
+ raise ConfigurationError, "#{name}: invalid constant name"
41
+ end
42
+ unless safe_detail.nil? || [true, false].include?(safe_detail) || safe_detail.respond_to?(:call)
43
+ raise ConfigurationError, "#{name}: safe_detail must be true, false or a callable"
44
+ end
45
+ payload = self.class.normalize_payload(name, payload)
46
+
47
+ super(name: name.dup.freeze, status: status, number: number, message: message.dup.freeze,
48
+ code: code&.dup&.freeze, external: external, payload: payload,
49
+ klass: klass, base: base ? true : false, fallback: fallback ? true : false,
50
+ safe_detail: safe_detail)
51
+ end
52
+
53
+ def self.normalize_payload(name, payload)
54
+ return payload ? true : false unless payload.is_a?(::Array)
55
+
56
+ keys = payload.map do |key|
57
+ unless key.is_a?(::Symbol) || key.is_a?(::String)
58
+ raise ConfigurationError, "#{name}: payload keys must be Symbol or String (got #{key.inspect})"
59
+ end
60
+
61
+ key.to_sym
62
+ end.uniq
63
+ raise ConfigurationError, "#{name}: payload cannot be an empty Array" if keys.empty?
64
+
65
+ keys.each do |key|
66
+ unless key.match?(PAYLOAD_KEY)
67
+ raise ConfigurationError, "#{name}: invalid payload key (#{key.inspect})"
68
+ end
69
+ if RESERVED_PAYLOAD_KEYS.include?(key) || PayloadError.method_defined?(key)
70
+ raise ConfigurationError, "#{name}: payload key '#{key}' is reserved"
71
+ end
72
+ end
73
+ keys.freeze
74
+ end
75
+
76
+ def declared_payload?
77
+ payload.is_a?(::Array)
78
+ end
79
+
80
+ def internal?
81
+ !external
82
+ end
83
+
84
+ def error_code(prefix = "")
85
+ return code if code
86
+
87
+ "#{status}-#{prefix}#{format("%02d", number || 0)}"
88
+ end
89
+
90
+ def resolve_klass
91
+ return nil unless klass
92
+
93
+ klass.respond_to?(:call) ? klass.call : klass
94
+ rescue ::NameError
95
+ nil
96
+ end
97
+ end
98
+
99
+ Definition::UNHANDLED = Definition.new(name: "Unhandled", status: 500, number: 0,
100
+ message: "Unhandled exception", fallback: true)
101
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ZappbException
4
+ module Describer
5
+ Description = Data.define(:status, :code, :message) do
6
+ def level
7
+ Describer.level_for(status)
8
+ end
9
+ end
10
+
11
+ module_function
12
+
13
+ def level_for(status)
14
+ status.to_i >= 500 ? :error : :warn
15
+ end
16
+
17
+ def level(exception, config: ZappbException.configuration)
18
+ call(exception, config: config).level
19
+ end
20
+
21
+ def call(exception, config: ZappbException.configuration)
22
+ if exception.is_a?(ForwardedError) && exception.error_hash.is_a?(::Hash)
23
+ hash = exception.error_hash
24
+ return Description.new(status: Envelope.forwarded_status(hash), code: Envelope.forwarded_code(hash),
25
+ message: RemoteError.from(hash).message)
26
+ end
27
+
28
+ definition = Resolver.call(exception)
29
+ Description.new(status: definition.status, code: definition.error_code(config.code_prefix),
30
+ message: Envelope.message_for(definition, config: config))
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ZappbException
4
+ module DetailSanitizer
5
+ module_function
6
+
7
+ def call(exception, setting)
8
+ case setting
9
+ when true
10
+ exception.is_a?(BaseError) ? exception.message : exception.class.name.to_s
11
+ when false, nil
12
+ exception.message
13
+ else
14
+ setting.call(exception).to_s
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ZappbException
4
+ module Envelope
5
+ Result = Data.define(:status, :body, :definition)
6
+
7
+ I18N_SCOPE = "zappb_exception.errors"
8
+ RESERVED_ERROR_KEYS = %i[exception detail].freeze
9
+
10
+ module_function
11
+
12
+ def call(exception, meta: {}, definition: nil, config: ZappbException.configuration)
13
+ signed = signed_meta(exception, meta, config)
14
+
15
+ if exception.is_a?(ForwardedError) && exception.error_hash.is_a?(::Hash)
16
+ hash = exception.error_hash
17
+ body = hash.symbolize_keys.except(:meta)
18
+ final = merge_meta(remote_meta(hash), signed)
19
+ return Result.new(status: forwarded_status(hash), body: with_meta(body, final), definition: nil)
20
+ end
21
+
22
+ definition ||= Resolver.call(exception)
23
+ body = {
24
+ success: false,
25
+ status: definition.status,
26
+ code: definition.error_code(config.code_prefix),
27
+ message: message_for(definition, config: config),
28
+ error: error_for(exception, config, definition)
29
+ }
30
+ Result.new(status: definition.status, body: with_meta(body, signed), definition: definition)
31
+ end
32
+
33
+ def signed_meta(exception, meta, config)
34
+ own = exception.respond_to?(:meta) ? exception.meta : nil
35
+ local = deep_symbolize(hash_or_empty(own)).merge(deep_symbolize(hash_or_empty(meta)))
36
+ return {} if local.empty?
37
+
38
+ key = config.meta_key
39
+ key ? { key.to_sym => local } : local
40
+ rescue ::StandardError
41
+ {}
42
+ end
43
+
44
+ def remote_meta(hash)
45
+ value = hash.key?("meta") ? hash["meta"] : hash[:meta]
46
+ deep_symbolize(hash_or_empty(value))
47
+ end
48
+
49
+ def merge_meta(remote, local)
50
+ remote.merge(local) do |_key, remote_value, local_value|
51
+ if remote_value.is_a?(::Hash) && local_value.is_a?(::Hash)
52
+ merge_meta(remote_value, local_value)
53
+ else
54
+ local_value
55
+ end
56
+ end
57
+ end
58
+
59
+ def with_meta(body, meta)
60
+ meta.empty? ? body : body.merge(meta: meta)
61
+ end
62
+
63
+ def hash_or_empty(value)
64
+ value.is_a?(::Hash) ? value : {}
65
+ end
66
+
67
+ def deep_symbolize(value)
68
+ case value
69
+ when ::Hash
70
+ value.each_with_object({}) do |(key, item), acc|
71
+ acc[key.respond_to?(:to_sym) ? key.to_sym : key] = deep_symbolize(item)
72
+ end
73
+ when ::Array then value.map { |item| deep_symbolize(item) }
74
+ else value
75
+ end
76
+ end
77
+
78
+ def error_for(exception, config, definition = nil)
79
+ { exception: exception.class.name, detail: detail_for(exception, config, definition) }
80
+ .merge(extra_for(exception))
81
+ end
82
+
83
+ def extra_for(exception)
84
+ extra = exception.respond_to?(:extra_data) ? exception.extra_data : nil
85
+ return {} unless extra.is_a?(::Hash) && !extra.empty?
86
+
87
+ extra.transform_keys(&:to_sym).except(*RESERVED_ERROR_KEYS)
88
+ end
89
+
90
+ def forwarded_status(hash)
91
+ status = Integer(hash["status"] || hash[:status] || 500, exception: false)
92
+ status && (100..599).cover?(status) ? status : 500
93
+ end
94
+
95
+ def forwarded_code(hash)
96
+ hash["code"] || hash[:code]
97
+ end
98
+
99
+ def message_for(definition, config: ZappbException.configuration)
100
+ return definition.message unless defined?(::I18n)
101
+
102
+ options = { default: definition.message }
103
+ options[:locale] = config.locale if config.locale
104
+ ::I18n.t("#{I18N_SCOPE}.#{definition.name}", **options)
105
+ rescue ::StandardError
106
+ definition.message
107
+ end
108
+
109
+ def detail_for(exception, config, definition = nil)
110
+ setting = definition.nil? || definition.safe_detail.nil? ? config.safe_detail : definition.safe_detail
111
+ setting = true if definition&.fallback && definition.safe_detail.nil? && !setting
112
+ text = DetailSanitizer.call(exception, setting)
113
+ label = config.service_label
114
+ label.nil? || label.to_s.empty? ? text : "[#{label}] #{text}"
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ZappbException
4
+ class ConfigurationError < ::StandardError; end
5
+
6
+ class FrozenRegistryError < ConfigurationError; end
7
+
8
+ class BaseError < ::StandardError
9
+ def extra_data
10
+ {}
11
+ end
12
+
13
+ def meta
14
+ @meta ||= {}
15
+ end
16
+
17
+ def initialize(message = nil, meta: {})
18
+ @meta = meta.is_a?(::Hash) ? meta.dup : {}
19
+ super(message)
20
+ end
21
+
22
+ def with_meta(hash)
23
+ meta.merge!(hash) if hash.is_a?(::Hash)
24
+ self
25
+ end
26
+ end
27
+
28
+ class PayloadError < BaseError
29
+ class << self
30
+ def payload_keys
31
+ return @payload_keys if defined?(@payload_keys)
32
+
33
+ superclass.respond_to?(:payload_keys) ? superclass.payload_keys : nil
34
+ end
35
+
36
+ def payload_keys=(keys)
37
+ @payload_keys = keys.freeze
38
+ keys.each { |key| define_method(key) { payload[key] } }
39
+ end
40
+ end
41
+
42
+ attr_reader :payload
43
+
44
+ def initialize(message = nil, meta: {}, **payload)
45
+ keys = self.class.payload_keys
46
+ if keys
47
+ unknown = payload.keys - keys
48
+ unless unknown.empty?
49
+ raise ::ArgumentError, "Undeclared payload keys: #{unknown.join(", ")} (declared: #{keys.join(", ")})"
50
+ end
51
+
52
+ payload = keys.to_h { |key| [key, payload[key]] }
53
+ end
54
+ @payload = payload
55
+ super(message, meta: meta)
56
+ end
57
+
58
+ def extra_data
59
+ payload
60
+ end
61
+ end
62
+
63
+ class ForwardedError < BaseError
64
+ attr_reader :error_hash
65
+
66
+ def initialize(error_hash = nil, meta: {}, **rest)
67
+ error_hash = rest if error_hash.nil? && !rest.empty?
68
+ @error_hash = error_hash
69
+ message = if error_hash.is_a?(::Hash)
70
+ error_hash["message"] || error_hash[:message] || error_hash.to_s
71
+ else
72
+ error_hash.to_s
73
+ end
74
+ super(message, meta: meta)
75
+ end
76
+
77
+ def remote
78
+ @remote ||= RemoteError.from(error_hash)
79
+ end
80
+
81
+ def status = remote.status
82
+
83
+ def code = remote.code
84
+
85
+ def kind?(*names) = remote.kind?(*names)
86
+
87
+ def success? = remote.success?
88
+
89
+ def extra = remote.extra
90
+
91
+ def detail(strip_label: false) = remote.detail(strip_label: strip_label)
92
+
93
+ def remote_meta = remote.meta
94
+ end
95
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ZappbException
4
+ module Handler
5
+ HANDLER = proc { |exception| ZappbException::Handler.handle(self, exception) }
6
+
7
+ def self.included(base)
8
+ return unless base.respond_to?(:rescue_from)
9
+
10
+ base.rescue_from ::StandardError, with: HANDLER
11
+ end
12
+
13
+ def self.handle(controller, exception, meta: {}, config: ZappbException.configuration)
14
+ result = Envelope.call(exception, meta: meta, config: config)
15
+
16
+ Notifier.notify(exception, result, config: config)
17
+
18
+ render(controller, result)
19
+ end
20
+
21
+ def self.render(controller, result)
22
+ return unless controller.respond_to?(:render)
23
+
24
+ controller.render status: result.status, json: result.body.compact
25
+ rescue ::StandardError => e
26
+ config = ZappbException.configuration
27
+ config.logger.error "ZappbException::Handler render failed: #{e.class}: #{e.message}"
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ZappbException
4
+ module Nats
5
+ module_function
6
+
7
+ def reply(meta: {}, notify: true, config: ZappbException.configuration)
8
+ yield
9
+ rescue ::StandardError => e
10
+ result = Envelope.call(e, meta: meta, config: config)
11
+ notify_safely(e, result, config) if notify
12
+ result.body.compact
13
+ end
14
+
15
+ def error_response?(response)
16
+ return false unless response.is_a?(::Hash)
17
+
18
+ success = RemoteError.read(response, :success)
19
+ return true if success == false || success.to_s == "false"
20
+
21
+ present?(RemoteError.read(response, :error))
22
+ end
23
+
24
+ def raise_on_error!(response, on_nil: GatewayTimeout, meta: {})
25
+ if response.nil?
26
+ valid = on_nil.is_a?(::Exception) || (on_nil.is_a?(::Class) && on_nil <= ::Exception)
27
+ raise ::ArgumentError, "on_nil must be an Exception class or instance, got #{on_nil.inspect}" unless valid
28
+
29
+ raise on_nil.new(meta: meta) if on_nil.is_a?(::Class) && on_nil <= BaseError
30
+ raise on_nil.with_meta(meta) if on_nil.is_a?(BaseError)
31
+
32
+ raise on_nil
33
+ end
34
+ raise ForwardedError.new(response, meta: meta) if error_response?(response)
35
+
36
+ response
37
+ end
38
+
39
+ def present?(value)
40
+ value.respond_to?(:empty?) ? !value.empty? : !!value
41
+ end
42
+
43
+ def notify_safely(exception, result, config)
44
+ Notifier.notify(exception, result, config: config)
45
+ rescue ::StandardError => e
46
+ config.logger.error "ZappbException::Nats notify failed: #{e.class}: #{e.message}"
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module ZappbException
6
+ module Notifier
7
+ ERROR_EVENT = "zappb_exception.error"
8
+ FORWARDED_EVENT = "zappb_exception.forwarded_error"
9
+
10
+ module_function
11
+
12
+ def error(exception, definition, meta: nil, config: ZappbException.configuration)
13
+ status = definition.status
14
+ server_error = status >= 500
15
+ emit(ERROR_EVENT, status, config, {
16
+ exception_class: exception.class.name,
17
+ http_status: status,
18
+ error_code: definition.error_code(config.code_prefix),
19
+ message: parse_message(exception.message),
20
+ backtrace: server_error ? exception.backtrace&.first(10) : exception.backtrace&.first(1),
21
+ meta: presence(meta),
22
+ extra: presence(Envelope.extra_for(exception))
23
+ }.compact)
24
+ end
25
+
26
+ def forwarded(exception, status:, error_code:, meta: nil, config: ZappbException.configuration)
27
+ emit(FORWARDED_EVENT, status, config, {
28
+ http_status: status,
29
+ error_code: error_code,
30
+ message: exception.message,
31
+ meta: presence(meta),
32
+ extra: presence(remote_extra(exception))
33
+ }.compact)
34
+ end
35
+
36
+ def notify(exception, result, config: ZappbException.configuration)
37
+ if result.definition
38
+ error(exception, result.definition, meta: result.body[:meta], config: config)
39
+ elsif exception.is_a?(ForwardedError) && exception.error_hash.is_a?(::Hash)
40
+ forwarded(exception, status: result.status,
41
+ error_code: Envelope.forwarded_code(exception.error_hash),
42
+ meta: result.body[:meta], config: config)
43
+ end
44
+ end
45
+
46
+ def presence(meta)
47
+ meta.is_a?(::Hash) && !meta.empty? ? meta : nil
48
+ end
49
+
50
+ def remote_extra(exception)
51
+ hash = exception.respond_to?(:error_hash) ? exception.error_hash : nil
52
+ hash.is_a?(::Hash) ? RemoteError.from(hash).extra : {}
53
+ end
54
+
55
+ def parse_message(message)
56
+ JSON.parse(message)
57
+ rescue ::StandardError
58
+ message
59
+ end
60
+
61
+ def event_reporter
62
+ ::Rails.event if defined?(::Rails) && ::Rails.respond_to?(:event)
63
+ end
64
+
65
+ def emit(name, status, config, payload)
66
+ level = Describer.level_for(status)
67
+ reporter = event_reporter
68
+ if reporter
69
+ reporter.tagged(level: level) { reporter.notify(name, payload) }
70
+ else
71
+ config.logger.public_send(level, JSON.generate({ event: name }.merge(payload)))
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module ZappbException
6
+ module Rack
7
+ module_function
8
+
9
+ def render(exception, meta: {}, notify: true, config: ZappbException.configuration)
10
+ result = Envelope.call(exception, meta: meta, config: config)
11
+ Notifier.notify(exception, result, config: config) if notify
12
+ [result.status, { "content-type" => "application/json" }, [JSON.generate(result.body.compact)]]
13
+ end
14
+ end
15
+ end