bitfab 0.33.4 → 0.33.6
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 +68 -4
- data/lib/bitfab/client.rb +26 -51
- data/lib/bitfab/http_client.rb +62 -74
- data/lib/bitfab/otel.rb +805 -0
- data/lib/bitfab/otel_collector.rb +28 -0
- data/lib/bitfab/replay.rb +65 -31
- data/lib/bitfab/serialize.rb +40 -0
- data/lib/bitfab/transport.rb +27 -0
- data/lib/bitfab/version.rb +1 -1
- metadata +38 -1
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "opentelemetry-exporter-otlp"
|
|
4
|
+
|
|
5
|
+
module Bitfab
|
|
6
|
+
module Otel
|
|
7
|
+
# OTLP/HTTP protobuf delivery to a customer's Collector.
|
|
8
|
+
class CollectorExporter < OpenTelemetry::Exporter::OTLP::Exporter
|
|
9
|
+
def self.traces_endpoint(endpoint)
|
|
10
|
+
base = endpoint.chomp("/")
|
|
11
|
+
base.end_with?("/v1/traces") ? base : "#{base}/v1/traces"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def initialize(api_key:, **options)
|
|
15
|
+
super(**options)
|
|
16
|
+
@api_key = api_key
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# The OTLP exporter reads headers once at construction, while a Bitfab key
|
|
20
|
+
# is resolved at request time (an env var may load after the client is built).
|
|
21
|
+
def export(span_data, timeout: nil)
|
|
22
|
+
key = @api_key.respond_to?(:call) ? @api_key.call : @api_key
|
|
23
|
+
@headers = @headers.merge("Authorization" => "Bearer #{key}")
|
|
24
|
+
super
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
data/lib/bitfab/replay.rb
CHANGED
|
@@ -9,6 +9,7 @@ require_relative "constants"
|
|
|
9
9
|
require_relative "mock_override"
|
|
10
10
|
require_relative "serialize"
|
|
11
11
|
require_relative "traceable"
|
|
12
|
+
require_relative "transport"
|
|
12
13
|
|
|
13
14
|
module Bitfab
|
|
14
15
|
# Replay mock strategies. Mirrors the Python and TypeScript SDKs.
|
|
@@ -29,12 +30,8 @@ module Bitfab
|
|
|
29
30
|
|
|
30
31
|
# Execute a block with replay context set on the current thread.
|
|
31
32
|
# The context is automatically cleared when the block completes.
|
|
32
|
-
#
|
|
33
|
-
# pending_persistence, when given, collects the root span's persistence
|
|
34
|
-
# threads (span uploads + trace completion) so the replay runner can join
|
|
35
|
-
# them before complete_replay builds the trace-ID mapping.
|
|
36
33
|
def with_context(test_run_id:, input_source_span_id: nil, input_source_trace_id: nil, trace_id: nil,
|
|
37
|
-
mock_tree: nil, mock_strategy: nil, mock_overrides: nil, fetch_span_output: nil,
|
|
34
|
+
mock_tree: nil, mock_strategy: nil, mock_overrides: nil, fetch_span_output: nil,
|
|
38
35
|
db_branch_lease: nil, source_bitfab_trace_id: nil)
|
|
39
36
|
previous = Thread.current[REPLAY_CONTEXT_KEY]
|
|
40
37
|
ctx = {
|
|
@@ -43,7 +40,6 @@ module Bitfab
|
|
|
43
40
|
input_source_trace_id:,
|
|
44
41
|
trace_id:
|
|
45
42
|
}
|
|
46
|
-
ctx[:pending_persistence] = pending_persistence if pending_persistence
|
|
47
43
|
if mock_tree
|
|
48
44
|
ctx[:mock_tree] = mock_tree
|
|
49
45
|
ctx[:mock_strategy] = mock_strategy || "marked"
|
|
@@ -82,6 +78,8 @@ module Bitfab
|
|
|
82
78
|
# Replay historical traces through a traced method and create a test run.
|
|
83
79
|
module Replay
|
|
84
80
|
CODE_CHANGE_UNSET = Object.new.freeze
|
|
81
|
+
PERSISTENCE_TIMEOUT_SECONDS = 30.0
|
|
82
|
+
PERSISTENCE_POLL_SECONDS = 0.1
|
|
85
83
|
|
|
86
84
|
module_function
|
|
87
85
|
|
|
@@ -127,10 +125,10 @@ module Bitfab
|
|
|
127
125
|
# source_trace_id/source_span_id aliases), and returns [new_args, new_kwargs].
|
|
128
126
|
# Runs per item inside the same rescue as the method, so a raising adapter
|
|
129
127
|
# sets that item's :error rather than crashing the run.
|
|
130
|
-
# @param db_branch [Hash, nil]
|
|
131
|
-
#
|
|
132
|
-
# +:min_cu+/+:max_cu+ are the branch compute's autoscaling floor
|
|
133
|
-
# ceiling in Neon Compute Units, and +:warmup_sql+ warms the branch's
|
|
128
|
+
# @param db_branch [Boolean, Hash, nil] +true+ branches with the mirror
|
|
129
|
+
# project's own sizing; a Hash tunes it and +false+/+nil+ leave branching
|
|
130
|
+
# off. Keys +:min_cu+/+:max_cu+ are the branch compute's autoscaling floor
|
|
131
|
+
# and ceiling in Neon Compute Units, and +:warmup_sql+ warms the branch's
|
|
134
132
|
# cache before the replayed method sees it. Read the resolved branch
|
|
135
133
|
# inside the method with +Bitfab.current_replay_branch+.
|
|
136
134
|
# @param on_progress [#call, nil] optional callback invoked once per item as
|
|
@@ -192,7 +190,7 @@ module Bitfab
|
|
|
192
190
|
# the count), so it's omitted from the request entirely.
|
|
193
191
|
effective_limit = trace_ids ? nil : (limit || 5)
|
|
194
192
|
|
|
195
|
-
include_db_branch_lease =
|
|
193
|
+
include_db_branch_lease = db_branch_enabled?(db_branch)
|
|
196
194
|
|
|
197
195
|
# code_change_files controls capture: omitted auto-captures, an array
|
|
198
196
|
# wins, and explicit nil opts out. Preserve a caller-supplied description
|
|
@@ -233,11 +231,15 @@ module Bitfab
|
|
|
233
231
|
[]
|
|
234
232
|
end
|
|
235
233
|
|
|
236
|
-
#
|
|
237
|
-
#
|
|
238
|
-
#
|
|
239
|
-
#
|
|
240
|
-
|
|
234
|
+
# Spans and completions ride a batched transport, so the run is only safe
|
|
235
|
+
# to finalize once the server confirms every replay trace it queued: the
|
|
236
|
+
# trace-ID mapping complete_replay builds would otherwise race the
|
|
237
|
+
# in-flight batches and hand back nil for every item.
|
|
238
|
+
wait_for_replay_persistence(http_client, test_run_id, result_items.map { |item| item[:_sdk_trace_id] })
|
|
239
|
+
|
|
240
|
+
# complete_replay finalizes the run and returns the token/diagnostic
|
|
241
|
+
# mapping; its failures propagate loudly because a run that never
|
|
242
|
+
# completed can't be finalized.
|
|
241
243
|
complete_response = http_client.complete_replay(test_run_id)
|
|
242
244
|
trace_id_map = complete_response&.dig("traceIds")
|
|
243
245
|
# Per-replay-trace token usage keyed by server trace id: the REPLAYED
|
|
@@ -277,7 +279,7 @@ module Bitfab
|
|
|
277
279
|
raise "Replay completed but the server has no persisted trace for " \
|
|
278
280
|
"any of the #{completed_count} completed item(s) " \
|
|
279
281
|
"(test_run_id #{test_run_id}).#{server_count} Trace uploads were " \
|
|
280
|
-
"
|
|
282
|
+
"flushed, so either the uploads failed or the replayed method is " \
|
|
281
283
|
"not traced (no root span was emitted)."
|
|
282
284
|
end
|
|
283
285
|
# SOME completed items missing: per-item upload failure. Warn, but
|
|
@@ -335,16 +337,26 @@ module Bitfab
|
|
|
335
337
|
CC_MAX_FILE_BYTES = 500_000
|
|
336
338
|
CC_MAX_TOTAL_BYTES = 2_000_000
|
|
337
339
|
|
|
340
|
+
# Whether the caller asked for database branching. +true+ and a Hash both
|
|
341
|
+
# turn it on; +false+ and +nil+ leave it off. Kept separate from
|
|
342
|
+
# +db_branch_settings+ because +db_branch: true+ enables branching while
|
|
343
|
+
# contributing no settings, so the presence of settings can't stand in for
|
|
344
|
+
# the switch.
|
|
345
|
+
def db_branch_enabled?(db_branch)
|
|
346
|
+
!db_branch.nil? && db_branch != false
|
|
347
|
+
end
|
|
348
|
+
|
|
338
349
|
# Convert the caller's +db_branch+ options to the wire shape, or nil when
|
|
339
350
|
# nothing was set. Dropping the key entirely keeps the request identical to
|
|
340
|
-
# what older SDKs send.
|
|
351
|
+
# what older SDKs send. Booleans carry no settings: they only move the
|
|
352
|
+
# switch.
|
|
341
353
|
#
|
|
342
354
|
# Keys: :min_cu and :max_cu are the branch compute's autoscaling floor and
|
|
343
355
|
# ceiling in Neon Compute Units (equal values pin the size, keeping items
|
|
344
356
|
# comparable); :warmup_sql is appended to the branch's readiness check so it
|
|
345
357
|
# warms the cache before the replayed function sees the lease.
|
|
346
358
|
def db_branch_settings(db_branch)
|
|
347
|
-
return nil if db_branch.nil? || db_branch.empty?
|
|
359
|
+
return nil if db_branch.nil? || db_branch == true || db_branch == false || db_branch.empty?
|
|
348
360
|
|
|
349
361
|
# Symbol or string keys, matching normalize_code_change_files: a hash
|
|
350
362
|
# assembled from JSON or YAML would otherwise be read as empty, leaving
|
|
@@ -866,6 +878,40 @@ module Bitfab
|
|
|
866
878
|
}
|
|
867
879
|
end
|
|
868
880
|
|
|
881
|
+
# Block until the server has persisted every replay trace this run queued.
|
|
882
|
+
#
|
|
883
|
+
# Flushing only proves the batches left the process. The server writes a
|
|
884
|
+
# trace's spans and completion independently, so the barrier also polls
|
|
885
|
+
# replay status with the span count each trace owes: a trace is ready only
|
|
886
|
+
# once its completion and all of its spans have landed.
|
|
887
|
+
def wait_for_replay_persistence(http_client, test_run_id, sdk_trace_ids)
|
|
888
|
+
expected_span_counts = Transport.take_replay_span_counts(sdk_trace_ids.compact)
|
|
889
|
+
return if expected_span_counts.empty?
|
|
890
|
+
|
|
891
|
+
unless Bitfab.flush_traces(timeout: PERSISTENCE_TIMEOUT_SECONDS)
|
|
892
|
+
raise "Replay traces could not be flushed before the delivery deadline " \
|
|
893
|
+
"(test_run_id #{test_run_id})."
|
|
894
|
+
end
|
|
895
|
+
|
|
896
|
+
deadline = Otel.monotonic_now + PERSISTENCE_TIMEOUT_SECONDS
|
|
897
|
+
missing = expected_span_counts.keys
|
|
898
|
+
|
|
899
|
+
while missing.any?
|
|
900
|
+
status = http_client.get_replay_status(test_run_id, expected_span_counts)
|
|
901
|
+
ready = status["traceIds"]
|
|
902
|
+
ready = {} unless ready.is_a?(Hash)
|
|
903
|
+
missing = expected_span_counts.keys - ready.keys
|
|
904
|
+
return if missing.empty?
|
|
905
|
+
break if Otel.monotonic_now >= deadline
|
|
906
|
+
|
|
907
|
+
sleep((deadline - Otel.monotonic_now).clamp(0, PERSISTENCE_POLL_SECONDS))
|
|
908
|
+
end
|
|
909
|
+
|
|
910
|
+
raise "Replay traces were not fully persisted before the delivery deadline " \
|
|
911
|
+
"(test_run_id #{test_run_id}, missing #{missing.length} of " \
|
|
912
|
+
"#{expected_span_counts.length} trace(s))."
|
|
913
|
+
end
|
|
914
|
+
|
|
869
915
|
# Normalize a complete-replay tokens hash (string-keyed JSON) into the
|
|
870
916
|
# symbol-keyed shape the replay item exposes. Nil when the server reported
|
|
871
917
|
# no token data for this trace.
|
|
@@ -894,11 +940,6 @@ module Bitfab
|
|
|
894
940
|
# complete-replay loop). Carried on the item under :_sdk_trace_id, never
|
|
895
941
|
# surfaced as the public :trace_id.
|
|
896
942
|
sdk_trace_id = SecureRandom.uuid
|
|
897
|
-
# Collects the root span's persistence threads (span uploads + trace
|
|
898
|
-
# completion). Joined below so this item's trace is on the server
|
|
899
|
-
# before run() calls complete_replay: otherwise the server's trace-ID
|
|
900
|
-
# mapping races the uploads and the item's trace_id comes back nil.
|
|
901
|
-
pending_persistence = []
|
|
902
943
|
|
|
903
944
|
ReplayContext.with_context(
|
|
904
945
|
test_run_id:,
|
|
@@ -909,7 +950,6 @@ module Bitfab
|
|
|
909
950
|
mock_strategy:,
|
|
910
951
|
mock_overrides:,
|
|
911
952
|
fetch_span_output:,
|
|
912
|
-
pending_persistence:,
|
|
913
953
|
db_branch_lease:,
|
|
914
954
|
source_bitfab_trace_id:
|
|
915
955
|
) do
|
|
@@ -935,12 +975,6 @@ module Bitfab
|
|
|
935
975
|
fn_error = e.message
|
|
936
976
|
end
|
|
937
977
|
|
|
938
|
-
# Wait for this item's trace (spans + completion) to be fully persisted
|
|
939
|
-
# before the item resolves. Runs on the error path too: a raising
|
|
940
|
-
# method still emits a root span whose trace must land before
|
|
941
|
-
# complete_replay. Joins are bounded by the HTTP layer's own timeouts.
|
|
942
|
-
pending_persistence.each(&:join)
|
|
943
|
-
|
|
944
978
|
{
|
|
945
979
|
input: args,
|
|
946
980
|
result: fn_result,
|
data/lib/bitfab/serialize.rb
CHANGED
|
@@ -236,5 +236,45 @@ module Bitfab
|
|
|
236
236
|
[[raw_input], {}]
|
|
237
237
|
end
|
|
238
238
|
end
|
|
239
|
+
|
|
240
|
+
# JSON-encode a request or carrier body without ever raising on a stray
|
|
241
|
+
# value.
|
|
242
|
+
#
|
|
243
|
+
# Upstream serialization (serialize_value) should already have flattened
|
|
244
|
+
# user data. This is the boundary backstop: if anything non-serializable
|
|
245
|
+
# still slips through, it is run through serialize_value (which never
|
|
246
|
+
# raises and stubs strays) instead of letting JSON.generate raise and drop
|
|
247
|
+
# the whole span/trace silently. A degraded payload warns loudly so the
|
|
248
|
+
# trace isn't quietly left incomplete or not replayable.
|
|
249
|
+
def safe_generate(payload)
|
|
250
|
+
JSON.generate(payload)
|
|
251
|
+
rescue => e
|
|
252
|
+
Bitfab.warn_once(
|
|
253
|
+
"request-body-stubbed",
|
|
254
|
+
"a request body held a non-serializable value (#{e.message}); it was " \
|
|
255
|
+
"stubbed so the span still sends, but the trace may be incomplete or " \
|
|
256
|
+
"not replayable. Capture a JSON-safe projection of this input to make " \
|
|
257
|
+
"it replayable."
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
begin
|
|
261
|
+
JSON.generate(sanitize_payload(payload))
|
|
262
|
+
rescue
|
|
263
|
+
# Truly pathological. Still never drop silently: send a marker body.
|
|
264
|
+
JSON.generate({"error" => "payload_serialize_failed"})
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# Serialize each top-level value so a bad or oversize value is stubbed in
|
|
269
|
+
# place while the payload keeps its object shape. Running serialize_value on
|
|
270
|
+
# the whole payload could collapse the entire body to a single stub string
|
|
271
|
+
# (oversize/cyclic), sending a JSON string instead of a span object.
|
|
272
|
+
def sanitize_payload(payload)
|
|
273
|
+
return {"error" => "payload_serialize_failed"} unless payload.is_a?(Hash)
|
|
274
|
+
|
|
275
|
+
payload.each_with_object({}) do |(k, v), acc|
|
|
276
|
+
acc[k.to_s] = serialize_value(v)
|
|
277
|
+
end
|
|
278
|
+
end
|
|
239
279
|
end
|
|
240
280
|
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "otel"
|
|
4
|
+
|
|
5
|
+
module Bitfab
|
|
6
|
+
# Framework-neutral seam between span/trace sending and the delivery
|
|
7
|
+
# mechanism, so callers never depend on OpenTelemetry directly.
|
|
8
|
+
module Transport
|
|
9
|
+
class << self
|
|
10
|
+
def create_trace_transport(api_key:, direct_sender:)
|
|
11
|
+
Otel.create_transport(api_key:, direct_sender:)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def flush_trace_transports(timeout = 30.0)
|
|
15
|
+
Otel.flush_transports(timeout)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def shutdown_trace_transports(timeout = 30.0)
|
|
19
|
+
Otel.shutdown_transports(timeout)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def take_replay_span_counts(trace_ids)
|
|
23
|
+
Otel.take_replay_span_counts(trace_ids)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
data/lib/bitfab/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: bitfab
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.33.
|
|
4
|
+
version: 0.33.6
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Harvest Team
|
|
@@ -23,6 +23,40 @@ dependencies:
|
|
|
23
23
|
- - ">="
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
25
|
version: '0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: opentelemetry-sdk
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '1.2'
|
|
33
|
+
- - "<"
|
|
34
|
+
- !ruby/object:Gem::Version
|
|
35
|
+
version: '2'
|
|
36
|
+
type: :runtime
|
|
37
|
+
prerelease: false
|
|
38
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
39
|
+
requirements:
|
|
40
|
+
- - ">="
|
|
41
|
+
- !ruby/object:Gem::Version
|
|
42
|
+
version: '1.2'
|
|
43
|
+
- - "<"
|
|
44
|
+
- !ruby/object:Gem::Version
|
|
45
|
+
version: '2'
|
|
46
|
+
- !ruby/object:Gem::Dependency
|
|
47
|
+
name: opentelemetry-exporter-otlp
|
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
|
49
|
+
requirements:
|
|
50
|
+
- - ">="
|
|
51
|
+
- !ruby/object:Gem::Version
|
|
52
|
+
version: '0.30'
|
|
53
|
+
type: :development
|
|
54
|
+
prerelease: false
|
|
55
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
56
|
+
requirements:
|
|
57
|
+
- - ">="
|
|
58
|
+
- !ruby/object:Gem::Version
|
|
59
|
+
version: '0.30'
|
|
26
60
|
- !ruby/object:Gem::Dependency
|
|
27
61
|
name: rake
|
|
28
62
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -122,11 +156,14 @@ files:
|
|
|
122
156
|
- lib/bitfab/db_snapshot.rb
|
|
123
157
|
- lib/bitfab/http_client.rb
|
|
124
158
|
- lib/bitfab/mock_override.rb
|
|
159
|
+
- lib/bitfab/otel.rb
|
|
160
|
+
- lib/bitfab/otel_collector.rb
|
|
125
161
|
- lib/bitfab/replay.rb
|
|
126
162
|
- lib/bitfab/replay_branch.rb
|
|
127
163
|
- lib/bitfab/serialize.rb
|
|
128
164
|
- lib/bitfab/span_context.rb
|
|
129
165
|
- lib/bitfab/traceable.rb
|
|
166
|
+
- lib/bitfab/transport.rb
|
|
130
167
|
- lib/bitfab/version.rb
|
|
131
168
|
- lib/bitfab/warn_once.rb
|
|
132
169
|
homepage: https://bitfab.ai
|