easyop 0.1.5 → 0.1.7
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/CHANGELOG.md +105 -12
- data/README.md +61 -15
- data/lib/easyop/configuration.rb +9 -8
- data/lib/easyop/plugins/recording.rb +138 -56
- data/lib/easyop/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5fda2bb2b17350fc5da2939e9bc4ec4bd816c2e9828f440adcf6750555f02e06
|
|
4
|
+
data.tar.gz: af2eb053c15796a702dc01b6a44ebacbd15ec644ff5dce1e7fd97734e9e35199
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a9461b76dfd41ad12bf5a14b5e2e0a1f59a073402a23a5f89d3b5f773314c63b290e8d15e946394c013f73717a97ba51df717e861d6e9d67cb3109c243e32e25
|
|
7
|
+
data.tar.gz: 5d1387e43e468478267f29bca65289652a092b1727355f48b71604863daf35933d0199bb810729ac07abdf57deaf0e264febab3e0e3fca6bd3a2ed71584335be
|
data/CHANGELOG.md
CHANGED
|
@@ -7,42 +7,133 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.1.7] — 2026-04-15
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`execution_index` for `Easyop::Plugins::Recording`** — an optional `:integer` column that records the 1-based call order of each child operation within its parent. Root operations store `nil`. Each child's counter resets independently under its own parent, so siblings of different parents both start at `1`. Add via migration:
|
|
15
|
+
|
|
16
|
+
```ruby
|
|
17
|
+
add_column :operation_logs, :execution_index, :integer
|
|
18
|
+
add_index :operation_logs, [:parent_reference_id, :execution_index],
|
|
19
|
+
name: 'index_operation_logs_on_parent_ref_and_exec_index'
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Example tree:
|
|
23
|
+
```
|
|
24
|
+
FullCheckout (execution_index: nil — root)
|
|
25
|
+
ValidateCart (execution_index: 1)
|
|
26
|
+
ApplyDiscount (execution_index: 2)
|
|
27
|
+
LookupCode (execution_index: 1) ← resets under new parent
|
|
28
|
+
DeductAmt (execution_index: 2)
|
|
29
|
+
CreateOrder (execution_index: 3)
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The column is fully optional and backward-compatible — missing columns are silently skipped.
|
|
33
|
+
|
|
34
|
+
## [0.1.6] — 2026-04-15
|
|
35
|
+
|
|
36
|
+
### Added
|
|
37
|
+
|
|
38
|
+
- **`record_result: true` form for `Easyop::Plugins::Recording`** — passing `true` at install time (or via the class-level `record_result true` DSL) now records the full ctx snapshot after the operation completes. `FILTERED_KEYS` are applied and `INTERNAL_CTX_KEYS` are excluded. The `true` form captures computed values set during `#call`, making it a full output snapshot.
|
|
39
|
+
|
|
40
|
+
- **`record_params` class-level DSL** — parallel to `record_result`, operations can now declare params recording behaviour per-class:
|
|
41
|
+
|
|
42
|
+
```ruby
|
|
43
|
+
record_params false # disable params recording entirely
|
|
44
|
+
record_params attrs: %i[email name] # selective keys only
|
|
45
|
+
record_params { |ctx| { id: ctx.user_id } } # block form
|
|
46
|
+
record_params :build_safe_params # private method form
|
|
47
|
+
record_params true # explicit full ctx (default)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Inherited through the class hierarchy; subclasses can override.
|
|
51
|
+
|
|
52
|
+
- **`record_params:` install option extended** — the `plugin Easyop::Plugins::Recording, record_params:` option now accepts the same forms as the DSL: `false`, `true`, `{ attrs: }`, `Proc`, `Symbol`.
|
|
53
|
+
|
|
54
|
+
- **Input-only `params_data` by default** — when `record_params` is `true` (the default), `params_data` now records only the keys that were present in `ctx` **before** the operation body ran. Computed values written during `#call` are excluded from `params_data`. Custom forms (attrs/block/symbol) are user-controlled and evaluated after the call, so they can access computed values.
|
|
55
|
+
|
|
56
|
+
### Changed
|
|
57
|
+
|
|
58
|
+
- **`record_result` default is now `false`** — previously defaulted to `nil` (falsy). Explicit `false` makes intent clear and prevents accidental result logging.
|
|
59
|
+
|
|
10
60
|
## [0.1.5] — 2026-04-14
|
|
11
61
|
|
|
12
62
|
### Added
|
|
13
63
|
|
|
14
|
-
- **`
|
|
64
|
+
- **`filter_params` DSL for `Easyop::Plugins::Recording`** — declare additional params keys/patterns to filter in `params_data` on a per-class basis. Matched keys are kept but their value is replaced with `"[FILTERED]"`. Accepts `Symbol`, `String`, or `Regexp`. Additive with `FILTERED_KEYS` and never replaces the built-in list. Inherited by subclasses; any level of the hierarchy can override.
|
|
15
65
|
|
|
16
66
|
```ruby
|
|
17
67
|
class ApplicationOperation < ...
|
|
18
|
-
|
|
68
|
+
filter_params :api_token, /access.?key/i
|
|
19
69
|
end
|
|
20
70
|
|
|
21
71
|
class Orders::CreateOrder < ApplicationOperation
|
|
22
|
-
|
|
72
|
+
filter_params :card_number # stacks on top of parent's filter list
|
|
23
73
|
end
|
|
24
74
|
```
|
|
25
75
|
|
|
26
|
-
- **`
|
|
76
|
+
- **`filter_keys:` option on `plugin Easyop::Plugins::Recording`** — supply a list of extra keys/patterns to filter at plugin install time. Inherited by all classes that share the same `plugin` declaration.
|
|
27
77
|
|
|
28
78
|
```ruby
|
|
29
79
|
plugin Easyop::Plugins::Recording,
|
|
30
80
|
model: OperationLog,
|
|
31
|
-
|
|
81
|
+
filter_keys: [:stripe_token, /secret/i]
|
|
32
82
|
```
|
|
33
83
|
|
|
34
|
-
- **`Easyop::Configuration#
|
|
84
|
+
- **`Easyop::Configuration#recording_filter_keys`** — new global config key. Set once at boot and every recorded operation will filter these keys in addition to `FILTERED_KEYS` and any class-level declarations. Accepts `Symbol`, `String`, or `Regexp`.
|
|
35
85
|
|
|
36
86
|
```ruby
|
|
37
87
|
Easyop.configure do |c|
|
|
38
|
-
c.
|
|
88
|
+
c.recording_filter_keys = [:api_token, /token/i]
|
|
89
|
+
end
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
- **`[FILTERED]` value replacement** — sensitive keys are now kept in `params_data` with their value replaced by `"[FILTERED]"` instead of being removed entirely. This means audit logs show *which* sensitive fields were passed without exposing their values.
|
|
93
|
+
|
|
94
|
+
**Filter precedence (all layers are additive):**
|
|
95
|
+
1. `FILTERED_KEYS` — always applied (built-in list)
|
|
96
|
+
2. `Easyop.config.recording_filter_keys` — global config
|
|
97
|
+
3. `filter_keys:` plugin option + `filter_params` DSL — class hierarchy
|
|
98
|
+
|
|
99
|
+
- **`params_data` records only input keys by default** — `params_data` now snapshots the ctx keys present *before* the operation body runs. Values computed during `#call` (e.g. `ctx.user = User.create!(...)`) are excluded from `params_data`; use `record_result` to capture them. Custom `record_params` forms (attrs, block, symbol) are evaluated *after* the call and can access computed values when explicitly requested. FILTERED_KEYS and INTERNAL_CTX_KEYS are always excluded.
|
|
100
|
+
|
|
101
|
+
- **`record_result: true` form** — passing `true` records the full ctx snapshot (all non-internal keys, FILTERED_KEYS applied). Works at both the plugin install level and the class-level DSL.
|
|
102
|
+
|
|
103
|
+
```ruby
|
|
104
|
+
# Plugin level — all operations inherit this default
|
|
105
|
+
plugin Easyop::Plugins::Recording, model: OperationLog, record_result: true
|
|
106
|
+
|
|
107
|
+
# Class level DSL
|
|
108
|
+
class Orders::CreateOrder < ApplicationOperation
|
|
109
|
+
record_result true
|
|
39
110
|
end
|
|
40
111
|
```
|
|
41
112
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
113
|
+
- **`record_params` class-level DSL** — parallel to `record_result`, `record_params` can now be declared on any class to control what ends up in `params_data`. Supports four forms:
|
|
114
|
+
|
|
115
|
+
```ruby
|
|
116
|
+
record_params false # disable params entirely
|
|
117
|
+
record_params true # explicit full ctx (default)
|
|
118
|
+
record_params attrs: :user_id # selective keys
|
|
119
|
+
record_params attrs: [:user_id, :event_id]
|
|
120
|
+
record_params { |ctx| { custom: ctx[:name] } } # block
|
|
121
|
+
record_params :build_params # private method name
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
FILTERED_KEYS are **always applied** to the extracted hash regardless of form (keys are kept, values replaced with `"[FILTERED]"`). Config is inheritable and overridable per subclass.
|
|
125
|
+
|
|
126
|
+
- **`record_params:` install option extended** — the plugin install-level `record_params:` option now accepts the same forms as the DSL: `Hash` (`{ attrs: }` ), `Proc`, and `Symbol`, in addition to `true`/`false`.
|
|
127
|
+
|
|
128
|
+
```ruby
|
|
129
|
+
plugin Easyop::Plugins::Recording,
|
|
130
|
+
model: OperationLog,
|
|
131
|
+
record_params: { attrs: %i[user_id plan] }
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Changed
|
|
135
|
+
|
|
136
|
+
- **`record_result` default is now `false`** (was `nil`). Behaviour is identical — no `result_data` is written unless explicitly configured — but the default value is now consistent with the boolean-style `record_params: true` API convention.
|
|
46
137
|
|
|
47
138
|
## [0.1.4] — 2026-04-14
|
|
48
139
|
|
|
@@ -295,7 +386,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
295
386
|
- `examples/easyop_test_app/` — full Rails 8 blog application demonstrating all features in real-world code
|
|
296
387
|
- `examples/usage.rb` — 13 runnable plain-Ruby examples
|
|
297
388
|
|
|
298
|
-
[Unreleased]: https://github.com/pniemczyk/easyop/compare/v0.1.
|
|
389
|
+
[Unreleased]: https://github.com/pniemczyk/easyop/compare/v0.1.7...HEAD
|
|
390
|
+
[0.1.7]: https://github.com/pniemczyk/easyop/compare/v0.1.6...v0.1.7
|
|
391
|
+
[0.1.6]: https://github.com/pniemczyk/easyop/compare/v0.1.5...v0.1.6
|
|
299
392
|
[0.1.5]: https://github.com/pniemczyk/easyop/compare/v0.1.4...v0.1.5
|
|
300
393
|
[0.1.4]: https://github.com/pniemczyk/easyop/compare/v0.1.3...v0.1.4
|
|
301
394
|
[0.1.3]: https://github.com/pniemczyk/easyop/compare/v0.1.2...v0.1.3
|
data/README.md
CHANGED
|
@@ -645,9 +645,9 @@ end
|
|
|
645
645
|
| Option | Default | Description |
|
|
646
646
|
|---|---|---|
|
|
647
647
|
| `model:` | required | ActiveRecord class to write logs into |
|
|
648
|
-
| `record_params:` | `true` |
|
|
649
|
-
| `record_result:` | `
|
|
650
|
-
| `
|
|
648
|
+
| `record_params:` | `true` | Control params serialization: `false` skips it; `true` uses full ctx; also accepts `{ attrs: }`, `Proc`, or `Symbol` |
|
|
649
|
+
| `record_result:` | `false` | Plugin-level default for result capture: `false` skips; `true` uses full ctx; also accepts `{ attrs: }`, `Proc`, or `Symbol` |
|
|
650
|
+
| `filter_keys:` | `[]` | Extra keys/patterns to filter in `params_data` (Symbol, String, Regexp) — values replaced with `[FILTERED]` |
|
|
651
651
|
|
|
652
652
|
**Required model columns:**
|
|
653
653
|
|
|
@@ -656,9 +656,12 @@ create_table :operation_logs do |t|
|
|
|
656
656
|
t.string :operation_name, null: false
|
|
657
657
|
t.boolean :success, null: false
|
|
658
658
|
t.string :error_message
|
|
659
|
-
t.text :params_data # JSON — ctx attrs (sensitive keys
|
|
659
|
+
t.text :params_data # JSON — ctx attrs (sensitive keys replaced with [FILTERED])
|
|
660
660
|
t.float :duration_ms
|
|
661
661
|
t.datetime :performed_at, null: false
|
|
662
|
+
|
|
663
|
+
# Optional — add when using the record_result DSL to capture output data:
|
|
664
|
+
t.text :result_data # JSON — selected ctx keys after the operation runs
|
|
662
665
|
end
|
|
663
666
|
```
|
|
664
667
|
|
|
@@ -697,28 +700,65 @@ root_log = OperationLog.find_by(operation_name: "FullCheckout", parent_reference
|
|
|
697
700
|
OperationLog.for_tree(root_log.root_reference_id)
|
|
698
701
|
```
|
|
699
702
|
|
|
700
|
-
**
|
|
703
|
+
**Filtering params** — sensitive keys are kept in `params_data` but their value is replaced with `"[FILTERED]"`, so the audit log shows which fields were passed without exposing their values. All layers are additive:
|
|
701
704
|
|
|
702
|
-
1. **Built-in `
|
|
705
|
+
1. **Built-in `FILTERED_KEYS`** — always applied: `:password`, `:password_confirmation`, `:token`, `:secret`, `:api_key`
|
|
703
706
|
2. **Global config** — applied to every recorded operation:
|
|
704
707
|
```ruby
|
|
705
|
-
Easyop.configure { |c| c.
|
|
708
|
+
Easyop.configure { |c| c.recording_filter_keys = [:api_token, /token/i] }
|
|
706
709
|
```
|
|
707
|
-
3. **Plugin `
|
|
710
|
+
3. **Plugin `filter_keys:` option** — applied to all subclasses that share the plugin install:
|
|
708
711
|
```ruby
|
|
709
|
-
plugin Easyop::Plugins::Recording, model: OperationLog,
|
|
712
|
+
plugin Easyop::Plugins::Recording, model: OperationLog, filter_keys: [:stripe_secret]
|
|
710
713
|
```
|
|
711
|
-
4. **`
|
|
714
|
+
4. **`filter_params` DSL** — per-class, inheritable, and stackable at any level of the hierarchy:
|
|
712
715
|
```ruby
|
|
713
716
|
class ApplicationOperation < ...
|
|
714
|
-
|
|
717
|
+
filter_params :internal_token, /access.?key/i
|
|
715
718
|
end
|
|
716
719
|
class Payments::ChargeCard < ApplicationOperation
|
|
717
|
-
|
|
720
|
+
filter_params :card_number # stacks on top of parent's list
|
|
718
721
|
end
|
|
719
722
|
```
|
|
720
723
|
|
|
721
|
-
Internal tracing keys (`__recording_*`) are always
|
|
724
|
+
Internal tracing keys (`__recording_*`) are always fully removed. ActiveRecord objects are serialized as `{ id:, class: }` rather than their full representation.
|
|
725
|
+
|
|
726
|
+
**`record_params` DSL — control what goes into `params_data`:**
|
|
727
|
+
|
|
728
|
+
By default, `params_data` records only the keys that were **present in ctx when the operation was called** (inputs), not values computed during the call body. This means `ctx.user = User.create!(...)` set during `#call` will not appear in `params_data` — use `record_result` to capture output values. FILTERED_KEYS and INTERNAL_CTX_KEYS are always excluded.
|
|
729
|
+
|
|
730
|
+
Use the `record_params` DSL to further customize or suppress params recording:
|
|
731
|
+
|
|
732
|
+
```ruby
|
|
733
|
+
# Disable entirely — no params_data written
|
|
734
|
+
class Users::Authenticate < ApplicationOperation
|
|
735
|
+
record_params false
|
|
736
|
+
end
|
|
737
|
+
|
|
738
|
+
# Selective keys — only these ctx attrs are recorded
|
|
739
|
+
class Tickets::GenerateTickets < ApplicationOperation
|
|
740
|
+
record_params attrs: %i[event_id seat_count]
|
|
741
|
+
end
|
|
742
|
+
|
|
743
|
+
# Block — full control over the extracted hash
|
|
744
|
+
class Reports::GeneratePdf < ApplicationOperation
|
|
745
|
+
record_params { |ctx| { report_type: ctx.report_type, page_count: ctx.pages } }
|
|
746
|
+
end
|
|
747
|
+
|
|
748
|
+
# Symbol — delegates to a private method on the instance
|
|
749
|
+
class Payments::ChargeCard < ApplicationOperation
|
|
750
|
+
record_params :safe_params
|
|
751
|
+
|
|
752
|
+
private
|
|
753
|
+
def safe_params
|
|
754
|
+
{ user_id: ctx.user.id, amount_cents: ctx.amount_cents }
|
|
755
|
+
end
|
|
756
|
+
end
|
|
757
|
+
```
|
|
758
|
+
|
|
759
|
+
FILTERED_KEYS are **always applied** to the extracted hash regardless of form — including custom attrs, blocks, and symbol methods. Plugin install-level `record_params:` accepts the same forms as the DSL.
|
|
760
|
+
|
|
761
|
+
> **Input vs. output**: The `true` (default) form records only keys present *before* the call body runs — values computed during `#call` (like `ctx.user`) are excluded. Custom forms (attrs, block, symbol) are evaluated *after* the call, so they can access computed values. Use `record_result` to capture outputs alongside inputs.
|
|
722
762
|
|
|
723
763
|
**`record_result` DSL — capture output data:**
|
|
724
764
|
|
|
@@ -728,9 +768,14 @@ Add an optional `result_data :text` column to persist selected ctx values after
|
|
|
728
768
|
add_column :operation_logs, :result_data, :text # stored as JSON
|
|
729
769
|
```
|
|
730
770
|
|
|
731
|
-
Then declare what to record using the `record_result` DSL (
|
|
771
|
+
Then declare what to record using the `record_result` DSL (four forms):
|
|
732
772
|
|
|
733
773
|
```ruby
|
|
774
|
+
# True form — full ctx snapshot (FILTERED_KEYS applied, internal keys excluded)
|
|
775
|
+
class Users::Register < ApplicationOperation
|
|
776
|
+
record_result true
|
|
777
|
+
end
|
|
778
|
+
|
|
734
779
|
# Attrs form — one or more ctx keys
|
|
735
780
|
class PlaceOrder < ApplicationOperation
|
|
736
781
|
record_result attrs: :order_id
|
|
@@ -761,7 +806,8 @@ Set a plugin-level default inherited by all subclasses:
|
|
|
761
806
|
|
|
762
807
|
```ruby
|
|
763
808
|
plugin Easyop::Plugins::Recording, model: OperationLog,
|
|
764
|
-
record_result:
|
|
809
|
+
record_result: true
|
|
810
|
+
# or: record_result: { attrs: :metadata }
|
|
765
811
|
# or: record_result: ->(ctx) { { id: ctx.record_id } }
|
|
766
812
|
# or: record_result: :build_result
|
|
767
813
|
```
|
data/lib/easyop/configuration.rb
CHANGED
|
@@ -15,19 +15,20 @@ module Easyop
|
|
|
15
15
|
# Easyop.configure { |c| c.event_bus = :active_support }
|
|
16
16
|
attr_accessor :event_bus
|
|
17
17
|
|
|
18
|
-
# Extra keys to
|
|
19
|
-
# Appended to Recording::
|
|
18
|
+
# Extra keys to filter from params_data across all recorded operations.
|
|
19
|
+
# Appended to Recording::FILTERED_KEYS — never replaces the built-in list.
|
|
20
|
+
# Matched keys are kept in params_data but their value is replaced with "[FILTERED]".
|
|
20
21
|
# Accepts Symbol, String, or Regexp (matched against the stringified key name).
|
|
21
22
|
#
|
|
22
23
|
# @example
|
|
23
|
-
# Easyop.configure { |c| c.
|
|
24
|
-
attr_accessor :
|
|
24
|
+
# Easyop.configure { |c| c.recording_filter_keys = [:api_token, /token/i] }
|
|
25
|
+
attr_accessor :recording_filter_keys
|
|
25
26
|
|
|
26
27
|
def initialize
|
|
27
|
-
@type_adapter
|
|
28
|
-
@strict_types
|
|
29
|
-
@event_bus
|
|
30
|
-
@
|
|
28
|
+
@type_adapter = :native
|
|
29
|
+
@strict_types = false
|
|
30
|
+
@event_bus = nil # nil = Memory bus (see Easyop::Events::Registry)
|
|
31
|
+
@recording_filter_keys = []
|
|
31
32
|
end
|
|
32
33
|
end
|
|
33
34
|
|
|
@@ -29,6 +29,9 @@ module Easyop
|
|
|
29
29
|
# Optional result column:
|
|
30
30
|
# result_data :text # stored as JSON — selected ctx keys after call
|
|
31
31
|
#
|
|
32
|
+
# Optional execution order column:
|
|
33
|
+
# execution_index :integer # nil for root; 1-based within each parent
|
|
34
|
+
#
|
|
32
35
|
# Opt out per operation class:
|
|
33
36
|
# class MyOp < ApplicationOperation
|
|
34
37
|
# recording false
|
|
@@ -36,28 +39,34 @@ module Easyop
|
|
|
36
39
|
#
|
|
37
40
|
# Options:
|
|
38
41
|
# model: (required) ActiveRecord class
|
|
39
|
-
# record_params: true pass false to skip params serialization
|
|
40
|
-
#
|
|
41
|
-
#
|
|
42
|
-
# (
|
|
42
|
+
# record_params: true pass false to skip params serialization; also accepts
|
|
43
|
+
# Hash ({ attrs: :key }), Proc, or Symbol (method name)
|
|
44
|
+
# record_result: false configure result capture at plugin level; also accepts
|
|
45
|
+
# true (full ctx snapshot), Hash, Proc, or Symbol
|
|
46
|
+
# filter_keys: [] additional keys/patterns to filter from params_data
|
|
47
|
+
# (Symbol, String, or Regexp — additive with FILTERED_KEYS)
|
|
48
|
+
# Filtered keys are kept in params_data but their value
|
|
49
|
+
# is replaced with "[FILTERED]".
|
|
43
50
|
module Recording
|
|
44
|
-
# Sensitive keys always
|
|
45
|
-
|
|
51
|
+
# Sensitive keys always filtered in params_data before persisting.
|
|
52
|
+
# Their values are replaced with "[FILTERED]" rather than removed.
|
|
53
|
+
FILTERED_KEYS = %i[password password_confirmation token secret api_key].freeze
|
|
46
54
|
|
|
47
55
|
# Internal ctx keys used for flow tracing — excluded from params_data.
|
|
48
56
|
INTERNAL_CTX_KEYS = %i[
|
|
49
57
|
__recording_root_reference_id
|
|
50
58
|
__recording_parent_operation_name
|
|
51
59
|
__recording_parent_reference_id
|
|
60
|
+
__recording_child_counts
|
|
52
61
|
].freeze
|
|
53
62
|
|
|
54
|
-
def self.install(base, model:, record_params: true, record_result:
|
|
63
|
+
def self.install(base, model:, record_params: true, record_result: false, filter_keys: [], **_options)
|
|
55
64
|
base.extend(ClassMethods)
|
|
56
65
|
base.prepend(RunWrapper)
|
|
57
|
-
base.instance_variable_set(:@_recording_model,
|
|
66
|
+
base.instance_variable_set(:@_recording_model, model)
|
|
58
67
|
base.instance_variable_set(:@_recording_record_params, record_params)
|
|
59
68
|
base.instance_variable_set(:@_recording_record_result, record_result)
|
|
60
|
-
base.instance_variable_set(:@
|
|
69
|
+
base.instance_variable_set(:@_recording_filter_keys, Array(filter_keys))
|
|
61
70
|
end
|
|
62
71
|
|
|
63
72
|
module ClassMethods
|
|
@@ -76,30 +85,48 @@ module Easyop
|
|
|
76
85
|
(superclass.respond_to?(:_recording_model) ? superclass._recording_model : nil)
|
|
77
86
|
end
|
|
78
87
|
|
|
79
|
-
|
|
88
|
+
# DSL for controlling params capture. Forms:
|
|
89
|
+
# record_params false # disable params recording entirely
|
|
90
|
+
# record_params true # explicit full ctx (default)
|
|
91
|
+
# record_params attrs: :key # selective keys
|
|
92
|
+
# record_params { |ctx| {...} } # block
|
|
93
|
+
# record_params :build_params # private method name
|
|
94
|
+
def record_params(value = nil, attrs: nil, &block)
|
|
95
|
+
@_recording_record_params = if block
|
|
96
|
+
block
|
|
97
|
+
elsif attrs
|
|
98
|
+
{ attrs: attrs }
|
|
99
|
+
elsif !value.nil?
|
|
100
|
+
value
|
|
101
|
+
else
|
|
102
|
+
true
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def _recording_record_params_config
|
|
80
107
|
if instance_variable_defined?(:@_recording_record_params)
|
|
81
108
|
@_recording_record_params
|
|
82
|
-
elsif superclass.respond_to?(:
|
|
83
|
-
superclass.
|
|
109
|
+
elsif superclass.respond_to?(:_recording_record_params_config)
|
|
110
|
+
superclass._recording_record_params_config
|
|
84
111
|
else
|
|
85
112
|
true
|
|
86
113
|
end
|
|
87
114
|
end
|
|
88
115
|
|
|
89
|
-
# DSL for capturing result data after the operation runs.
|
|
90
|
-
#
|
|
91
|
-
# record_result attrs: :key
|
|
92
|
-
# record_result { |ctx| {
|
|
93
|
-
# record_result :build_result
|
|
116
|
+
# DSL for capturing result data after the operation runs. Forms:
|
|
117
|
+
# record_result true # full ctx snapshot (FILTERED_KEYS applied)
|
|
118
|
+
# record_result attrs: :key # one or more ctx keys
|
|
119
|
+
# record_result { |ctx| {...} } # block
|
|
120
|
+
# record_result :build_result # private instance method name
|
|
94
121
|
def record_result(value = nil, attrs: nil, &block)
|
|
95
122
|
@_recording_record_result = if block
|
|
96
123
|
block
|
|
97
124
|
elsif attrs
|
|
98
125
|
{ attrs: attrs }
|
|
99
|
-
elsif value.
|
|
126
|
+
elsif !value.nil?
|
|
100
127
|
value
|
|
101
128
|
else
|
|
102
|
-
|
|
129
|
+
nil
|
|
103
130
|
end
|
|
104
131
|
end
|
|
105
132
|
|
|
@@ -109,34 +136,35 @@ module Easyop
|
|
|
109
136
|
elsif superclass.respond_to?(:_recording_record_result_config)
|
|
110
137
|
superclass._recording_record_result_config
|
|
111
138
|
else
|
|
112
|
-
|
|
139
|
+
false
|
|
113
140
|
end
|
|
114
141
|
end
|
|
115
142
|
|
|
116
|
-
# DSL to declare additional keys/patterns to
|
|
117
|
-
# Accepts Symbol, String, or Regexp. Additive with
|
|
118
|
-
# any
|
|
143
|
+
# DSL to declare additional keys/patterns to filter in params_data.
|
|
144
|
+
# Accepts Symbol, String, or Regexp. Additive with FILTERED_KEYS and
|
|
145
|
+
# any filter_keys declared on parent classes or at the plugin install level.
|
|
146
|
+
# Matched keys are kept in params_data but their value is replaced with "[FILTERED]".
|
|
119
147
|
#
|
|
120
148
|
# @example
|
|
121
149
|
# class ApplicationOperation < ...
|
|
122
|
-
#
|
|
150
|
+
# filter_params :api_token, /access.?key/i
|
|
123
151
|
# end
|
|
124
|
-
def
|
|
125
|
-
@
|
|
152
|
+
def filter_params(*keys)
|
|
153
|
+
@_recording_filter_keys = _own_recording_filter_keys + keys
|
|
126
154
|
end
|
|
127
155
|
|
|
128
|
-
# Returns the merged
|
|
129
|
-
# Does NOT include
|
|
156
|
+
# Returns the merged filter list: parent class keys + this class's own keys.
|
|
157
|
+
# Does NOT include FILTERED_KEYS or the global config list — those are
|
|
130
158
|
# merged at persist time so they stay hot-reloadable.
|
|
131
|
-
def
|
|
132
|
-
parent = superclass.respond_to?(:
|
|
133
|
-
parent +
|
|
159
|
+
def _recording_filter_keys
|
|
160
|
+
parent = superclass.respond_to?(:_recording_filter_keys) ? superclass._recording_filter_keys : []
|
|
161
|
+
parent + _own_recording_filter_keys
|
|
134
162
|
end
|
|
135
163
|
|
|
136
164
|
private
|
|
137
165
|
|
|
138
|
-
def
|
|
139
|
-
instance_variable_defined?(:@
|
|
166
|
+
def _own_recording_filter_keys
|
|
167
|
+
instance_variable_defined?(:@_recording_filter_keys) ? @_recording_filter_keys : []
|
|
140
168
|
end
|
|
141
169
|
end
|
|
142
170
|
|
|
@@ -146,6 +174,12 @@ module Easyop
|
|
|
146
174
|
return super unless (model = self.class._recording_model)
|
|
147
175
|
return super unless self.class.name # skip anonymous classes
|
|
148
176
|
|
|
177
|
+
# Snapshot the keys present in ctx RIGHT NOW — before any internal
|
|
178
|
+
# tracing keys are written and before the operation body runs. This
|
|
179
|
+
# lets _recording_safe_params emit only what was passed IN, not values
|
|
180
|
+
# computed during the call (those belong in result_data).
|
|
181
|
+
input_keys = ctx.to_h.keys
|
|
182
|
+
|
|
149
183
|
# -- Flow tracing --
|
|
150
184
|
# Each operation gets its own reference_id. The root_reference_id is
|
|
151
185
|
# shared across the entire execution tree via ctx (set once, inherited).
|
|
@@ -156,6 +190,14 @@ module Easyop
|
|
|
156
190
|
parent_operation_name = ctx[:__recording_parent_operation_name]
|
|
157
191
|
parent_reference_id = ctx[:__recording_parent_reference_id]
|
|
158
192
|
|
|
193
|
+
# Execution index: 1-based position among siblings (nil for root operations).
|
|
194
|
+
# Counts are tracked per-parent in a hash stored in ctx so sibling chains
|
|
195
|
+
# and nested sub-trees each maintain independent counters.
|
|
196
|
+
execution_index = if parent_reference_id
|
|
197
|
+
counts = ctx[:__recording_child_counts] ||= {}
|
|
198
|
+
counts[parent_reference_id] = (counts[parent_reference_id] || 0) + 1
|
|
199
|
+
end
|
|
200
|
+
|
|
159
201
|
# Set THIS operation as the parent for any children that run inside super.
|
|
160
202
|
# Save the previous values so we can restore them after (for siblings).
|
|
161
203
|
prev_parent_name = parent_operation_name
|
|
@@ -180,7 +222,9 @@ module Easyop
|
|
|
180
222
|
root_reference_id: root_reference_id,
|
|
181
223
|
reference_id: reference_id,
|
|
182
224
|
parent_operation_name: parent_operation_name,
|
|
183
|
-
parent_reference_id: parent_reference_id
|
|
225
|
+
parent_reference_id: parent_reference_id,
|
|
226
|
+
execution_index: execution_index,
|
|
227
|
+
input_keys: input_keys)
|
|
184
228
|
end
|
|
185
229
|
end
|
|
186
230
|
|
|
@@ -188,7 +232,8 @@ module Easyop
|
|
|
188
232
|
|
|
189
233
|
def _recording_persist!(ctx, model, duration_ms,
|
|
190
234
|
root_reference_id: nil, reference_id: nil,
|
|
191
|
-
parent_operation_name: nil, parent_reference_id: nil
|
|
235
|
+
parent_operation_name: nil, parent_reference_id: nil,
|
|
236
|
+
execution_index: nil, input_keys: nil)
|
|
192
237
|
attrs = {
|
|
193
238
|
operation_name: self.class.name,
|
|
194
239
|
success: ctx.success?,
|
|
@@ -198,9 +243,11 @@ module Easyop
|
|
|
198
243
|
root_reference_id: root_reference_id,
|
|
199
244
|
reference_id: reference_id,
|
|
200
245
|
parent_operation_name: parent_operation_name,
|
|
201
|
-
parent_reference_id: parent_reference_id
|
|
246
|
+
parent_reference_id: parent_reference_id,
|
|
247
|
+
execution_index: execution_index
|
|
202
248
|
}
|
|
203
|
-
|
|
249
|
+
params_config = self.class._recording_record_params_config
|
|
250
|
+
attrs[:params_data] = _recording_safe_params(ctx, params_config, input_keys) unless params_config == false
|
|
204
251
|
attrs[:result_data] = _recording_safe_result(ctx) if self.class._recording_record_result_config
|
|
205
252
|
|
|
206
253
|
# Only write columns the model actually has
|
|
@@ -210,26 +257,43 @@ module Easyop
|
|
|
210
257
|
_recording_warn(e)
|
|
211
258
|
end
|
|
212
259
|
|
|
213
|
-
def _recording_safe_params(ctx)
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
260
|
+
def _recording_safe_params(ctx, config, input_keys = nil)
|
|
261
|
+
raw = case config
|
|
262
|
+
when true
|
|
263
|
+
# Restrict to keys that existed when the operation was invoked —
|
|
264
|
+
# values computed during the call body are excluded here and
|
|
265
|
+
# should be captured via record_result instead.
|
|
266
|
+
# INTERNAL_CTX_KEYS are stripped from input_keys because nested
|
|
267
|
+
# operations inherit parent tracing keys before their snapshot.
|
|
268
|
+
base = ctx.to_h.except(*INTERNAL_CTX_KEYS)
|
|
269
|
+
if input_keys
|
|
270
|
+
clean = input_keys - INTERNAL_CTX_KEYS
|
|
271
|
+
base.slice(*clean)
|
|
272
|
+
else
|
|
273
|
+
base
|
|
274
|
+
end
|
|
275
|
+
when Hash
|
|
276
|
+
keys = Array(config[:attrs])
|
|
277
|
+
keys.each_with_object({}) { |k, h| h[k] = ctx[k] }
|
|
278
|
+
when Proc
|
|
279
|
+
result = config.call(ctx)
|
|
280
|
+
return nil unless result.is_a?(Hash)
|
|
281
|
+
result
|
|
282
|
+
when Symbol
|
|
283
|
+
result = send(config)
|
|
284
|
+
return nil unless result.is_a?(Hash)
|
|
285
|
+
result
|
|
286
|
+
end
|
|
287
|
+
_recording_apply_and_serialize(raw).to_json
|
|
224
288
|
rescue
|
|
225
289
|
nil
|
|
226
290
|
end
|
|
227
291
|
|
|
228
|
-
# Returns true when +key+ matches any entry in +
|
|
292
|
+
# Returns true when +key+ matches any entry in +filter_list+.
|
|
229
293
|
# Regexp entries match against the stringified key name (case-sensitive
|
|
230
294
|
# unless the Regexp itself uses /i). Symbol/String entries match by value.
|
|
231
|
-
def
|
|
232
|
-
return true if
|
|
295
|
+
def _recording_filter_key?(key, extra_list)
|
|
296
|
+
return true if FILTERED_KEYS.include?(key.to_sym)
|
|
233
297
|
|
|
234
298
|
extra_list.any? do |pattern|
|
|
235
299
|
case pattern
|
|
@@ -245,23 +309,41 @@ module Easyop
|
|
|
245
309
|
return nil unless config
|
|
246
310
|
|
|
247
311
|
raw = case config
|
|
312
|
+
when true
|
|
313
|
+
ctx.to_h.except(*INTERNAL_CTX_KEYS)
|
|
248
314
|
when Hash
|
|
249
315
|
keys = Array(config[:attrs])
|
|
250
316
|
keys.each_with_object({}) { |k, h| h[k] = ctx[k] }
|
|
251
317
|
when Proc
|
|
252
|
-
config.call(ctx)
|
|
318
|
+
result = config.call(ctx)
|
|
319
|
+
return nil unless result.is_a?(Hash)
|
|
320
|
+
result
|
|
253
321
|
when Symbol
|
|
254
|
-
send(config)
|
|
322
|
+
result = send(config)
|
|
323
|
+
return nil unless result.is_a?(Hash)
|
|
324
|
+
result
|
|
255
325
|
end
|
|
256
326
|
|
|
257
327
|
return nil unless raw.is_a?(Hash)
|
|
258
|
-
|
|
259
|
-
raw.transform_values { |v| v.is_a?(ActiveRecord::Base) ? { id: v.id, class: v.class.name } : v }
|
|
260
|
-
.to_json
|
|
328
|
+
_recording_apply_and_serialize(raw).to_json
|
|
261
329
|
rescue
|
|
262
330
|
nil
|
|
263
331
|
end
|
|
264
332
|
|
|
333
|
+
# Applies FILTERED_KEYS + class/global filter lists to +hash+, replacing
|
|
334
|
+
# matched values with "[FILTERED]" and serializing ActiveRecord objects.
|
|
335
|
+
def _recording_apply_and_serialize(hash)
|
|
336
|
+
extra = Easyop.config.recording_filter_keys.to_a + self.class._recording_filter_keys
|
|
337
|
+
hash.each_with_object({}) do |(k, v), h|
|
|
338
|
+
h[k] = _recording_filter_key?(k, extra) ? '[FILTERED]' : _recording_serialize_value(v)
|
|
339
|
+
end
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
# Serializes a single value: AR objects become {id:, class:}, everything else passthrough.
|
|
343
|
+
def _recording_serialize_value(v)
|
|
344
|
+
v.is_a?(ActiveRecord::Base) ? { id: v.id, class: v.class.name } : v
|
|
345
|
+
end
|
|
346
|
+
|
|
265
347
|
def _recording_warn(err)
|
|
266
348
|
return unless defined?(Rails) && Rails.respond_to?(:logger)
|
|
267
349
|
Rails.logger.warn "[EasyOp::Recording] Failed to record #{self.class.name}: #{err.message}"
|
data/lib/easyop/version.rb
CHANGED