easyop 0.1.4 → 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 +132 -1
- data/README.md +83 -15
- data/lib/easyop/configuration.rb +13 -3
- data/lib/easyop/plugins/recording.rb +163 -31
- 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,6 +7,134 @@ 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
|
+
|
|
60
|
+
## [0.1.5] — 2026-04-14
|
|
61
|
+
|
|
62
|
+
### Added
|
|
63
|
+
|
|
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.
|
|
65
|
+
|
|
66
|
+
```ruby
|
|
67
|
+
class ApplicationOperation < ...
|
|
68
|
+
filter_params :api_token, /access.?key/i
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
class Orders::CreateOrder < ApplicationOperation
|
|
72
|
+
filter_params :card_number # stacks on top of parent's filter list
|
|
73
|
+
end
|
|
74
|
+
```
|
|
75
|
+
|
|
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.
|
|
77
|
+
|
|
78
|
+
```ruby
|
|
79
|
+
plugin Easyop::Plugins::Recording,
|
|
80
|
+
model: OperationLog,
|
|
81
|
+
filter_keys: [:stripe_token, /secret/i]
|
|
82
|
+
```
|
|
83
|
+
|
|
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`.
|
|
85
|
+
|
|
86
|
+
```ruby
|
|
87
|
+
Easyop.configure do |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
|
|
110
|
+
end
|
|
111
|
+
```
|
|
112
|
+
|
|
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.
|
|
137
|
+
|
|
10
138
|
## [0.1.4] — 2026-04-14
|
|
11
139
|
|
|
12
140
|
### Added
|
|
@@ -258,6 +386,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
258
386
|
- `examples/easyop_test_app/` — full Rails 8 blog application demonstrating all features in real-world code
|
|
259
387
|
- `examples/usage.rb` — 13 runnable plain-Ruby examples
|
|
260
388
|
|
|
261
|
-
[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
|
|
392
|
+
[0.1.5]: https://github.com/pniemczyk/easyop/compare/v0.1.4...v0.1.5
|
|
262
393
|
[0.1.4]: https://github.com/pniemczyk/easyop/compare/v0.1.3...v0.1.4
|
|
263
394
|
[0.1.3]: https://github.com/pniemczyk/easyop/compare/v0.1.2...v0.1.3
|
data/README.md
CHANGED
|
@@ -645,8 +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:` | `
|
|
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]` |
|
|
650
651
|
|
|
651
652
|
**Required model columns:**
|
|
652
653
|
|
|
@@ -655,9 +656,12 @@ create_table :operation_logs do |t|
|
|
|
655
656
|
t.string :operation_name, null: false
|
|
656
657
|
t.boolean :success, null: false
|
|
657
658
|
t.string :error_message
|
|
658
|
-
t.text :params_data # JSON — ctx attrs (sensitive keys
|
|
659
|
+
t.text :params_data # JSON — ctx attrs (sensitive keys replaced with [FILTERED])
|
|
659
660
|
t.float :duration_ms
|
|
660
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
|
|
661
665
|
end
|
|
662
666
|
```
|
|
663
667
|
|
|
@@ -696,7 +700,65 @@ root_log = OperationLog.find_by(operation_name: "FullCheckout", parent_reference
|
|
|
696
700
|
OperationLog.for_tree(root_log.root_reference_id)
|
|
697
701
|
```
|
|
698
702
|
|
|
699
|
-
|
|
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:
|
|
704
|
+
|
|
705
|
+
1. **Built-in `FILTERED_KEYS`** — always applied: `:password`, `:password_confirmation`, `:token`, `:secret`, `:api_key`
|
|
706
|
+
2. **Global config** — applied to every recorded operation:
|
|
707
|
+
```ruby
|
|
708
|
+
Easyop.configure { |c| c.recording_filter_keys = [:api_token, /token/i] }
|
|
709
|
+
```
|
|
710
|
+
3. **Plugin `filter_keys:` option** — applied to all subclasses that share the plugin install:
|
|
711
|
+
```ruby
|
|
712
|
+
plugin Easyop::Plugins::Recording, model: OperationLog, filter_keys: [:stripe_secret]
|
|
713
|
+
```
|
|
714
|
+
4. **`filter_params` DSL** — per-class, inheritable, and stackable at any level of the hierarchy:
|
|
715
|
+
```ruby
|
|
716
|
+
class ApplicationOperation < ...
|
|
717
|
+
filter_params :internal_token, /access.?key/i
|
|
718
|
+
end
|
|
719
|
+
class Payments::ChargeCard < ApplicationOperation
|
|
720
|
+
filter_params :card_number # stacks on top of parent's list
|
|
721
|
+
end
|
|
722
|
+
```
|
|
723
|
+
|
|
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.
|
|
700
762
|
|
|
701
763
|
**`record_result` DSL — capture output data:**
|
|
702
764
|
|
|
@@ -706,9 +768,14 @@ Add an optional `result_data :text` column to persist selected ctx values after
|
|
|
706
768
|
add_column :operation_logs, :result_data, :text # stored as JSON
|
|
707
769
|
```
|
|
708
770
|
|
|
709
|
-
Then declare what to record using the `record_result` DSL (
|
|
771
|
+
Then declare what to record using the `record_result` DSL (four forms):
|
|
710
772
|
|
|
711
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
|
+
|
|
712
779
|
# Attrs form — one or more ctx keys
|
|
713
780
|
class PlaceOrder < ApplicationOperation
|
|
714
781
|
record_result attrs: :order_id
|
|
@@ -739,7 +806,8 @@ Set a plugin-level default inherited by all subclasses:
|
|
|
739
806
|
|
|
740
807
|
```ruby
|
|
741
808
|
plugin Easyop::Plugins::Recording, model: OperationLog,
|
|
742
|
-
record_result:
|
|
809
|
+
record_result: true
|
|
810
|
+
# or: record_result: { attrs: :metadata }
|
|
743
811
|
# or: record_result: ->(ctx) { { id: ctx.record_id } }
|
|
744
812
|
# or: record_result: :build_result
|
|
745
813
|
```
|
|
@@ -1459,7 +1527,7 @@ Edit `lib/easyop/version.rb` and increment the version string following [Semanti
|
|
|
1459
1527
|
```ruby
|
|
1460
1528
|
# lib/easyop/version.rb
|
|
1461
1529
|
module Easyop
|
|
1462
|
-
VERSION = "0.1.
|
|
1530
|
+
VERSION = "0.1.5" # was 0.1.4
|
|
1463
1531
|
end
|
|
1464
1532
|
```
|
|
1465
1533
|
|
|
@@ -1470,45 +1538,45 @@ In `CHANGELOG.md`, move everything under `[Unreleased]` into a new versioned sec
|
|
|
1470
1538
|
```markdown
|
|
1471
1539
|
## [Unreleased]
|
|
1472
1540
|
|
|
1473
|
-
## [0.1.
|
|
1541
|
+
## [0.1.5] — YYYY-MM-DD # ← new section
|
|
1474
1542
|
|
|
1475
1543
|
### Added
|
|
1476
1544
|
- …
|
|
1477
1545
|
|
|
1478
|
-
## [0.1.
|
|
1546
|
+
## [0.1.4] — 2026-04-14
|
|
1479
1547
|
```
|
|
1480
1548
|
|
|
1481
1549
|
Add a comparison link at the bottom of the file:
|
|
1482
1550
|
|
|
1483
1551
|
```markdown
|
|
1484
|
-
[Unreleased]: https://github.com/pniemczyk/easyop/compare/v0.1.
|
|
1552
|
+
[Unreleased]: https://github.com/pniemczyk/easyop/compare/v0.1.5...HEAD
|
|
1553
|
+
[0.1.5]: https://github.com/pniemczyk/easyop/compare/v0.1.4...v0.1.5
|
|
1485
1554
|
[0.1.4]: https://github.com/pniemczyk/easyop/compare/v0.1.3...v0.1.4
|
|
1486
|
-
[0.1.3]: https://github.com/pniemczyk/easyop/compare/v0.1.2...v0.1.3
|
|
1487
1555
|
```
|
|
1488
1556
|
|
|
1489
1557
|
### 3. Commit the release changes
|
|
1490
1558
|
|
|
1491
1559
|
```bash
|
|
1492
1560
|
git add lib/easyop/version.rb CHANGELOG.md
|
|
1493
|
-
git commit -m "Release v0.1.
|
|
1561
|
+
git commit -m "Release v0.1.5"
|
|
1494
1562
|
```
|
|
1495
1563
|
|
|
1496
1564
|
### 4. Tag the commit
|
|
1497
1565
|
|
|
1498
1566
|
```bash
|
|
1499
|
-
git tag -a v0.1.
|
|
1567
|
+
git tag -a v0.1.5 -m "Release v0.1.5"
|
|
1500
1568
|
```
|
|
1501
1569
|
|
|
1502
1570
|
### 5. Push the commit and tag
|
|
1503
1571
|
|
|
1504
1572
|
```bash
|
|
1505
1573
|
git push origin master
|
|
1506
|
-
git push origin v0.1.
|
|
1574
|
+
git push origin v0.1.5
|
|
1507
1575
|
```
|
|
1508
1576
|
|
|
1509
1577
|
### 6. Build and push the gem (optional)
|
|
1510
1578
|
|
|
1511
1579
|
```bash
|
|
1512
1580
|
gem build easyop.gemspec
|
|
1513
|
-
gem push easyop-0.1.
|
|
1581
|
+
gem push easyop-0.1.5.gem
|
|
1514
1582
|
```
|
data/lib/easyop/configuration.rb
CHANGED
|
@@ -15,10 +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 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]".
|
|
21
|
+
# Accepts Symbol, String, or Regexp (matched against the stringified key name).
|
|
22
|
+
#
|
|
23
|
+
# @example
|
|
24
|
+
# Easyop.configure { |c| c.recording_filter_keys = [:api_token, /token/i] }
|
|
25
|
+
attr_accessor :recording_filter_keys
|
|
26
|
+
|
|
18
27
|
def initialize
|
|
19
|
-
@type_adapter
|
|
20
|
-
@strict_types
|
|
21
|
-
@event_bus
|
|
28
|
+
@type_adapter = :native
|
|
29
|
+
@strict_types = false
|
|
30
|
+
@event_bus = nil # nil = Memory bus (see Easyop::Events::Registry)
|
|
31
|
+
@recording_filter_keys = []
|
|
22
32
|
end
|
|
23
33
|
end
|
|
24
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,25 +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
|
-
#
|
|
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]".
|
|
41
50
|
module Recording
|
|
42
|
-
# Sensitive keys
|
|
43
|
-
|
|
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
|
|
44
54
|
|
|
45
55
|
# Internal ctx keys used for flow tracing — excluded from params_data.
|
|
46
56
|
INTERNAL_CTX_KEYS = %i[
|
|
47
57
|
__recording_root_reference_id
|
|
48
58
|
__recording_parent_operation_name
|
|
49
59
|
__recording_parent_reference_id
|
|
60
|
+
__recording_child_counts
|
|
50
61
|
].freeze
|
|
51
62
|
|
|
52
|
-
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)
|
|
53
64
|
base.extend(ClassMethods)
|
|
54
65
|
base.prepend(RunWrapper)
|
|
55
|
-
base.instance_variable_set(:@_recording_model,
|
|
66
|
+
base.instance_variable_set(:@_recording_model, model)
|
|
56
67
|
base.instance_variable_set(:@_recording_record_params, record_params)
|
|
57
68
|
base.instance_variable_set(:@_recording_record_result, record_result)
|
|
69
|
+
base.instance_variable_set(:@_recording_filter_keys, Array(filter_keys))
|
|
58
70
|
end
|
|
59
71
|
|
|
60
72
|
module ClassMethods
|
|
@@ -73,30 +85,48 @@ module Easyop
|
|
|
73
85
|
(superclass.respond_to?(:_recording_model) ? superclass._recording_model : nil)
|
|
74
86
|
end
|
|
75
87
|
|
|
76
|
-
|
|
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
|
|
77
107
|
if instance_variable_defined?(:@_recording_record_params)
|
|
78
108
|
@_recording_record_params
|
|
79
|
-
elsif superclass.respond_to?(:
|
|
80
|
-
superclass.
|
|
109
|
+
elsif superclass.respond_to?(:_recording_record_params_config)
|
|
110
|
+
superclass._recording_record_params_config
|
|
81
111
|
else
|
|
82
112
|
true
|
|
83
113
|
end
|
|
84
114
|
end
|
|
85
115
|
|
|
86
|
-
# DSL for capturing result data after the operation runs.
|
|
87
|
-
#
|
|
88
|
-
# record_result attrs: :key
|
|
89
|
-
# record_result { |ctx| {
|
|
90
|
-
# 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
|
|
91
121
|
def record_result(value = nil, attrs: nil, &block)
|
|
92
122
|
@_recording_record_result = if block
|
|
93
123
|
block
|
|
94
124
|
elsif attrs
|
|
95
125
|
{ attrs: attrs }
|
|
96
|
-
elsif value.
|
|
126
|
+
elsif !value.nil?
|
|
97
127
|
value
|
|
98
128
|
else
|
|
99
|
-
|
|
129
|
+
nil
|
|
100
130
|
end
|
|
101
131
|
end
|
|
102
132
|
|
|
@@ -106,9 +136,36 @@ module Easyop
|
|
|
106
136
|
elsif superclass.respond_to?(:_recording_record_result_config)
|
|
107
137
|
superclass._recording_record_result_config
|
|
108
138
|
else
|
|
109
|
-
|
|
139
|
+
false
|
|
110
140
|
end
|
|
111
141
|
end
|
|
142
|
+
|
|
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]".
|
|
147
|
+
#
|
|
148
|
+
# @example
|
|
149
|
+
# class ApplicationOperation < ...
|
|
150
|
+
# filter_params :api_token, /access.?key/i
|
|
151
|
+
# end
|
|
152
|
+
def filter_params(*keys)
|
|
153
|
+
@_recording_filter_keys = _own_recording_filter_keys + keys
|
|
154
|
+
end
|
|
155
|
+
|
|
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
|
|
158
|
+
# merged at persist time so they stay hot-reloadable.
|
|
159
|
+
def _recording_filter_keys
|
|
160
|
+
parent = superclass.respond_to?(:_recording_filter_keys) ? superclass._recording_filter_keys : []
|
|
161
|
+
parent + _own_recording_filter_keys
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
private
|
|
165
|
+
|
|
166
|
+
def _own_recording_filter_keys
|
|
167
|
+
instance_variable_defined?(:@_recording_filter_keys) ? @_recording_filter_keys : []
|
|
168
|
+
end
|
|
112
169
|
end
|
|
113
170
|
|
|
114
171
|
module RunWrapper
|
|
@@ -117,6 +174,12 @@ module Easyop
|
|
|
117
174
|
return super unless (model = self.class._recording_model)
|
|
118
175
|
return super unless self.class.name # skip anonymous classes
|
|
119
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
|
+
|
|
120
183
|
# -- Flow tracing --
|
|
121
184
|
# Each operation gets its own reference_id. The root_reference_id is
|
|
122
185
|
# shared across the entire execution tree via ctx (set once, inherited).
|
|
@@ -127,6 +190,14 @@ module Easyop
|
|
|
127
190
|
parent_operation_name = ctx[:__recording_parent_operation_name]
|
|
128
191
|
parent_reference_id = ctx[:__recording_parent_reference_id]
|
|
129
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
|
+
|
|
130
201
|
# Set THIS operation as the parent for any children that run inside super.
|
|
131
202
|
# Save the previous values so we can restore them after (for siblings).
|
|
132
203
|
prev_parent_name = parent_operation_name
|
|
@@ -151,7 +222,9 @@ module Easyop
|
|
|
151
222
|
root_reference_id: root_reference_id,
|
|
152
223
|
reference_id: reference_id,
|
|
153
224
|
parent_operation_name: parent_operation_name,
|
|
154
|
-
parent_reference_id: parent_reference_id
|
|
225
|
+
parent_reference_id: parent_reference_id,
|
|
226
|
+
execution_index: execution_index,
|
|
227
|
+
input_keys: input_keys)
|
|
155
228
|
end
|
|
156
229
|
end
|
|
157
230
|
|
|
@@ -159,7 +232,8 @@ module Easyop
|
|
|
159
232
|
|
|
160
233
|
def _recording_persist!(ctx, model, duration_ms,
|
|
161
234
|
root_reference_id: nil, reference_id: nil,
|
|
162
|
-
parent_operation_name: nil, parent_reference_id: nil
|
|
235
|
+
parent_operation_name: nil, parent_reference_id: nil,
|
|
236
|
+
execution_index: nil, input_keys: nil)
|
|
163
237
|
attrs = {
|
|
164
238
|
operation_name: self.class.name,
|
|
165
239
|
success: ctx.success?,
|
|
@@ -169,9 +243,11 @@ module Easyop
|
|
|
169
243
|
root_reference_id: root_reference_id,
|
|
170
244
|
reference_id: reference_id,
|
|
171
245
|
parent_operation_name: parent_operation_name,
|
|
172
|
-
parent_reference_id: parent_reference_id
|
|
246
|
+
parent_reference_id: parent_reference_id,
|
|
247
|
+
execution_index: execution_index
|
|
173
248
|
}
|
|
174
|
-
|
|
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
|
|
175
251
|
attrs[:result_data] = _recording_safe_result(ctx) if self.class._recording_record_result_config
|
|
176
252
|
|
|
177
253
|
# Only write columns the model actually has
|
|
@@ -181,37 +257,93 @@ module Easyop
|
|
|
181
257
|
_recording_warn(e)
|
|
182
258
|
end
|
|
183
259
|
|
|
184
|
-
def _recording_safe_params(ctx)
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
|
189
288
|
rescue
|
|
190
289
|
nil
|
|
191
290
|
end
|
|
192
291
|
|
|
292
|
+
# Returns true when +key+ matches any entry in +filter_list+.
|
|
293
|
+
# Regexp entries match against the stringified key name (case-sensitive
|
|
294
|
+
# unless the Regexp itself uses /i). Symbol/String entries match by value.
|
|
295
|
+
def _recording_filter_key?(key, extra_list)
|
|
296
|
+
return true if FILTERED_KEYS.include?(key.to_sym)
|
|
297
|
+
|
|
298
|
+
extra_list.any? do |pattern|
|
|
299
|
+
case pattern
|
|
300
|
+
when Regexp then pattern.match?(key.to_s)
|
|
301
|
+
when Symbol then pattern == key.to_sym
|
|
302
|
+
else pattern.to_s == key.to_s
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
|
|
193
307
|
def _recording_safe_result(ctx)
|
|
194
308
|
config = self.class._recording_record_result_config
|
|
195
309
|
return nil unless config
|
|
196
310
|
|
|
197
311
|
raw = case config
|
|
312
|
+
when true
|
|
313
|
+
ctx.to_h.except(*INTERNAL_CTX_KEYS)
|
|
198
314
|
when Hash
|
|
199
315
|
keys = Array(config[:attrs])
|
|
200
316
|
keys.each_with_object({}) { |k, h| h[k] = ctx[k] }
|
|
201
317
|
when Proc
|
|
202
|
-
config.call(ctx)
|
|
318
|
+
result = config.call(ctx)
|
|
319
|
+
return nil unless result.is_a?(Hash)
|
|
320
|
+
result
|
|
203
321
|
when Symbol
|
|
204
|
-
send(config)
|
|
322
|
+
result = send(config)
|
|
323
|
+
return nil unless result.is_a?(Hash)
|
|
324
|
+
result
|
|
205
325
|
end
|
|
206
326
|
|
|
207
327
|
return nil unless raw.is_a?(Hash)
|
|
208
|
-
|
|
209
|
-
raw.transform_values { |v| v.is_a?(ActiveRecord::Base) ? { id: v.id, class: v.class.name } : v }
|
|
210
|
-
.to_json
|
|
328
|
+
_recording_apply_and_serialize(raw).to_json
|
|
211
329
|
rescue
|
|
212
330
|
nil
|
|
213
331
|
end
|
|
214
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
|
+
|
|
215
347
|
def _recording_warn(err)
|
|
216
348
|
return unless defined?(Rails) && Rails.respond_to?(:logger)
|
|
217
349
|
Rails.logger.warn "[EasyOp::Recording] Failed to record #{self.class.name}: #{err.message}"
|
data/lib/easyop/version.rb
CHANGED