bitfab 0.21.0 → 0.21.1
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 +84 -49
- data/lib/bitfab/http_client.rb +13 -13
- data/lib/bitfab/traceable.rb +6 -1
- data/lib/bitfab/version.rb +1 -1
- data/lib/bitfab/warn_once.rb +35 -0
- data/lib/bitfab.rb +8 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 364e0ec3b3aa00cc47a398872911e18b1a3e5253f4448214da67f9c12af51814
|
|
4
|
+
data.tar.gz: 482340c71e8d2cb0f8ed92efa7a214278529a670e836e46b6209cc18126f230b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d9c10548592e02d52a74b96b268bea01d33200c3ff338b61758ec6d54dc2adcf07ca5c442212d464cb53cb86a1aeeccb7466c9ca71ba49810902ff6f5445b0b5
|
|
7
|
+
data.tar.gz: 3f12de6d0a9eeb5f7c8218d977c4d344a972f762f6c7380e5123d8976032c2215f63337698e4c7d6f18fd669786e4fded0084e0be2ef8fce1e7a696324c2f9b5
|
data/lib/bitfab/client.rb
CHANGED
|
@@ -8,6 +8,7 @@ require_relative "http_client"
|
|
|
8
8
|
require_relative "replay"
|
|
9
9
|
require_relative "span_context"
|
|
10
10
|
require_relative "serialize"
|
|
11
|
+
require_relative "warn_once"
|
|
11
12
|
|
|
12
13
|
module Bitfab
|
|
13
14
|
class Client
|
|
@@ -101,59 +102,93 @@ module Bitfab
|
|
|
101
102
|
mock_on_replay: false)
|
|
102
103
|
return yield unless @enabled
|
|
103
104
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
105
|
+
# Span setup runs before the user's block. Tracing is a side-channel, so
|
|
106
|
+
# if anything here raises (id generation, trace-state bookkeeping, a
|
|
107
|
+
# malformed replay mock tree) the user's method must still run. On failure
|
|
108
|
+
# we clean up any partially registered trace state, warn once, and run the
|
|
109
|
+
# block untraced. `trace_id` is declared out here so the rescue can clean
|
|
110
|
+
# it up; the other locals stay visible to the real path below.
|
|
111
|
+
trace_id = nil
|
|
112
|
+
span_id = nil
|
|
113
|
+
parent_span_id = nil
|
|
114
|
+
is_root_span = nil
|
|
115
|
+
started_at = nil
|
|
116
|
+
resolved_test_run_id = nil
|
|
117
|
+
resolved_input_source_span_id = nil
|
|
118
|
+
begin
|
|
119
|
+
parent = SpanContext.current
|
|
120
|
+
replay_ctx = ReplayContext.current
|
|
121
|
+
trace_id = parent ? parent[:trace_id] : (replay_ctx&.dig(:trace_id) || SecureRandom.uuid)
|
|
122
|
+
span_id = SecureRandom.uuid
|
|
123
|
+
parent_span_id = parent&.dig(:span_id)
|
|
124
|
+
is_root_span = parent_span_id.nil?
|
|
125
|
+
started_at = Time.now.utc.strftime("%Y-%m-%dT%H:%M:%S.%3NZ")
|
|
126
|
+
resolved_test_run_id = replay_ctx&.dig(:test_run_id)
|
|
127
|
+
resolved_input_source_span_id = replay_ctx&.dig(:input_source_span_id)
|
|
128
|
+
resolved_input_source_trace_id = replay_ctx&.dig(:input_source_trace_id)
|
|
129
|
+
|
|
130
|
+
# Register trace state for root spans
|
|
131
|
+
if is_root_span && !TraceState.get(trace_id)
|
|
132
|
+
TraceState.create(
|
|
133
|
+
trace_id,
|
|
134
|
+
test_run_id: resolved_test_run_id,
|
|
135
|
+
input_source_trace_id: resolved_input_source_trace_id
|
|
136
|
+
)
|
|
137
|
+
end
|
|
123
138
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
139
|
+
if is_root_span
|
|
140
|
+
@pending_span_mutex.synchronize { @pending_span_threads[trace_id] = [] }
|
|
141
|
+
end
|
|
127
142
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
)
|
|
139
|
-
if mocked_output != MOCK_REPLAY_MISS
|
|
140
|
-
send_mocked_span(
|
|
141
|
-
trace_function_key:,
|
|
142
|
-
trace_id:,
|
|
143
|
-
span_id:,
|
|
144
|
-
parent_span_id:,
|
|
145
|
-
span_name:,
|
|
146
|
-
span_type:,
|
|
147
|
-
function_name:,
|
|
148
|
-
args:,
|
|
149
|
-
kwargs:,
|
|
150
|
-
mocked_output:,
|
|
151
|
-
started_at:,
|
|
152
|
-
test_run_id: resolved_test_run_id,
|
|
153
|
-
input_source_span_id: resolved_input_source_span_id
|
|
143
|
+
# Advance the per-(key, name) call counter for any non-root span under
|
|
144
|
+
# an active mock tree, even when this span won't itself be mocked.
|
|
145
|
+
# Unmarked spans must consume an index so subsequent marked siblings
|
|
146
|
+
# line up with `build_mock_tree`'s sequential numbering for the same
|
|
147
|
+
# (key, name) pair. Different (key, name) pairs have independent
|
|
148
|
+
# counters: they cannot shift each other.
|
|
149
|
+
call_index = advance_mock_counter(replay_ctx, trace_function_key, span_name, is_root_span:)
|
|
150
|
+
if call_index
|
|
151
|
+
mocked_output = check_mock_replay(
|
|
152
|
+
replay_ctx, trace_function_key, span_name, call_index, mock_on_replay:
|
|
154
153
|
)
|
|
155
|
-
|
|
154
|
+
if mocked_output != MOCK_REPLAY_MISS
|
|
155
|
+
send_mocked_span(
|
|
156
|
+
trace_function_key:,
|
|
157
|
+
trace_id:,
|
|
158
|
+
span_id:,
|
|
159
|
+
parent_span_id:,
|
|
160
|
+
span_name:,
|
|
161
|
+
span_type:,
|
|
162
|
+
function_name:,
|
|
163
|
+
args:,
|
|
164
|
+
kwargs:,
|
|
165
|
+
mocked_output:,
|
|
166
|
+
started_at:,
|
|
167
|
+
test_run_id: resolved_test_run_id,
|
|
168
|
+
input_source_span_id: resolved_input_source_span_id
|
|
169
|
+
)
|
|
170
|
+
return mocked_output
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
rescue
|
|
174
|
+
# Clean up any trace state this partial setup registered so it does not
|
|
175
|
+
# leak.
|
|
176
|
+
if trace_id
|
|
177
|
+
TraceState.delete(trace_id)
|
|
178
|
+
@pending_span_mutex.synchronize { @pending_span_threads.delete(trace_id) }
|
|
156
179
|
end
|
|
180
|
+
# During replay (a controlled eval) a setup failure must surface, not
|
|
181
|
+
# silently run the block untraced: swallowing it would execute real code
|
|
182
|
+
# with real side effects and skew the mock call counter, defeating the
|
|
183
|
+
# replay. The never-crash fallback is for production hosts only.
|
|
184
|
+
raise if ReplayContext.current
|
|
185
|
+
|
|
186
|
+
Bitfab.warn_once(
|
|
187
|
+
"span-setup:#{trace_function_key}",
|
|
188
|
+
"span setup failed for '#{trace_function_key}'; this call runs untraced. " \
|
|
189
|
+
"Your method still executes and returns normally."
|
|
190
|
+
)
|
|
191
|
+
return yield
|
|
157
192
|
end
|
|
158
193
|
|
|
159
194
|
result = nil
|
data/lib/bitfab/http_client.rb
CHANGED
|
@@ -7,6 +7,7 @@ require "uri"
|
|
|
7
7
|
require_relative "constants"
|
|
8
8
|
require_relative "serialize"
|
|
9
9
|
require_relative "version"
|
|
10
|
+
require_relative "warn_once"
|
|
10
11
|
|
|
11
12
|
module Bitfab
|
|
12
13
|
class HttpClient
|
|
@@ -181,14 +182,13 @@ module Bitfab
|
|
|
181
182
|
def safe_generate(payload, endpoint)
|
|
182
183
|
JSON.generate(payload)
|
|
183
184
|
rescue => e
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
end
|
|
185
|
+
Bitfab.warn_once(
|
|
186
|
+
"request-body-stubbed",
|
|
187
|
+
"a request body held a non-serializable value (#{e.message}); it was " \
|
|
188
|
+
"stubbed so the span still sends, but the trace may be incomplete or " \
|
|
189
|
+
"not replayable. Capture a JSON-safe projection of this input to make " \
|
|
190
|
+
"it replayable."
|
|
191
|
+
)
|
|
192
192
|
|
|
193
193
|
begin
|
|
194
194
|
JSON.generate(sanitize_payload(payload))
|
|
@@ -242,11 +242,11 @@ module Bitfab
|
|
|
242
242
|
thread = Thread.new do
|
|
243
243
|
block.call
|
|
244
244
|
rescue => e
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
#
|
|
249
|
-
|
|
245
|
+
Bitfab.warn_once(
|
|
246
|
+
"send-request-failed",
|
|
247
|
+
"failed to send a request to the backend (further occurrences " \
|
|
248
|
+
"suppressed): #{e.message}"
|
|
249
|
+
)
|
|
250
250
|
ensure
|
|
251
251
|
@pending_threads_mutex.synchronize { @pending_threads.delete(Thread.current) }
|
|
252
252
|
end
|
data/lib/bitfab/traceable.rb
CHANGED
|
@@ -173,7 +173,12 @@ module Bitfab
|
|
|
173
173
|
|
|
174
174
|
wrapper = Module.new do
|
|
175
175
|
define_method(method_name) do |*args, **kwargs, &block|
|
|
176
|
-
Bitfab
|
|
176
|
+
# If Bitfab was never configured, run the real method untraced
|
|
177
|
+
# rather than raising a "not configured" error into the host app.
|
|
178
|
+
client = Bitfab.client_or_nil
|
|
179
|
+
return super(*args, **kwargs, &block) unless client
|
|
180
|
+
|
|
181
|
+
client.send(:execute_span,
|
|
177
182
|
trace_function_key:,
|
|
178
183
|
span_name:,
|
|
179
184
|
span_type: type,
|
data/lib/bitfab/version.rb
CHANGED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Bitfab
|
|
4
|
+
# Emit a warning at most once per distinct key for the life of the process.
|
|
5
|
+
#
|
|
6
|
+
# The SDK must never crash or spam the host app, so every failure on the
|
|
7
|
+
# user's path degrades quietly: a span is dropped, a call runs untraced, a
|
|
8
|
+
# payload is stubbed. Silent is safe but undebuggable; warning on every call
|
|
9
|
+
# from a hot path is its own problem. A one-time warning per distinct issue
|
|
10
|
+
# restores the signal without the flood. Keys should identify the specific
|
|
11
|
+
# degradation (e.g. include the trace_function_key) so each distinct issue
|
|
12
|
+
# warns once, not just the first one seen.
|
|
13
|
+
@warn_once_seen = Set.new
|
|
14
|
+
@warn_once_mutex = Mutex.new
|
|
15
|
+
|
|
16
|
+
class << self
|
|
17
|
+
def warn_once(key, message)
|
|
18
|
+
@warn_once_mutex.synchronize do
|
|
19
|
+
return if @warn_once_seen.include?(key)
|
|
20
|
+
|
|
21
|
+
@warn_once_seen.add(key)
|
|
22
|
+
end
|
|
23
|
+
begin
|
|
24
|
+
warn "Bitfab: #{message}"
|
|
25
|
+
rescue Exception # rubocop:disable Lint/RescueException
|
|
26
|
+
# Logging must never crash the host app (e.g. a closed $stderr).
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Test-only: clear the dedup set so a warning can fire again.
|
|
31
|
+
def _reset_warn_once
|
|
32
|
+
@warn_once_mutex.synchronize { @warn_once_seen.clear }
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
data/lib/bitfab.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "bitfab/version"
|
|
4
4
|
require_relative "bitfab/constants"
|
|
5
|
+
require_relative "bitfab/warn_once"
|
|
5
6
|
require_relative "bitfab/serialize"
|
|
6
7
|
require_relative "bitfab/db_snapshot"
|
|
7
8
|
require_relative "bitfab/span_context"
|
|
@@ -65,6 +66,13 @@ module Bitfab
|
|
|
65
66
|
@client or raise "Bitfab not configured. Call Bitfab.configure(api_key: '...') first."
|
|
66
67
|
end
|
|
67
68
|
|
|
69
|
+
# Returns the global client, or nil if not configured. Used on the traced
|
|
70
|
+
# call path so a method invoked before Bitfab.configure runs untraced
|
|
71
|
+
# rather than crashing the host app with a "not configured" error.
|
|
72
|
+
def client_or_nil
|
|
73
|
+
@client
|
|
74
|
+
end
|
|
75
|
+
|
|
68
76
|
# Reset the global client (primarily for testing).
|
|
69
77
|
def reset!
|
|
70
78
|
@client = nil
|
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.21.
|
|
4
|
+
version: 0.21.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Harvest Team
|
|
@@ -127,6 +127,7 @@ files:
|
|
|
127
127
|
- lib/bitfab/span_context.rb
|
|
128
128
|
- lib/bitfab/traceable.rb
|
|
129
129
|
- lib/bitfab/version.rb
|
|
130
|
+
- lib/bitfab/warn_once.rb
|
|
130
131
|
homepage: https://bitfab.ai
|
|
131
132
|
licenses:
|
|
132
133
|
- MIT
|