bitfab 0.51.10 → 0.57.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/README.md +2 -2
- data/lib/bitfab/client.rb +67 -24
- data/lib/bitfab/http_client.rb +20 -2
- data/lib/bitfab/otel.rb +1 -1
- data/lib/bitfab/simulation_plan.rb +115 -56
- data/lib/bitfab/subtree.rb +72 -23
- data/lib/bitfab/traceable.rb +18 -3
- data/lib/bitfab/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: 1cd6fe2aaa2d03dea82e1d6a14b63e5a60eec3169c33489ce54795840ab6db8c
|
|
4
|
+
data.tar.gz: 1637726e4aa1cfa7940ecb65e1b10684b0d1236885a3c1ddd0d3748c8c7a874f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 391ca0dbc155bf7826f8ff4245d305d6d426cd8cd330a2c92564de8eac72ee229d928faad6d74f72f3d7163cf8a950fab1c6dfc96c5ddefdbd215ba0167382a1
|
|
7
|
+
data.tar.gz: c8a4c5172ecb6f78b309303062167073384bbdd7079c5fc5618200b877526a44bd495f85b2e6120b9a602542f883b80d2872ba2519f81109d3e17d83977b301f
|
data/README.md
CHANGED
|
@@ -235,7 +235,7 @@ class OrderPipeline
|
|
|
235
235
|
end
|
|
236
236
|
```
|
|
237
237
|
|
|
238
|
-
Capture uses `TracePoint` only during the root call. It skips Ruby/runtime code, dependency gems, Bitfab SDK internals, blocks, lambdas, and forwarding wrappers. When the root belongs to an installed application gem, that gem is the first-party boundary; neighboring dependency gems remain excluded. It follows recursion, modules, inheritance, `define_method`, and same-thread child Fibers; aliases are recorded and excluded by the name that was invoked. Child Fibers created during capture inherit node policies, trace identity, parenting, and depth limits while retaining separate call stacks. Pre-existing Fibers, `Fiber.new(storage: nil)`, and child threads do not inherit subtree context. When an inner capture ends, child Fibers retain any still-active outer capture; inherited context expires once all enclosing captures end. While a root remains active, hitting
|
|
238
|
+
Capture uses `TracePoint` only during the root call. It skips Ruby/runtime code, dependency gems, Bitfab SDK internals, blocks, lambdas, and forwarding wrappers. When the root belongs to an installed application gem, that gem is the first-party boundary; neighboring dependency gems remain excluded. It follows recursion, modules, inheritance, `define_method`, and same-thread child Fibers; aliases are recorded and excluded by the name that was invoked. Child Fibers created during capture inherit node policies, trace identity, parenting, and depth limits while retaining separate call stacks. Pre-existing Fibers, `Fiber.new(storage: nil)`, and child threads do not inherit subtree context. When an inner capture ends, child Fibers retain any still-active outer capture; inherited context expires once all enclosing captures end. While a root remains active, hitting a span limit stops new capture but allows already-open spans in every Fiber to finish. `max_depth:` (default `30`), `max_captured_subtree_spans:` (default `512`), `max_spans:` (default `2048`), `exclude:`, and `include_wrappers:` control the subtree. `max_spans:` is a strict cap on the total number of spans one trace root sends. It counts the root span itself, discovered calls, `bitfab_node` calls, nested trace roots, and spans whose capture is on or off, including spans sent without inputs and outputs because the sim plan could not be read. Nothing past it is sent. It must be at least 1, and a lower value raises `ArgumentError`. `max_captured_subtree_spans:` caps discovered calls sent with their inputs and outputs. It does not count `bitfab_node` calls, nested trace roots, or spans the loaded sim plan turned capture off for, so those keep recording past it up to `max_spans:`. Spans sent without inputs and outputs only because the sim plan could not be read still count toward it. Past it, further discovered calls with capture on record nothing. It is never higher than `max_spans:`. Once the plan has loaded, the SDK never builds the inputs or outputs of a span whose capture it turned off. The client starts reading the sim plan when it is built. A `bitfab_trace` root that starts while that first read is still running waits for it, for at most five seconds. Once the first read has finished, whether it succeeded or failed, no later root waits. Nested roots and the calls beneath a root never wait, and nothing waits when the plan is disabled. If that first read fails, spans other than each trace's root span are sent without inputs and outputs until a read succeeds, and they still count toward both limits. Automatically captured calls are not replay mock targets. Declare a call with `bitfab_node` when replay must substitute it. The configured node records once in each enclosing trace. Mixing explicit spans with subtree tracing raises `Bitfab::MixedTracingError`.
|
|
239
239
|
|
|
240
240
|
When capture ends, already-open automatic calls in suspended Fibers are emitted once with a nil output and `Subtree capture ended before the call returned` as the error, unless a real error was already observed. Their inputs and parenting are retained. The SDK does not resume Fibers, and later resumption cannot add spans to the ended capture. If hot reload moves the root to an unresolved source, the last valid first-party boundary and its exclusions remain in use.
|
|
241
241
|
|
|
@@ -258,7 +258,7 @@ end
|
|
|
258
258
|
|
|
259
259
|
Nodes support `name:`, `type:`, `capture:`, `test_run_id:`, `mock_on_replay:`, and `finalize:`. `capture: false` omits the method while attaching captured descendants to its nearest visible parent. `finalize:` changes only the recorded output, never the return value. A finalizer failure is recorded on the node without crashing the caller. The before-`def`, inline, and after-`def` forms all preserve method visibility. For external code, use `Bitfab.client.node(MyClass, :method)`.
|
|
260
260
|
|
|
261
|
-
`bitfab_trace` prepares its project boundary and exclusions when the method is declared, refreshes the boundary if the root definition moves during hot reload, caches canonical source paths, and shares one `TracePoint` dispatcher across active roots on the calling thread. After
|
|
261
|
+
`bitfab_trace` prepares its project boundary and exclusions when the method is declared, refreshes the boundary if the root definition moves during hot reload, caches canonical source paths, and shares one `TracePoint` dispatcher across active roots on the calling thread. After a span limit is reached, it stops reading inputs and outputs immediately, then stops processing events for that session once its captured calls finish. Each active root still captures and serializes its own copy of every overlapping call, so this remains a discovery tool rather than the production default. Prefer explicit `bitfab_span` declarations on hot paths. Run `mise exec -- ruby -Ilib benchmark/subtree.rb` from this package to measure the no-network caller path on your Ruby build.
|
|
262
262
|
|
|
263
263
|
### Input/Output Capture
|
|
264
264
|
|
data/lib/bitfab/client.rb
CHANGED
|
@@ -379,7 +379,8 @@ module Bitfab
|
|
|
379
379
|
def execute_span(trace_function_key:, span_name:, span_type:, function_name:, args:, kwargs:,
|
|
380
380
|
capture_when: "always", mock_on_replay: false, independent_root: false,
|
|
381
381
|
explicit_span_receiver: nil, explicit_span_method_name: nil, test_run_id: nil, finalize: nil,
|
|
382
|
-
surface: nil, on_start: nil, on_complete: nil, around_body: nil, span_data: {}, managed_root: false
|
|
382
|
+
surface: nil, on_start: nil, on_complete: nil, around_body: nil, span_data: {}, managed_root: false,
|
|
383
|
+
result_recorded_elsewhere: false)
|
|
383
384
|
# Decide at CALL time, not construction. The key may be set after the
|
|
384
385
|
# client is built (env loaded later), so re-checking per call lets a
|
|
385
386
|
# late-resolved key take effect.
|
|
@@ -525,7 +526,12 @@ module Bitfab
|
|
|
525
526
|
ended_at = Bitfab.now_iso_timestamp
|
|
526
527
|
recorded_result = final_result
|
|
527
528
|
recorded_error = final_error
|
|
528
|
-
|
|
529
|
+
recorded_name = recording_override&.dig(:name) || span_name
|
|
530
|
+
recorded_instrumentation = recording_override&.dig(:instrumentation) || ((surface == :trace) ? "trace" : "span")
|
|
531
|
+
content_off = content_off_by_simulation_plan?(
|
|
532
|
+
trace_id:, trace_function_key:, span_name: recorded_name, instrumentation: recorded_instrumentation
|
|
533
|
+
) || (!parent_span_id.nil? && simulation_plan_unavailable?(instrumentation: recorded_instrumentation))
|
|
534
|
+
if finalize && final_error.nil? && !(content_off && !result_recorded_elsewhere)
|
|
529
535
|
begin
|
|
530
536
|
recorded_result = finalize.call(final_result)
|
|
531
537
|
rescue => e
|
|
@@ -537,13 +543,13 @@ module Bitfab
|
|
|
537
543
|
on_complete&.call(result: recorded_result, error: recorded_error, ended_at:)
|
|
538
544
|
|
|
539
545
|
send_span(
|
|
540
|
-
span_data
|
|
541
|
-
instrumentation:
|
|
546
|
+
span_data: content_off ? span_data.merge("content_off_by_simulation_plan" => true) : span_data,
|
|
547
|
+
instrumentation: recorded_instrumentation,
|
|
542
548
|
trace_function_key:,
|
|
543
549
|
trace_id:,
|
|
544
550
|
span_id:,
|
|
545
551
|
parent_span_id:,
|
|
546
|
-
span_name:
|
|
552
|
+
span_name: recorded_name,
|
|
547
553
|
span_type: recording_override&.dig(:type) || span_type,
|
|
548
554
|
function_name: recording_override&.dig(:function_name) || function_name,
|
|
549
555
|
contexts: span_contexts,
|
|
@@ -672,6 +678,32 @@ module Bitfab
|
|
|
672
678
|
raise ArgumentError, "id must be a valid Bitfab span ID" unless id.is_a?(String) && UUID_PATTERN.match?(id)
|
|
673
679
|
end
|
|
674
680
|
|
|
681
|
+
def content_off_by_simulation_plan?(trace_id:, trace_function_key:, span_name:, instrumentation: "span")
|
|
682
|
+
return false if SimulationPlan::FRAMEWORKS.include?(instrumentation)
|
|
683
|
+
|
|
684
|
+
@http_client.simulation_plan_content_off?(root_trace_function_key(trace_id, trace_function_key), span_name)
|
|
685
|
+
end
|
|
686
|
+
|
|
687
|
+
def simulation_plan_unavailable?(instrumentation: "span")
|
|
688
|
+
return false if SimulationPlan::FRAMEWORKS.include?(instrumentation)
|
|
689
|
+
|
|
690
|
+
@http_client.simulation_plan_unavailable?
|
|
691
|
+
end
|
|
692
|
+
|
|
693
|
+
def simulation_plan_content_off_names?(trace_id:, trace_function_key:)
|
|
694
|
+
@http_client.simulation_plan_content_off_names?(root_trace_function_key(trace_id, trace_function_key))
|
|
695
|
+
end
|
|
696
|
+
|
|
697
|
+
def root_trace_function_key(trace_id, trace_function_key)
|
|
698
|
+
TraceState.get(trace_id)&.dig(:trace_function_key) || trace_function_key
|
|
699
|
+
end
|
|
700
|
+
|
|
701
|
+
def wait_for_simulation_plan
|
|
702
|
+
@http_client.wait_for_simulation_plan
|
|
703
|
+
rescue
|
|
704
|
+
nil
|
|
705
|
+
end
|
|
706
|
+
|
|
675
707
|
# Resolve the API key lazily, the first time a span actually needs it.
|
|
676
708
|
#
|
|
677
709
|
# The key is intentionally NOT read at construction: a client built when a
|
|
@@ -890,25 +922,34 @@ module Bitfab
|
|
|
890
922
|
# Human-readable JSON (input/output fields). The *_with_report variants
|
|
891
923
|
# also report what could not be faithfully captured so a lossy span is
|
|
892
924
|
# marked non-replayable below instead of shipping as if it round-trips.
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
925
|
+
content_off = extra_span_data["content_off_by_simulation_plan"] == true ||
|
|
926
|
+
content_off_by_simulation_plan?(trace_id:, trace_function_key:, span_name:, instrumentation:) ||
|
|
927
|
+
(!parent_span_id.nil? && simulation_plan_unavailable?(instrumentation:))
|
|
928
|
+
if content_off
|
|
929
|
+
dropped = []
|
|
930
|
+
span_data = {"name" => span_name, "type" => span_type, "function_name" => function_name}
|
|
931
|
+
span_data.merge!(extra_span_data.except(*SimulationPlan::CONTENT_KEYS))
|
|
932
|
+
span_data["content_off_by_simulation_plan"] = true
|
|
933
|
+
else
|
|
934
|
+
human_inputs, input_dropped = Serialize.serialize_inputs_with_report(args, kwargs)
|
|
935
|
+
human_output, output_dropped = Serialize.serialize_value_with_report(result)
|
|
936
|
+
dropped = input_dropped + output_dropped
|
|
937
|
+
|
|
938
|
+
raw_input = (args.length == 1 && kwargs.empty?) ? args[0] : {args:, kwargs:}
|
|
939
|
+
marshalled_input = Serialize.marshal_value(raw_input)
|
|
940
|
+
marshalled_output = Serialize.marshal_value(result)
|
|
941
|
+
|
|
942
|
+
span_data = {
|
|
943
|
+
"name" => span_name,
|
|
944
|
+
"type" => span_type,
|
|
945
|
+
"input" => human_inputs,
|
|
946
|
+
"output" => human_output,
|
|
947
|
+
"function_name" => function_name
|
|
948
|
+
}
|
|
949
|
+
span_data.merge!(extra_span_data)
|
|
950
|
+
span_data["input_serialized"] = marshalled_input if marshalled_input
|
|
951
|
+
span_data["output_serialized"] = marshalled_output if marshalled_output
|
|
952
|
+
end
|
|
912
953
|
if error
|
|
913
954
|
span_data["error"] = error
|
|
914
955
|
span_data["error_source"] = "code"
|
|
@@ -1172,6 +1213,7 @@ module Bitfab
|
|
|
1172
1213
|
# Experimentally trace an existing method and its first-party call subtree.
|
|
1173
1214
|
def trace(klass, method_name, name: nil, type: "custom",
|
|
1174
1215
|
max_depth: Subtree::DEFAULT_MAX_DEPTH, max_spans: Subtree::DEFAULT_MAX_SPANS,
|
|
1216
|
+
max_captured_subtree_spans: Subtree::DEFAULT_MAX_CAPTURED_SUBTREE_SPANS,
|
|
1175
1217
|
exclude: [], include_wrappers: false, mock_on_replay_default: false)
|
|
1176
1218
|
Bitfab::Traceable.trace(
|
|
1177
1219
|
klass, method_name,
|
|
@@ -1180,6 +1222,7 @@ module Bitfab
|
|
|
1180
1222
|
type:,
|
|
1181
1223
|
max_depth:,
|
|
1182
1224
|
max_spans:,
|
|
1225
|
+
max_captured_subtree_spans:,
|
|
1183
1226
|
exclude:,
|
|
1184
1227
|
include_wrappers:,
|
|
1185
1228
|
mock_on_replay_default:,
|
data/lib/bitfab/http_client.rb
CHANGED
|
@@ -55,12 +55,30 @@ module Bitfab
|
|
|
55
55
|
|
|
56
56
|
def configure_simulation_plan(enabled: true)
|
|
57
57
|
@simulation_plan = SimulationPlan.new(self, enabled:)
|
|
58
|
+
@simulation_plan.refresh
|
|
59
|
+
@simulation_plan
|
|
58
60
|
end
|
|
59
61
|
|
|
60
62
|
def refresh_simulation_plan
|
|
61
63
|
@simulation_plan&.refresh
|
|
62
64
|
end
|
|
63
65
|
|
|
66
|
+
def wait_for_simulation_plan
|
|
67
|
+
@simulation_plan&.wait_for_first_read
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def simulation_plan_content_off?(trace_function_key, span_name)
|
|
71
|
+
@simulation_plan&.content_off?(trace_function_key, span_name) == true
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def simulation_plan_content_off_names?(trace_function_key)
|
|
75
|
+
@simulation_plan&.content_off_names?(trace_function_key) == true
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def simulation_plan_unavailable?
|
|
79
|
+
@simulation_plan&.unavailable? == true
|
|
80
|
+
end
|
|
81
|
+
|
|
64
82
|
def get_simulation_plan
|
|
65
83
|
get("/api/sdk/sim-plan", timeout: SimulationPlan::READ_TIMEOUT, connection_close: true)
|
|
66
84
|
end
|
|
@@ -82,8 +100,8 @@ module Bitfab
|
|
|
82
100
|
result = false
|
|
83
101
|
begin
|
|
84
102
|
remaining = [deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC), 0].max
|
|
85
|
-
|
|
86
|
-
@simulation_plan
|
|
103
|
+
@simulation_plan&.release([remaining / 2.0, SimulationPlan::READ_TIMEOUT].min)
|
|
104
|
+
released = @simulation_plan.nil? || @simulation_plan.stop
|
|
87
105
|
remaining = [deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC), 0].max
|
|
88
106
|
transport = @transport_mutex.synchronize do
|
|
89
107
|
current = @transport
|
data/lib/bitfab/otel.rb
CHANGED
|
@@ -26,7 +26,7 @@ module Bitfab
|
|
|
26
26
|
MAX_DECOMPRESSED_REQUEST_BYTES = 8_000_000
|
|
27
27
|
MAX_QUEUE_SIZE = 8_192
|
|
28
28
|
DIRECT_MAX_EXPORT_BATCH_SIZE = 512
|
|
29
|
-
DIRECT_MAX_REQUEST_BATCH_SIZE =
|
|
29
|
+
DIRECT_MAX_REQUEST_BATCH_SIZE = 128
|
|
30
30
|
DEFAULT_EXPORT_CONCURRENCY = 32
|
|
31
31
|
MAX_EXPORT_CONCURRENCY = 64
|
|
32
32
|
SCHEDULE_DELAY_MILLIS = 5_000
|
|
@@ -25,8 +25,8 @@ module Bitfab
|
|
|
25
25
|
released = true
|
|
26
26
|
plans.each do |plan|
|
|
27
27
|
result = plan.release([deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC), 0].max)
|
|
28
|
+
result = plan.stop if stop
|
|
28
29
|
released = result && released
|
|
29
|
-
plan.stop if stop
|
|
30
30
|
end
|
|
31
31
|
released
|
|
32
32
|
end
|
|
@@ -53,6 +53,7 @@ module Bitfab
|
|
|
53
53
|
@held = []
|
|
54
54
|
@draining = Set.new
|
|
55
55
|
@stopped = false
|
|
56
|
+
@first_read_finished = false
|
|
56
57
|
self.class.register(self)
|
|
57
58
|
end
|
|
58
59
|
|
|
@@ -71,29 +72,27 @@ module Bitfab
|
|
|
71
72
|
refresh
|
|
72
73
|
data = payload.dig("rawSpan", "span_data")
|
|
73
74
|
key = payload["rootTraceFunctionKey"] || payload["traceFunctionKey"]
|
|
74
|
-
if disabled? ||
|
|
75
|
+
if disabled? || key.nil? || !data.is_a?(Hash) || framework?(payload)
|
|
75
76
|
submit.call(payload)
|
|
76
77
|
return
|
|
77
78
|
end
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
true
|
|
82
|
-
end
|
|
83
|
-
end
|
|
84
|
-
submit.call(apply(payload, key)) unless held
|
|
79
|
+
entry = [payload, key, submit]
|
|
80
|
+
ready = @mutex.synchronize { reading? ? hold(entry) : entry }
|
|
81
|
+
deliver(*ready) if ready
|
|
85
82
|
end
|
|
86
83
|
|
|
87
84
|
def send_trace(payload, &submit)
|
|
88
85
|
refresh
|
|
89
|
-
|
|
86
|
+
entry = [payload, nil, submit]
|
|
87
|
+
ready = @mutex.synchronize do
|
|
90
88
|
id = trace_id(payload)
|
|
91
|
-
if !disabled? && !@stopped && id && (@draining.include?(id) || @held.any? { |
|
|
92
|
-
hold(
|
|
93
|
-
|
|
89
|
+
if !disabled? && !@stopped && id && (@draining.include?(id) || @held.any? { |held| held[1] && trace_id(held[0]) == id })
|
|
90
|
+
hold(entry)
|
|
91
|
+
else
|
|
92
|
+
entry
|
|
94
93
|
end
|
|
95
94
|
end
|
|
96
|
-
|
|
95
|
+
deliver(*ready) if ready
|
|
97
96
|
end
|
|
98
97
|
|
|
99
98
|
def release(timeout)
|
|
@@ -114,10 +113,48 @@ module Bitfab
|
|
|
114
113
|
|
|
115
114
|
def stop
|
|
116
115
|
reset_after_fork
|
|
117
|
-
@mutex.synchronize do
|
|
116
|
+
queued = @mutex.synchronize do
|
|
118
117
|
@stopped = true
|
|
119
|
-
@held.clear
|
|
120
118
|
@condition.broadcast
|
|
119
|
+
entries, @held = @held, []
|
|
120
|
+
entries
|
|
121
|
+
end
|
|
122
|
+
queued.each { |entry| deliver_safely(*entry) }
|
|
123
|
+
@mutex.synchronize { @held.empty? && @draining.empty? }
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def content_off?(key, name)
|
|
127
|
+
reset_after_fork
|
|
128
|
+
return false if disabled? || @stopped
|
|
129
|
+
|
|
130
|
+
@mutex.synchronize { @content_off&.fetch(key, nil)&.include?(name) == true }
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def content_off_names?(key)
|
|
134
|
+
reset_after_fork
|
|
135
|
+
return false if disabled? || @stopped
|
|
136
|
+
|
|
137
|
+
@mutex.synchronize { @content_off&.fetch(key, nil)&.any? == true }
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def unavailable?
|
|
141
|
+
reset_after_fork
|
|
142
|
+
return false if disabled?
|
|
143
|
+
|
|
144
|
+
@mutex.synchronize { @content_off.nil? && !reading? }
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def wait_for_first_read(timeout = READ_TIMEOUT)
|
|
148
|
+
reset_after_fork
|
|
149
|
+
return if disabled?
|
|
150
|
+
|
|
151
|
+
deadline = now + [timeout, 0].max
|
|
152
|
+
@mutex.synchronize do
|
|
153
|
+
while reading? && @thread
|
|
154
|
+
remaining = deadline - now
|
|
155
|
+
break unless remaining.positive?
|
|
156
|
+
@condition.wait(@mutex, remaining)
|
|
157
|
+
end
|
|
121
158
|
end
|
|
122
159
|
end
|
|
123
160
|
|
|
@@ -128,9 +165,7 @@ module Bitfab
|
|
|
128
165
|
data = payload.dig("rawSpan", "span_data")
|
|
129
166
|
return payload unless data.is_a?(Hash) && names&.include?(data["name"])
|
|
130
167
|
|
|
131
|
-
|
|
132
|
-
kept["content_off_by_simulation_plan"] = true
|
|
133
|
-
payload.merge("rawSpan" => payload["rawSpan"].merge("span_data" => kept))
|
|
168
|
+
without_content(payload, data)
|
|
134
169
|
end
|
|
135
170
|
|
|
136
171
|
private
|
|
@@ -139,6 +174,10 @@ module Bitfab
|
|
|
139
174
|
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
140
175
|
end
|
|
141
176
|
|
|
177
|
+
def reading?
|
|
178
|
+
@content_off.nil? && !@first_read_finished && !@stopped
|
|
179
|
+
end
|
|
180
|
+
|
|
142
181
|
def trace_id(payload)
|
|
143
182
|
payload["traceId"] || payload["sourceTraceId"] || payload.dig("externalTrace", "id") || payload["id"]
|
|
144
183
|
end
|
|
@@ -147,19 +186,53 @@ module Bitfab
|
|
|
147
186
|
FRAMEWORKS.include?(payload.dig("rawSpan", "span_origin", "instrumentation", "name"))
|
|
148
187
|
end
|
|
149
188
|
|
|
189
|
+
def root?(payload)
|
|
190
|
+
payload.dig("rawSpan", "parent_id").nil?
|
|
191
|
+
end
|
|
192
|
+
|
|
150
193
|
def hold(entry)
|
|
151
194
|
@held << entry
|
|
152
|
-
if @held.length > MAX_HELD
|
|
153
|
-
@held.shift
|
|
154
|
-
Bitfab.warn_once("sim-plan-held-overflow", "Simulation plan has not loaded; dropping oldest records beyond #{MAX_HELD}")
|
|
155
|
-
end
|
|
156
195
|
start_reader([@refresh_after - now, 0].max) if @thread.nil? && !@stopped && !disabled?
|
|
196
|
+
@held.shift if @held.length > MAX_HELD
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def deliver(payload, key, submit)
|
|
200
|
+
submit.call(prepare(payload, key))
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def deliver_safely(payload, key, submit)
|
|
204
|
+
deliver(payload, key, submit)
|
|
205
|
+
rescue => error
|
|
206
|
+
Bitfab.warn_once("sim-plan-held-span-dropped", "Held record could not be sent: #{error}")
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def prepare(payload, key)
|
|
210
|
+
return payload if key.nil? || disabled?
|
|
211
|
+
return apply(payload, key) if @mutex.synchronize { @content_off }
|
|
212
|
+
|
|
213
|
+
without_unread_content(payload)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def without_unread_content(payload)
|
|
217
|
+
data = payload.dig("rawSpan", "span_data")
|
|
218
|
+
return payload if !data.is_a?(Hash) || root?(payload) || framework?(payload)
|
|
219
|
+
|
|
220
|
+
Bitfab.warn_once("sim-plan-unread-content",
|
|
221
|
+
"Sending spans without inputs and outputs because the simulation plan could not be read")
|
|
222
|
+
without_content(payload, data)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def without_content(payload, data)
|
|
226
|
+
kept = data.except(*CONTENT_KEYS)
|
|
227
|
+
kept["content_off_by_simulation_plan"] = true
|
|
228
|
+
payload.merge("rawSpan" => payload["rawSpan"].merge("span_data" => kept))
|
|
157
229
|
end
|
|
158
230
|
|
|
159
231
|
def start_reader(delay)
|
|
160
232
|
@thread = Thread.new { read_until_loaded(delay) }
|
|
161
233
|
@thread.name = "bitfab-sim-plan"
|
|
162
234
|
rescue ThreadError
|
|
235
|
+
@first_read_finished = true
|
|
163
236
|
@refresh_after = now + RETRY_SECONDS
|
|
164
237
|
end
|
|
165
238
|
|
|
@@ -175,7 +248,7 @@ module Bitfab
|
|
|
175
248
|
def read_once
|
|
176
249
|
body = @reader.get_simulation_plan
|
|
177
250
|
unless body.is_a?(Hash) && body["nodes"].is_a?(Array)
|
|
178
|
-
Bitfab.warn_once("sim-plan-unreadable", "Simulation plan response was not understood;
|
|
251
|
+
Bitfab.warn_once("sim-plan-unreadable", "Simulation plan response was not understood; sending spans without inputs and outputs until it loads")
|
|
179
252
|
return
|
|
180
253
|
end
|
|
181
254
|
names = {}
|
|
@@ -187,49 +260,35 @@ module Bitfab
|
|
|
187
260
|
rescue => error
|
|
188
261
|
return {} if error.respond_to?(:response) && error.response&.code.to_i == 404
|
|
189
262
|
|
|
190
|
-
Bitfab.warn_once("sim-plan-unavailable", "Could not read simulation plan: #{error};
|
|
263
|
+
Bitfab.warn_once("sim-plan-unavailable", "Could not read simulation plan: #{error}; sending spans without inputs and outputs until it loads")
|
|
191
264
|
nil
|
|
192
265
|
end
|
|
193
266
|
|
|
194
267
|
def read_until_loaded(delay)
|
|
195
268
|
pause(delay) if delay.positive?
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
if finished
|
|
213
|
-
until entries.empty?
|
|
214
|
-
entries.each do |payload, key, submit|
|
|
215
|
-
submit.call(key ? apply(payload, key) : payload)
|
|
216
|
-
rescue => error
|
|
217
|
-
Bitfab.warn_once("sim-plan-held-span-dropped", "Held record could not be sent: #{error}")
|
|
218
|
-
end
|
|
219
|
-
entries = @mutex.synchronize do
|
|
220
|
-
queued, @held = @held, []
|
|
221
|
-
@draining.clear if queued.empty?
|
|
222
|
-
queued
|
|
223
|
-
end
|
|
224
|
-
end
|
|
225
|
-
return
|
|
269
|
+
parsed = (disabled? || @stopped) ? nil : read_once
|
|
270
|
+
entries = @mutex.synchronize do
|
|
271
|
+
@first_read_finished = true
|
|
272
|
+
@content_off = parsed if parsed
|
|
273
|
+
@refresh_after = now + (parsed ? REFRESH_SECONDS : RETRY_SECONDS)
|
|
274
|
+
queued, @held = @held, []
|
|
275
|
+
@draining = queued.filter_map { |payload, key, _| trace_id(payload) if key }.to_set
|
|
276
|
+
@condition.broadcast
|
|
277
|
+
queued
|
|
278
|
+
end
|
|
279
|
+
until entries.empty?
|
|
280
|
+
entries.each { |entry| deliver_safely(*entry) }
|
|
281
|
+
entries = @mutex.synchronize do
|
|
282
|
+
queued, @held = @held, []
|
|
283
|
+
@draining.clear if queued.empty?
|
|
284
|
+
queued
|
|
226
285
|
end
|
|
227
|
-
pause(RETRY_SECONDS)
|
|
228
286
|
end
|
|
229
287
|
ensure
|
|
230
288
|
@mutex.synchronize do
|
|
231
289
|
@draining.clear
|
|
232
290
|
@thread = nil
|
|
291
|
+
@first_read_finished = true
|
|
233
292
|
@condition.broadcast
|
|
234
293
|
end
|
|
235
294
|
end
|
data/lib/bitfab/subtree.rb
CHANGED
|
@@ -10,7 +10,8 @@ module Bitfab
|
|
|
10
10
|
|
|
11
11
|
module Subtree
|
|
12
12
|
DEFAULT_MAX_DEPTH = 30
|
|
13
|
-
DEFAULT_MAX_SPANS =
|
|
13
|
+
DEFAULT_MAX_SPANS = 2048
|
|
14
|
+
DEFAULT_MAX_CAPTURED_SUBTREE_SPANS = 512
|
|
14
15
|
SESSION_KEY = :bitfab_subtree_session
|
|
15
16
|
DISPATCHER_KEY = :bitfab_subtree_dispatcher
|
|
16
17
|
FIBER_CONTEXT_KEY = :bitfab_subtree_fiber_context
|
|
@@ -98,6 +99,10 @@ module Bitfab
|
|
|
98
99
|
@owner = @records.find { |_, entry, _| entry }&.first unless nested_trace_key
|
|
99
100
|
end
|
|
100
101
|
|
|
102
|
+
def recorded_elsewhere?
|
|
103
|
+
@records.any? { |session, entry, _| entry && !session.equal?(@owner) }
|
|
104
|
+
end
|
|
105
|
+
|
|
101
106
|
def start(trace_id:, span_id:)
|
|
102
107
|
@records.each do |session, entry, _|
|
|
103
108
|
next unless entry
|
|
@@ -170,7 +175,7 @@ module Bitfab
|
|
|
170
175
|
end
|
|
171
176
|
|
|
172
177
|
def capture(client:, trace_function_key:, config:, max_depth:, max_spans:, include_wrappers:,
|
|
173
|
-
root_receiver:, root_method_name:, mock_on_replay_default: false)
|
|
178
|
+
root_receiver:, root_method_name:, max_captured_subtree_spans: DEFAULT_MAX_CAPTURED_SUBTREE_SPANS, mock_on_replay_default: false)
|
|
174
179
|
return yield unless config.root
|
|
175
180
|
|
|
176
181
|
span = SpanContext.current || ReplayContext.current_span
|
|
@@ -188,6 +193,7 @@ module Bitfab
|
|
|
188
193
|
config:,
|
|
189
194
|
max_depth:,
|
|
190
195
|
max_spans:,
|
|
196
|
+
max_captured_subtree_spans:,
|
|
191
197
|
include_wrappers:,
|
|
192
198
|
mock_on_replay_default:,
|
|
193
199
|
root_receiver:,
|
|
@@ -412,7 +418,7 @@ module Bitfab
|
|
|
412
418
|
|
|
413
419
|
def initialize(client:, trace_function_key:, trace_id:, root_span_id:, test_run_id:,
|
|
414
420
|
input_source_span_id:, config:, max_depth:, max_spans:, include_wrappers:,
|
|
415
|
-
root_receiver:, root_method_name:, mock_on_replay_default: false)
|
|
421
|
+
root_receiver:, root_method_name:, max_captured_subtree_spans: DEFAULT_MAX_CAPTURED_SUBTREE_SPANS, mock_on_replay_default: false)
|
|
416
422
|
@mock_on_replay_default = mock_on_replay_default
|
|
417
423
|
@client = client
|
|
418
424
|
@trace_function_key = trace_function_key
|
|
@@ -427,6 +433,7 @@ module Bitfab
|
|
|
427
433
|
@skip_root_call = true
|
|
428
434
|
@max_depth = max_depth
|
|
429
435
|
@max_spans = max_spans
|
|
436
|
+
@max_captured_subtree_spans = [max_captured_subtree_spans, max_spans].min
|
|
430
437
|
@exclude = config.exclude
|
|
431
438
|
@include_wrappers = include_wrappers
|
|
432
439
|
@stacks = {}
|
|
@@ -435,7 +442,8 @@ module Bitfab
|
|
|
435
442
|
@suppressed_depths = {}
|
|
436
443
|
@owned_paths = {}
|
|
437
444
|
@method_metadata = Hash.new { |owners, owner| owners[owner] = {} }
|
|
438
|
-
@spans_used =
|
|
445
|
+
@spans_used = 1
|
|
446
|
+
@captured_subtree_count = 0
|
|
439
447
|
@truncated = false
|
|
440
448
|
@exhausted = false
|
|
441
449
|
@drained = false
|
|
@@ -518,14 +526,11 @@ module Bitfab
|
|
|
518
526
|
@truncated = true
|
|
519
527
|
return false
|
|
520
528
|
end
|
|
521
|
-
if
|
|
522
|
-
@truncated = true
|
|
523
|
-
@exhausted = true
|
|
524
|
-
return false
|
|
525
|
-
end
|
|
529
|
+
return true if take_slot(discovered: false)
|
|
526
530
|
|
|
527
|
-
@
|
|
528
|
-
true
|
|
531
|
+
@truncated = true
|
|
532
|
+
@exhausted = true
|
|
533
|
+
false
|
|
529
534
|
end
|
|
530
535
|
|
|
531
536
|
def configured_entry(name:, type:, args:, kwargs:, test_run_id:)
|
|
@@ -637,14 +642,10 @@ module Bitfab
|
|
|
637
642
|
suppress(fiber_id)
|
|
638
643
|
return
|
|
639
644
|
end
|
|
640
|
-
|
|
645
|
+
content_off = content_off?(metadata.name)
|
|
646
|
+
if (limit = limit_reached(discovered: true, content_off:))
|
|
641
647
|
@truncated = true
|
|
642
|
-
|
|
643
|
-
if captured_calls_drained?
|
|
644
|
-
@drained = true
|
|
645
|
-
else
|
|
646
|
-
suppress(fiber_id)
|
|
647
|
-
end
|
|
648
|
+
stop_discovery(fiber_id) unless limit == :max_captured_subtree_spans && content_off_names?
|
|
648
649
|
return
|
|
649
650
|
end
|
|
650
651
|
|
|
@@ -655,9 +656,10 @@ module Bitfab
|
|
|
655
656
|
return
|
|
656
657
|
end
|
|
657
658
|
|
|
658
|
-
|
|
659
|
+
count_span(discovered: true, content_off:)
|
|
659
660
|
span_id = SecureRandom.uuid
|
|
660
|
-
|
|
661
|
+
without_content = content_off || plan_unavailable?
|
|
662
|
+
args, kwargs = without_content ? [[], {}] : inputs(event)
|
|
661
663
|
# A nested subtree root temporarily owns the ambient span context, but
|
|
662
664
|
# an outer session must never parent its spans across trace boundaries.
|
|
663
665
|
active_parent_span_id = if active_parent&.dig(:trace_id) == @trace_id
|
|
@@ -671,7 +673,8 @@ module Bitfab
|
|
|
671
673
|
name: metadata.name,
|
|
672
674
|
args:,
|
|
673
675
|
kwargs:,
|
|
674
|
-
started_at: now
|
|
676
|
+
started_at: now,
|
|
677
|
+
span_data: without_content ? {"content_off_by_simulation_plan" => true} : nil
|
|
675
678
|
)
|
|
676
679
|
end
|
|
677
680
|
|
|
@@ -878,14 +881,60 @@ module Bitfab
|
|
|
878
881
|
@suppressed_depths[fiber_id] = 1
|
|
879
882
|
end
|
|
880
883
|
|
|
884
|
+
def limit_reached(discovered:, content_off: false)
|
|
885
|
+
return :max_spans if @spans_used >= @max_spans
|
|
886
|
+
|
|
887
|
+
:max_captured_subtree_spans if discovered && !content_off && @captured_subtree_count >= @max_captured_subtree_spans
|
|
888
|
+
end
|
|
889
|
+
|
|
890
|
+
def take_slot(discovered:)
|
|
891
|
+
return false if limit_reached(discovered:)
|
|
892
|
+
|
|
893
|
+
count_span(discovered:, content_off: false)
|
|
894
|
+
true
|
|
895
|
+
end
|
|
896
|
+
|
|
897
|
+
def count_span(discovered:, content_off:)
|
|
898
|
+
@spans_used += 1
|
|
899
|
+
@captured_subtree_count += 1 if discovered && !content_off
|
|
900
|
+
end
|
|
901
|
+
|
|
902
|
+
def stop_discovery(fiber_id)
|
|
903
|
+
@exhausted = true
|
|
904
|
+
if captured_calls_drained?
|
|
905
|
+
@drained = true
|
|
906
|
+
else
|
|
907
|
+
suppress(fiber_id)
|
|
908
|
+
end
|
|
909
|
+
end
|
|
910
|
+
|
|
911
|
+
def content_off?(name)
|
|
912
|
+
@client.send(:content_off_by_simulation_plan?, trace_id: @trace_id, trace_function_key: @trace_function_key,
|
|
913
|
+
span_name: name, instrumentation: "trace")
|
|
914
|
+
rescue
|
|
915
|
+
false
|
|
916
|
+
end
|
|
917
|
+
|
|
918
|
+
def content_off_names?
|
|
919
|
+
@client.send(:simulation_plan_content_off_names?, trace_id: @trace_id, trace_function_key: @trace_function_key)
|
|
920
|
+
rescue
|
|
921
|
+
false
|
|
922
|
+
end
|
|
923
|
+
|
|
924
|
+
def plan_unavailable?
|
|
925
|
+
@client.send(:simulation_plan_unavailable?, instrumentation: "trace")
|
|
926
|
+
rescue
|
|
927
|
+
false
|
|
928
|
+
end
|
|
929
|
+
|
|
881
930
|
def warn_if_truncated
|
|
882
931
|
return unless @truncated
|
|
883
932
|
|
|
884
933
|
Bitfab.warn_once(
|
|
885
934
|
"subtree-truncated:#{@trace_function_key}",
|
|
886
935
|
"'#{@trace_function_key}' hit a subtree capture limit (max_depth=#{@max_depth}, " \
|
|
887
|
-
"max_spans=#{@max_spans}) and its trace is incomplete.
|
|
888
|
-
"what is traced with exclude: [...]."
|
|
936
|
+
"max_captured_subtree_spans=#{@max_captured_subtree_spans}, max_spans=#{@max_spans}) and its trace is incomplete. " \
|
|
937
|
+
"Raise the limits, add bitfab_node to the calls that matter, or narrow what is traced with exclude: [...]."
|
|
889
938
|
)
|
|
890
939
|
end
|
|
891
940
|
|
data/lib/bitfab/traceable.rb
CHANGED
|
@@ -83,7 +83,9 @@ module Bitfab
|
|
|
83
83
|
# beneath it record automatically as function spans.
|
|
84
84
|
def self.trace(klass, method_name, trace_function_key:, name: nil, type: "custom",
|
|
85
85
|
max_depth: Subtree::DEFAULT_MAX_DEPTH, max_spans: Subtree::DEFAULT_MAX_SPANS,
|
|
86
|
+
max_captured_subtree_spans: Subtree::DEFAULT_MAX_CAPTURED_SUBTREE_SPANS,
|
|
86
87
|
exclude: [], include_wrappers: false, mock_on_replay_default: false, client: nil)
|
|
88
|
+
validate_trace_options!(max_spans:)
|
|
87
89
|
capture_config = Subtree.prepare(root_source: source_of(klass, method_name), exclude:)
|
|
88
90
|
method_name_str = method_name.to_s
|
|
89
91
|
bound_client = client
|
|
@@ -95,6 +97,8 @@ module Bitfab
|
|
|
95
97
|
Subtree.assert_surface!(:trace)
|
|
96
98
|
next super(*args, **kwargs, &block) if Subtree.nested_root_suppressed?
|
|
97
99
|
|
|
100
|
+
target_client.send(:wait_for_simulation_plan) unless Subtree.current_session
|
|
101
|
+
|
|
98
102
|
span_name = name || Subtree.qualified_name(self, method_name)
|
|
99
103
|
managed = SpanContext.current&.dig(:recording_override)
|
|
100
104
|
if managed && managed.empty? && (ReplayContext.current || SeedContext.current)
|
|
@@ -102,7 +106,7 @@ module Bitfab
|
|
|
102
106
|
SpanContext.current[:surface] = :trace
|
|
103
107
|
next Subtree.capture(
|
|
104
108
|
client: target_client, trace_function_key:, config: capture_config,
|
|
105
|
-
max_depth:, max_spans:, include_wrappers:, mock_on_replay_default:,
|
|
109
|
+
max_depth:, max_spans:, max_captured_subtree_spans:, include_wrappers:, mock_on_replay_default:,
|
|
106
110
|
root_receiver: self, root_method_name: method_name
|
|
107
111
|
) { super(*args, **kwargs, &block) }
|
|
108
112
|
end
|
|
@@ -116,7 +120,7 @@ module Bitfab
|
|
|
116
120
|
around_body: call.method(:run)) do
|
|
117
121
|
Subtree.capture(
|
|
118
122
|
client: target_client, trace_function_key:, config: capture_config,
|
|
119
|
-
max_depth:, max_spans:, include_wrappers:, mock_on_replay_default:,
|
|
123
|
+
max_depth:, max_spans:, max_captured_subtree_spans:, include_wrappers:, mock_on_replay_default:,
|
|
120
124
|
root_receiver: self, root_method_name: method_name
|
|
121
125
|
) { super(*args, **kwargs, &block) }
|
|
122
126
|
end
|
|
@@ -161,7 +165,7 @@ module Bitfab
|
|
|
161
165
|
span_name:, span_type: type, function_name: method_name_str,
|
|
162
166
|
args:, kwargs:, capture_when: "nested", mock_on_replay: policy,
|
|
163
167
|
explicit_span_receiver: self, explicit_span_method_name: method_name,
|
|
164
|
-
test_run_id:, finalize:, surface: :trace,
|
|
168
|
+
test_run_id:, finalize:, surface: :trace, result_recorded_elsewhere: call.recorded_elsewhere?,
|
|
165
169
|
on_start: call.method(:start), on_complete: call.method(:complete),
|
|
166
170
|
around_body: call.method(:run)) { super(*args, **kwargs, &block) }
|
|
167
171
|
end
|
|
@@ -189,6 +193,14 @@ module Bitfab
|
|
|
189
193
|
end
|
|
190
194
|
private_class_method :preserve_visibility
|
|
191
195
|
|
|
196
|
+
def self.validate_trace_options!(max_spans:)
|
|
197
|
+
return if max_spans.is_a?(Integer) && max_spans >= 1
|
|
198
|
+
|
|
199
|
+
raise ArgumentError,
|
|
200
|
+
"bitfab_trace max_spans must be an integer of at least 1 because the root span takes one slot, got #{max_spans.inspect}."
|
|
201
|
+
end
|
|
202
|
+
private_class_method :validate_trace_options!
|
|
203
|
+
|
|
192
204
|
def self.validate_node_options!(capture:, mock_on_replay:)
|
|
193
205
|
return unless !capture && mock_on_replay
|
|
194
206
|
|
|
@@ -293,7 +305,9 @@ module Bitfab
|
|
|
293
305
|
# Descendants require no Bitfab declarations and record as function spans.
|
|
294
306
|
def bitfab_trace(method_name, trace_function_key: nil, name: nil, type: "custom",
|
|
295
307
|
max_depth: Subtree::DEFAULT_MAX_DEPTH, max_spans: Subtree::DEFAULT_MAX_SPANS,
|
|
308
|
+
max_captured_subtree_spans: Subtree::DEFAULT_MAX_CAPTURED_SUBTREE_SPANS,
|
|
296
309
|
exclude: [], include_wrappers: false, mock_on_replay_default: false)
|
|
310
|
+
Traceable.send(:validate_trace_options!, max_spans:)
|
|
297
311
|
trace_function_key ||= @bitfab_function_key
|
|
298
312
|
unless trace_function_key
|
|
299
313
|
raise "No trace function key provided. Pass `trace_function_key:` to `bitfab_trace` " \
|
|
@@ -306,6 +320,7 @@ module Bitfab
|
|
|
306
320
|
type:,
|
|
307
321
|
max_depth:,
|
|
308
322
|
max_spans:,
|
|
323
|
+
max_captured_subtree_spans:,
|
|
309
324
|
exclude:,
|
|
310
325
|
include_wrappers:,
|
|
311
326
|
mock_on_replay_default:
|
data/lib/bitfab/version.rb
CHANGED