yes-core 2.4.1 → 2.4.3
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 +6 -0
- data/lib/yes/core/command_handling/command_executor.rb +2 -0
- data/lib/yes/core/command_handling/command_group_executor.rb +2 -0
- data/lib/yes/core/command_handling/command_group_handler.rb +12 -0
- data/lib/yes/core/command_handling/command_handler.rb +17 -0
- data/lib/yes/core/command_handling/event_publisher.rb +4 -1
- data/lib/yes/core/commands/stateless/handler.rb +4 -2
- data/lib/yes/core/open_telemetry/otl_span.rb +127 -34
- data/lib/yes/core/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: 70e0c755ba6b965b16e2331495e59061de1ed745c80bcd4cb4d6da4a5e8cdd8b
|
|
4
|
+
data.tar.gz: fe6b3f3e8600ecb1c0558850eef7302d1d938296c391c7539c1caa5de69135d8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2c0ef42cdb28eb261828d7eb189e03b22fdb154294145aa7966f63fe9ae3f2dcdacef5c6890037ef481420a7460ad02c1f234fcab4c8c1d79a6f1c46529448ee
|
|
7
|
+
data.tar.gz: fa787ad495b279695b2d1100fcf24237630fef1e426e617e0979231442751e8943893d14eaa3a0902d42580bfddd7fdad2b686f0d59e48f353642e9c10531b20
|
data/CHANGELOG.md
CHANGED
|
@@ -35,6 +35,7 @@ module Yes
|
|
|
35
35
|
def call(group_name, payload, guards: true, metadata: nil)
|
|
36
36
|
prepared = prepare_payload(payload, metadata)
|
|
37
37
|
cmd = command_utilities.build_group_command(group_name, prepared)
|
|
38
|
+
otl_record_command(cmd)
|
|
38
39
|
guard_evaluator_class = command_utilities.fetch_guard_evaluator_class_for_group(group_name)
|
|
39
40
|
|
|
40
41
|
ReadModelRecoveryService.check_and_recover_with_retries(read_model, aggregate:) if aggregate.class.read_model_enabled?
|
|
@@ -49,6 +50,17 @@ module Yes
|
|
|
49
50
|
|
|
50
51
|
attr_reader :aggregate, :command_utilities, :read_model
|
|
51
52
|
|
|
53
|
+
# Records which command group the span ran; see CommandHandler#otl_record_command.
|
|
54
|
+
#
|
|
55
|
+
# @param cmd [Object] the built command group
|
|
56
|
+
# @return [void]
|
|
57
|
+
def otl_record_command(cmd)
|
|
58
|
+
name = cmd.class.name
|
|
59
|
+
return if name.nil?
|
|
60
|
+
|
|
61
|
+
self.class.current_span&.add_attributes({ CommandHandler::COMMAND_ATTRIBUTE => name })
|
|
62
|
+
end
|
|
63
|
+
|
|
52
64
|
# Prepares the payload before constructing the group command.
|
|
53
65
|
# Mirrors the metadata-injection logic in {CommandHandler#prepare_payload}.
|
|
54
66
|
#
|
|
@@ -11,6 +11,10 @@ module Yes
|
|
|
11
11
|
# response = handler.call(:approve_documents, { document_ids: '123', another: 'value' })
|
|
12
12
|
#
|
|
13
13
|
class CommandHandler
|
|
14
|
+
# Span attribute naming the command that ran. The span keeps a fixed name so its
|
|
15
|
+
# latency histogram stays one series per service; this is how the command is identified.
|
|
16
|
+
COMMAND_ATTRIBUTE = 'command'
|
|
17
|
+
|
|
14
18
|
include Yes::Core::OpenTelemetry::Trackable
|
|
15
19
|
|
|
16
20
|
# Initializes a new CommandHandler
|
|
@@ -32,6 +36,7 @@ module Yes
|
|
|
32
36
|
def call(command_name, payload, guards: true, metadata: nil)
|
|
33
37
|
prepared_payload = prepare_payload(command_name, payload, metadata)
|
|
34
38
|
cmd = command_utilities.build_command(command_name, prepared_payload)
|
|
39
|
+
otl_record_command(cmd)
|
|
35
40
|
|
|
36
41
|
guard_evaluator_class = command_utilities.fetch_guard_evaluator_class(command_name)
|
|
37
42
|
|
|
@@ -51,6 +56,18 @@ module Yes
|
|
|
51
56
|
|
|
52
57
|
attr_reader :aggregate, :command_utilities, :read_model
|
|
53
58
|
|
|
59
|
+
# Records which command the span ran. The span itself keeps a fixed name so its latency
|
|
60
|
+
# histogram stays a single series per service; the command goes on an attribute instead.
|
|
61
|
+
#
|
|
62
|
+
# @param cmd [Object] the built command
|
|
63
|
+
# @return [void]
|
|
64
|
+
def otl_record_command(cmd)
|
|
65
|
+
name = cmd.class.name
|
|
66
|
+
return if name.nil?
|
|
67
|
+
|
|
68
|
+
self.class.current_span&.add_attributes({ COMMAND_ATTRIBUTE => name })
|
|
69
|
+
end
|
|
70
|
+
|
|
54
71
|
# Prepares the command payload
|
|
55
72
|
#
|
|
56
73
|
# @param command_name [Symbol] The command name
|
|
@@ -59,7 +59,10 @@ module Yes
|
|
|
59
59
|
|
|
60
60
|
otl_trackable(
|
|
61
61
|
:call,
|
|
62
|
-
Yes::Core::OpenTelemetry::OtlSpan::OtlData.new(
|
|
62
|
+
Yes::Core::OpenTelemetry::OtlSpan::OtlData.new(
|
|
63
|
+
span_name: 'Publish Event', span_kind: :producer, track_sql: true,
|
|
64
|
+
tolerated_errors: Yes::Core::OpenTelemetry::OtlSpan::REVISION_CONFLICT
|
|
65
|
+
)
|
|
63
66
|
)
|
|
64
67
|
|
|
65
68
|
private
|
|
@@ -122,7 +122,10 @@ module Yes
|
|
|
122
122
|
middlewares: Middlewares.for_write
|
|
123
123
|
).tap { otl_record_response(_1) }
|
|
124
124
|
end
|
|
125
|
-
otl_trackable :publish_event, OpenTelemetry::OtlSpan::OtlData.new(
|
|
125
|
+
otl_trackable :publish_event, OpenTelemetry::OtlSpan::OtlData.new(
|
|
126
|
+
span_name: 'Publish Event', span_kind: :producer,
|
|
127
|
+
tolerated_errors: OpenTelemetry::OtlSpan::REVISION_CONFLICT
|
|
128
|
+
)
|
|
126
129
|
|
|
127
130
|
private
|
|
128
131
|
|
|
@@ -208,7 +211,6 @@ module Yes
|
|
|
208
211
|
PgEventstore::WrongExpectedRevisionError.new(
|
|
209
212
|
revision:, expected_revision:, stream:, verdict: :unmatched_stream_revision
|
|
210
213
|
).tap do |error|
|
|
211
|
-
self.class.current_span&.status = ::OpenTelemetry::Trace::Status.error('Wrong expected revision')
|
|
212
214
|
self.class.current_span&.add_attributes(
|
|
213
215
|
{
|
|
214
216
|
current_revision: revision,
|
|
@@ -9,15 +9,29 @@ module Yes
|
|
|
9
9
|
# span = OtlSpan.new(otl_data: OtlData.new(span_name: 'MySpan'), otl_tracer: tracer)
|
|
10
10
|
# span.otl_span(arg1, arg2) { do_work }
|
|
11
11
|
class OtlSpan
|
|
12
|
+
# Span attribute that names the tolerated error a span ended with (see {OtlData#tolerated_errors}).
|
|
13
|
+
OUTCOME_ATTRIBUTE = 'outcome'
|
|
14
|
+
|
|
15
|
+
# Span attribute with the number of retries a command needed before it settled.
|
|
16
|
+
RETRIES_ATTRIBUTE = 'retries'
|
|
17
|
+
|
|
18
|
+
# A revision conflict is an optimistic-concurrency retry signal, not a failure: the
|
|
19
|
+
# command is re-run and almost always succeeds. Publishing spans tolerate it so the
|
|
20
|
+
# span status stays clean and the conflict is countable on its own.
|
|
21
|
+
REVISION_CONFLICT = { PgEventstore::WrongExpectedRevisionError => 'revision_conflict' }.freeze
|
|
22
|
+
|
|
12
23
|
# Configuration struct for OpenTelemetry span data
|
|
13
|
-
OtlData = Struct.new(:span_name, :span_kind, :span_attributes, :links_extractor, :track_sql
|
|
24
|
+
OtlData = Struct.new(:span_name, :span_kind, :span_attributes, :links_extractor, :track_sql,
|
|
25
|
+
:tolerated_errors) do
|
|
14
26
|
# @param span_name [String, nil] name of the span
|
|
15
27
|
# @param span_kind [Symbol] kind of span (:internal, :client, :server, :producer, :consumer)
|
|
16
28
|
# @param span_attributes [Hash] additional span attributes
|
|
17
29
|
# @param links_extractor [Proc] extracts OTL context links from arguments
|
|
18
30
|
# @param track_sql [Boolean] whether to track SQL queries within the span
|
|
31
|
+
# @param tolerated_errors [Hash{Class => String}] errors that are re-raised but do not mark the
|
|
32
|
+
# span as failed; the span gets the mapped value as its +outcome+ attribute instead
|
|
19
33
|
def initialize(span_name: nil, span_kind: :internal, span_attributes: {}, links_extractor: proc { [] },
|
|
20
|
-
track_sql: false)
|
|
34
|
+
track_sql: false, tolerated_errors: {})
|
|
21
35
|
super
|
|
22
36
|
end
|
|
23
37
|
end
|
|
@@ -28,6 +42,17 @@ module Yes
|
|
|
28
42
|
# @return [Object] OpenTelemetry tracer instance
|
|
29
43
|
attr_reader :otl_tracer
|
|
30
44
|
|
|
45
|
+
# Stamps the number of retries a command needed on the span it is running in, so
|
|
46
|
+
# contention shows up per command and not only per publish attempt.
|
|
47
|
+
#
|
|
48
|
+
# @param retries [Integer] retries performed before the command settled
|
|
49
|
+
# @return [void]
|
|
50
|
+
def self.record_retries(retries)
|
|
51
|
+
return if retries.zero? || Yes::Core.configuration.otl_tracer.nil?
|
|
52
|
+
|
|
53
|
+
::OpenTelemetry::Trace.current_span.set_attribute(RETRIES_ATTRIBUTE, retries)
|
|
54
|
+
end
|
|
55
|
+
|
|
31
56
|
# @param otl_data [OtlData] span configuration
|
|
32
57
|
# @param otl_tracer [Object] OpenTelemetry tracer instance
|
|
33
58
|
def initialize(otl_data:, otl_tracer:)
|
|
@@ -37,50 +62,118 @@ module Yes
|
|
|
37
62
|
|
|
38
63
|
# Creates a span and executes the given block within it.
|
|
39
64
|
#
|
|
65
|
+
# A tolerated error (see {OtlData#tolerated_errors}) is recorded on the span as an exception
|
|
66
|
+
# event and as the +outcome+ attribute, but the span keeps a non-error status. The error is
|
|
67
|
+
# re-raised once the span has ended, so callers see exactly the same exception as before.
|
|
68
|
+
#
|
|
40
69
|
# @param args [Array] positional arguments passed to the links extractor
|
|
41
70
|
# @param kwargs [Hash] keyword arguments passed to the links extractor
|
|
42
71
|
# @yield the block to execute within the span
|
|
43
72
|
# @return [Object] the return value of the block
|
|
44
|
-
def otl_span(*args, **kwargs, &
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
root_track_sql = parent_span&.try(:attributes)&.[]('root_track_sql') ||
|
|
49
|
-
parent_span&.try(:attributes)&.[]('track_sql')
|
|
73
|
+
def otl_span(*args, **kwargs, &)
|
|
74
|
+
parent_span = current_parent_span
|
|
75
|
+
root_track_sql = root_track_sql?(parent_span)
|
|
76
|
+
tolerated_error = nil
|
|
50
77
|
|
|
51
|
-
otl_tracer.in_span(
|
|
78
|
+
result = otl_tracer.in_span(
|
|
52
79
|
otl_data.span_name || 'UnknownName',
|
|
53
|
-
links
|
|
80
|
+
links: otl_links(args, kwargs),
|
|
54
81
|
kind: otl_data.span_kind,
|
|
55
|
-
attributes:
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
next yield if !root_track_sql && !otl_data.track_sql
|
|
62
|
-
next yield if parent_span.present? && root_track_sql
|
|
63
|
-
|
|
64
|
-
callback = lambda do |sql_event|
|
|
65
|
-
next if %w[SCHEMA TRANSACTION].include?(sql_event.payload[:name])
|
|
66
|
-
|
|
67
|
-
otl_tracer.in_span("SQL #{sql_event.payload[:name]}") do |span|
|
|
68
|
-
span.set_attribute('db.system', 'postgresql')
|
|
69
|
-
span.set_attribute('db.statement', sql_event.payload[:sql])
|
|
70
|
-
span.set_attribute('db.binds', sql_event.payload[:binds].map do |attr|
|
|
71
|
-
next { name: attr.name, value: attr.value } if attr.respond_to?(:name) && attr.respond_to?(:value)
|
|
72
|
-
|
|
73
|
-
{ name: attr.class.to_s, value: attr }
|
|
74
|
-
end.to_json)
|
|
75
|
-
span.set_attribute('db.event_name', sql_event.payload[:name])
|
|
76
|
-
end
|
|
77
|
-
end
|
|
78
|
-
ActiveSupport::Notifications.subscribed(callback, 'sql.active_record', &block)
|
|
82
|
+
attributes: span_attributes(root_track_sql)
|
|
83
|
+
) do |span|
|
|
84
|
+
run_block(parent_span, root_track_sql, &)
|
|
85
|
+
rescue StandardError => e
|
|
86
|
+
tolerated_error = tolerate(span, e)
|
|
87
|
+
nil
|
|
79
88
|
end
|
|
89
|
+
|
|
90
|
+
raise tolerated_error if tolerated_error
|
|
91
|
+
|
|
92
|
+
result
|
|
80
93
|
end
|
|
81
94
|
|
|
82
95
|
private
|
|
83
96
|
|
|
97
|
+
# @return [::OpenTelemetry::Trace::Span, nil] the span this one is created under, if any
|
|
98
|
+
def current_parent_span
|
|
99
|
+
span = ::OpenTelemetry::Trace.current_span
|
|
100
|
+
span.context.valid? ? span : nil
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# @param parent_span [::OpenTelemetry::Trace::Span, nil] the enclosing span, if any
|
|
104
|
+
# @return [Boolean, nil] whether an enclosing span already tracks SQL
|
|
105
|
+
def root_track_sql?(parent_span)
|
|
106
|
+
parent_span&.try(:attributes)&.[]('root_track_sql') || parent_span&.try(:attributes)&.[]('track_sql')
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# @param root_track_sql [Boolean, nil] whether an enclosing span already tracks SQL
|
|
110
|
+
# @return [Hash] the attributes the span starts with
|
|
111
|
+
def span_attributes(root_track_sql)
|
|
112
|
+
{
|
|
113
|
+
'track_sql' => otl_data.track_sql,
|
|
114
|
+
'root_track_sql' => root_track_sql || false
|
|
115
|
+
}.merge(otl_data.span_attributes)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Runs the traced block, wrapping it in SQL tracking when the span asks for it.
|
|
119
|
+
#
|
|
120
|
+
# @param parent_span [OpenTelemetry::Trace::Span, nil] the enclosing span, if any
|
|
121
|
+
# @param root_track_sql [Boolean, nil] whether an enclosing span already tracks SQL
|
|
122
|
+
# @yield the block to execute
|
|
123
|
+
# @return [Object, nil] the return value of the block
|
|
124
|
+
def run_block(parent_span, root_track_sql, &)
|
|
125
|
+
return unless block_given?
|
|
126
|
+
return yield if !root_track_sql && !otl_data.track_sql
|
|
127
|
+
return yield if parent_span.present? && root_track_sql
|
|
128
|
+
|
|
129
|
+
ActiveSupport::Notifications.subscribed(sql_span_callback, 'sql.active_record', &)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# @return [Proc] records one child span per SQL statement executed inside the traced block
|
|
133
|
+
def sql_span_callback
|
|
134
|
+
lambda do |sql_event|
|
|
135
|
+
next if %w[SCHEMA TRANSACTION].include?(sql_event.payload[:name])
|
|
136
|
+
|
|
137
|
+
record_sql_span(sql_event.payload)
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# @param payload [Hash] the +sql.active_record+ notification payload
|
|
142
|
+
# @return [void]
|
|
143
|
+
def record_sql_span(payload)
|
|
144
|
+
otl_tracer.in_span("SQL #{payload[:name]}") do |span|
|
|
145
|
+
span.set_attribute('db.system', 'postgresql')
|
|
146
|
+
span.set_attribute('db.statement', payload[:sql])
|
|
147
|
+
span.set_attribute('db.binds', sql_binds(payload[:binds]))
|
|
148
|
+
span.set_attribute('db.event_name', payload[:name])
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# @param binds [Array] the statement's bind parameters
|
|
153
|
+
# @return [String] the binds as JSON, one +name+/+value+ pair each
|
|
154
|
+
def sql_binds(binds)
|
|
155
|
+
binds.map do |attr|
|
|
156
|
+
next { name: attr.name, value: attr.value } if attr.respond_to?(:name) && attr.respond_to?(:value)
|
|
157
|
+
|
|
158
|
+
{ name: attr.class.to_s, value: attr }
|
|
159
|
+
end.to_json
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Records a tolerated error on the span without failing it.
|
|
163
|
+
#
|
|
164
|
+
# @param span [OpenTelemetry::Trace::Span] the span the error was raised in
|
|
165
|
+
# @param error [StandardError] the error raised by the traced block
|
|
166
|
+
# @return [StandardError] the error, to be re-raised once the span has ended
|
|
167
|
+
# @raise [StandardError] the error itself when it is not tolerated
|
|
168
|
+
def tolerate(span, error)
|
|
169
|
+
outcome = otl_data.tolerated_errors.find { |klass, _outcome| error.is_a?(klass) }&.last
|
|
170
|
+
raise error unless outcome
|
|
171
|
+
|
|
172
|
+
span.record_exception(error)
|
|
173
|
+
span.set_attribute(OUTCOME_ATTRIBUTE, outcome)
|
|
174
|
+
error
|
|
175
|
+
end
|
|
176
|
+
|
|
84
177
|
# Extracts OpenTelemetry links from arguments using the configured links_extractor.
|
|
85
178
|
#
|
|
86
179
|
# @param args [Array] positional arguments
|
data/lib/yes/core/version.rb
CHANGED