escalate 0.2.0.pre.1 → 0.3.0

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