dexkit 0.9.0 → 0.10.0
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 +22 -0
- data/README.md +44 -16
- data/gemfiles/mongoid_no_ar.gemfile.lock +2 -2
- data/guides/llm/EVENT.md +24 -19
- data/guides/llm/FORM.md +200 -59
- data/guides/llm/OPERATION.md +27 -3
- data/guides/llm/QUERY.md +50 -0
- data/lib/dex/context_dsl.rb +56 -0
- data/lib/dex/context_setup.rb +2 -33
- data/lib/dex/event/bus.rb +78 -8
- data/lib/dex/event/handler.rb +18 -0
- data/lib/dex/event/metadata.rb +16 -9
- data/lib/dex/event/processor.rb +1 -1
- data/lib/dex/event/test_helpers.rb +1 -1
- data/lib/dex/event/trace.rb +14 -27
- data/lib/dex/event.rb +2 -7
- data/lib/dex/form/context.rb +27 -0
- data/lib/dex/form/export.rb +128 -0
- data/lib/dex/form/nesting.rb +2 -0
- data/lib/dex/form.rb +119 -3
- data/lib/dex/id.rb +38 -0
- data/lib/dex/operation/async_proxy.rb +12 -2
- data/lib/dex/operation/jobs.rb +5 -4
- data/lib/dex/operation/once_wrapper.rb +1 -0
- data/lib/dex/operation/record_backend.rb +2 -1
- data/lib/dex/operation/record_wrapper.rb +14 -4
- data/lib/dex/operation/test_helpers/assertions.rb +24 -0
- data/lib/dex/operation/test_helpers.rb +11 -1
- data/lib/dex/operation/trace_wrapper.rb +20 -0
- data/lib/dex/operation.rb +2 -0
- data/lib/dex/query/export.rb +64 -0
- data/lib/dex/query.rb +41 -0
- data/lib/dex/test_log.rb +62 -4
- data/lib/dex/trace.rb +291 -0
- data/lib/dex/version.rb +1 -1
- data/lib/dexkit.rb +3 -0
- metadata +8 -1
data/guides/llm/OPERATION.md
CHANGED
|
@@ -264,7 +264,7 @@ info = Order::Place.explain(product: product, customer: customer, quantity: 2)
|
|
|
264
264
|
# transaction: { enabled: true },
|
|
265
265
|
# rescue_from: { "Stripe::CardError" => :card_declined },
|
|
266
266
|
# callbacks: { before: 1, after: 2, around: 0 },
|
|
267
|
-
# pipeline: [:result, :guard, :once, :lock, :record, :transaction, :rescue, :callback],
|
|
267
|
+
# pipeline: [:trace, :result, :guard, :once, :lock, :record, :transaction, :rescue, :callback],
|
|
268
268
|
# callable: true
|
|
269
269
|
# }
|
|
270
270
|
```
|
|
@@ -481,8 +481,12 @@ Props serialize/deserialize automatically (Date, Time, BigDecimal, Symbol, `_Ref
|
|
|
481
481
|
Record execution to database. Requires `Dex.configure { |c| c.record_class = OperationRecord }`.
|
|
482
482
|
|
|
483
483
|
```ruby
|
|
484
|
-
create_table :operation_records do |t|
|
|
484
|
+
create_table :operation_records, id: :string do |t|
|
|
485
485
|
t.string :name, null: false # operation class name
|
|
486
|
+
t.string :trace_id # shared trace / correlation ID
|
|
487
|
+
t.string :actor_type # root actor type
|
|
488
|
+
t.string :actor_id # root actor ID
|
|
489
|
+
t.jsonb :trace # full trace snapshot
|
|
486
490
|
t.jsonb :params # serialized props (nil = not captured)
|
|
487
491
|
t.jsonb :result # serialized return value
|
|
488
492
|
t.string :status, null: false # pending/running/completed/error/failed
|
|
@@ -498,6 +502,8 @@ end
|
|
|
498
502
|
add_index :operation_records, :name
|
|
499
503
|
add_index :operation_records, :status
|
|
500
504
|
add_index :operation_records, [:name, :status]
|
|
505
|
+
add_index :operation_records, :trace_id
|
|
506
|
+
add_index :operation_records, [:actor_type, :actor_id]
|
|
501
507
|
```
|
|
502
508
|
|
|
503
509
|
Control per-operation:
|
|
@@ -518,12 +524,30 @@ Required attributes by feature:
|
|
|
518
524
|
- Async record jobs: `params`
|
|
519
525
|
- `once`: `once_key`, plus `once_key_expires_at` when `expires_in:` is used
|
|
520
526
|
|
|
527
|
+
Trace columns (`id`, `trace_id`, `actor_type`, `actor_id`, `trace`) are recommended for tracing. Dex persists them when present, omits them when missing.
|
|
528
|
+
|
|
521
529
|
Untyped results are sanitized to JSON-safe values before persistence: Hash keys round-trip as strings, and objects fall back to `as_json`/`to_s` under `"_dex_value"`.
|
|
522
530
|
|
|
523
531
|
Status values: `pending` (async enqueued), `running` (async executing), `completed` (success), `error` (business error via `error!`), `failed` (unhandled exception).
|
|
524
532
|
|
|
525
533
|
When both async and recording are enabled, dexkit automatically stores only the record ID in the job payload instead of full params.
|
|
526
534
|
|
|
535
|
+
## Execution tracing
|
|
536
|
+
|
|
537
|
+
Every operation call gets an `op_...` execution ID and joins a fiber-local trace shared across operations, handlers, and async jobs.
|
|
538
|
+
|
|
539
|
+
```ruby
|
|
540
|
+
Dex::Trace.start(actor: { type: :user, id: current_user.id }) do
|
|
541
|
+
Order::Place.call(product: product, customer: customer, quantity: 2)
|
|
542
|
+
end
|
|
543
|
+
|
|
544
|
+
Dex::Trace.trace_id # => "tr_..."
|
|
545
|
+
Dex::Trace.current # => [{ type: :actor, ... }, { type: :operation, ... }]
|
|
546
|
+
Dex::Trace.to_s # => "user:42 > Order::Place(op_2nFg7K)"
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
Tracing is always on – no opt-in needed. Async operations serialize and restore the trace automatically. When recording is enabled, `trace_id`, `actor_type`, `actor_id`, and `trace` are persisted alongside the usual record fields.
|
|
550
|
+
|
|
527
551
|
---
|
|
528
552
|
|
|
529
553
|
## Idempotency (once)
|
|
@@ -577,7 +601,7 @@ ChargeOrder.clear_once!("webhook-123") # by raw string key
|
|
|
577
601
|
|
|
578
602
|
Clearing is idempotent — clearing a non-existent key is a no-op. After clearing, the next call executes normally.
|
|
579
603
|
|
|
580
|
-
**Pipeline position:** result → **once** → lock → record → transaction → rescue →
|
|
604
|
+
**Pipeline position:** trace → result → guard → **once** → lock → record → transaction → rescue → callback. The once check runs before locking and recording, so duplicate calls short-circuit early.
|
|
581
605
|
|
|
582
606
|
**Requirements:**
|
|
583
607
|
|
data/guides/llm/QUERY.md
CHANGED
|
@@ -10,12 +10,17 @@ All examples below build on this query unless noted otherwise:
|
|
|
10
10
|
|
|
11
11
|
```ruby
|
|
12
12
|
class UserSearch < Dex::Query
|
|
13
|
+
description "Search and filter users"
|
|
14
|
+
|
|
13
15
|
scope { User.all }
|
|
14
16
|
|
|
15
17
|
prop? :name, String
|
|
16
18
|
prop? :role, _Array(String)
|
|
17
19
|
prop? :age_min, Integer
|
|
18
20
|
prop? :status, String
|
|
21
|
+
prop? :tenant, String
|
|
22
|
+
|
|
23
|
+
context tenant: :current_tenant
|
|
19
24
|
|
|
20
25
|
filter :name, :contains
|
|
21
26
|
filter :role, :in
|
|
@@ -71,6 +76,51 @@ prop? :age_min, Integer # optional integer
|
|
|
71
76
|
|
|
72
77
|
Reserved prop names: `scope`, `sort`, `resolve`, `call`, `from_params`, `to_params`, `param_key`.
|
|
73
78
|
|
|
79
|
+
### Description
|
|
80
|
+
|
|
81
|
+
```ruby
|
|
82
|
+
class UserSearch < Dex::Query
|
|
83
|
+
description "Search and filter users"
|
|
84
|
+
end
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Context
|
|
88
|
+
|
|
89
|
+
Same `context` DSL as Operation and Event. Auto-fills props from `Dex.with_context`:
|
|
90
|
+
|
|
91
|
+
```ruby
|
|
92
|
+
class UserSearch < Dex::Query
|
|
93
|
+
prop? :tenant, String
|
|
94
|
+
context tenant: :current_tenant
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# In a controller around_action:
|
|
98
|
+
Dex.with_context(current_tenant: current_tenant) do
|
|
99
|
+
UserSearch.call(name: "ali") # tenant auto-filled
|
|
100
|
+
end
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Identity shorthand: `context :locale` maps prop `:locale` to context key `:locale`.
|
|
104
|
+
|
|
105
|
+
Explicit values always win over ambient context. Inheritance merges parent + child mappings.
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## Registry & Export
|
|
110
|
+
|
|
111
|
+
Queries extend `Registry` — same API as Operation, Event, and Form:
|
|
112
|
+
|
|
113
|
+
```ruby
|
|
114
|
+
Dex::Query.registry # => Set of all named Query subclasses
|
|
115
|
+
UserSearch.description # => "Search and filter users"
|
|
116
|
+
|
|
117
|
+
UserSearch.to_h # => { name:, description:, props:, filters:, sorts:, context: }
|
|
118
|
+
UserSearch.to_json_schema # => JSON Schema (Draft 2020-12)
|
|
119
|
+
|
|
120
|
+
Dex::Query.export(format: :hash) # => sorted array of all query to_h
|
|
121
|
+
Dex::Query.export(format: :json_schema) # => sorted array of all query to_json_schema
|
|
122
|
+
```
|
|
123
|
+
|
|
74
124
|
---
|
|
75
125
|
|
|
76
126
|
## Filters
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dex
|
|
4
|
+
# Shared context DSL extracted from ContextSetup.
|
|
5
|
+
#
|
|
6
|
+
# Provides the `context` class method, `context_mappings` inheritance,
|
|
7
|
+
# and `_context_own` storage. Includers must implement:
|
|
8
|
+
# _context_prop_declared?(name) → true/false
|
|
9
|
+
# and may override:
|
|
10
|
+
# _context_field_label → "prop" | "field" (used in error messages)
|
|
11
|
+
module ContextDSL
|
|
12
|
+
def context(*names, **mappings)
|
|
13
|
+
names.each do |name|
|
|
14
|
+
unless name.is_a?(Symbol)
|
|
15
|
+
raise ArgumentError, "context shorthand must be a Symbol, got: #{name.inspect}"
|
|
16
|
+
end
|
|
17
|
+
mappings[name] = name
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
raise ArgumentError, "context requires at least one mapping" if mappings.empty?
|
|
21
|
+
|
|
22
|
+
label = _context_field_label
|
|
23
|
+
mappings.each do |prop_name, context_key|
|
|
24
|
+
unless _context_prop_declared?(prop_name)
|
|
25
|
+
raise ArgumentError,
|
|
26
|
+
"context references undeclared #{label} :#{prop_name}. Declare the #{label} before calling context."
|
|
27
|
+
end
|
|
28
|
+
unless context_key.is_a?(Symbol)
|
|
29
|
+
raise ArgumentError,
|
|
30
|
+
"context key must be a Symbol, got: #{context_key.inspect} for #{label} :#{prop_name}"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
_context_own.merge!(mappings)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def context_mappings
|
|
38
|
+
parent = superclass.respond_to?(:context_mappings) ? superclass.context_mappings : {}
|
|
39
|
+
parent.merge(_context_own)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def _context_own
|
|
45
|
+
@_context_own_mappings ||= {}
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def _context_prop_declared?(_name)
|
|
49
|
+
raise NotImplementedError, "#{self} must implement _context_prop_declared?"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def _context_field_label
|
|
53
|
+
"prop"
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
data/lib/dex/context_setup.rb
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Dex
|
|
4
|
-
#
|
|
4
|
+
# Context DSL for Operation and Event (Literal::Properties-backed).
|
|
5
5
|
#
|
|
6
6
|
# Maps declared props to ambient context keys so they can be auto-filled
|
|
7
7
|
# from Dex.context when not passed explicitly as kwargs.
|
|
@@ -9,34 +9,7 @@ module Dex
|
|
|
9
9
|
extend Dex::Concern
|
|
10
10
|
|
|
11
11
|
module ClassMethods
|
|
12
|
-
|
|
13
|
-
names.each do |name|
|
|
14
|
-
unless name.is_a?(Symbol)
|
|
15
|
-
raise ArgumentError, "context shorthand must be a Symbol, got: #{name.inspect}"
|
|
16
|
-
end
|
|
17
|
-
mappings[name] = name
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
raise ArgumentError, "context requires at least one mapping" if mappings.empty?
|
|
21
|
-
|
|
22
|
-
mappings.each do |prop_name, context_key|
|
|
23
|
-
unless _context_prop_declared?(prop_name)
|
|
24
|
-
raise ArgumentError,
|
|
25
|
-
"context references undeclared prop :#{prop_name}. Declare the prop before calling context."
|
|
26
|
-
end
|
|
27
|
-
unless context_key.is_a?(Symbol)
|
|
28
|
-
raise ArgumentError,
|
|
29
|
-
"context key must be a Symbol, got: #{context_key.inspect} for prop :#{prop_name}"
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
_context_own.merge!(mappings)
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
def context_mappings
|
|
37
|
-
parent = superclass.respond_to?(:context_mappings) ? superclass.context_mappings : {}
|
|
38
|
-
parent.merge(_context_own)
|
|
39
|
-
end
|
|
12
|
+
include ContextDSL
|
|
40
13
|
|
|
41
14
|
def new(**kwargs)
|
|
42
15
|
mappings = context_mappings
|
|
@@ -52,10 +25,6 @@ module Dex
|
|
|
52
25
|
|
|
53
26
|
private
|
|
54
27
|
|
|
55
|
-
def _context_own
|
|
56
|
-
@_context_own_mappings ||= {}
|
|
57
|
-
end
|
|
58
|
-
|
|
59
28
|
def _context_prop_declared?(name)
|
|
60
29
|
respond_to?(:literal_properties) && literal_properties.any? { |p| p.name == name }
|
|
61
30
|
end
|
data/lib/dex/event/bus.rb
CHANGED
|
@@ -44,19 +44,18 @@ module Dex
|
|
|
44
44
|
def publish(event, sync:)
|
|
45
45
|
return if Suppression.suppressed?(event.class)
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
trace_data = trace_data_for(event)
|
|
48
|
+
persist(event, trace_data)
|
|
48
49
|
handlers = subscribers_for(event.class)
|
|
49
50
|
return if handlers.empty?
|
|
50
51
|
|
|
51
|
-
event_frame = event.trace_frame
|
|
52
|
-
|
|
53
52
|
handlers.each do |handler_class|
|
|
54
53
|
if sync
|
|
55
|
-
Trace.restore(
|
|
54
|
+
Dex::Trace.restore(trace_data) do
|
|
56
55
|
handler_class._event_handle(event)
|
|
57
56
|
end
|
|
58
57
|
else
|
|
59
|
-
enqueue(handler_class, event,
|
|
58
|
+
enqueue(handler_class, event, trace_data)
|
|
60
59
|
end
|
|
61
60
|
end
|
|
62
61
|
end
|
|
@@ -67,15 +66,23 @@ module Dex
|
|
|
67
66
|
|
|
68
67
|
private
|
|
69
68
|
|
|
70
|
-
def persist(event)
|
|
69
|
+
def persist(event, trace_data)
|
|
71
70
|
store = Dex.configuration.event_store
|
|
72
71
|
return unless store
|
|
73
72
|
|
|
74
|
-
|
|
73
|
+
actor = actor_from_trace(trace_data[:frames])
|
|
74
|
+
attrs = safe_store_attributes(store, {
|
|
75
|
+
id: event.id,
|
|
76
|
+
trace_id: event.trace_id,
|
|
77
|
+
actor_type: actor&.dig(:actor_type),
|
|
78
|
+
actor_id: actor&.dig(:id),
|
|
79
|
+
trace: trace_data[:frames],
|
|
75
80
|
event_type: event.class.name,
|
|
76
81
|
payload: event._props_as_json,
|
|
77
82
|
metadata: event.metadata.as_json
|
|
78
|
-
)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
store.create!(**attrs)
|
|
79
86
|
rescue => e
|
|
80
87
|
Event._warn("Failed to persist event: #{e.message}")
|
|
81
88
|
end
|
|
@@ -94,6 +101,69 @@ module Dex
|
|
|
94
101
|
)
|
|
95
102
|
end
|
|
96
103
|
|
|
104
|
+
def trace_data_for(event)
|
|
105
|
+
ambient = Dex::Trace.dump
|
|
106
|
+
frames = if ambient && trace_matches_event?(ambient, event)
|
|
107
|
+
trace_frames(ambient)
|
|
108
|
+
elsif ambient
|
|
109
|
+
actor_frames(trace_frames(ambient))
|
|
110
|
+
else
|
|
111
|
+
[]
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
{
|
|
115
|
+
trace_id: event.trace_id,
|
|
116
|
+
frames: frames,
|
|
117
|
+
event_context: event_context_for(event)
|
|
118
|
+
}
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def actor_from_trace(frames)
|
|
122
|
+
Array(frames).find do |frame|
|
|
123
|
+
frame_type = frame[:type] || frame["type"]
|
|
124
|
+
frame_type && frame_type.to_sym == :actor
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def actor_frames(frames)
|
|
129
|
+
Array(frames).select do |frame|
|
|
130
|
+
frame_type = frame[:type] || frame["type"]
|
|
131
|
+
frame_type && frame_type.to_sym == :actor
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def trace_frames(trace_data)
|
|
136
|
+
Array(trace_data[:frames] || trace_data["frames"])
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def trace_matches_event?(trace_data, event)
|
|
140
|
+
trace_id = trace_data[:trace_id] || trace_data["trace_id"]
|
|
141
|
+
trace_id.to_s == event.trace_id.to_s
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def event_context_for(event)
|
|
145
|
+
{
|
|
146
|
+
id: event.id,
|
|
147
|
+
trace_id: event.trace_id,
|
|
148
|
+
event_class: event.class.name,
|
|
149
|
+
event_ancestry: event.event_ancestry
|
|
150
|
+
}
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def safe_store_attributes(store, attributes)
|
|
154
|
+
if store.respond_to?(:column_names)
|
|
155
|
+
allowed = store.column_names.to_set
|
|
156
|
+
attributes.select { |key, _| allowed.include?(key.to_s) }
|
|
157
|
+
elsif store.respond_to?(:fields)
|
|
158
|
+
attributes.select do |key, _|
|
|
159
|
+
field_name = key.to_s
|
|
160
|
+
store.fields.key?(field_name) || (field_name == "id" && store.fields.key?("_id"))
|
|
161
|
+
end
|
|
162
|
+
else
|
|
163
|
+
attributes
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
97
167
|
def ensure_active_job_loaded!
|
|
98
168
|
return if defined?(ActiveJob::Base)
|
|
99
169
|
|
data/lib/dex/event/handler.rb
CHANGED
|
@@ -69,9 +69,26 @@ module Dex
|
|
|
69
69
|
end
|
|
70
70
|
|
|
71
71
|
def self._event_handle(event)
|
|
72
|
+
execution_id = Dex::Id.generate("hd_")
|
|
73
|
+
auto_started = Dex::Trace.ensure_started!(trace_id: event.trace_id)
|
|
74
|
+
pushed = false
|
|
75
|
+
Dex::Trace.push(
|
|
76
|
+
type: :handler,
|
|
77
|
+
id: execution_id,
|
|
78
|
+
class: name,
|
|
79
|
+
event_class: event.class.name,
|
|
80
|
+
event_id: event.id,
|
|
81
|
+
event_ancestry: event.metadata.event_ancestry
|
|
82
|
+
)
|
|
83
|
+
pushed = true
|
|
84
|
+
|
|
72
85
|
instance = new
|
|
73
86
|
instance.instance_variable_set(:@event, event)
|
|
87
|
+
instance.instance_variable_set(:@_dex_execution_id, execution_id)
|
|
74
88
|
instance.send(:call)
|
|
89
|
+
ensure
|
|
90
|
+
Dex::Trace.pop if pushed
|
|
91
|
+
Dex::Trace.stop! if auto_started
|
|
75
92
|
end
|
|
76
93
|
|
|
77
94
|
def self._event_handle_from_payload(event_class_name, payload, metadata_hash)
|
|
@@ -96,6 +113,7 @@ module Dex
|
|
|
96
113
|
timestamp: Time.parse(metadata_hash["timestamp"]),
|
|
97
114
|
trace_id: metadata_hash["trace_id"],
|
|
98
115
|
caused_by_id: metadata_hash["caused_by_id"],
|
|
116
|
+
event_ancestry: metadata_hash["event_ancestry"] || [],
|
|
99
117
|
context: metadata_hash["context"]
|
|
100
118
|
)
|
|
101
119
|
instance.instance_variable_set(:@metadata, metadata)
|
data/lib/dex/event/metadata.rb
CHANGED
|
@@ -1,25 +1,30 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "securerandom"
|
|
4
|
-
|
|
5
3
|
module Dex
|
|
6
4
|
class Event
|
|
7
5
|
class Metadata
|
|
8
|
-
attr_reader :id, :timestamp, :trace_id, :caused_by_id, :context
|
|
6
|
+
attr_reader :id, :timestamp, :trace_id, :caused_by_id, :event_ancestry, :context
|
|
9
7
|
|
|
10
|
-
def initialize(id:, timestamp:, trace_id:, caused_by_id:, context:)
|
|
8
|
+
def initialize(id:, timestamp:, trace_id:, caused_by_id:, event_ancestry:, context:)
|
|
11
9
|
@id = id
|
|
12
10
|
@timestamp = timestamp
|
|
13
11
|
@trace_id = trace_id
|
|
14
12
|
@caused_by_id = caused_by_id
|
|
13
|
+
@event_ancestry = event_ancestry
|
|
15
14
|
@context = context
|
|
16
15
|
freeze
|
|
17
16
|
end
|
|
18
17
|
|
|
19
|
-
def self.build
|
|
20
|
-
id =
|
|
21
|
-
trace_id = Trace.
|
|
22
|
-
|
|
18
|
+
def self.build
|
|
19
|
+
id = Dex::Id.generate("ev_")
|
|
20
|
+
trace_id = Dex::Trace.trace_id || Dex::Id.generate("tr_")
|
|
21
|
+
current_event = Dex::Trace.current_event_context
|
|
22
|
+
caused = current_event&.dig(:id)
|
|
23
|
+
ancestry = if current_event
|
|
24
|
+
Array(current_event[:event_ancestry]) + [caused].compact
|
|
25
|
+
else
|
|
26
|
+
[]
|
|
27
|
+
end
|
|
23
28
|
|
|
24
29
|
ctx = if Dex.configuration.event_context
|
|
25
30
|
begin
|
|
@@ -35,6 +40,7 @@ module Dex
|
|
|
35
40
|
timestamp: Time.now.utc,
|
|
36
41
|
trace_id: trace_id,
|
|
37
42
|
caused_by_id: caused,
|
|
43
|
+
event_ancestry: ancestry,
|
|
38
44
|
context: ctx
|
|
39
45
|
)
|
|
40
46
|
end
|
|
@@ -43,7 +49,8 @@ module Dex
|
|
|
43
49
|
h = {
|
|
44
50
|
"id" => @id,
|
|
45
51
|
"timestamp" => @timestamp.iso8601(6),
|
|
46
|
-
"trace_id" => @trace_id
|
|
52
|
+
"trace_id" => @trace_id,
|
|
53
|
+
"event_ancestry" => @event_ancestry
|
|
47
54
|
}
|
|
48
55
|
h["caused_by_id"] = @caused_by_id if @caused_by_id
|
|
49
56
|
h["context"] = @context if @context
|
data/lib/dex/event/processor.rb
CHANGED
|
@@ -13,7 +13,7 @@ module Dex
|
|
|
13
13
|
handler = Object.const_get(handler_class)
|
|
14
14
|
retry_config = handler._event_handler_retry_config
|
|
15
15
|
|
|
16
|
-
Dex::
|
|
16
|
+
Dex::Trace.restore(trace) do
|
|
17
17
|
handler._event_handle_from_payload(event_class, payload, metadata)
|
|
18
18
|
end
|
|
19
19
|
rescue => _e
|
data/lib/dex/event/trace.rb
CHANGED
|
@@ -3,52 +3,39 @@
|
|
|
3
3
|
module Dex
|
|
4
4
|
class Event
|
|
5
5
|
module Trace
|
|
6
|
-
STACK_KEY = :_dex_event_trace_stack
|
|
7
|
-
|
|
8
6
|
class << self
|
|
9
|
-
include ExecutionState
|
|
10
|
-
|
|
11
7
|
def with_event(event, &block)
|
|
12
|
-
|
|
13
|
-
stack.push(event.trace_frame)
|
|
14
|
-
yield
|
|
15
|
-
ensure
|
|
16
|
-
stack.pop
|
|
8
|
+
Dex::Trace.with_event_context(event, &block)
|
|
17
9
|
end
|
|
18
10
|
|
|
19
11
|
def current_event_id
|
|
20
|
-
|
|
12
|
+
Dex::Trace.current_event_id
|
|
21
13
|
end
|
|
22
14
|
|
|
23
15
|
def current_trace_id
|
|
24
|
-
|
|
16
|
+
Dex::Trace.trace_id
|
|
25
17
|
end
|
|
26
18
|
|
|
27
19
|
def dump
|
|
28
|
-
|
|
29
|
-
return nil unless frame
|
|
30
|
-
|
|
31
|
-
{ id: frame[:id], trace_id: frame[:trace_id] }
|
|
20
|
+
Dex::Trace.dump
|
|
32
21
|
end
|
|
33
22
|
|
|
34
23
|
def restore(data, &block)
|
|
35
24
|
return yield unless data
|
|
36
25
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
26
|
+
if data.is_a?(Hash) && (data.key?(:frames) || data.key?("frames"))
|
|
27
|
+
Dex::Trace.restore(data, &block)
|
|
28
|
+
else
|
|
29
|
+
Dex::Trace.restore_event_context(
|
|
30
|
+
event_id: data[:id] || data["id"],
|
|
31
|
+
trace_id: data[:trace_id] || data["trace_id"],
|
|
32
|
+
&block
|
|
33
|
+
)
|
|
34
|
+
end
|
|
42
35
|
end
|
|
43
36
|
|
|
44
37
|
def clear!
|
|
45
|
-
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
private
|
|
49
|
-
|
|
50
|
-
def _stack
|
|
51
|
-
_execution_state[STACK_KEY] ||= []
|
|
38
|
+
Dex::Trace.clear!
|
|
52
39
|
end
|
|
53
40
|
end
|
|
54
41
|
end
|
data/lib/dex/event.rb
CHANGED
|
@@ -9,7 +9,7 @@ require_relative "event/suppression"
|
|
|
9
9
|
module Dex
|
|
10
10
|
class Event
|
|
11
11
|
RESERVED_PROP_NAMES = %i[
|
|
12
|
-
id timestamp trace_id caused_by_id caused_by
|
|
12
|
+
id timestamp trace_id caused_by_id caused_by event_ancestry
|
|
13
13
|
context publish metadata sync
|
|
14
14
|
].to_set.freeze
|
|
15
15
|
|
|
@@ -67,8 +67,8 @@ module Dex
|
|
|
67
67
|
def timestamp = metadata.timestamp
|
|
68
68
|
def trace_id = metadata.trace_id
|
|
69
69
|
def caused_by_id = metadata.caused_by_id
|
|
70
|
+
def event_ancestry = metadata.event_ancestry
|
|
70
71
|
def context = metadata.context
|
|
71
|
-
def trace_frame = { id: id, trace_id: trace_id }
|
|
72
72
|
|
|
73
73
|
# Publishing
|
|
74
74
|
def publish(sync: false)
|
|
@@ -85,11 +85,6 @@ module Dex
|
|
|
85
85
|
end
|
|
86
86
|
end
|
|
87
87
|
|
|
88
|
-
# Tracing
|
|
89
|
-
def trace(&block)
|
|
90
|
-
Trace.with_event(self, &block)
|
|
91
|
-
end
|
|
92
|
-
|
|
93
88
|
# Suppression
|
|
94
89
|
def self.suppress(*classes, &block)
|
|
95
90
|
Suppression.suppress(*classes, &block)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dex
|
|
4
|
+
class Form
|
|
5
|
+
# Context DSL for Form (ActiveModel::Attributes-backed).
|
|
6
|
+
#
|
|
7
|
+
# Same DSL as Operation/Event context, but checks attribute_names
|
|
8
|
+
# instead of literal_properties. Injection happens in Form#initialize.
|
|
9
|
+
module Context
|
|
10
|
+
extend Dex::Concern
|
|
11
|
+
|
|
12
|
+
module ClassMethods
|
|
13
|
+
include ContextDSL
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def _context_prop_declared?(name)
|
|
18
|
+
attribute_names.include?(name.to_s)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def _context_field_label
|
|
22
|
+
"field"
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|