logbrew-sdk 0.1.0 → 0.1.2
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/README.md +425 -21
- data/examples/Makefile +21 -1
- data/examples/automatic_delivery.rb +32 -0
- data/examples/first_useful_telemetry.rb +118 -0
- data/examples/http_trace_correlation.rb +99 -0
- data/examples/persistent_worker_delivery.rb +33 -0
- data/examples/real_user_smoke.rb +15 -0
- data/examples/sidekiq_tracing.rb +22 -0
- data/lib/logbrew/automatic_delivery.rb +493 -0
- data/lib/logbrew/bounded_event_queue.rb +196 -0
- data/lib/logbrew/event_batcher.rb +67 -0
- data/lib/logbrew/faraday_tracing.rb +55 -0
- data/lib/logbrew/http_client_tracing.rb +282 -0
- data/lib/logbrew/operation_tracing.rb +194 -0
- data/lib/logbrew/persistent_event_store.rb +403 -0
- data/lib/logbrew/product_timeline.rb +153 -0
- data/lib/logbrew/sidekiq.rb +466 -0
- data/lib/logbrew/span_events.rb +34 -0
- data/lib/logbrew/support_ticket.rb +187 -0
- data/lib/logbrew/trace.rb +298 -0
- data/lib/logbrew/traceparent.rb +145 -0
- data/lib/logbrew/worker_lifecycle.rb +211 -0
- data/lib/logbrew.rb +427 -40
- metadata +21 -2
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
|
|
5
|
+
module LogBrew
|
|
6
|
+
TraceContext = Struct.new(:trace_id, :span_id, :parent_span_id, :trace_flags, :sampled, keyword_init: true)
|
|
7
|
+
|
|
8
|
+
class TraceScope
|
|
9
|
+
def initialize(scope_id)
|
|
10
|
+
@scope_id = scope_id
|
|
11
|
+
@closed = false
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def close
|
|
15
|
+
return if @closed
|
|
16
|
+
|
|
17
|
+
LogBrew::Trace.close_scope(@scope_id)
|
|
18
|
+
@closed = true
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Request-local trace context for app-owned logs, errors, actions, and metrics.
|
|
23
|
+
module Trace
|
|
24
|
+
STACK_KEY = :logbrew_trace_stack
|
|
25
|
+
private_constant :STACK_KEY
|
|
26
|
+
|
|
27
|
+
module_function
|
|
28
|
+
|
|
29
|
+
def current
|
|
30
|
+
entry = stack.last
|
|
31
|
+
entry && entry[:context]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def activate(context)
|
|
35
|
+
unless context.is_a?(TraceContext)
|
|
36
|
+
raise SdkError.new("validation_error", "trace context must be a LogBrew::TraceContext")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
scope_id = Object.new.object_id
|
|
40
|
+
stack << { id: scope_id, context: context }
|
|
41
|
+
TraceScope.new(scope_id)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def with_context(context)
|
|
45
|
+
scope = activate(context)
|
|
46
|
+
yield context
|
|
47
|
+
ensure
|
|
48
|
+
scope.close if scope
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def close_scope(scope_id)
|
|
52
|
+
entries = stack
|
|
53
|
+
index = entries.rindex { |entry| entry[:id] == scope_id }
|
|
54
|
+
entries.delete_at(index) unless index.nil?
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def continue_or_create(traceparent)
|
|
58
|
+
text = traceparent.to_s.strip
|
|
59
|
+
return create_root if text.empty?
|
|
60
|
+
|
|
61
|
+
from_traceparent(text)
|
|
62
|
+
rescue SdkError
|
|
63
|
+
create_root
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def from_traceparent(traceparent)
|
|
67
|
+
context = Traceparent.parse(traceparent)
|
|
68
|
+
create(
|
|
69
|
+
trace_id: context.trace_id,
|
|
70
|
+
span_id: generate_span_id,
|
|
71
|
+
parent_span_id: context.parent_span_id,
|
|
72
|
+
trace_flags: context.trace_flags
|
|
73
|
+
)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def create_root
|
|
77
|
+
create(trace_id: generate_trace_id, span_id: generate_span_id, trace_flags: "01")
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def create(trace_id:, span_id:, trace_flags: "01", parent_span_id: nil)
|
|
81
|
+
normalized_traceparent = Traceparent.create(trace_id: trace_id, span_id: span_id, trace_flags: trace_flags)
|
|
82
|
+
_version, normalized_trace_id, normalized_span_id, normalized_flags = normalized_traceparent.split("-")
|
|
83
|
+
normalized_parent_span_id = nil
|
|
84
|
+
unless parent_span_id.nil?
|
|
85
|
+
parent_traceparent = Traceparent.create(
|
|
86
|
+
trace_id: normalized_trace_id,
|
|
87
|
+
span_id: parent_span_id,
|
|
88
|
+
trace_flags: normalized_flags
|
|
89
|
+
)
|
|
90
|
+
normalized_parent_span_id = parent_traceparent.split("-")[2]
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
TraceContext.new(
|
|
94
|
+
trace_id: normalized_trace_id,
|
|
95
|
+
span_id: normalized_span_id,
|
|
96
|
+
parent_span_id: normalized_parent_span_id,
|
|
97
|
+
trace_flags: normalized_flags,
|
|
98
|
+
sampled: (normalized_flags.to_i(16) & 1) == 1
|
|
99
|
+
)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def create_headers(context = current)
|
|
103
|
+
return {} unless context
|
|
104
|
+
|
|
105
|
+
Traceparent.create_headers(
|
|
106
|
+
trace_id: context.trace_id,
|
|
107
|
+
span_id: context.span_id,
|
|
108
|
+
trace_flags: context.trace_flags
|
|
109
|
+
)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def metadata(context = current)
|
|
113
|
+
return {} unless context
|
|
114
|
+
|
|
115
|
+
{
|
|
116
|
+
"traceId" => context.trace_id,
|
|
117
|
+
"spanId" => context.span_id,
|
|
118
|
+
"traceFlags" => context.trace_flags,
|
|
119
|
+
"traceSampled" => context.sampled
|
|
120
|
+
}.tap do |payload|
|
|
121
|
+
payload["parentSpanId"] = context.parent_span_id unless context.parent_span_id.nil?
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def add_metadata(target, context = current)
|
|
126
|
+
return target unless context
|
|
127
|
+
|
|
128
|
+
metadata(context).each do |key, value|
|
|
129
|
+
target[key] = value unless target.key?(key) || target.key?(key.to_sym)
|
|
130
|
+
end
|
|
131
|
+
target
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def merge_attributes(attributes, context = current)
|
|
135
|
+
return attributes unless context && attributes.is_a?(Hash)
|
|
136
|
+
|
|
137
|
+
metadata_value = attributes.key?("metadata") ? attributes["metadata"] : attributes[:metadata]
|
|
138
|
+
return attributes unless metadata_value.nil? || metadata_value.is_a?(Hash)
|
|
139
|
+
|
|
140
|
+
copied = attributes.dup
|
|
141
|
+
merged_metadata = metadata_value.nil? ? {} : metadata_value.dup
|
|
142
|
+
add_metadata(merged_metadata, context)
|
|
143
|
+
if copied.key?(:metadata) && !copied.key?("metadata")
|
|
144
|
+
copied[:metadata] = merged_metadata
|
|
145
|
+
else
|
|
146
|
+
copied["metadata"] = merged_metadata
|
|
147
|
+
end
|
|
148
|
+
copied
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def from_rack_env(env)
|
|
152
|
+
existing = env_value(env, "logbrew.trace")
|
|
153
|
+
return existing if existing.is_a?(TraceContext)
|
|
154
|
+
|
|
155
|
+
traceparent = env_value(env, "HTTP_TRACEPARENT") || env_value(env, "traceparent") || env_value(env, "logbrew.traceparent")
|
|
156
|
+
return continue_or_create(traceparent) unless traceparent.nil? || traceparent.empty?
|
|
157
|
+
|
|
158
|
+
trace_id = env_value(env, "logbrew.trace_id")
|
|
159
|
+
span_id = env_value(env, "logbrew.span_id")
|
|
160
|
+
if trace_id && span_id
|
|
161
|
+
begin
|
|
162
|
+
return create(
|
|
163
|
+
trace_id: trace_id,
|
|
164
|
+
span_id: span_id,
|
|
165
|
+
parent_span_id: env_value(env, "logbrew.parent_span_id"),
|
|
166
|
+
trace_flags: env_value(env, "logbrew.trace_flags") || "01"
|
|
167
|
+
)
|
|
168
|
+
rescue SdkError
|
|
169
|
+
nil
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
create_root
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def generate_trace_id
|
|
177
|
+
loop do
|
|
178
|
+
value = SecureRandom.hex(16)
|
|
179
|
+
return value unless value.delete("0").empty?
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def generate_span_id
|
|
184
|
+
loop do
|
|
185
|
+
value = SecureRandom.hex(8)
|
|
186
|
+
return value unless value.delete("0").empty?
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def stack
|
|
191
|
+
Thread.current[STACK_KEY] ||= []
|
|
192
|
+
end
|
|
193
|
+
private_class_method :stack
|
|
194
|
+
|
|
195
|
+
def env_value(env, key)
|
|
196
|
+
return nil unless env.respond_to?(:[])
|
|
197
|
+
|
|
198
|
+
value = env[key]
|
|
199
|
+
return value if value.is_a?(TraceContext)
|
|
200
|
+
return nil if value.nil?
|
|
201
|
+
|
|
202
|
+
text = value.to_s
|
|
203
|
+
text.empty? ? nil : text
|
|
204
|
+
end
|
|
205
|
+
private_class_method :env_value
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
module TraceClientMethods
|
|
209
|
+
def issue(id, timestamp, attributes)
|
|
210
|
+
super(id, timestamp, Trace.merge_attributes(attributes))
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def log(id, timestamp, attributes)
|
|
214
|
+
super(id, timestamp, Trace.merge_attributes(attributes))
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def metric(id, timestamp, attributes)
|
|
218
|
+
super(id, timestamp, Trace.merge_attributes(attributes))
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def action(id, timestamp, attributes)
|
|
222
|
+
super(id, timestamp, Trace.merge_attributes(attributes))
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
module TraceLoggerMethods
|
|
227
|
+
private
|
|
228
|
+
|
|
229
|
+
def logbrew_metadata(severity, message, progname)
|
|
230
|
+
Trace.add_metadata(super)
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
module TraceRackMiddlewareMethods
|
|
235
|
+
def call(env)
|
|
236
|
+
trace_context = Trace.from_rack_env(env)
|
|
237
|
+
env["logbrew.trace"] = trace_context if env.respond_to?(:[]=)
|
|
238
|
+
Trace.with_context(trace_context) { super(env) }
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
private
|
|
242
|
+
|
|
243
|
+
def capture_request_span(env, status_code, elapsed_ms, status)
|
|
244
|
+
context = rack_trace_context(env)
|
|
245
|
+
attributes = {
|
|
246
|
+
name: request_name(env),
|
|
247
|
+
traceId: context ? context.trace_id : trace_id(env),
|
|
248
|
+
spanId: context ? context.span_id : span_id(env),
|
|
249
|
+
status: status,
|
|
250
|
+
durationMs: elapsed_ms,
|
|
251
|
+
metadata: request_metadata(env, status_code)
|
|
252
|
+
}
|
|
253
|
+
attributes[:parentSpanId] = context.parent_span_id if context && context.parent_span_id
|
|
254
|
+
|
|
255
|
+
@client.span(next_event_id("span"), logbrew_timestamp, attributes)
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def trace_id(env)
|
|
259
|
+
context = rack_trace_context(env)
|
|
260
|
+
return context.trace_id if context
|
|
261
|
+
|
|
262
|
+
super
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def span_id(env)
|
|
266
|
+
context = rack_trace_context(env)
|
|
267
|
+
return context.span_id if context
|
|
268
|
+
|
|
269
|
+
super
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def request_metadata(env, status_code)
|
|
273
|
+
Trace.add_metadata(super, rack_trace_context(env))
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def exception_metadata(env, error)
|
|
277
|
+
Trace.add_metadata(super, rack_trace_context(env))
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def rack_trace_context(env)
|
|
281
|
+
trace = env["logbrew.trace"] if env.respond_to?(:[])
|
|
282
|
+
trace.is_a?(TraceContext) ? trace : Trace.current
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
module TraceRailsErrorSubscriberMethods
|
|
287
|
+
private
|
|
288
|
+
|
|
289
|
+
def rails_metadata(error, handled, severity, context, source)
|
|
290
|
+
Trace.add_metadata(super)
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
Client.prepend(TraceClientMethods)
|
|
295
|
+
Logger.prepend(TraceLoggerMethods)
|
|
296
|
+
RackMiddleware.prepend(TraceRackMiddlewareMethods)
|
|
297
|
+
RailsErrorSubscriber.prepend(TraceRailsErrorSubscriberMethods)
|
|
298
|
+
end
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LogBrew
|
|
4
|
+
TraceparentContext = Struct.new(:version, :trace_id, :parent_span_id, :trace_flags, :sampled, keyword_init: true)
|
|
5
|
+
TraceparentSpanInput = Struct.new(:name, :span_id, :status, :duration_ms, :metadata, keyword_init: true) do
|
|
6
|
+
def initialize(name:, span_id:, status: "ok", duration_ms: nil, metadata: nil)
|
|
7
|
+
super(
|
|
8
|
+
name: name,
|
|
9
|
+
span_id: span_id,
|
|
10
|
+
status: status,
|
|
11
|
+
duration_ms: duration_ms,
|
|
12
|
+
metadata: metadata
|
|
13
|
+
)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Dependency-free W3C traceparent helpers for explicit app-owned propagation.
|
|
18
|
+
module Traceparent
|
|
19
|
+
VERSION = "00"
|
|
20
|
+
private_constant :VERSION
|
|
21
|
+
|
|
22
|
+
module_function
|
|
23
|
+
|
|
24
|
+
def parse(traceparent)
|
|
25
|
+
Validation.require_non_empty("traceparent", traceparent)
|
|
26
|
+
parts = traceparent.to_s.strip.downcase.split("-")
|
|
27
|
+
raise SdkError.new("validation_error", "traceparent must have four fields") unless parts.length == 4
|
|
28
|
+
|
|
29
|
+
version, trace_id, parent_span_id, trace_flags = parts
|
|
30
|
+
require_version(version)
|
|
31
|
+
require_trace_id(trace_id)
|
|
32
|
+
require_span_id("traceparent parent span id", parent_span_id)
|
|
33
|
+
flags = normalize_trace_flags(trace_flags)
|
|
34
|
+
|
|
35
|
+
TraceparentContext.new(
|
|
36
|
+
version: version,
|
|
37
|
+
trace_id: trace_id,
|
|
38
|
+
parent_span_id: parent_span_id,
|
|
39
|
+
trace_flags: flags,
|
|
40
|
+
sampled: (flags.to_i(16) & 1) == 1
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def create(trace_id:, span_id:, trace_flags: "01")
|
|
45
|
+
normalized_trace_id = normalize_trace_id(trace_id)
|
|
46
|
+
normalized_span_id = normalize_span_id("traceparent span id", span_id)
|
|
47
|
+
flags = normalize_trace_flags(trace_flags)
|
|
48
|
+
|
|
49
|
+
"#{VERSION}-#{normalized_trace_id}-#{normalized_span_id}-#{flags}"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def create_headers(trace_id:, span_id:, trace_flags: "01")
|
|
53
|
+
{ "traceparent" => create(trace_id: trace_id, span_id: span_id, trace_flags: trace_flags) }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def span_attributes_from_traceparent(traceparent, input)
|
|
57
|
+
context = traceparent.is_a?(TraceparentContext) ? traceparent : parse(traceparent)
|
|
58
|
+
attributes = {
|
|
59
|
+
"name" => required_name(input.name),
|
|
60
|
+
"traceId" => context.trace_id,
|
|
61
|
+
"spanId" => normalize_span_id("span spanId", input.span_id),
|
|
62
|
+
"parentSpanId" => context.parent_span_id,
|
|
63
|
+
"status" => normalize_status(input.status)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
unless input.duration_ms.nil?
|
|
67
|
+
duration_ms = Validation.require_finite_number("span durationMs", input.duration_ms)
|
|
68
|
+
raise SdkError.new("validation_error", "span durationMs must be non-negative") if duration_ms.negative?
|
|
69
|
+
|
|
70
|
+
attributes["durationMs"] = duration_ms
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
metadata = Validation.require_metadata(input.metadata)
|
|
74
|
+
attributes["metadata"] = metadata unless metadata.nil?
|
|
75
|
+
attributes
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def require_version(version)
|
|
79
|
+
unless version.length == 2 && lower_hex?(version) && version != "ff"
|
|
80
|
+
raise SdkError.new("validation_error", "traceparent version must be two hex characters and not ff")
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
private_class_method :require_version
|
|
84
|
+
|
|
85
|
+
def normalize_trace_id(trace_id)
|
|
86
|
+
normalized = trace_id.to_s.strip.downcase
|
|
87
|
+
require_trace_id(normalized)
|
|
88
|
+
normalized
|
|
89
|
+
end
|
|
90
|
+
private_class_method :normalize_trace_id
|
|
91
|
+
|
|
92
|
+
def require_trace_id(trace_id)
|
|
93
|
+
unless trace_id.length == 32 && lower_hex?(trace_id) && !all_zero?(trace_id)
|
|
94
|
+
raise SdkError.new("validation_error", "traceparent trace id must be 32 non-zero hex characters")
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
private_class_method :require_trace_id
|
|
98
|
+
|
|
99
|
+
def normalize_span_id(label, span_id)
|
|
100
|
+
normalized = span_id.to_s.strip.downcase
|
|
101
|
+
require_span_id(label, normalized)
|
|
102
|
+
normalized
|
|
103
|
+
end
|
|
104
|
+
private_class_method :normalize_span_id
|
|
105
|
+
|
|
106
|
+
def require_span_id(label, span_id)
|
|
107
|
+
unless span_id.length == 16 && lower_hex?(span_id) && !all_zero?(span_id)
|
|
108
|
+
raise SdkError.new("validation_error", "#{label} must be 16 non-zero hex characters")
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
private_class_method :require_span_id
|
|
112
|
+
|
|
113
|
+
def normalize_trace_flags(trace_flags)
|
|
114
|
+
normalized = trace_flags.to_s.strip.downcase
|
|
115
|
+
unless normalized.length == 2 && lower_hex?(normalized)
|
|
116
|
+
raise SdkError.new("validation_error", "traceparent flags must be two hex characters")
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
normalized
|
|
120
|
+
end
|
|
121
|
+
private_class_method :normalize_trace_flags
|
|
122
|
+
|
|
123
|
+
def required_name(name)
|
|
124
|
+
Validation.require_non_empty("span name", name)
|
|
125
|
+
name.to_s.strip
|
|
126
|
+
end
|
|
127
|
+
private_class_method :required_name
|
|
128
|
+
|
|
129
|
+
def normalize_status(status)
|
|
130
|
+
Validation.require_allowed_value("span status", status, LogBrew::SPAN_STATUSES)
|
|
131
|
+
status
|
|
132
|
+
end
|
|
133
|
+
private_class_method :normalize_status
|
|
134
|
+
|
|
135
|
+
def lower_hex?(value)
|
|
136
|
+
value.match?(/\A[0-9a-f]+\z/)
|
|
137
|
+
end
|
|
138
|
+
private_class_method :lower_hex?
|
|
139
|
+
|
|
140
|
+
def all_zero?(value)
|
|
141
|
+
value.delete("0").empty?
|
|
142
|
+
end
|
|
143
|
+
private_class_method :all_zero?
|
|
144
|
+
end
|
|
145
|
+
end
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LogBrew
|
|
4
|
+
# Content-free delivery details safe for application-owned diagnostics.
|
|
5
|
+
class WorkerDeliveryFailure
|
|
6
|
+
attr_reader :stage, :code, :pending_events, :pending_event_bytes, :dropped_events
|
|
7
|
+
|
|
8
|
+
def initialize(stage:, code:, pending_events:, pending_event_bytes:, dropped_events:)
|
|
9
|
+
@stage = stage.dup.freeze
|
|
10
|
+
@code = code.dup.freeze
|
|
11
|
+
@pending_events = pending_events
|
|
12
|
+
@pending_event_bytes = pending_event_bytes
|
|
13
|
+
@dropped_events = dropped_events
|
|
14
|
+
freeze
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Explicit delivery boundaries for serialized prefork worker loops.
|
|
19
|
+
class WorkerLifecycle
|
|
20
|
+
SAFE_DELIVERY_CODES = %w[
|
|
21
|
+
delivery_error
|
|
22
|
+
flush_error
|
|
23
|
+
network_failure
|
|
24
|
+
shutdown_error
|
|
25
|
+
transport_error
|
|
26
|
+
unauthenticated
|
|
27
|
+
validation_error
|
|
28
|
+
].freeze
|
|
29
|
+
|
|
30
|
+
def self.create(client:, transport:, on_delivery_failure: nil)
|
|
31
|
+
unless client.is_a?(Client)
|
|
32
|
+
raise SdkError.new("validation_error", "client must be a LogBrew::Client")
|
|
33
|
+
end
|
|
34
|
+
unless transport.respond_to?(:send)
|
|
35
|
+
raise SdkError.new("validation_error", "transport must respond to send")
|
|
36
|
+
end
|
|
37
|
+
if !on_delivery_failure.nil? && !on_delivery_failure.respond_to?(:call)
|
|
38
|
+
raise SdkError.new("validation_error", "on_delivery_failure must be callable")
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
new(
|
|
42
|
+
client: client,
|
|
43
|
+
transport: transport,
|
|
44
|
+
on_delivery_failure: on_delivery_failure,
|
|
45
|
+
owner_process_id: current_process_id
|
|
46
|
+
)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def self.current_process_id
|
|
50
|
+
process_id = Process.pid
|
|
51
|
+
unless process_id.is_a?(Integer) && process_id.positive?
|
|
52
|
+
raise SdkError.new("process_ownership_error", "worker process identity is unavailable")
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
process_id
|
|
56
|
+
end
|
|
57
|
+
private_class_method :current_process_id
|
|
58
|
+
|
|
59
|
+
def initialize(client:, transport:, on_delivery_failure:, owner_process_id:)
|
|
60
|
+
@client = client
|
|
61
|
+
@transport = transport
|
|
62
|
+
@on_delivery_failure = on_delivery_failure
|
|
63
|
+
@owner_process_id = owner_process_id
|
|
64
|
+
@state_mutex = Mutex.new
|
|
65
|
+
@operation_active = false
|
|
66
|
+
@shutdown_response = nil
|
|
67
|
+
end
|
|
68
|
+
private_class_method :new
|
|
69
|
+
|
|
70
|
+
def run
|
|
71
|
+
assert_process_ownership
|
|
72
|
+
begin_run
|
|
73
|
+
begin
|
|
74
|
+
application_error = nil
|
|
75
|
+
result = nil
|
|
76
|
+
begin
|
|
77
|
+
result = yield
|
|
78
|
+
rescue Exception => error # rubocop:disable Lint/RescueException
|
|
79
|
+
application_error = error
|
|
80
|
+
ensure
|
|
81
|
+
finish_work_boundary(application_error)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
raise application_error unless application_error.nil?
|
|
85
|
+
|
|
86
|
+
result
|
|
87
|
+
ensure
|
|
88
|
+
end_operation
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def shutdown
|
|
93
|
+
assert_process_ownership
|
|
94
|
+
cached_response = begin_shutdown
|
|
95
|
+
return cached_response unless cached_response.nil?
|
|
96
|
+
|
|
97
|
+
completed = false
|
|
98
|
+
begin
|
|
99
|
+
begin
|
|
100
|
+
response = @client.shutdown(@transport)
|
|
101
|
+
rescue StandardError => delivery_error
|
|
102
|
+
report_delivery_failure("shutdown", delivery_error)
|
|
103
|
+
raise delivery_error
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
complete_shutdown(response)
|
|
107
|
+
completed = true
|
|
108
|
+
response
|
|
109
|
+
ensure
|
|
110
|
+
end_operation unless completed
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
private
|
|
115
|
+
|
|
116
|
+
def begin_run
|
|
117
|
+
@state_mutex.synchronize do
|
|
118
|
+
raise SdkError.new("shutdown_error", "worker lifecycle is already shut down") unless @shutdown_response.nil?
|
|
119
|
+
|
|
120
|
+
claim_operation
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def begin_shutdown
|
|
125
|
+
@state_mutex.synchronize do
|
|
126
|
+
return @shutdown_response unless @shutdown_response.nil?
|
|
127
|
+
|
|
128
|
+
claim_operation
|
|
129
|
+
nil
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def claim_operation
|
|
134
|
+
if @operation_active
|
|
135
|
+
raise SdkError.new("worker_lifecycle_error", "worker lifecycle operation is already in progress")
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
@operation_active = true
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def complete_shutdown(response)
|
|
142
|
+
@state_mutex.synchronize do
|
|
143
|
+
@shutdown_response = response
|
|
144
|
+
@operation_active = false
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def end_operation
|
|
149
|
+
if current_process_id == @owner_process_id
|
|
150
|
+
@state_mutex.synchronize { @operation_active = false }
|
|
151
|
+
else
|
|
152
|
+
# An inherited lifecycle is permanently unusable in the child.
|
|
153
|
+
@operation_active = false
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def assert_process_ownership
|
|
158
|
+
return if current_process_id == @owner_process_id
|
|
159
|
+
|
|
160
|
+
raise SdkError.new(
|
|
161
|
+
"process_ownership_error",
|
|
162
|
+
"worker lifecycle must be created in the current process"
|
|
163
|
+
)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def finish_work_boundary(application_error)
|
|
167
|
+
begin
|
|
168
|
+
assert_process_ownership
|
|
169
|
+
rescue SdkError => ownership_error
|
|
170
|
+
raise application_error unless application_error.nil?
|
|
171
|
+
|
|
172
|
+
raise ownership_error
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
begin
|
|
176
|
+
@client.flush(@transport)
|
|
177
|
+
rescue StandardError => delivery_error
|
|
178
|
+
report_delivery_failure("work_boundary", delivery_error)
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def current_process_id
|
|
183
|
+
process_id = Process.pid
|
|
184
|
+
unless process_id.is_a?(Integer) && process_id.positive?
|
|
185
|
+
raise SdkError.new("process_ownership_error", "worker process identity is unavailable")
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
process_id
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def report_delivery_failure(stage, error)
|
|
192
|
+
return if @on_delivery_failure.nil?
|
|
193
|
+
|
|
194
|
+
code = if error.is_a?(SdkError) && SAFE_DELIVERY_CODES.include?(error.code)
|
|
195
|
+
error.code
|
|
196
|
+
else
|
|
197
|
+
"delivery_error"
|
|
198
|
+
end
|
|
199
|
+
notice = WorkerDeliveryFailure.new(
|
|
200
|
+
stage: stage,
|
|
201
|
+
code: code,
|
|
202
|
+
pending_events: @client.pending_events,
|
|
203
|
+
pending_event_bytes: @client.pending_event_bytes,
|
|
204
|
+
dropped_events: @client.dropped_events
|
|
205
|
+
)
|
|
206
|
+
@on_delivery_failure.call(notice)
|
|
207
|
+
rescue StandardError
|
|
208
|
+
nil
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|