bitfab 0.38.2 → 0.40.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/lib/bitfab/client.rb +63 -29
- data/lib/bitfab/datasets.rb +107 -0
- data/lib/bitfab/span_context.rb +3 -1
- data/lib/bitfab/timestamp.rb +16 -0
- data/lib/bitfab/version.rb +1 -1
- data/lib/bitfab.rb +5 -4
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a178d05e7c40055c3ac8dec21a40eb60eb36b08a005dfa55d031c3c3b9cdf129
|
|
4
|
+
data.tar.gz: e71a921a9bad32b967c723245c1187257366073f13aab6f8dca2882d6b758fb8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 559fed18dd194a527ff477e057f465a172d7266ba05c4e6b3305a693091b0fa1de3847078d8f0df4c6822a4b28b2fd6a0f802eaf2db7d75e38233051f05fc887
|
|
7
|
+
data.tar.gz: f4a718c47568294809eaed27905b2d8b67a3058fc954eff2dac6b10a05ae2219463d1742b40bb0a7725d4979a5c86c2beb37e9e10621a588e007a9a7757d6376
|
data/lib/bitfab/client.rb
CHANGED
|
@@ -8,11 +8,12 @@ require_relative "http_client"
|
|
|
8
8
|
require_relative "replay"
|
|
9
9
|
require_relative "span_context"
|
|
10
10
|
require_relative "serialize"
|
|
11
|
+
require_relative "timestamp"
|
|
11
12
|
require_relative "warn_once"
|
|
12
13
|
|
|
13
14
|
module Bitfab
|
|
14
15
|
class Client
|
|
15
|
-
SPAN_TYPES =
|
|
16
|
+
SPAN_TYPES = ["llm", "agent", "function", "guardrail", "handoff", "custom"].freeze
|
|
16
17
|
UUID_PATTERN = /\A[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\z/i
|
|
17
18
|
|
|
18
19
|
# Sentinel returned by check_mock_replay when this span should run real
|
|
@@ -29,6 +30,9 @@ module Bitfab
|
|
|
29
30
|
|
|
30
31
|
attr_reader :service_url
|
|
31
32
|
|
|
33
|
+
# Dataset operations for the authenticated organization.
|
|
34
|
+
attr_reader :datasets
|
|
35
|
+
|
|
32
36
|
def initialize(api_key: nil, service_url: nil, enabled: true, strict: false)
|
|
33
37
|
@api_key_config = api_key
|
|
34
38
|
@service_url = service_url || DEFAULT_SERVICE_URL
|
|
@@ -42,6 +46,7 @@ module Bitfab
|
|
|
42
46
|
# The key is NOT read here. HttpClient gets a proc so the key is resolved
|
|
43
47
|
# at send time, after any env loading has run.
|
|
44
48
|
@http_client = HttpClient.new(api_key: -> { resolve_api_key }, service_url: @service_url)
|
|
49
|
+
@datasets = Datasets.new(@http_client)
|
|
45
50
|
# Mock overrides registered via register_mock_override, applied to every
|
|
46
51
|
# replay on this client (after any per-call mock_override). Instance
|
|
47
52
|
# state, no global; clear_mock_overrides resets it.
|
|
@@ -156,10 +161,25 @@ module Bitfab
|
|
|
156
161
|
name: nil, code_change_description: Replay::CODE_CHANGE_UNSET, code_change_files: Replay::CODE_CHANGE_UNSET, experiment_group_id: nil, dataset_id: nil, grader_ids: nil, mock: "marked",
|
|
157
162
|
adapt_inputs: nil, mock_override: nil, db_branch: nil, on_item_start: nil, on_item_finish: nil, on_progress: nil)
|
|
158
163
|
Replay.run(
|
|
159
|
-
self,
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
164
|
+
self,
|
|
165
|
+
receiver,
|
|
166
|
+
method_name,
|
|
167
|
+
trace_function_key:,
|
|
168
|
+
limit:,
|
|
169
|
+
trace_ids:,
|
|
170
|
+
name:,
|
|
171
|
+
max_concurrency:,
|
|
172
|
+
code_change_description:,
|
|
173
|
+
code_change_files:,
|
|
174
|
+
experiment_group_id:,
|
|
175
|
+
dataset_id:,
|
|
176
|
+
grader_ids:,
|
|
177
|
+
mock:,
|
|
178
|
+
adapt_inputs:,
|
|
179
|
+
mock_override:,
|
|
180
|
+
db_branch:,
|
|
181
|
+
on_item_start:,
|
|
182
|
+
on_item_finish:,
|
|
163
183
|
on_progress:
|
|
164
184
|
)
|
|
165
185
|
end
|
|
@@ -202,18 +222,18 @@ module Bitfab
|
|
|
202
222
|
match: ->(node) { node[:trace_function_key] == trace_function_key && keyed_match.call(node) },
|
|
203
223
|
value: keyed_override[:value]
|
|
204
224
|
}
|
|
205
|
-
return
|
|
225
|
+
return
|
|
206
226
|
end
|
|
207
227
|
if keyed_override.respond_to?(:call)
|
|
208
228
|
@mock_overrides << {
|
|
209
229
|
match: ->(node) { node[:trace_function_key] == trace_function_key },
|
|
210
230
|
value: keyed_override
|
|
211
231
|
}
|
|
212
|
-
return
|
|
232
|
+
return
|
|
213
233
|
end
|
|
214
234
|
raise ArgumentError,
|
|
215
235
|
"register_mock_override(trace_function_key, override) requires " \
|
|
216
|
-
|
|
236
|
+
"a { match:, value: } hash or callable resolver."
|
|
217
237
|
elsif positional.length == 1 && positional[0].is_a?(Hash)
|
|
218
238
|
override = positional[0]
|
|
219
239
|
match = override[:match]
|
|
@@ -222,7 +242,7 @@ module Bitfab
|
|
|
222
242
|
value = override.key?(:value) ? override[:value] : VALUE_UNSET
|
|
223
243
|
elsif positional.length == 1 && positional[0].respond_to?(:call)
|
|
224
244
|
@mock_overrides << {match: ->(_) { true }, value: positional[0]}
|
|
225
|
-
return
|
|
245
|
+
return
|
|
226
246
|
elsif positional.length == 2
|
|
227
247
|
match, value = positional
|
|
228
248
|
end
|
|
@@ -231,16 +251,16 @@ module Bitfab
|
|
|
231
251
|
unless match.respond_to?(:call)
|
|
232
252
|
raise ArgumentError,
|
|
233
253
|
"register_mock_override requires a callable match. Pass (match, value) " \
|
|
234
|
-
|
|
235
|
-
|
|
254
|
+
"positionally, as keywords (match:, value:), or as a { match:, value: } hash. " \
|
|
255
|
+
"value may be a flat value or a callable."
|
|
236
256
|
end
|
|
237
257
|
# A forgotten value must not silently inject nil. An explicit nil is a
|
|
238
258
|
# legitimate injected value and passes this guard.
|
|
239
259
|
if value.equal?(VALUE_UNSET)
|
|
240
260
|
raise ArgumentError,
|
|
241
261
|
"register_mock_override requires a value (the second argument, or " \
|
|
242
|
-
|
|
243
|
-
|
|
262
|
+
"value:). It may be a flat value or a callable; pass value: nil " \
|
|
263
|
+
"explicitly to inject nil."
|
|
244
264
|
end
|
|
245
265
|
|
|
246
266
|
@mock_overrides << {match:, value:}
|
|
@@ -280,12 +300,13 @@ module Bitfab
|
|
|
280
300
|
def get_trace_span(trace_id, id: nil, name: nil, occurrence: "last")
|
|
281
301
|
validate_trace_id(trace_id)
|
|
282
302
|
raise ArgumentError, "Provide exactly one of id or name" if id.nil? == name.nil?
|
|
303
|
+
|
|
283
304
|
validate_span_id(id) unless id.nil?
|
|
284
305
|
if !name.nil? && (!name.is_a?(String) || name.empty?)
|
|
285
306
|
raise ArgumentError, "name must be a non-empty string"
|
|
286
307
|
end
|
|
287
308
|
|
|
288
|
-
valid_occurrence =
|
|
309
|
+
valid_occurrence = ["first", "last"].include?(occurrence) || (occurrence.is_a?(Integer) && occurrence >= 0)
|
|
289
310
|
unless valid_occurrence
|
|
290
311
|
raise ArgumentError, 'occurrence must be "first", "last", or a non-negative integer'
|
|
291
312
|
end
|
|
@@ -303,11 +324,11 @@ module Bitfab
|
|
|
303
324
|
return yield unless tracing_enabled?
|
|
304
325
|
|
|
305
326
|
resolved_capture_when = capture_when.to_s
|
|
306
|
-
unless
|
|
327
|
+
unless ["always", "nested"].include?(resolved_capture_when)
|
|
307
328
|
Bitfab.warn_once(
|
|
308
329
|
"invalid-capture-when:#{trace_function_key}",
|
|
309
330
|
"unknown capture_when value #{capture_when.inspect}; defaulting to \"always\". " \
|
|
310
|
-
|
|
331
|
+
"Valid values: \"always\", \"nested\"."
|
|
311
332
|
)
|
|
312
333
|
resolved_capture_when = "always"
|
|
313
334
|
end
|
|
@@ -333,7 +354,7 @@ module Bitfab
|
|
|
333
354
|
span_id = SecureRandom.uuid
|
|
334
355
|
parent_span_id = parent&.dig(:span_id)
|
|
335
356
|
is_root_span = parent_span_id.nil?
|
|
336
|
-
started_at =
|
|
357
|
+
started_at = Bitfab.now_iso_timestamp
|
|
337
358
|
resolved_test_run_id = replay_ctx&.dig(:test_run_id)
|
|
338
359
|
resolved_input_source_span_id = replay_ctx&.dig(:input_source_span_id)
|
|
339
360
|
resolved_input_source_trace_id = replay_ctx&.dig(:input_source_trace_id)
|
|
@@ -356,8 +377,14 @@ module Bitfab
|
|
|
356
377
|
call_index = advance_mock_counter(replay_ctx, trace_function_key, span_name, is_root_span:)
|
|
357
378
|
if call_index
|
|
358
379
|
mock_result = check_mock_replay(
|
|
359
|
-
replay_ctx,
|
|
360
|
-
|
|
380
|
+
replay_ctx,
|
|
381
|
+
trace_function_key,
|
|
382
|
+
span_name,
|
|
383
|
+
call_index,
|
|
384
|
+
span_type:,
|
|
385
|
+
args:,
|
|
386
|
+
kwargs:,
|
|
387
|
+
mock_on_replay:
|
|
361
388
|
)
|
|
362
389
|
if mock_result != MOCK_REPLAY_MISS
|
|
363
390
|
mocked_output, mock_source = mock_result
|
|
@@ -393,7 +420,7 @@ module Bitfab
|
|
|
393
420
|
Bitfab.warn_once(
|
|
394
421
|
"span-setup:#{trace_function_key}",
|
|
395
422
|
"span setup failed for '#{trace_function_key}'; this call runs untraced. " \
|
|
396
|
-
|
|
423
|
+
"Your method still executes and returns normally."
|
|
397
424
|
)
|
|
398
425
|
return yield
|
|
399
426
|
end
|
|
@@ -409,10 +436,11 @@ module Bitfab
|
|
|
409
436
|
# only the first call sends the span. Subsequent calls (e.g. from the
|
|
410
437
|
# enumerator wrapper after iteration completes) are no-ops.
|
|
411
438
|
next if finalized
|
|
439
|
+
|
|
412
440
|
finalized = true
|
|
413
441
|
|
|
414
442
|
begin
|
|
415
|
-
ended_at =
|
|
443
|
+
ended_at = Bitfab.now_iso_timestamp
|
|
416
444
|
|
|
417
445
|
send_span(
|
|
418
446
|
trace_function_key:,
|
|
@@ -550,7 +578,7 @@ module Bitfab
|
|
|
550
578
|
end
|
|
551
579
|
if @explicitly_enabled && !@api_key_warned
|
|
552
580
|
@api_key_warned = true
|
|
553
|
-
warn
|
|
581
|
+
warn("Bitfab: api_key is empty: tracing is disabled. Provide a valid API key to enable tracing.")
|
|
554
582
|
end
|
|
555
583
|
nil
|
|
556
584
|
end
|
|
@@ -689,7 +717,7 @@ module Bitfab
|
|
|
689
717
|
# still rides out with dropped: true, so the server scrubs any sibling
|
|
690
718
|
# spans that already raced out before the flag was set.
|
|
691
719
|
trace_dropped = TraceState.get(trace_id)&.dig(:dropped) == true
|
|
692
|
-
return
|
|
720
|
+
return if trace_dropped
|
|
693
721
|
|
|
694
722
|
# Human-readable JSON (input/output fields). The *_with_report variants
|
|
695
723
|
# also report what could not be faithfully captured so a lossy span is
|
|
@@ -771,8 +799,8 @@ module Bitfab
|
|
|
771
799
|
# mocked) so unmarked spans don't silently shift subsequent marked
|
|
772
800
|
# spans' indices. Different (key, name) pairs have independent counters.
|
|
773
801
|
def advance_mock_counter(replay_ctx, trace_function_key, span_name, is_root_span:)
|
|
774
|
-
return
|
|
775
|
-
return
|
|
802
|
+
return if is_root_span
|
|
803
|
+
return unless replay_ctx&.dig(:mock_tree)
|
|
776
804
|
|
|
777
805
|
counters = replay_ctx[:call_counters]
|
|
778
806
|
counter_key = "#{trace_function_key}:#{span_name}"
|
|
@@ -818,6 +846,7 @@ module Bitfab
|
|
|
818
846
|
# output, wrap it in a callable value.
|
|
819
847
|
unless value.respond_to?(:call)
|
|
820
848
|
return [value, "override"] unless value.equal?(NO_MOCK_OVERRIDE)
|
|
849
|
+
|
|
821
850
|
next
|
|
822
851
|
end
|
|
823
852
|
|
|
@@ -825,6 +854,7 @@ module Bitfab
|
|
|
825
854
|
unless mock_entry
|
|
826
855
|
raise "No recorded span to source output for '#{trace_function_key}'."
|
|
827
856
|
end
|
|
857
|
+
|
|
828
858
|
resolve_recorded_output(mock_entry, replay_ctx)
|
|
829
859
|
end
|
|
830
860
|
resolved = value.call({node:, inputs: args, kwargs:, get_original_output:})
|
|
@@ -846,7 +876,7 @@ module Bitfab
|
|
|
846
876
|
unless mock_entry
|
|
847
877
|
raise StandardError,
|
|
848
878
|
"Replay selected span '#{trace_function_key}:#{span_name}' for mocking, " \
|
|
849
|
-
|
|
879
|
+
"but recorded occurrence #{call_index + 1} is unavailable. The real span was not executed."
|
|
850
880
|
end
|
|
851
881
|
|
|
852
882
|
[resolve_recorded_output(mock_entry, replay_ctx), "recorded"]
|
|
@@ -889,7 +919,7 @@ module Bitfab
|
|
|
889
919
|
def send_mocked_span(trace_function_key:, trace_id:, span_id:, parent_span_id:,
|
|
890
920
|
span_name:, span_type:, function_name:, args:, kwargs:, mocked_output:,
|
|
891
921
|
started_at:, test_run_id:, input_source_span_id:, mock_source:)
|
|
892
|
-
ended_at =
|
|
922
|
+
ended_at = Bitfab.now_iso_timestamp
|
|
893
923
|
send_span(
|
|
894
924
|
trace_function_key:,
|
|
895
925
|
trace_id:,
|
|
@@ -954,9 +984,13 @@ module Bitfab
|
|
|
954
984
|
# mock strategy. A missing selected occurrence fails closed.
|
|
955
985
|
def wrap(klass, method_name, name: nil, type: "custom", capture_when: "always", mock_on_replay: false)
|
|
956
986
|
Bitfab::Traceable.wrap(
|
|
957
|
-
klass,
|
|
987
|
+
klass,
|
|
988
|
+
method_name,
|
|
958
989
|
trace_function_key: @trace_function_key,
|
|
959
|
-
name:,
|
|
990
|
+
name:,
|
|
991
|
+
type:,
|
|
992
|
+
capture_when:,
|
|
993
|
+
mock_on_replay:,
|
|
960
994
|
client: @client
|
|
961
995
|
)
|
|
962
996
|
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Bitfab
|
|
4
|
+
# Dataset operations for the authenticated organization, reached as
|
|
5
|
+
# `client.datasets`. A dataset is a named bucket of traces scoped to one
|
|
6
|
+
# trace function. Experiments replay against it and its graders score its
|
|
7
|
+
# members. Every method returns the parsed JSON response as a Hash with
|
|
8
|
+
# string keys matching the HTTP API ("traceCount", "addedTraceIds", ...).
|
|
9
|
+
class Datasets
|
|
10
|
+
DEFAULT_RERUN_TIMEOUT_SECONDS = 90
|
|
11
|
+
DEFAULT_RERUN_POLL_INTERVAL_SECONDS = 1
|
|
12
|
+
TERMINAL_RERUN_STATUSES = ["completed", "errored"].freeze
|
|
13
|
+
|
|
14
|
+
def initialize(http_client)
|
|
15
|
+
@http_client = http_client
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Create a dataset, or update the one already named this way under the
|
|
19
|
+
# same trace function. The result's "created" reports which happened. A
|
|
20
|
+
# nil description leaves an existing description untouched.
|
|
21
|
+
def save(trace_function_key:, name:, description: nil)
|
|
22
|
+
payload = {"traceFunctionKey" => trace_function_key, "name" => name}
|
|
23
|
+
payload["description"] = description unless description.nil?
|
|
24
|
+
@http_client.request("/api/sdk/datasets", payload)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# List datasets, scoped to one trace function when given and
|
|
28
|
+
# organization-wide otherwise.
|
|
29
|
+
def list(trace_function_key: nil)
|
|
30
|
+
query = trace_function_key.nil? ? "" : "?#{URI.encode_www_form("traceFunctionKey" => trace_function_key)}"
|
|
31
|
+
@http_client.get("/api/sdk/datasets#{query}")["datasets"]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Fetch one dataset by id. A dataset outside this organization raises
|
|
35
|
+
# Net::HTTPError with a 404.
|
|
36
|
+
def get(dataset_id)
|
|
37
|
+
@http_client.get(dataset_path(dataset_id))["dataset"]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# The ids of every trace in the dataset, the same membership a replay with
|
|
41
|
+
# dataset_id: selects.
|
|
42
|
+
def list_traces(dataset_id)
|
|
43
|
+
@http_client.get(dataset_path(dataset_id, "/traces"))
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Add traces to the dataset (1 to 100 ids per call). Traces outside the
|
|
47
|
+
# organization or under another trace function are reported in
|
|
48
|
+
# "skippedTraceIds" rather than failing the call.
|
|
49
|
+
def add_traces(dataset_id, trace_ids)
|
|
50
|
+
@http_client.request(dataset_path(dataset_id, "/traces"), {"traceIds" => trace_ids})
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Remove traces from the dataset. The traces themselves are never deleted.
|
|
54
|
+
def remove_traces(dataset_id, trace_ids)
|
|
55
|
+
@http_client.request(dataset_path(dataset_id, "/removeTraces"), {"traceIds" => trace_ids})
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Assign graders to the dataset (1 to 100 ids per call). Graders outside
|
|
59
|
+
# the organization or under another trace function are reported in
|
|
60
|
+
# "skippedGraderIds" rather than failing the call.
|
|
61
|
+
def add_graders(dataset_id, grader_ids)
|
|
62
|
+
@http_client.request(dataset_path(dataset_id, "/graders"), {"graderIds" => grader_ids})
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Unassign graders from the dataset.
|
|
66
|
+
def remove_graders(dataset_id, grader_ids)
|
|
67
|
+
@http_client.request(dataset_path(dataset_id, "/removeGraders"), {"graderIds" => grader_ids})
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Re-run graders over every trace in the dataset. Defaults to every
|
|
71
|
+
# assigned grader, and an unassigned id is rejected. Waits for the run to
|
|
72
|
+
# finish (up to `timeout` seconds) unless `wait:` is false, and returns the
|
|
73
|
+
# last run state seen either way. A request matching an in-flight run
|
|
74
|
+
# joins it.
|
|
75
|
+
def rerun_graders(dataset_id, grader_ids: nil, wait: true,
|
|
76
|
+
timeout: DEFAULT_RERUN_TIMEOUT_SECONDS, poll_interval: DEFAULT_RERUN_POLL_INTERVAL_SECONDS)
|
|
77
|
+
payload = grader_ids.nil? ? {} : {"graderIds" => grader_ids}
|
|
78
|
+
started = @http_client.request(dataset_path(dataset_id, "/rerunGraders"), payload)
|
|
79
|
+
return started unless wait
|
|
80
|
+
|
|
81
|
+
deadline = monotonic_now + timeout
|
|
82
|
+
run = started["run"]
|
|
83
|
+
while !TERMINAL_RERUN_STATUSES.include?(run["status"]) && monotonic_now < deadline
|
|
84
|
+
sleep(poll_interval)
|
|
85
|
+
run = get_grader_rerun(dataset_id, run_id: run["id"]) || run
|
|
86
|
+
end
|
|
87
|
+
{"run" => run, "joinedExisting" => started["joinedExisting"]}
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# The dataset's active grader re-run, or the run named by run_id:. Returns
|
|
91
|
+
# nil when nothing is active or the run is not this dataset's.
|
|
92
|
+
def get_grader_rerun(dataset_id, run_id: nil)
|
|
93
|
+
query = run_id.nil? ? "" : "?#{URI.encode_www_form("runId" => run_id)}"
|
|
94
|
+
@http_client.get(dataset_path(dataset_id, "/rerunGraders#{query}"))["run"]
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
private
|
|
98
|
+
|
|
99
|
+
def dataset_path(dataset_id, suffix = "")
|
|
100
|
+
"/api/sdk/datasets/#{URI.encode_www_form_component(dataset_id)}#{suffix}"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def monotonic_now
|
|
104
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
data/lib/bitfab/span_context.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "timestamp"
|
|
4
|
+
|
|
3
5
|
module Bitfab
|
|
4
6
|
# Handle to the current active span, allowing context to be added.
|
|
5
7
|
class CurrentSpan
|
|
@@ -171,7 +173,7 @@ module Bitfab
|
|
|
171
173
|
def create(trace_id, test_run_id: nil, input_source_trace_id: nil)
|
|
172
174
|
@states_mutex.synchronize do
|
|
173
175
|
@states[trace_id] ||= begin
|
|
174
|
-
started_at =
|
|
176
|
+
started_at = Bitfab.now_iso_timestamp
|
|
175
177
|
{
|
|
176
178
|
trace_id:,
|
|
177
179
|
started_at:,
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
|
|
5
|
+
module Bitfab
|
|
6
|
+
@timestamp_mutex = Mutex.new
|
|
7
|
+
@last_timestamp_us = 0
|
|
8
|
+
|
|
9
|
+
def self.now_iso_timestamp
|
|
10
|
+
wall_clock_us = (Time.now.to_r * 1_000_000).to_i
|
|
11
|
+
timestamp_us = @timestamp_mutex.synchronize do
|
|
12
|
+
@last_timestamp_us = [wall_clock_us, @last_timestamp_us + 1].max
|
|
13
|
+
end
|
|
14
|
+
Time.at(Rational(timestamp_us, 1_000_000)).utc.strftime("%Y-%m-%dT%H:%M:%S.%6NZ")
|
|
15
|
+
end
|
|
16
|
+
end
|
data/lib/bitfab/version.rb
CHANGED
data/lib/bitfab.rb
CHANGED
|
@@ -10,6 +10,7 @@ require_relative "bitfab/serialize"
|
|
|
10
10
|
require_relative "bitfab/db_snapshot"
|
|
11
11
|
require_relative "bitfab/span_context"
|
|
12
12
|
require_relative "bitfab/http_client"
|
|
13
|
+
require_relative "bitfab/datasets"
|
|
13
14
|
require_relative "bitfab/mock_override"
|
|
14
15
|
require_relative "bitfab/replay"
|
|
15
16
|
require_relative "bitfab/replay_branch"
|
|
@@ -119,16 +120,16 @@ module Bitfab
|
|
|
119
120
|
# whose source trace carried no DB snapshot reference
|
|
120
121
|
def current_replay_branch
|
|
121
122
|
ctx = ReplayContext.current
|
|
122
|
-
return
|
|
123
|
+
return unless ctx
|
|
123
124
|
|
|
124
125
|
lease = ctx[:db_branch_lease]
|
|
125
|
-
return
|
|
126
|
+
return unless lease
|
|
126
127
|
|
|
127
128
|
# Surface the Bitfab trace ID (what the customer sees in the dashboard),
|
|
128
129
|
# not the external one. Falling back to the external ID keeps replays from
|
|
129
130
|
# external sources working until that path is fully wired.
|
|
130
131
|
trace_id = ctx[:source_bitfab_trace_id] || ctx[:input_source_trace_id]
|
|
131
|
-
return
|
|
132
|
+
return unless trace_id
|
|
132
133
|
|
|
133
134
|
ReplayBranch.new(lease, trace_id, ctx)
|
|
134
135
|
end
|
|
@@ -159,7 +160,7 @@ module Bitfab
|
|
|
159
160
|
# succeeded, errored, and item (the object replay already passes to
|
|
160
161
|
# on_item_finish)
|
|
161
162
|
def report_replay_progress(progress)
|
|
162
|
-
warn
|
|
163
|
+
warn("#{BITFAB_PROGRESS_PREFIX}#{Replay.json_safe(progress).to_json}")
|
|
163
164
|
rescue
|
|
164
165
|
nil
|
|
165
166
|
end
|
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.
|
|
4
|
+
version: 0.40.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Harvest Team
|
|
@@ -142,6 +142,7 @@ files:
|
|
|
142
142
|
- lib/bitfab/client.rb
|
|
143
143
|
- lib/bitfab/compress.rb
|
|
144
144
|
- lib/bitfab/constants.rb
|
|
145
|
+
- lib/bitfab/datasets.rb
|
|
145
146
|
- lib/bitfab/db_snapshot.rb
|
|
146
147
|
- lib/bitfab/http_client.rb
|
|
147
148
|
- lib/bitfab/mock_override.rb
|
|
@@ -153,6 +154,7 @@ files:
|
|
|
153
154
|
- lib/bitfab/replay_registry.rb
|
|
154
155
|
- lib/bitfab/serialize.rb
|
|
155
156
|
- lib/bitfab/span_context.rb
|
|
157
|
+
- lib/bitfab/timestamp.rb
|
|
156
158
|
- lib/bitfab/traceable.rb
|
|
157
159
|
- lib/bitfab/transport.rb
|
|
158
160
|
- lib/bitfab/version.rb
|