escalate 0.2.0.pre.1 → 0.3.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2a0dc76f8fb9f09d141c117b3b746b143078712c8d14687e9a413f9e0db0eee4
4
- data.tar.gz: 0a8861399988e40db304ff5bed43a5a3c6359eb2f0853f162515c5883e74c613
3
+ metadata.gz: 9844b8aaae39fe3b2e23417e7b39e1ec59ff8896e5a6f8be4e3737c3ccf8ce02
4
+ data.tar.gz: 7e03db14d0da2ec0e6458cd03a9da89cf949fce7d2c0a34bad65b69b7ede2409
5
5
  SHA512:
6
- metadata.gz: 95f0fd83333e4245756cee9c5fff725255a8b81e954102ad22e8d0d2c0f49bf27050c15154a00a309d4c0bce8f9edd7e5247aba3dfdc8b2f9b1fa64cbf281ab8
7
- data.tar.gz: 62f87cc4801ff0a5187c251ec2fe0e47433d52ab4bdc89558af29a57f6bbf50ee912c9e92127c4a152a5008cf1d7e3110735b8082ec540b6aac9e7d95bf2fa18
6
+ metadata.gz: e8eab0d9486440921db00178fa7560779976ed9e132377ca1dacea9960a9847f6eed01f964945f88719ef26583789b6bf1dad83f7dbcaef8c3625e83e8825c15
7
+ data.tar.gz: fa039e7c5c2c3e0c48e50840ba0831eb0f7a87d2902690b3740304f2b36b8aca91df8233c885c6e60ef9bd26fd0b4929604040082adefbdcbf0cbb14287a42ca
data/CHANGELOG.md CHANGED
@@ -4,7 +4,11 @@ All notable changes to this project will be documented in this file.
4
4
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5
5
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
- ## [0.2.0] - Unreleased
7
+ ## [0.3.0] - 2021-03-05
8
+ ### Added
9
+ - Added `rescue_and_escalate`.
10
+
11
+ ## [0.2.0] - 2021-03-02
8
12
  ### Added
9
13
  - Added support for `on_escalate(log_first: false)`. The `escalate` gem will log first before
10
14
  escalating if either of these is true: there are no `on_escalate` blocks registered, or
@@ -16,5 +20,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
16
20
  - Added `escalate` method for escalating rescued exceptions with default behavior of logging to `STDERR`
17
21
  - Added `Escalate.on_escalate` for registering escalation callbacks like `Honeybadger` or `Sentry`
18
22
 
23
+ [0.3.0]: https://github.com/Invoca/escalate/compare/v0.2.0...v0.3.0
19
24
  [0.2.0]: https://github.com/Invoca/escalate/compare/v0.1.0...v0.2.0
20
25
  [0.1.0]: https://github.com/Invoca/escalate/releases/tag/v0.1.0
data/Gemfile.lock CHANGED
@@ -1,8 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- escalate (0.2.0.pre.1)
5
- activesupport
4
+ escalate (0.3.0)
6
5
 
7
6
  GEM
8
7
  remote: https://rubygems.org/
data/README.md CHANGED
@@ -34,6 +34,7 @@ module SomeGem
34
34
  end
35
35
  ```
36
36
 
37
+ #### Using .escalate
37
38
  This will expose the `Escalate#escalate` method within your gem to be used instead
38
39
  of using `logger.error`.
39
40
 
@@ -46,20 +47,97 @@ module SomeGem
46
47
  end
47
48
 
48
49
  class SomeClass
49
- def something_dangerous
50
- # ...
51
- rescue => ex
52
- SomeGem.escalate(ex, "I was doing something dangerous and an exception was raised")
50
+ def something_dangerous(key)
51
+ # code here...
52
+ rescue => exception
53
+ SomeGem.escalate(exception,
54
+ "Exception raised in SomeGem::SomeClass#something_dangerous",
55
+ context: { key: key })
53
56
  end
54
57
  end
55
58
  end
56
59
  ```
60
+ The following arguments are supported by `.escalate`:
61
+
62
+ | argument | default | type | description |
63
+ |------------|---|----|-----|
64
+ | `exception` | | `Exception` | The exception to escalate. |
65
+ | `location_message` | | `String` | A message providing information about where and why this exception is being escalated. |
66
+ | `context:` | `{}` | `Hash` | An optional hash of context. This will be logged with the exception. |
57
67
 
58
68
  When `SomeGem.escalate` above is triggered, it will use the logger returned by `SomeGem.logger` or
59
69
  default to a `STDERR` logger and do the following:
60
70
 
61
- 1. Log an error containing the exception and any additional information about the current environment that is specified
62
- 2. Trigger any `escalation_callbacks` configured on the `Escalate` gem
71
+ 1. [optional] Log an error containing the exception, location_message, and context hash
72
+ 2. Trigger any `on_escalate_callbacks` configured on the `Escalate` gem
73
+
74
+ Step (1) is optional. It will happen if either of these is true:
75
+ - by default if no `on_escalate_callbacks` have been registered; or
76
+ - if any of the `on_escalate_callbacks` was registered with `on_escalate(log_first: true)`.
77
+
78
+ #### Using .rescue_and_escalate
79
+ The above pattern of `rescue` with `escalate` is very common, so a single method is provided to do both.
80
+ This is equivalent to the code above:
81
+ ```
82
+ class SomeClass
83
+ def something_dangerous(key)
84
+ SomeGem.rescue_and_escalate("Exception raised in SomeGem::SomeClass#something_dangerous",
85
+ context: { key: key }) do
86
+ # code here...
87
+ end
88
+ end
89
+ end
90
+ ```
91
+ The following arguments are supported by `.rescue_and_escalate`:
92
+
93
+ | argument | default | type | description |
94
+ |-------------|---|----|-----|
95
+ | `location_message` | _required_ | `String` | A message providing information about where and why this exception is being escalated. |
96
+ | `context:` | `{}` | `Hash` | An optional hash of context. This will be logged with the exception. |
97
+ | `exceptions:` | `StandardError` | `Class` or `Array(Class)` | The `Class` or `Array(Class)` to rescue. `Class` must be `Exception` or a sub-class. |
98
+ | `pass_through_exceptions:` | `[SystemExit, SystemStackError, NoMemoryError, SecurityError, SignalException]` | `Class` or `Array(Class)` | The `Class` or `Array(Class)` to pass through without rescuing. `Class` must be `Exception`, or a sub-class. These take precedence over `exceptions:`.|
99
+
100
+ ----------------------------
101
+
102
+ ### Registering an Escalate Callback
103
+
104
+ If you are using an error reporting service, you can register an `on_escalate` callback to escalate exceptions.
105
+ You have the option to handle logging yourself, or to let `escalate` log first, before yielding to callbacks.
106
+
107
+ The following arguments are supported by `.on_escalate`:
108
+
109
+ | argument | default | type | description |
110
+ |-------------|---|----|-----|
111
+ | `name:` | `block.source_location` | `String` or `Array` | Globally unique name for this callback. |
112
+ | `log_first:` | `true` | `Boolean` | Whether `escalate` should log the error first. `false` means the block will take care of this. |
113
+ | `&block` | _required_ | `Proc` | The callback block to yield to from `escalate`. |
114
+
115
+ #### Leave the Logging to the Escalate Gem
116
+ Here is an example that uses the default `log_first: true` so that logging is handled by the `Escalate` gem first:
117
+ ```
118
+ Escalate.on_escalate do |exception, location_message, **context|
119
+ # send exception, location_message, **context to the error reporting service here
120
+ end
121
+ ```
122
+
123
+ #### Callback Uniqueness
124
+ Each callback may be named with the `name:` keyword argument.
125
+ If a callback with the same name has been registered before, it will be overwritten with the new one.
126
+ ```
127
+ Escalate.on_escalate(name: 'abc gem') do |exception, location_message, **context|
128
+ # send exception, location_message, **context to the error reporting service here
129
+ end
130
+ ```
131
+ If not given, the `name:` defaults to the `.source_location` property of the passed-in block.
132
+
133
+ #### Handle the Logging in the `on_escalate` Callback
134
+ Here is an example that handles logging itself with `log_first: false`:
135
+ ```
136
+ Escalate.on_escalate(log_first: false) do |exception, location_message, **context|
137
+ # log here first
138
+ # send exception, location_message, **context to the error reporting service here
139
+ end
140
+ ```
63
141
  ## Development
64
142
 
65
143
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
data/escalate.gemspec CHANGED
@@ -26,6 +26,4 @@ Gem::Specification.new do |spec|
26
26
  end
27
27
  spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
28
28
  spec.require_paths = ["lib"]
29
-
30
- spec.add_dependency "activesupport"
31
29
  end
data/lib/escalate.rb CHANGED
@@ -9,7 +9,16 @@ require_relative "escalate/mixin"
9
9
  module Escalate
10
10
  class Error < StandardError; end
11
11
 
12
+ LOG_FIRST_INSTANCE_VARIABLE = :@_escalate_log_first
13
+
14
+ DEFAULT_RESCUE_EXCEPTIONS = [StandardError].freeze
15
+ DEFAULT_PASS_THROUGH_EXCEPTIONS = [SystemExit, SystemStackError, NoMemoryError, SecurityError, SignalException].freeze
16
+
17
+ @on_escalate_callbacks = {}
18
+
12
19
  class << self
20
+ attr_reader :on_escalate_callbacks
21
+
13
22
  # Logs and escalated an exception
14
23
  #
15
24
  # @param [Exception] exception
@@ -23,23 +32,24 @@ module Escalate
23
32
  #
24
33
  # @param [Hash] context
25
34
  # Any additional context to be tied to the escalation
26
- def escalate(exception, location_message, logger, **context)
35
+ def escalate(exception, location_message, logger, context: {})
27
36
  ensure_failsafe("Exception rescued while escalating #{exception.inspect}") do
28
- if on_escalate_blocks.any? || on_escalate_no_log_first_blocks.none?
37
+ if on_escalate_callbacks.none? || on_escalate_callbacks.values.any? { |block| block.instance_variable_get(LOG_FIRST_INSTANCE_VARIABLE) }
38
+ logger_allows_added_context?(logger) or context_string = " (#{context.inspect})"
29
39
  error_message = <<~EOS
30
- [Escalate] #{location_message} (#{context.inspect})
40
+ [Escalate] #{location_message}#{context_string}
31
41
  #{exception.class.name}: #{exception.message}
32
42
  #{exception.backtrace.join("\n")}
33
43
  EOS
34
44
 
35
- if logger_allows_added_context?(logger)
36
- logger.error(error_message, **context)
37
- else
45
+ if context_string
38
46
  logger.error(error_message)
47
+ else
48
+ logger.error(error_message, **context)
39
49
  end
40
50
  end
41
51
 
42
- all_on_escalate_blocks.each do |block|
52
+ on_escalate_callbacks.values.each do |block|
43
53
  ensure_failsafe("Exception rescued while escalating #{exception.inspect} to #{block.inspect}") do
44
54
  block.call(exception, location_message, **context)
45
55
  end
@@ -65,8 +75,21 @@ module Escalate
65
75
 
66
76
  attr_accessor :escalate_logger_block
67
77
 
68
- def escalate(exception, location_message, **context)
69
- Escalate.escalate(exception, location_message, escalate_logger, **context)
78
+ def escalate(exception, location_message, context: {})
79
+ Escalate.escalate(exception, location_message, escalate_logger, context: context)
80
+ end
81
+
82
+ def rescue_and_escalate(location_message, context: {},
83
+ exceptions: DEFAULT_RESCUE_EXCEPTIONS,
84
+ pass_through_exceptions: DEFAULT_PASS_THROUGH_EXCEPTIONS,
85
+ &block)
86
+
87
+ yield
88
+
89
+ rescue *Array(pass_through_exceptions)
90
+ raise
91
+ rescue *Array(exceptions) => exception
92
+ escalate(exception, location_message, context: context)
70
93
  end
71
94
 
72
95
  private
@@ -81,22 +104,21 @@ module Escalate
81
104
  end
82
105
  end
83
106
 
84
- # Registers an escalation callback to be executed when `escalate`
85
- # is invoked.
107
+ # Registers an escalation callback to be executed when `escalate` is invoked.
86
108
  #
87
109
  # @param [boolean] log_first: true
88
110
  # whether escalate should log first before escalating, or leave the logging to the escalate block
89
- def on_escalate(log_first: true, &block)
90
- if log_first
91
- on_escalate_blocks.add(block)
92
- else
93
- on_escalate_no_log_first_blocks.add(block)
94
- end
111
+ # @param [string | Array] name:
112
+ # unique name for this callback block
113
+ # any previously-registered block with the same name will be discarded
114
+ # if not provided, name defaults to `block.source_location`
115
+ def on_escalate(log_first: true, name: nil, &block)
116
+ block.instance_variable_set(LOG_FIRST_INSTANCE_VARIABLE, log_first)
117
+ on_escalate_callbacks[name || block.source_location] = block
95
118
  end
96
119
 
97
- def clear_all_on_escalate_blocks
98
- on_escalate_blocks.clear
99
- on_escalate_no_log_first_blocks.clear
120
+ def clear_on_escalate_callbacks
121
+ on_escalate_callbacks.clear
100
122
  end
101
123
 
102
124
  private
@@ -111,17 +133,5 @@ module Escalate
111
133
  defined?(ContextualLogger::LoggerMixin) &&
112
134
  logger.is_a?(ContextualLogger::LoggerMixin)
113
135
  end
114
-
115
- def on_escalate_blocks
116
- @on_escalate_blocks ||= Set.new
117
- end
118
-
119
- def on_escalate_no_log_first_blocks
120
- @on_escalate_no_log_first_blocks ||= Set.new
121
- end
122
-
123
- def all_on_escalate_blocks
124
- on_escalate_blocks + on_escalate_no_log_first_blocks
125
- end
126
136
  end
127
137
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Escalate
4
- VERSION = "0.2.0.pre.1"
4
+ VERSION = "0.3.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: escalate
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0.pre.1
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Invoca Development
@@ -9,22 +9,8 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2021-02-25 00:00:00.000000000 Z
13
- dependencies:
14
- - !ruby/object:Gem::Dependency
15
- name: activesupport
16
- requirement: !ruby/object:Gem::Requirement
17
- requirements:
18
- - - ">="
19
- - !ruby/object:Gem::Version
20
- version: '0'
21
- type: :runtime
22
- prerelease: false
23
- version_requirements: !ruby/object:Gem::Requirement
24
- requirements:
25
- - - ">="
26
- - !ruby/object:Gem::Version
27
- version: '0'
12
+ date: 2021-03-05 00:00:00.000000000 Z
13
+ dependencies: []
28
14
  description: A simple and lightweight gem to escalate rescued exceptions.
29
15
  email:
30
16
  - development@invoca.com
@@ -67,9 +53,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
67
53
  version: 2.4.0
68
54
  required_rubygems_version: !ruby/object:Gem::Requirement
69
55
  requirements:
70
- - - ">"
56
+ - - ">="
71
57
  - !ruby/object:Gem::Version
72
- version: 1.3.1
58
+ version: '0'
73
59
  requirements: []
74
60
  rubygems_version: 3.1.2
75
61
  signing_key: