easyop 0.1.3 → 0.1.4
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 +60 -1
- data/README.md +117 -0
- data/lib/easyop/flow.rb +48 -0
- data/lib/easyop/plugins/recording.rb +112 -11
- data/lib/easyop/schema.rb +1 -1
- 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: 2b880af1b623158d3acfac130198bd96ce8c268195fd073554283738a880a45a
|
|
4
|
+
data.tar.gz: 0c4f5554b456ac252b13734d0964155e2a80909e81946e28f89c529cc60f9081
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c408d6d42a357ba1a6df4da21e13a7b0498d36fa4252e86c68aff289e8843d94d056c430f654f023e6d801ee650fc1bc4c400cb6d058a6b209129e1fbda644fd
|
|
7
|
+
data.tar.gz: e89087c84c8fbc77abe96b6713a144dd34b78c2cfd35066f28a14e37516f0e7ae8b0f0cbbcfe0ca9eb9de646c7890552b64d7d2848c9976ff6f51637531a4a8a
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,64 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.1.4] — 2026-04-14
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **Flow-tracing forwarding in `Easyop::Flow`** — `CallBehavior#call` now automatically sets the `__recording_parent_*` ctx keys before running steps, so every child operation's log entry carries the flow class as its `parent_operation_name` and `parent_reference_id`. This works even when Recording is not installed on the flow class itself (bare `include Easyop::Flow`). When Recording IS installed (recommended: inherit from ApplicationOperation and add `transactional false`), the flow appears in the log as the root entry and RunWrapper handles the ctx setup — no double-setup occurs.
|
|
15
|
+
|
|
16
|
+
```ruby
|
|
17
|
+
# Bare flow — Recording on steps only; flow itself is not recorded but
|
|
18
|
+
# steps correctly show parent_operation_name: "Flows::Checkout"
|
|
19
|
+
class Flows::Checkout
|
|
20
|
+
include Easyop::Flow
|
|
21
|
+
flow Orders::CreateOrder, Orders::ProcessPayment
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Recommended — flow is recorded as root, steps as children:
|
|
25
|
+
class Flows::Checkout < ApplicationOperation
|
|
26
|
+
include Easyop::Flow
|
|
27
|
+
transactional false # steps manage their own transactions
|
|
28
|
+
flow Orders::CreateOrder, Orders::ProcessPayment
|
|
29
|
+
end
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Result in operation_logs:
|
|
33
|
+
```
|
|
34
|
+
Flows::Checkout root=aaa ref=bbb parent=nil
|
|
35
|
+
Orders::CreateOrder root=aaa ref=ccc parent=Flows::Checkout/bbb
|
|
36
|
+
Orders::ProcessPayment root=aaa ref=ddd parent=Flows::Checkout/bbb
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
- **`record_result` DSL for `Easyop::Plugins::Recording`** — selectively persist ctx output data into a new optional `result_data :text` column (stored as JSON). Supports three forms:
|
|
40
|
+
|
|
41
|
+
```ruby
|
|
42
|
+
# Attrs form — one or more ctx keys
|
|
43
|
+
record_result attrs: :invoice_id
|
|
44
|
+
record_result attrs: [:invoice_id, :total]
|
|
45
|
+
|
|
46
|
+
# Block form — custom extraction
|
|
47
|
+
record_result { |ctx| { total: ctx.total, items: ctx.items.count } }
|
|
48
|
+
|
|
49
|
+
# Symbol form — delegates to a private instance method
|
|
50
|
+
record_result :build_result
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Plugin-level default (inherited by all subclasses):
|
|
54
|
+
```ruby
|
|
55
|
+
plugin Easyop::Plugins::Recording, model: OperationLog, record_result: { attrs: :metadata }
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Class-level `record_result` overrides the plugin-level default. Missing ctx keys produce `nil` (no error). ActiveRecord objects are serialized as `{ id:, class: }`. Serialization errors are swallowed. The `result_data` column is silently skipped when absent from the model table — fully backward-compatible.
|
|
59
|
+
|
|
60
|
+
### Fixed
|
|
61
|
+
|
|
62
|
+
- **`Easyop::Schema` type-mismatch warning suppressed when `$VERBOSE` is `nil`** — `warn` is a no-op in Ruby when `$VERBOSE` is `nil` (e.g. when running under certain test setups or with `-W0`). Changed to `$stderr.puts` so the `[EasyOp]` type-mismatch message always reaches stderr regardless of Ruby's verbosity flag.
|
|
63
|
+
|
|
64
|
+
- **`Easyop::Schema` spec `strict_types` contamination across examples** — Setting `strict_types = true` inside an example without a corresponding teardown left the global config dirty for later examples. Added `after(:each) { Easyop.reset_config! }` at the top-level `RSpec.describe` so every example begins with a clean configuration.
|
|
65
|
+
|
|
66
|
+
- **`Easyop::Plugins::Instrumentation` flaky duration assertion** — The `:duration` payload is computed as `(elapsed_ms).round(2)`, which rounds to `0.0` for operations completing in under 0.005 ms. Relaxed the spec assertion from `be > 0` to `be >= 0` to reflect that a rounded-to-zero duration is a valid measurement, not an error.
|
|
67
|
+
|
|
10
68
|
## [0.1.3] — 2026-04-14
|
|
11
69
|
|
|
12
70
|
### Added
|
|
@@ -200,5 +258,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
200
258
|
- `examples/easyop_test_app/` — full Rails 8 blog application demonstrating all features in real-world code
|
|
201
259
|
- `examples/usage.rb` — 13 runnable plain-Ruby examples
|
|
202
260
|
|
|
203
|
-
[Unreleased]: https://github.com/pniemczyk/easyop/compare/v0.1.
|
|
261
|
+
[Unreleased]: https://github.com/pniemczyk/easyop/compare/v0.1.4...HEAD
|
|
262
|
+
[0.1.4]: https://github.com/pniemczyk/easyop/compare/v0.1.3...v0.1.4
|
|
204
263
|
[0.1.3]: https://github.com/pniemczyk/easyop/compare/v0.1.2...v0.1.3
|
data/README.md
CHANGED
|
@@ -428,6 +428,39 @@ class FullCheckout
|
|
|
428
428
|
end
|
|
429
429
|
```
|
|
430
430
|
|
|
431
|
+
### Recording plugin integration — full call-tree tracing
|
|
432
|
+
|
|
433
|
+
When step operations have the Recording plugin installed, `Easyop::Flow` automatically forwards the parent-tracing ctx so every step's log entry shows the flow as its `parent_operation_name`. All steps and the flow share the same `root_reference_id`.
|
|
434
|
+
|
|
435
|
+
**Bare flow** (Recording only on steps — flow is NOT recorded itself, but steps carry correct parent info):
|
|
436
|
+
|
|
437
|
+
```ruby
|
|
438
|
+
class ProcessCheckout
|
|
439
|
+
include Easyop::Flow
|
|
440
|
+
flow ValidateCart, ChargePayment, CreateOrder
|
|
441
|
+
end
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
**Recommended** — inherit from your recorded base class so the **flow itself appears in operation_logs** as the tree root. Add `transactional false` so step-level transactions aren't shadowed by an outer one:
|
|
445
|
+
|
|
446
|
+
```ruby
|
|
447
|
+
class ProcessCheckout < ApplicationOperation
|
|
448
|
+
include Easyop::Flow
|
|
449
|
+
transactional false # EasyOp handles rollback; each step owns its transaction
|
|
450
|
+
|
|
451
|
+
flow ValidateCart, ChargePayment, CreateOrder
|
|
452
|
+
end
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
Result in `operation_logs`:
|
|
456
|
+
|
|
457
|
+
```
|
|
458
|
+
ProcessCheckout root=aaa ref=bbb parent=nil
|
|
459
|
+
ValidateCart root=aaa ref=ccc parent=ProcessCheckout/bbb
|
|
460
|
+
ChargePayment root=aaa ref=ddd parent=ProcessCheckout/bbb
|
|
461
|
+
CreateOrder root=aaa ref=eee parent=ProcessCheckout/bbb
|
|
462
|
+
```
|
|
463
|
+
|
|
431
464
|
---
|
|
432
465
|
|
|
433
466
|
## `prepare` — Pre-registered Callbacks
|
|
@@ -613,6 +646,7 @@ end
|
|
|
613
646
|
|---|---|---|
|
|
614
647
|
| `model:` | required | ActiveRecord class to write logs into |
|
|
615
648
|
| `record_params:` | `true` | Set `false` to skip serializing ctx params |
|
|
649
|
+
| `record_result:` | `nil` | Plugin-level default for result capture (Hash/Proc/Symbol — see below) |
|
|
616
650
|
|
|
617
651
|
**Required model columns:**
|
|
618
652
|
|
|
@@ -627,8 +661,91 @@ create_table :operation_logs do |t|
|
|
|
627
661
|
end
|
|
628
662
|
```
|
|
629
663
|
|
|
664
|
+
**Optional flow-tracing columns:**
|
|
665
|
+
|
|
666
|
+
Add these columns to reconstruct the full call tree when nested flows run. They are populated automatically when present — missing columns are silently skipped (backward-compatible):
|
|
667
|
+
|
|
668
|
+
```ruby
|
|
669
|
+
add_column :operation_logs, :root_reference_id, :string
|
|
670
|
+
add_column :operation_logs, :reference_id, :string
|
|
671
|
+
add_column :operation_logs, :parent_operation_name, :string
|
|
672
|
+
add_column :operation_logs, :parent_reference_id, :string
|
|
673
|
+
|
|
674
|
+
add_index :operation_logs, :root_reference_id
|
|
675
|
+
add_index :operation_logs, :reference_id, unique: true
|
|
676
|
+
add_index :operation_logs, :parent_reference_id
|
|
677
|
+
```
|
|
678
|
+
|
|
679
|
+
All operations triggered by a single top-level call share the same `root_reference_id`. The `parent_operation_name` and `parent_reference_id` columns link each operation to its direct caller. `Easyop::Flow` automatically forwards these ctx keys to child steps — see the [Flow section](#flow--composing-operations) for how to make the flow itself appear as the tree root. Example (flow with nested steps):
|
|
680
|
+
|
|
681
|
+
```
|
|
682
|
+
FullCheckout root=aaa ref=bbb parent=nil
|
|
683
|
+
AuthAndValidate root=aaa ref=ccc parent=FullCheckout/bbb
|
|
684
|
+
AuthUser root=aaa ref=ddd parent=AuthAndValidate/ccc
|
|
685
|
+
ProcessPayment root=aaa ref=eee parent=FullCheckout/bbb
|
|
686
|
+
```
|
|
687
|
+
|
|
688
|
+
Useful model helpers:
|
|
689
|
+
|
|
690
|
+
```ruby
|
|
691
|
+
scope :for_tree, ->(id) { where(root_reference_id: id).order(:performed_at) }
|
|
692
|
+
def root?; parent_reference_id.nil?; end
|
|
693
|
+
|
|
694
|
+
# Fetch the entire execution tree for one top-level call:
|
|
695
|
+
root_log = OperationLog.find_by(operation_name: "FullCheckout", parent_reference_id: nil)
|
|
696
|
+
OperationLog.for_tree(root_log.root_reference_id)
|
|
697
|
+
```
|
|
698
|
+
|
|
630
699
|
The plugin automatically scrubs these keys from `params_data` before persisting: `:password`, `:password_confirmation`, `:token`, `:secret`, `:api_key`. ActiveRecord objects are serialized as `{ id:, class: }` rather than their full representation.
|
|
631
700
|
|
|
701
|
+
**`record_result` DSL — capture output data:**
|
|
702
|
+
|
|
703
|
+
Add an optional `result_data :text` column to persist selected ctx values after the operation runs:
|
|
704
|
+
|
|
705
|
+
```ruby
|
|
706
|
+
add_column :operation_logs, :result_data, :text # stored as JSON
|
|
707
|
+
```
|
|
708
|
+
|
|
709
|
+
Then declare what to record using the `record_result` DSL (three forms):
|
|
710
|
+
|
|
711
|
+
```ruby
|
|
712
|
+
# Attrs form — one or more ctx keys
|
|
713
|
+
class PlaceOrder < ApplicationOperation
|
|
714
|
+
record_result attrs: :order_id
|
|
715
|
+
end
|
|
716
|
+
|
|
717
|
+
class ProcessPayment < ApplicationOperation
|
|
718
|
+
record_result attrs: [:charge_id, :amount_cents]
|
|
719
|
+
end
|
|
720
|
+
|
|
721
|
+
# Block form — custom extraction
|
|
722
|
+
class GenerateReport < ApplicationOperation
|
|
723
|
+
record_result { |ctx| { rows: ctx.rows.count, format: ctx.format } }
|
|
724
|
+
end
|
|
725
|
+
|
|
726
|
+
# Symbol form — delegates to a private instance method
|
|
727
|
+
class BuildInvoice < ApplicationOperation
|
|
728
|
+
record_result :build_result
|
|
729
|
+
|
|
730
|
+
private
|
|
731
|
+
|
|
732
|
+
def build_result
|
|
733
|
+
{ invoice_id: ctx.invoice.id, total: ctx.total }
|
|
734
|
+
end
|
|
735
|
+
end
|
|
736
|
+
```
|
|
737
|
+
|
|
738
|
+
Set a plugin-level default inherited by all subclasses:
|
|
739
|
+
|
|
740
|
+
```ruby
|
|
741
|
+
plugin Easyop::Plugins::Recording, model: OperationLog,
|
|
742
|
+
record_result: { attrs: :metadata }
|
|
743
|
+
# or: record_result: ->(ctx) { { id: ctx.record_id } }
|
|
744
|
+
# or: record_result: :build_result
|
|
745
|
+
```
|
|
746
|
+
|
|
747
|
+
Class-level `record_result` overrides the plugin-level default. Missing ctx keys produce `nil` — no error. The `result_data` column is silently skipped when absent from the model table — fully backward-compatible.
|
|
748
|
+
|
|
632
749
|
**Opt out per class:**
|
|
633
750
|
|
|
634
751
|
```ruby
|
data/lib/easyop/flow.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
require 'securerandom'
|
|
2
|
+
|
|
1
3
|
module Easyop
|
|
2
4
|
# Compose a sequence of operations that share a single ctx.
|
|
3
5
|
#
|
|
@@ -11,6 +13,27 @@ module Easyop
|
|
|
11
13
|
# flow ValidateCart, ChargeCard, CreateOrder, NotifyUser
|
|
12
14
|
# end
|
|
13
15
|
#
|
|
16
|
+
# ## Recording plugin integration (flow tracing)
|
|
17
|
+
#
|
|
18
|
+
# When steps have the Recording plugin installed, `CallBehavior#call` forwards
|
|
19
|
+
# the parent ctx keys so every step log entry shows the flow as its parent:
|
|
20
|
+
#
|
|
21
|
+
# # Bare flow — flow itself is not recorded but steps see it as parent:
|
|
22
|
+
# class ProcessOrder
|
|
23
|
+
# include Easyop::Flow
|
|
24
|
+
# flow ValidateCart, ChargeCard
|
|
25
|
+
# end
|
|
26
|
+
#
|
|
27
|
+
# For full tree reconstruction (flow appears in operation_logs as the root
|
|
28
|
+
# entry) inherit from your recorded base class and opt out of the transaction
|
|
29
|
+
# so step-level transactions are not shadowed by an outer one:
|
|
30
|
+
#
|
|
31
|
+
# class ProcessOrder < ApplicationOperation
|
|
32
|
+
# include Easyop::Flow
|
|
33
|
+
# transactional false # EasyOp handles rollback; steps own their transactions
|
|
34
|
+
# flow ValidateCart, ChargeCard
|
|
35
|
+
# end
|
|
36
|
+
#
|
|
14
37
|
# result = ProcessOrder.call(user: user, cart: cart)
|
|
15
38
|
# result.on_success { |ctx| redirect_to order_path(ctx.order) }
|
|
16
39
|
# result.on_failure { |ctx| flash[:alert] = ctx.error }
|
|
@@ -31,6 +54,25 @@ module Easyop
|
|
|
31
54
|
# otherwise place Operation earlier in the ancestor chain than Flow itself).
|
|
32
55
|
module CallBehavior
|
|
33
56
|
def call
|
|
57
|
+
# ── Flow-tracing forwarding for the Recording plugin ──────────────────
|
|
58
|
+
# When Recording is NOT installed on this flow class (i.e. the flow does
|
|
59
|
+
# not inherit from a base operation that has Recording), set the
|
|
60
|
+
# __recording_parent_* ctx keys manually so every step operation knows
|
|
61
|
+
# this flow is its parent. When Recording IS installed on the flow (its
|
|
62
|
+
# RunWrapper runs before `call` is reached), it has already set up the
|
|
63
|
+
# parent context correctly — we detect that via _recording_enabled? and
|
|
64
|
+
# skip to avoid a conflict. If Recording is not used at all, these ctx
|
|
65
|
+
# keys are unused and ignored.
|
|
66
|
+
_flow_tracing = self.class.name &&
|
|
67
|
+
!self.class.respond_to?(:_recording_enabled?)
|
|
68
|
+
if _flow_tracing
|
|
69
|
+
ctx[:__recording_root_reference_id] ||= SecureRandom.uuid
|
|
70
|
+
_prev_parent_name = ctx[:__recording_parent_operation_name]
|
|
71
|
+
_prev_parent_id = ctx[:__recording_parent_reference_id]
|
|
72
|
+
ctx[:__recording_parent_operation_name] = self.class.name
|
|
73
|
+
ctx[:__recording_parent_reference_id] = SecureRandom.uuid
|
|
74
|
+
end
|
|
75
|
+
|
|
34
76
|
pending_guard = nil
|
|
35
77
|
|
|
36
78
|
self.class._flow_steps.each do |step|
|
|
@@ -56,6 +98,12 @@ module Easyop
|
|
|
56
98
|
rescue Ctx::Failure
|
|
57
99
|
ctx.rollback!
|
|
58
100
|
raise
|
|
101
|
+
ensure
|
|
102
|
+
# Restore parent context so any caller above this flow sees the right parent.
|
|
103
|
+
if _flow_tracing
|
|
104
|
+
ctx[:__recording_parent_operation_name] = _prev_parent_name
|
|
105
|
+
ctx[:__recording_parent_reference_id] = _prev_parent_id
|
|
106
|
+
end
|
|
59
107
|
end
|
|
60
108
|
end
|
|
61
109
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'securerandom'
|
|
4
|
+
|
|
3
5
|
module Easyop
|
|
4
6
|
module Plugins
|
|
5
7
|
# Records operation executions to a database model.
|
|
@@ -18,6 +20,15 @@ module Easyop
|
|
|
18
20
|
# duration_ms :float
|
|
19
21
|
# performed_at :datetime, null: false
|
|
20
22
|
#
|
|
23
|
+
# Optional flow-tracing columns:
|
|
24
|
+
# root_reference_id :string # shared across entire execution tree
|
|
25
|
+
# reference_id :string # unique to this operation execution
|
|
26
|
+
# parent_operation_name :string # class name of the direct parent
|
|
27
|
+
# parent_reference_id :string # reference_id of the direct parent
|
|
28
|
+
#
|
|
29
|
+
# Optional result column:
|
|
30
|
+
# result_data :text # stored as JSON — selected ctx keys after call
|
|
31
|
+
#
|
|
21
32
|
# Opt out per operation class:
|
|
22
33
|
# class MyOp < ApplicationOperation
|
|
23
34
|
# recording false
|
|
@@ -26,15 +37,24 @@ module Easyop
|
|
|
26
37
|
# Options:
|
|
27
38
|
# model: (required) ActiveRecord class
|
|
28
39
|
# record_params: true pass false to skip params serialization
|
|
40
|
+
# record_result: nil configure result capture at plugin level (Hash/Proc/Symbol)
|
|
29
41
|
module Recording
|
|
30
42
|
# Sensitive keys scrubbed from params_data before persisting.
|
|
31
43
|
SCRUBBED_KEYS = %i[password password_confirmation token secret api_key].freeze
|
|
32
44
|
|
|
33
|
-
|
|
45
|
+
# Internal ctx keys used for flow tracing — excluded from params_data.
|
|
46
|
+
INTERNAL_CTX_KEYS = %i[
|
|
47
|
+
__recording_root_reference_id
|
|
48
|
+
__recording_parent_operation_name
|
|
49
|
+
__recording_parent_reference_id
|
|
50
|
+
].freeze
|
|
51
|
+
|
|
52
|
+
def self.install(base, model:, record_params: true, record_result: nil, **_options)
|
|
34
53
|
base.extend(ClassMethods)
|
|
35
54
|
base.prepend(RunWrapper)
|
|
36
|
-
base.instance_variable_set(:@_recording_model,
|
|
37
|
-
base.instance_variable_set(:@_recording_record_params,
|
|
55
|
+
base.instance_variable_set(:@_recording_model, model)
|
|
56
|
+
base.instance_variable_set(:@_recording_record_params, record_params)
|
|
57
|
+
base.instance_variable_set(:@_recording_record_result, record_result)
|
|
38
58
|
end
|
|
39
59
|
|
|
40
60
|
module ClassMethods
|
|
@@ -62,6 +82,33 @@ module Easyop
|
|
|
62
82
|
true
|
|
63
83
|
end
|
|
64
84
|
end
|
|
85
|
+
|
|
86
|
+
# DSL for capturing result data after the operation runs.
|
|
87
|
+
# Three forms:
|
|
88
|
+
# record_result attrs: :key # one or more ctx keys
|
|
89
|
+
# record_result { |ctx| { k: ctx.k } } # block
|
|
90
|
+
# record_result :build_result # private instance method name
|
|
91
|
+
def record_result(value = nil, attrs: nil, &block)
|
|
92
|
+
@_recording_record_result = if block
|
|
93
|
+
block
|
|
94
|
+
elsif attrs
|
|
95
|
+
{ attrs: attrs }
|
|
96
|
+
elsif value.is_a?(Symbol)
|
|
97
|
+
value
|
|
98
|
+
else
|
|
99
|
+
value
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def _recording_record_result_config
|
|
104
|
+
if instance_variable_defined?(:@_recording_record_result)
|
|
105
|
+
@_recording_record_result
|
|
106
|
+
elsif superclass.respond_to?(:_recording_record_result_config)
|
|
107
|
+
superclass._recording_record_result_config
|
|
108
|
+
else
|
|
109
|
+
nil
|
|
110
|
+
end
|
|
111
|
+
end
|
|
65
112
|
end
|
|
66
113
|
|
|
67
114
|
module RunWrapper
|
|
@@ -70,6 +117,23 @@ module Easyop
|
|
|
70
117
|
return super unless (model = self.class._recording_model)
|
|
71
118
|
return super unless self.class.name # skip anonymous classes
|
|
72
119
|
|
|
120
|
+
# -- Flow tracing --
|
|
121
|
+
# Each operation gets its own reference_id. The root_reference_id is
|
|
122
|
+
# shared across the entire execution tree via ctx (set once, inherited).
|
|
123
|
+
reference_id = SecureRandom.uuid
|
|
124
|
+
root_reference_id = ctx[:__recording_root_reference_id] ||= SecureRandom.uuid
|
|
125
|
+
|
|
126
|
+
# Read current parent context — these become THIS operation's parent fields.
|
|
127
|
+
parent_operation_name = ctx[:__recording_parent_operation_name]
|
|
128
|
+
parent_reference_id = ctx[:__recording_parent_reference_id]
|
|
129
|
+
|
|
130
|
+
# Set THIS operation as the parent for any children that run inside super.
|
|
131
|
+
# Save the previous values so we can restore them after (for siblings).
|
|
132
|
+
prev_parent_name = parent_operation_name
|
|
133
|
+
prev_parent_id = parent_reference_id
|
|
134
|
+
ctx[:__recording_parent_operation_name] = self.class.name
|
|
135
|
+
ctx[:__recording_parent_reference_id] = reference_id
|
|
136
|
+
|
|
73
137
|
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
74
138
|
super
|
|
75
139
|
ensure
|
|
@@ -78,22 +142,37 @@ module Easyop
|
|
|
78
142
|
# branch the tap block would be skipped and failures inside flows would
|
|
79
143
|
# never be persisted.
|
|
80
144
|
if start
|
|
145
|
+
# Restore parent context so sibling steps see the correct parent.
|
|
146
|
+
ctx[:__recording_parent_operation_name] = prev_parent_name
|
|
147
|
+
ctx[:__recording_parent_reference_id] = prev_parent_id
|
|
148
|
+
|
|
81
149
|
ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1000).round(2)
|
|
82
|
-
_recording_persist!(ctx, model, ms
|
|
150
|
+
_recording_persist!(ctx, model, ms,
|
|
151
|
+
root_reference_id: root_reference_id,
|
|
152
|
+
reference_id: reference_id,
|
|
153
|
+
parent_operation_name: parent_operation_name,
|
|
154
|
+
parent_reference_id: parent_reference_id)
|
|
83
155
|
end
|
|
84
156
|
end
|
|
85
157
|
|
|
86
158
|
private
|
|
87
159
|
|
|
88
|
-
def _recording_persist!(ctx, model, duration_ms
|
|
160
|
+
def _recording_persist!(ctx, model, duration_ms,
|
|
161
|
+
root_reference_id: nil, reference_id: nil,
|
|
162
|
+
parent_operation_name: nil, parent_reference_id: nil)
|
|
89
163
|
attrs = {
|
|
90
|
-
operation_name:
|
|
91
|
-
success:
|
|
92
|
-
error_message:
|
|
93
|
-
performed_at:
|
|
94
|
-
duration_ms:
|
|
164
|
+
operation_name: self.class.name,
|
|
165
|
+
success: ctx.success?,
|
|
166
|
+
error_message: ctx.error,
|
|
167
|
+
performed_at: Time.current,
|
|
168
|
+
duration_ms: duration_ms,
|
|
169
|
+
root_reference_id: root_reference_id,
|
|
170
|
+
reference_id: reference_id,
|
|
171
|
+
parent_operation_name: parent_operation_name,
|
|
172
|
+
parent_reference_id: parent_reference_id
|
|
95
173
|
}
|
|
96
174
|
attrs[:params_data] = _recording_safe_params(ctx) if self.class._recording_record_params?
|
|
175
|
+
attrs[:result_data] = _recording_safe_result(ctx) if self.class._recording_record_result_config
|
|
97
176
|
|
|
98
177
|
# Only write columns the model actually has
|
|
99
178
|
safe = attrs.select { |k, _| model.column_names.include?(k.to_s) }
|
|
@@ -104,13 +183,35 @@ module Easyop
|
|
|
104
183
|
|
|
105
184
|
def _recording_safe_params(ctx)
|
|
106
185
|
ctx.to_h
|
|
107
|
-
.except(*SCRUBBED_KEYS)
|
|
186
|
+
.except(*SCRUBBED_KEYS, *INTERNAL_CTX_KEYS)
|
|
108
187
|
.transform_values { |v| v.is_a?(ActiveRecord::Base) ? { id: v.id, class: v.class.name } : v }
|
|
109
188
|
.to_json
|
|
110
189
|
rescue
|
|
111
190
|
nil
|
|
112
191
|
end
|
|
113
192
|
|
|
193
|
+
def _recording_safe_result(ctx)
|
|
194
|
+
config = self.class._recording_record_result_config
|
|
195
|
+
return nil unless config
|
|
196
|
+
|
|
197
|
+
raw = case config
|
|
198
|
+
when Hash
|
|
199
|
+
keys = Array(config[:attrs])
|
|
200
|
+
keys.each_with_object({}) { |k, h| h[k] = ctx[k] }
|
|
201
|
+
when Proc
|
|
202
|
+
config.call(ctx)
|
|
203
|
+
when Symbol
|
|
204
|
+
send(config)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
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
|
|
211
|
+
rescue
|
|
212
|
+
nil
|
|
213
|
+
end
|
|
214
|
+
|
|
114
215
|
def _recording_warn(err)
|
|
115
216
|
return unless defined?(Rails) && Rails.respond_to?(:logger)
|
|
116
217
|
Rails.logger.warn "[EasyOp::Recording] Failed to record #{self.class.name}: #{err.message}"
|
data/lib/easyop/schema.rb
CHANGED
data/lib/easyop/version.rb
CHANGED