axn 0.1.0.pre.alpha.2.5.3.1 → 0.1.0.pre.alpha.2.6.1
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 +4 -4
- data/.rubocop.yml +4 -1
- data/CHANGELOG.md +25 -1
- data/README.md +2 -11
- data/docs/reference/action-result.md +2 -0
- data/docs/reference/class.md +12 -4
- data/docs/reference/configuration.md +53 -20
- data/docs/reference/instance.md +2 -2
- data/docs/strategies/index.md +1 -1
- data/docs/usage/setup.md +1 -1
- data/docs/usage/writing.md +9 -9
- data/lib/action/attachable/steps.rb +16 -1
- data/lib/action/attachable.rb +3 -3
- data/lib/action/{core/configuration.rb → configuration.rb} +3 -4
- data/lib/action/context.rb +38 -0
- data/lib/action/core/automatic_logging.rb +93 -0
- data/lib/action/core/context/facade.rb +69 -0
- data/lib/action/core/context/facade_inspector.rb +63 -0
- data/lib/action/core/context/internal.rb +32 -0
- data/lib/action/core/contract.rb +167 -211
- data/lib/action/core/contract_for_subfields.rb +84 -82
- data/lib/action/core/contract_validation.rb +62 -0
- data/lib/action/core/flow/callbacks.rb +54 -0
- data/lib/action/core/flow/exception_execution.rb +79 -0
- data/lib/action/core/flow/messages.rb +61 -0
- data/lib/action/core/flow.rb +19 -0
- data/lib/action/core/hoist_errors.rb +42 -40
- data/lib/action/core/hooks.rb +123 -0
- data/lib/action/core/logging.rb +22 -20
- data/lib/action/core/timing.rb +40 -0
- data/lib/action/core/tracing.rb +17 -0
- data/lib/action/core/use_strategy.rb +19 -17
- data/lib/action/core/validation/fields.rb +2 -0
- data/lib/action/core.rb +100 -0
- data/lib/action/enqueueable/via_sidekiq.rb +2 -2
- data/lib/action/enqueueable.rb +1 -1
- data/lib/action/{core/exceptions.rb → exceptions.rb} +1 -19
- data/lib/action/result.rb +95 -0
- data/lib/axn/factory.rb +27 -9
- data/lib/axn/version.rb +1 -1
- data/lib/axn.rb +10 -47
- metadata +19 -21
- data/lib/action/core/context_facade.rb +0 -209
- data/lib/action/core/handle_exceptions.rb +0 -163
- data/lib/action/core/top_level_around_hook.rb +0 -108
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 30c00229f0062bdba5979d0e69de4ffddb6bece3f31413f83b7f7778d3075ef7
|
4
|
+
data.tar.gz: 92d944caf300ddfc9a134d34ae2cb73e6d235e0af9f7a10a3ee93065ae834690
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 600b1572f324b8cbbaf2b5dd48108546db86e63e3c7d49a04e1eb9f0f2e83720eafdaabb94ccf0550ff1c0e63b3ec555950443eb5c064698dbc65cf97c2b1733
|
7
|
+
data.tar.gz: e9229f25cd10dcc54f1f7c5b83ff81b997d96263296818caaf2cfc245fde93ab24d7f0605700ea042c085ab7456f5fc1b20c2cc643324609bc0f4662ecbdcd21
|
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -3,6 +3,30 @@
|
|
3
3
|
## Unreleased
|
4
4
|
* N/A
|
5
5
|
|
6
|
+
## 0.1.0-alpha.2.6.1
|
7
|
+
* [FEAT] Added `elapsed_time` and `outcome` methods to `Action::Result`
|
8
|
+
* `elapsed_time` returns execution time in milliseconds (Float)
|
9
|
+
* `outcome` returns execution outcome as symbol (`:success`, `:failure`, or `:exception`)
|
10
|
+
* [BREAKING] `emit_metrics` hook now receives the full `Action::Result` object instead of just the outcome
|
11
|
+
* Provides access to both outcome and elapsed time for richer metrics
|
12
|
+
* Example: `proc { |resource, result| TS::Metrics.histogram("action.duration", result.elapsed_time) }`
|
13
|
+
* [BREAKING] Replaced `Action.config.default_log_level` and `default_autolog_level` with simpler `log_level`
|
14
|
+
* [BREAKING] `autolog_level` method overrides with e.g. `auto_log :warn` or `auto_log false`
|
15
|
+
* [BREAKING] Direct access to exposed fields in callables no longer works -- `foo` becomes `result.foo`
|
16
|
+
* [BREAKING] Removed `success?` check on Action::Result (use `ok?` instead)
|
17
|
+
* [FEAT] Added callback and strategy support to Axn::Factory.build
|
18
|
+
|
19
|
+
## 0.1.0-alpha.2.6
|
20
|
+
* Inline interactor code (no more dependency on unpublished forked branch to support inheritance)
|
21
|
+
* Refactor internals to clean implementation now that we have direct control
|
22
|
+
* [BREAKING] Replaced `Action.config.top_level_around_hook` with `.wrap_with_trace` and `.emit_metrics`
|
23
|
+
* [BREAKING] the order of hooks with inheritance has changed to more intuitively follow the natural pattern of setup (general → specific) and teardown (specific → general):
|
24
|
+
* **Before hooks**: Parent → Child (general setup first, then specific)
|
25
|
+
* **After hooks**: Child → Parent (specific cleanup first, then general)
|
26
|
+
* **Around hooks**: Parent wraps child (parent outside, child inside)
|
27
|
+
* Removed non-functional #rollback traces (use on_exception hook instead)
|
28
|
+
* Clean requires structure
|
29
|
+
|
6
30
|
## 0.1.0-alpha.2.5.3.1
|
7
31
|
* Remove explicit 'require rspec' from `axn/testing/spec_helpers` (must already be loaded)
|
8
32
|
|
@@ -13,7 +37,7 @@
|
|
13
37
|
## 0.1.0-alpha.2.5.2
|
14
38
|
* [BREAKING] Removing `EnqueueAllInBackground` + `EnqueueAllWorker` - better + simply solved at application level
|
15
39
|
* [TEST] Expose spec helpers to consumers (add `require "axn/testing/spec_helpers"` to your `spec_helper.rb`)
|
16
|
-
|
40
|
+
* [FEAT] Added ability to use custom Strategies (via e.g. `use :transaction`)
|
17
41
|
|
18
42
|
## 0.1.0-alpha.2.5.1.2
|
19
43
|
* [BUGFIX] Subfield expectations: now support hashes with string keys (using with_indifferent_access)
|
data/README.md
CHANGED
@@ -1,20 +1,11 @@
|
|
1
1
|
# Axn -- [AHK-sin] (a.k.a. "Action")
|
2
2
|
|
3
|
-
Just spinning this up -- not yet released
|
3
|
+
Just spinning this up -- not yet publicly released, changes coming frequently.
|
4
4
|
|
5
5
|
## Installation & Usage
|
6
6
|
|
7
7
|
See our [User Guide](https://teamshares.github.io/axn/) for details.
|
8
8
|
|
9
|
-
## [!!] Inheritance Support
|
10
|
-
|
11
|
-
Out of the box Axn only supports a direct style (every action must `include Action`).
|
12
|
-
|
13
|
-
If you want to support inheritance, you'll need to add this line to your `Gemfile` (we're layered over Interactor, and their released version doesn't yet support inheritance):
|
14
|
-
|
15
|
-
gem "interactor", github: "kaspermeyer/interactor", branch: "fix-hook-inheritance"
|
16
|
-
|
17
|
-
|
18
9
|
## Development
|
19
10
|
|
20
11
|
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.
|
@@ -27,4 +18,4 @@ See our [contribution guidelines](CONTRIBUTING.md) for more information.
|
|
27
18
|
|
28
19
|
## Thank You
|
29
20
|
|
30
|
-
A very special thank you to [Collective Idea](https://collectiveidea.com/)'s fantastic [Interactor](https://github.com/collectiveidea/interactor?tab=readme-ov-file#interactor) library, which [we](https://www.teamshares.com/) used successfully for a number of years and which
|
21
|
+
A very special thank you to [Collective Idea](https://collectiveidea.com/)'s fantastic [Interactor](https://github.com/collectiveidea/interactor?tab=readme-ov-file#interactor) library, which [we](https://www.teamshares.com/) used successfully for a number of years and which we used to scaffold early versions of this library.
|
@@ -9,6 +9,8 @@ Every `call` invocation on an Action will return an `Action::Result` instance, w
|
|
9
9
|
| `success` | User-facing success message (string), if `ok?` (else nil)
|
10
10
|
| `message` | User-facing message (string), always defined (`ok? ? success : error`)
|
11
11
|
| `exception` | If not `ok?` because an exception was swallowed, will be set to the swallowed exception (note: rarely used outside development; prefer to let the library automatically handle exception handling for you)
|
12
|
+
| `outcome` | The execution outcome as a symbol (`:success`, `:failure`, or `:exception`)
|
13
|
+
| `elapsed_time` | Execution time in milliseconds (Float)
|
12
14
|
| any `expose`d values | guaranteed to be set if `ok?` (since they have outgoing presence validations by default; any missing would have failed the action)
|
13
15
|
|
14
16
|
NOTE: `success` and `error` (and so implicitly `message`) can be configured per-action via [the `messages` declaration](/reference/class#messages).
|
data/docs/reference/class.md
CHANGED
@@ -89,10 +89,16 @@ will succeed if given _either_ an actual Date object _or_ a string that Date.par
|
|
89
89
|
|
90
90
|
The `messages` declaration allows you to customize the `error` and `success` messages on the returned result.
|
91
91
|
|
92
|
-
Accepts `error` and/or `success` keys. Values can be a string (returned directly) or a callable (evaluated in the action's context, so can access instance methods). If `error` is provided with a callable that expects a positional argument, the exception that was raised will be passed in as that value.
|
92
|
+
Accepts `error` and/or `success` keys. Values can be a string (returned directly) or a callable (evaluated in the action's context, so can access instance methods and variables). If `error` is provided with a callable that expects a positional argument, the exception that was raised will be passed in as that value.
|
93
|
+
|
94
|
+
In callables, you can access:
|
95
|
+
- **Input data**: Use field names directly (e.g., `name`)
|
96
|
+
- **Output data**: Use `result.field` pattern (e.g., `result.greeting`)
|
97
|
+
- **Instance methods and variables**: Direct access
|
93
98
|
|
94
99
|
```ruby
|
95
|
-
messages success:
|
100
|
+
messages success: -> { "Hello #{name}, your greeting: #{result.greeting}" },
|
101
|
+
error: ->(e) { "Bad news: #{e.message}" }
|
96
102
|
```
|
97
103
|
|
98
104
|
## `error_from` and `rescues`
|
@@ -101,15 +107,17 @@ While `.messages` sets the _default_ error/success messages and is more commonly
|
|
101
107
|
|
102
108
|
`error_from` and `rescues` both register a matcher (exception class, exception class name (string), or callable) and a message to use if the matcher succeeds. They act exactly the same, except if a matcher registered with `rescues` succeeds, the exception _will not_ trigger the configured exception handlers (global or specific to this class).
|
103
109
|
|
110
|
+
Callable matchers and messages follow the same data access patterns as other callables: input fields directly, output fields via `result.field`, instance variables, and methods.
|
111
|
+
|
104
112
|
```ruby
|
105
113
|
messages error: "bad"
|
106
114
|
|
107
115
|
# Note this will NOT trigger Action.config.on_exception
|
108
116
|
rescues ActiveRecord::InvalidRecord => "Invalid params provided"
|
109
117
|
|
110
|
-
# These WILL trigger error handler (
|
118
|
+
# These WILL trigger error handler (callable matcher + message with data access)
|
111
119
|
error_from ArgumentError, ->(e) { "Argument error: #{e.message}" }
|
112
|
-
error_from -> { name == "bad" }, -> { "
|
120
|
+
error_from -> { name == "bad" }, -> { "Bad input #{name}, result: #{result.status}" }
|
113
121
|
```
|
114
122
|
|
115
123
|
## Callbacks
|
@@ -4,18 +4,16 @@ Somewhere at boot (e.g. `config/initializers/actions.rb` in Rails), you can call
|
|
4
4
|
|
5
5
|
|
6
6
|
```ruby
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
c.default_autolog_level = :debug
|
16
|
-
|
17
|
-
c.logger = ...
|
7
|
+
Action.configure do |c|
|
8
|
+
c.log_level = :info
|
9
|
+
c.logger = ...
|
10
|
+
c.on_exception = proc do |e, action:, context:|
|
11
|
+
message = "[#{action.class.name}] Failing due to #{e.class.name}: #{e.message}"
|
12
|
+
|
13
|
+
Rails.logger.warn(message)
|
14
|
+
Honeybadger.notify(message, context: { axn_context: context })
|
18
15
|
end
|
16
|
+
end
|
19
17
|
```
|
20
18
|
|
21
19
|
## `on_exception`
|
@@ -63,24 +61,36 @@ A couple notes:
|
|
63
61
|
|
64
62
|
If you're using an APM provider, observability can be greatly enhanced by adding automatic _tracing_ of Action calls and/or emitting count metrics after each call completes.
|
65
63
|
|
64
|
+
### Tracing and Metrics
|
65
|
+
|
66
|
+
The framework provides two distinct hooks for observability:
|
67
|
+
|
68
|
+
- **`wrap_with_trace`**: An around hook that wraps the entire action execution. You MUST call the provided block to execute the action.
|
69
|
+
- **`emit_metrics`**: A post-execution hook that receives the action result. Do NOT call any blocks.
|
70
|
+
|
66
71
|
For example, to wire up Datadog:
|
67
72
|
|
68
73
|
```ruby
|
69
74
|
Action.configure do |c|
|
70
|
-
c.
|
75
|
+
c.wrap_with_trace = proc do |resource, &action|
|
71
76
|
Datadog::Tracing.trace("Action", resource:) do
|
72
|
-
|
73
|
-
|
74
|
-
TS::Metrics.increment("action.#{resource.underscore}", tags: { outcome:, resource: })
|
77
|
+
action.call
|
75
78
|
end
|
76
79
|
end
|
80
|
+
|
81
|
+
c.emit_metrics = proc do |resource, result|
|
82
|
+
TS::Metrics.increment("action.#{resource.underscore}", tags: { outcome: result.outcome, resource: })
|
83
|
+
TS::Metrics.histogram("action.duration", result.elapsed_time, tags: { resource: })
|
84
|
+
end
|
77
85
|
end
|
78
86
|
```
|
79
87
|
|
80
88
|
A couple notes:
|
81
89
|
|
82
90
|
* `Datadog::Tracing` is provided by [the datadog gem](https://rubygems.org/gems/datadog)
|
83
|
-
* `TS::Metrics` is a custom implementation to set a Datadog count metric, but the relevant part to note is that outcome (`success`, `failure`, `exception`)
|
91
|
+
* `TS::Metrics` is a custom implementation to set a Datadog count metric, but the relevant part to note is that the result object provides access to the outcome (`success`, `failure`, `exception`) and elapsed time of the action.
|
92
|
+
* The `wrap_with_trace` hook is an around hook - you must call the provided block to execute the action
|
93
|
+
* The `emit_metrics` hook is called after execution with the result - do not call any blocks
|
84
94
|
|
85
95
|
|
86
96
|
## `logger`
|
@@ -101,11 +111,11 @@ For example:
|
|
101
111
|
|
102
112
|
For a practical example of this in practice, see [our 'memoization' recipe](/recipes/memoization).
|
103
113
|
|
104
|
-
## `
|
114
|
+
## `log_level`
|
105
115
|
|
106
|
-
Sets the log level used when you call `log "Some message"` in your Action. Note this is read via a `
|
116
|
+
Sets the log level used when you call `log "Some message"` in your Action. Note this is read via a `log_level` class method, so you can easily use inheritance to support different log levels for different sets of actions.
|
107
117
|
|
108
|
-
##
|
118
|
+
## Automatic Logging
|
109
119
|
|
110
120
|
By default, every `action.call` will emit log lines when it is called and after it completes:
|
111
121
|
|
@@ -114,4 +124,27 @@ By default, every `action.call` will emit log lines when it is called and after
|
|
114
124
|
[YourCustomAction] Execution completed (with outcome: success) in 0.957 milliseconds
|
115
125
|
```
|
116
126
|
|
117
|
-
|
127
|
+
Automatic logging will log at `Action.config.log_level` by default, but can be overridden or disabled using the declarative `auto_log` method:
|
128
|
+
|
129
|
+
```ruby
|
130
|
+
# Set default for all actions (affects both explicit logging and automatic logging)
|
131
|
+
Action.configure do |c|
|
132
|
+
c.log_level = :debug
|
133
|
+
end
|
134
|
+
|
135
|
+
# Override for specific actions
|
136
|
+
class MyAction
|
137
|
+
auto_log :warn # Use warn level for this action
|
138
|
+
end
|
139
|
+
|
140
|
+
class SilentAction
|
141
|
+
auto_log false # Disable automatic logging for this action
|
142
|
+
end
|
143
|
+
|
144
|
+
# Use default level (no auto_log call needed)
|
145
|
+
class DefaultAction
|
146
|
+
# Uses Action.config.log_level
|
147
|
+
end
|
148
|
+
```
|
149
|
+
|
150
|
+
The `auto_log` method supports inheritance, so subclasses will inherit the setting from their parent class unless explicitly overridden.
|
data/docs/reference/instance.md
CHANGED
@@ -12,7 +12,7 @@ Primarily used for its side effects, but it does return a Hash with the key/valu
|
|
12
12
|
|
13
13
|
## `#fail!`
|
14
14
|
|
15
|
-
Called with a string, it immediately halts execution
|
15
|
+
Called with a string, it immediately halts execution and sets `result.error` to the provided string.
|
16
16
|
|
17
17
|
## `#log`
|
18
18
|
|
@@ -31,7 +31,7 @@ A few details:
|
|
31
31
|
* An explicit `fail!` call _will_ still fail the action
|
32
32
|
* Any exceptions swallowed _will_ still be reported via the `on_exception` handler
|
33
33
|
|
34
|
-
This is primarily useful in an after block, e.g. trigger notifications after an action has been taken. If the notification fails to send you DO want to log the failure somewhere to investigate, but since the core action has already been taken often you do _not_ want to fail
|
34
|
+
This is primarily useful in an after block, e.g. trigger notifications after an action has been taken. If the notification fails to send you DO want to log the failure somewhere to investigate, but since the core action has already been taken often you do _not_ want to fail.
|
35
35
|
|
36
36
|
Example:
|
37
37
|
|
data/docs/strategies/index.md
CHANGED
@@ -155,7 +155,7 @@ Action::Strategies.register(:retry, RetryStrategy)
|
|
155
155
|
|
156
156
|
### Example: Complete Custom Strategy
|
157
157
|
|
158
|
-
Here's a complete example of a custom strategy that adds performance monitoring:
|
158
|
+
Here's a complete example of a custom strategy that adds performance monitoring (note Axn already logs elapsed time, this is just a toy example):
|
159
159
|
|
160
160
|
```ruby
|
161
161
|
module PerformanceMonitoringStrategy
|
data/docs/usage/setup.md
CHANGED
@@ -19,6 +19,6 @@ By default any swallowed errors are noted in the logs, but it's _highly recommen
|
|
19
19
|
|
20
20
|
### Metrics / Tracing
|
21
21
|
|
22
|
-
If you're using an APM provider, observability can be greatly enhanced by [configuring
|
22
|
+
If you're using an APM provider, observability can be greatly enhanced by [configuring tracing and metrics hooks](/reference/configuration#tracing-and-metrics).
|
23
23
|
|
24
24
|
|
data/docs/usage/writing.md
CHANGED
@@ -79,7 +79,7 @@ class Foo
|
|
79
79
|
expects :name, type: String
|
80
80
|
exposes :meaning_of_life
|
81
81
|
|
82
|
-
messages success: -> { "Revealed
|
82
|
+
messages success: -> { "Revealed to #{name}: #{result.meaning_of_life}" }, # [!code focus:2]
|
83
83
|
error: ->(e) { "No secret of life for you: #{e.message}" }
|
84
84
|
|
85
85
|
def call
|
@@ -104,18 +104,11 @@ Foo.call(name: "Adams").meaning_of_life # => "Hello Adams, the meaning of life i
|
|
104
104
|
|
105
105
|
In addition to `#call`, there are a few additional pieces to be aware of:
|
106
106
|
|
107
|
-
<!-- ### `#rollback`
|
108
|
-
*** TODO: rollback actually only applies to rolling back *completed* steps of a multi-step Axn chain. Do not document for now -- need to decide if adding a trigger-when-axn-itself-fails rollback path. ***
|
109
107
|
|
110
|
-
::: danger ALPHA
|
111
|
-
* ⚠️ `#rollback` is _expected_ to be added shortly, but is not yet functional!
|
112
|
-
:::
|
113
|
-
|
114
|
-
If you define a `#rollback` method, it'll be called (_before_ returning an `Action::Result` to the caller) whenever your action fails. -->
|
115
108
|
|
116
109
|
### Hooks
|
117
110
|
|
118
|
-
`before` and `
|
111
|
+
`before`, `after`, and `around` hooks are supported. They can receive a block directly, or the symbol name of a local method.
|
119
112
|
|
120
113
|
Note execution is halted whenever `fail!` is called or an exception is raised (so a `before` block failure won't execute `call` or `after`, while an `after` block failure will make `result.ok?` be false even though `call` completed successfully).
|
121
114
|
|
@@ -150,6 +143,13 @@ in call
|
|
150
143
|
after hook
|
151
144
|
```
|
152
145
|
|
146
|
+
**Hook Ordering with Inheritance:**
|
147
|
+
- **Around hooks**: Parent wraps child (parent outside, child inside)
|
148
|
+
- **Before hooks**: Parent → Child (general setup first, then specific)
|
149
|
+
- **After hooks**: Child → Parent (specific cleanup first, then general)
|
150
|
+
|
151
|
+
This follows the natural pattern of setup (general → specific) and teardown (specific → general).
|
152
|
+
|
153
153
|
### Callbacks
|
154
154
|
|
155
155
|
A number of custom callback are available for you as well, if you want to take specific actions when a given Axn succeeds or fails. See the [Class Interface docs](/reference/class#callbacks) for details.
|
@@ -38,10 +38,25 @@ module Action
|
|
38
38
|
step = Entry.new(label: "Step #{idx + 1}", axn: step) if step.is_a?(Class)
|
39
39
|
|
40
40
|
hoist_errors(prefix: "#{step.label} step") do
|
41
|
-
step.axn.call(
|
41
|
+
step.axn.call(**merged_context_data).tap do |step_result|
|
42
|
+
merge_step_exposures!(step_result)
|
43
|
+
end
|
42
44
|
end
|
43
45
|
end
|
44
46
|
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def merged_context_data
|
51
|
+
@__context.__combined_data
|
52
|
+
end
|
53
|
+
|
54
|
+
# Each step can expect the data exposed from the previous steps
|
55
|
+
def merge_step_exposures!(step_result)
|
56
|
+
step_result.declared_fields.each do |field|
|
57
|
+
@__context.exposed_data[field] = step_result.public_send(field)
|
58
|
+
end
|
59
|
+
end
|
45
60
|
end
|
46
61
|
end
|
47
62
|
end
|
data/lib/action/attachable.rb
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
3
|
+
require "action/attachable/base"
|
4
|
+
require "action/attachable/steps"
|
5
|
+
require "action/attachable/subactions"
|
6
6
|
|
7
7
|
module Action
|
8
8
|
module Attachable
|
@@ -2,11 +2,10 @@
|
|
2
2
|
|
3
3
|
module Action
|
4
4
|
class Configuration
|
5
|
-
attr_accessor :
|
6
|
-
attr_writer :logger, :env, :on_exception, :additional_includes, :
|
5
|
+
attr_accessor :wrap_with_trace, :emit_metrics
|
6
|
+
attr_writer :logger, :env, :on_exception, :additional_includes, :log_level
|
7
7
|
|
8
|
-
def
|
9
|
-
def default_autolog_level = @default_autolog_level ||= :info
|
8
|
+
def log_level = @log_level ||= :info
|
10
9
|
|
11
10
|
def additional_includes = @additional_includes ||= []
|
12
11
|
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Action
|
4
|
+
class Context
|
5
|
+
attr_accessor :provided_data, :exposed_data
|
6
|
+
|
7
|
+
def initialize(**provided_data)
|
8
|
+
@provided_data = provided_data
|
9
|
+
@exposed_data = {}
|
10
|
+
|
11
|
+
# Framework-managed fields
|
12
|
+
@failure = false
|
13
|
+
@exception = nil
|
14
|
+
@error_from_user = nil
|
15
|
+
@error_prefix = nil
|
16
|
+
@elapsed_time = nil
|
17
|
+
end
|
18
|
+
|
19
|
+
def fail!(message = nil)
|
20
|
+
@error_from_user = message if message.present?
|
21
|
+
raise Action::Failure, message
|
22
|
+
end
|
23
|
+
|
24
|
+
# INTERNAL: base for further filtering (for logging) or providing user with usage hints
|
25
|
+
def __combined_data = @provided_data.merge(@exposed_data)
|
26
|
+
|
27
|
+
# Framework state methods
|
28
|
+
def ok? = !@failure
|
29
|
+
def failed? = @failure || false
|
30
|
+
|
31
|
+
# Framework field accessors
|
32
|
+
attr_accessor :exception, :error_from_user, :error_prefix, :elapsed_time
|
33
|
+
|
34
|
+
# Internal failure state setter (for framework use)
|
35
|
+
attr_writer :failure
|
36
|
+
private :failure=
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Action
|
4
|
+
module Core
|
5
|
+
module AutomaticLogging
|
6
|
+
def self.included(base)
|
7
|
+
base.class_eval do
|
8
|
+
extend ClassMethods
|
9
|
+
include InstanceMethods
|
10
|
+
|
11
|
+
# Single class_attribute - nil means disabled, any level means enabled
|
12
|
+
class_attribute :auto_log_level, default: Action.config.log_level
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
module ClassMethods
|
17
|
+
def auto_log(level)
|
18
|
+
self.auto_log_level = level.presence
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
module InstanceMethods
|
23
|
+
private
|
24
|
+
|
25
|
+
def _with_logging
|
26
|
+
_log_before if self.class.auto_log_level
|
27
|
+
yield
|
28
|
+
ensure
|
29
|
+
_log_after if self.class.auto_log_level
|
30
|
+
end
|
31
|
+
|
32
|
+
def _log_before
|
33
|
+
level = self.class.auto_log_level
|
34
|
+
return unless level
|
35
|
+
|
36
|
+
self.class.public_send(
|
37
|
+
level,
|
38
|
+
[
|
39
|
+
"About to execute",
|
40
|
+
_log_context(:inbound),
|
41
|
+
].compact.join(" with: "),
|
42
|
+
before: Action.config.env.production? ? nil : "\n------\n",
|
43
|
+
)
|
44
|
+
rescue StandardError => e
|
45
|
+
Axn::Util.piping_error("logging before hook", action: self, exception: e)
|
46
|
+
end
|
47
|
+
|
48
|
+
def _log_after
|
49
|
+
level = self.class.auto_log_level
|
50
|
+
return unless level
|
51
|
+
|
52
|
+
self.class.public_send(
|
53
|
+
level,
|
54
|
+
[
|
55
|
+
"Execution completed (with outcome: #{result.outcome}) in #{result.elapsed_time} milliseconds",
|
56
|
+
_log_context(:outbound),
|
57
|
+
].compact.join(". Set: "),
|
58
|
+
after: Action.config.env.production? ? nil : "\n------\n",
|
59
|
+
)
|
60
|
+
rescue StandardError => e
|
61
|
+
Axn::Util.piping_error("logging after hook", action: self, exception: e)
|
62
|
+
end
|
63
|
+
|
64
|
+
def _log_context(direction)
|
65
|
+
data = context_for_logging(direction)
|
66
|
+
return unless data.present?
|
67
|
+
|
68
|
+
max_length = 150
|
69
|
+
suffix = "…<truncated>…"
|
70
|
+
|
71
|
+
_log_object(data).tap do |str|
|
72
|
+
return str[0, max_length - suffix.length] + suffix if str.length > max_length
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def _log_object(data)
|
77
|
+
case data
|
78
|
+
when Hash
|
79
|
+
# NOTE: slightly more manual in order to avoid quotes around ActiveRecord objects' <Class#id> formatting
|
80
|
+
"{#{data.map { |k, v| "#{k}: #{_log_object(v)}" }.join(", ")}}"
|
81
|
+
when Array
|
82
|
+
data.map { |v| _log_object(v) }
|
83
|
+
else
|
84
|
+
return data.to_unsafe_h if defined?(ActionController::Parameters) && data.is_a?(ActionController::Parameters)
|
85
|
+
return "<#{data.class.name}##{data.to_param.presence || "unpersisted"}>" if defined?(ActiveRecord::Base) && data.is_a?(ActiveRecord::Base)
|
86
|
+
|
87
|
+
data.inspect
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/parameter_filter"
|
4
|
+
|
5
|
+
module Action
|
6
|
+
class ContextFacade
|
7
|
+
def initialize(action:, context:, declared_fields:, implicitly_allowed_fields: nil)
|
8
|
+
if self.class.name == "Action::ContextFacade" # rubocop:disable Style/ClassEqualityComparison
|
9
|
+
raise "Action::ContextFacade is an abstract class and should not be instantiated directly"
|
10
|
+
end
|
11
|
+
|
12
|
+
@context = context
|
13
|
+
@action = action
|
14
|
+
@declared_fields = declared_fields
|
15
|
+
|
16
|
+
(@declared_fields + Array(implicitly_allowed_fields)).each do |field|
|
17
|
+
singleton_class.define_method(field) do
|
18
|
+
context_data_source[field]
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
attr_reader :declared_fields
|
24
|
+
|
25
|
+
def inspect = ContextFacadeInspector.new(facade: self, action:, context:).call
|
26
|
+
|
27
|
+
def fail!(...)
|
28
|
+
raise Action::ContractViolation::MethodNotAllowed, "Call fail! directly rather than on the context"
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
attr_reader :action, :context
|
34
|
+
|
35
|
+
def action_name = @action.class.name.presence || "The action"
|
36
|
+
|
37
|
+
def context_data_source = raise NotImplementedError
|
38
|
+
|
39
|
+
def determine_error_message(only_default: false)
|
40
|
+
return @context.error_from_user if @context.error_from_user.present?
|
41
|
+
|
42
|
+
# We need an exception for interceptors, and also in case the messages.error callable expects an argument
|
43
|
+
exception = @context.exception || Action::Failure.new
|
44
|
+
|
45
|
+
msg = action._error_msg
|
46
|
+
|
47
|
+
unless only_default
|
48
|
+
interceptor = action.class._error_interceptor_for(exception:, action:)
|
49
|
+
msg = interceptor.message if interceptor
|
50
|
+
end
|
51
|
+
|
52
|
+
stringified(msg, exception:).presence || "Something went wrong"
|
53
|
+
end
|
54
|
+
|
55
|
+
# Allow for callable OR string messages
|
56
|
+
def stringified(msg, exception: nil)
|
57
|
+
return msg.presence unless msg.respond_to?(:call)
|
58
|
+
|
59
|
+
# The error message callable can take the exception as an argument
|
60
|
+
if exception && msg.arity == 1
|
61
|
+
action.instance_exec(exception, &msg)
|
62
|
+
else
|
63
|
+
action.instance_exec(&msg)
|
64
|
+
end
|
65
|
+
rescue StandardError => e
|
66
|
+
Axn::Util.piping_error("determining message callable", action:, exception: e)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Action
|
4
|
+
class ContextFacadeInspector
|
5
|
+
def initialize(action:, facade:, context:)
|
6
|
+
@action = action
|
7
|
+
@facade = facade
|
8
|
+
@context = context
|
9
|
+
end
|
10
|
+
|
11
|
+
def call
|
12
|
+
str = [status, visible_fields].compact_blank.join(" ")
|
13
|
+
|
14
|
+
"#<#{class_name} #{str}>"
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
attr_reader :action, :facade, :context
|
20
|
+
|
21
|
+
def status
|
22
|
+
return unless facade.is_a?(Action::Result)
|
23
|
+
|
24
|
+
return "[OK]" if context.ok?
|
25
|
+
unless context.exception
|
26
|
+
return context.error_from_user.present? ? "[failed with '#{context.error_from_user}']" : "[failed]"
|
27
|
+
end
|
28
|
+
|
29
|
+
%([failed with #{context.exception.class.name}: '#{context.exception.message}'])
|
30
|
+
end
|
31
|
+
|
32
|
+
def visible_fields
|
33
|
+
declared_fields.map do |field|
|
34
|
+
value = facade.public_send(field)
|
35
|
+
|
36
|
+
"#{field}: #{format_for_inspect(field, value)}"
|
37
|
+
end.join(", ")
|
38
|
+
end
|
39
|
+
|
40
|
+
def class_name = facade.class.name
|
41
|
+
def declared_fields = facade.send(:declared_fields)
|
42
|
+
|
43
|
+
def format_for_inspect(field, value)
|
44
|
+
return value.inspect if value.nil?
|
45
|
+
|
46
|
+
# Initially based on https://github.com/rails/rails/blob/800976975253be2912d09a80757ee70a2bb1e984/activerecord/lib/active_record/attribute_methods.rb#L527
|
47
|
+
inspected_value = if value.is_a?(String) && value.length > 50
|
48
|
+
"#{value[0, 50]}...".inspect
|
49
|
+
elsif value.is_a?(Date) || value.is_a?(Time)
|
50
|
+
%("#{value.to_fs(:inspect)}")
|
51
|
+
elsif defined?(::ActiveRecord::Relation) && value.instance_of?(::ActiveRecord::Relation)
|
52
|
+
# Avoid hydrating full AR relation (i.e. avoid loading records just to report an error)
|
53
|
+
"#{value.name}::ActiveRecord_Relation"
|
54
|
+
else
|
55
|
+
value.inspect
|
56
|
+
end
|
57
|
+
|
58
|
+
inspection_filter.filter_param(field, inspected_value)
|
59
|
+
end
|
60
|
+
|
61
|
+
def inspection_filter = action.send(:inspection_filter)
|
62
|
+
end
|
63
|
+
end
|