allstak 0.3.0 → 0.3.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/CHANGELOG.md +7 -0
- data/lib/allstak/client.rb +43 -0
- data/lib/allstak/diagnostics.rb +47 -0
- data/lib/allstak/integrations/rack.rb +19 -12
- data/lib/allstak/modules/database.rb +14 -0
- data/lib/allstak/modules/errors.rb +43 -6
- data/lib/allstak/modules/http_monitor.rb +14 -0
- data/lib/allstak/modules/logs.rb +14 -0
- data/lib/allstak/modules/tracing.rb +39 -2
- data/lib/allstak/propagation.rb +70 -12
- data/lib/allstak/sanitizer.rb +31 -1
- data/lib/allstak/session_tracker.rb +4 -0
- data/lib/allstak/transport/flush_buffer.rb +6 -0
- data/lib/allstak/transport/http_transport.rb +104 -7
- data/lib/allstak/version.rb +1 -1
- data/lib/allstak.rb +9 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6749cec382749239156fb82d5feb81db7c0c63916d06f037ec6d3fbcea037a53
|
|
4
|
+
data.tar.gz: ae060d2498ee40e52b6c4ce26dc5fc283bfeff5c9ab0756fff1ecdcccf26ebad
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 26a946694cf36a20b64023727ca4f9b8c0a8fab9248d6481f527252ad879bb83b9901e5349524b11156bf959ef6eec7cf1b09deb21bfa5f0e57149cba21f0f2a
|
|
7
|
+
data.tar.gz: c1dbec9f5fb40f38a2aca6ac7f52a5515edd9ce6b87dd8702030fdf6d334b8f3cf3b102ea24260a30bb9d7219b177a7f14a3158e8499cdb6c29ee0ffc020f689
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,13 @@ This project follows [Semantic Versioning](https://semver.org/).
|
|
|
5
5
|
|
|
6
6
|
## Unreleased
|
|
7
7
|
|
|
8
|
+
## [0.3.1] — 2026-06-06
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- Treated terminal non-429 4xx ingest responses, including feature-gated `402`
|
|
13
|
+
responses, as non-retryable so offline replay cannot wedge permanently.
|
|
14
|
+
|
|
8
15
|
## [0.3.0] — 2026-05-30
|
|
9
16
|
|
|
10
17
|
### Added — Automatic breadcrumbs across all instrumentation
|
data/lib/allstak/client.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
require_relative "diagnostics"
|
|
2
|
+
|
|
1
3
|
module AllStak
|
|
2
4
|
# The AllStak SDK client. Create once via {AllStak.configure}.
|
|
3
5
|
class Client
|
|
@@ -130,8 +132,49 @@ module AllStak
|
|
|
130
132
|
@session_tracker&.end rescue nil
|
|
131
133
|
end
|
|
132
134
|
|
|
135
|
+
# Privacy-safe SDK diagnostics. Contains counters and queue sizes only.
|
|
136
|
+
def diagnostics
|
|
137
|
+
transport_stats =
|
|
138
|
+
if @transport.respond_to?(:diagnostics)
|
|
139
|
+
@transport.diagnostics
|
|
140
|
+
else
|
|
141
|
+
{}
|
|
142
|
+
end
|
|
143
|
+
modules = [@errors, @logs, @http, @tracing, @database]
|
|
144
|
+
buffer_queue = modules.sum { |m| safe_count(m, :buffer_count) }
|
|
145
|
+
buffer_drops = modules.sum { |m| safe_count(m, :dropped_count) }
|
|
146
|
+
|
|
147
|
+
Diagnostics.new(
|
|
148
|
+
events_captured: modules.sum { |m| safe_count(m, :captured_count) },
|
|
149
|
+
events_sent: transport_stats.fetch(:sent, 0),
|
|
150
|
+
events_failed: transport_stats.fetch(:failed, 0),
|
|
151
|
+
events_dropped: transport_stats.fetch(:dropped, 0) + buffer_drops,
|
|
152
|
+
events_persisted: transport_stats.fetch(:persisted, 0),
|
|
153
|
+
events_replayed: transport_stats.fetch(:replayed, 0),
|
|
154
|
+
queue_size: transport_stats.fetch(:queue_size, 0) + buffer_queue,
|
|
155
|
+
retry_attempts: transport_stats.fetch(:retry_attempts, 0),
|
|
156
|
+
rate_limited_count: transport_stats.fetch(:rate_limited, 0),
|
|
157
|
+
compressed_payloads: transport_stats.fetch(:compressed, 0),
|
|
158
|
+
uncompressed_payloads: transport_stats.fetch(:uncompressed, 0),
|
|
159
|
+
compression_bytes_saved: transport_stats.fetch(:compression_bytes_saved, 0),
|
|
160
|
+
sanitizer_redaction_count: AllStak::Sanitizer.redaction_count,
|
|
161
|
+
active_trace_count: safe_count(@tracing, :active_trace_count),
|
|
162
|
+
active_span_count: safe_count(@tracing, :active_span_count),
|
|
163
|
+
breadcrumb_count: safe_count(@errors, :breadcrumb_count),
|
|
164
|
+
session_recovery_count: safe_count(@session_tracker, :recovery_count),
|
|
165
|
+
disabled: @transport.disabled?
|
|
166
|
+
)
|
|
167
|
+
end
|
|
168
|
+
|
|
133
169
|
private
|
|
134
170
|
|
|
171
|
+
def safe_count(target, method_name)
|
|
172
|
+
return 0 unless target.respond_to?(method_name)
|
|
173
|
+
target.public_send(method_name).to_i
|
|
174
|
+
rescue StandardError
|
|
175
|
+
0
|
|
176
|
+
end
|
|
177
|
+
|
|
135
178
|
# Mark the active session errored/crashed based on the captured event's
|
|
136
179
|
# mechanism. An at_exit/unhandled event (handled=false) is a crash;
|
|
137
180
|
# everything else is a handled error. Fail-open — never raises.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AllStak
|
|
4
|
+
# Privacy-safe SDK diagnostics snapshot.
|
|
5
|
+
#
|
|
6
|
+
# Contains counters and queue sizes only. It intentionally never includes
|
|
7
|
+
# telemetry payloads, headers, tags, context values, user data, or breadcrumbs.
|
|
8
|
+
class Diagnostics
|
|
9
|
+
ATTRIBUTES = [
|
|
10
|
+
:events_captured,
|
|
11
|
+
:events_sent,
|
|
12
|
+
:events_failed,
|
|
13
|
+
:events_dropped,
|
|
14
|
+
:events_persisted,
|
|
15
|
+
:events_replayed,
|
|
16
|
+
:queue_size,
|
|
17
|
+
:retry_attempts,
|
|
18
|
+
:rate_limited_count,
|
|
19
|
+
:compressed_payloads,
|
|
20
|
+
:uncompressed_payloads,
|
|
21
|
+
:compression_bytes_saved,
|
|
22
|
+
:sanitizer_redaction_count,
|
|
23
|
+
:active_trace_count,
|
|
24
|
+
:active_span_count,
|
|
25
|
+
:breadcrumb_count,
|
|
26
|
+
:session_recovery_count,
|
|
27
|
+
:disabled
|
|
28
|
+
].freeze
|
|
29
|
+
|
|
30
|
+
attr_reader(*ATTRIBUTES)
|
|
31
|
+
|
|
32
|
+
def initialize(values = {})
|
|
33
|
+
ATTRIBUTES.each do |name|
|
|
34
|
+
default = name == :disabled ? false : 0
|
|
35
|
+
instance_variable_set("@#{name}", values.fetch(name, default))
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def to_h
|
|
40
|
+
ATTRIBUTES.each_with_object({}) do |name, out|
|
|
41
|
+
out[name] = public_send(name)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
alias to_hash to_h
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -23,14 +23,16 @@ module AllStak
|
|
|
23
23
|
start = now_ms
|
|
24
24
|
started_at = Time.now.utc.iso8601(3)
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
incoming_trace = trace_context_from_env(env)
|
|
27
|
+
trace_id = incoming_trace[:trace_id]
|
|
28
|
+
parent_span_id = incoming_trace[:parent_span_id]
|
|
28
29
|
request_id = env["HTTP_X_REQUEST_ID"] || env["HTTP_X_ALLSTAK_REQUEST_ID"] || SecureRandom.hex(16)
|
|
29
30
|
if trace_id && !trace_id.empty?
|
|
30
31
|
client.tracing.set_trace_id(trace_id)
|
|
31
32
|
else
|
|
32
33
|
client.tracing.reset_trace
|
|
33
34
|
end
|
|
35
|
+
client.tracing.set_parent_span_id(parent_span_id) if parent_span_id && client.tracing.respond_to?(:set_parent_span_id)
|
|
34
36
|
trace_id = client.tracing.current_trace_id
|
|
35
37
|
span = client.tracing.start_span(
|
|
36
38
|
"http.server",
|
|
@@ -154,18 +156,23 @@ module AllStak
|
|
|
154
156
|
(Process.clock_gettime(Process::CLOCK_MONOTONIC) * 1000).to_i
|
|
155
157
|
end
|
|
156
158
|
|
|
157
|
-
def
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
env["HTTP_X_ALLSTAK_TRACE_ID"] || env["HTTP_X_TRACE_ID"]
|
|
159
|
+
def trace_context_from_env(env)
|
|
160
|
+
parsed = AllStak::Propagation.parse_traceparent(env["HTTP_TRACEPARENT"])
|
|
161
|
+
return parsed if parsed
|
|
162
|
+
|
|
163
|
+
trace_id = valid_trace_header(env["HTTP_X_ALLSTAK_TRACE_ID"]) || valid_trace_header(env["HTTP_X_TRACE_ID"])
|
|
164
|
+
parent_span_id = trace_id ? (valid_span_header(env["HTTP_X_ALLSTAK_SPAN_ID"]) || valid_span_header(env["HTTP_X_SPAN_ID"])) : nil
|
|
165
|
+
{ trace_id: trace_id, parent_span_id: parent_span_id, sampled: nil }
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def valid_trace_header(value)
|
|
169
|
+
normalized = value.to_s.downcase
|
|
170
|
+
AllStak::Propagation.valid_trace_id?(normalized) ? normalized : nil
|
|
162
171
|
end
|
|
163
172
|
|
|
164
|
-
def
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
return parts[2] if parts.length >= 3 && parts[2].length == 16
|
|
168
|
-
env["HTTP_X_ALLSTAK_SPAN_ID"] || env["HTTP_X_SPAN_ID"]
|
|
173
|
+
def valid_span_header(value)
|
|
174
|
+
normalized = value.to_s.downcase
|
|
175
|
+
AllStak::Propagation.valid_span_id?(normalized) ? normalized : nil
|
|
169
176
|
end
|
|
170
177
|
|
|
171
178
|
def extract_user_id(env)
|
|
@@ -11,6 +11,7 @@ module AllStak
|
|
|
11
11
|
@transport = transport
|
|
12
12
|
@config = config
|
|
13
13
|
@logger = logger
|
|
14
|
+
@captured_count = 0
|
|
14
15
|
@buffer = Transport::FlushBuffer.new(
|
|
15
16
|
name: "database",
|
|
16
17
|
max_size: config.buffer_size,
|
|
@@ -42,6 +43,7 @@ module AllStak
|
|
|
42
43
|
spanId: span_id,
|
|
43
44
|
rowsAffected: rows_affected
|
|
44
45
|
}.compact)
|
|
46
|
+
@captured_count += 1
|
|
45
47
|
end
|
|
46
48
|
|
|
47
49
|
def flush
|
|
@@ -69,6 +71,18 @@ module AllStak
|
|
|
69
71
|
%w[SELECT INSERT UPDATE DELETE].include?(first) ? first : "OTHER"
|
|
70
72
|
end
|
|
71
73
|
|
|
74
|
+
def buffer_count
|
|
75
|
+
@buffer.count
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def dropped_count
|
|
79
|
+
@buffer.dropped_count
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def captured_count
|
|
83
|
+
@captured_count
|
|
84
|
+
end
|
|
85
|
+
|
|
72
86
|
private
|
|
73
87
|
|
|
74
88
|
def flush_batch(items)
|
|
@@ -23,6 +23,8 @@ module AllStak
|
|
|
23
23
|
# Optional callable returning the active release-health session id, so
|
|
24
24
|
# the backend's error consumer can mark the session errored/crashed.
|
|
25
25
|
@session_id_provider = session_id_provider
|
|
26
|
+
@captured_count = 0
|
|
27
|
+
@dropped_count = 0
|
|
26
28
|
end
|
|
27
29
|
|
|
28
30
|
def set_user(id: nil, email: nil, ip: nil)
|
|
@@ -58,8 +60,12 @@ module AllStak
|
|
|
58
60
|
end
|
|
59
61
|
|
|
60
62
|
def capture_exception(exc, level: "error", user: nil, request_context: nil, trace_id: nil, metadata: nil)
|
|
61
|
-
|
|
63
|
+
if @transport.disabled?
|
|
64
|
+
@dropped_count += 1
|
|
65
|
+
return nil
|
|
66
|
+
end
|
|
62
67
|
begin
|
|
68
|
+
@captured_count += 1
|
|
63
69
|
crumbs = drain_breadcrumbs
|
|
64
70
|
|
|
65
71
|
payload = {
|
|
@@ -90,9 +96,15 @@ module AllStak
|
|
|
90
96
|
# Sampling first, then pre-hook scrub, before_send, and final
|
|
91
97
|
# transport scrub. Hooks never see raw secrets and cannot reintroduce
|
|
92
98
|
# values that escape the wire-path sanitizer.
|
|
93
|
-
|
|
99
|
+
unless Sampling.sampled?(@config.sample_rate)
|
|
100
|
+
@dropped_count += 1
|
|
101
|
+
return nil
|
|
102
|
+
end
|
|
94
103
|
payload = apply_before_send(payload)
|
|
95
|
-
|
|
104
|
+
if payload.nil?
|
|
105
|
+
@dropped_count += 1
|
|
106
|
+
return nil
|
|
107
|
+
end
|
|
96
108
|
|
|
97
109
|
status, body = @transport.post(PATH, payload)
|
|
98
110
|
return nil unless status == 202
|
|
@@ -114,8 +126,12 @@ module AllStak
|
|
|
114
126
|
end
|
|
115
127
|
|
|
116
128
|
def capture_error(exception_class, message, stack_trace: nil, level: "error", user: nil, request_context: nil, trace_id: nil, metadata: nil)
|
|
117
|
-
|
|
129
|
+
if @transport.disabled?
|
|
130
|
+
@dropped_count += 1
|
|
131
|
+
return nil
|
|
132
|
+
end
|
|
118
133
|
begin
|
|
134
|
+
@captured_count += 1
|
|
119
135
|
payload = {
|
|
120
136
|
exceptionClass: exception_class,
|
|
121
137
|
message: message,
|
|
@@ -134,9 +150,15 @@ module AllStak
|
|
|
134
150
|
|
|
135
151
|
# Sampling first, then pre-hook scrub, before_send, and final
|
|
136
152
|
# transport scrub.
|
|
137
|
-
|
|
153
|
+
unless Sampling.sampled?(@config.sample_rate)
|
|
154
|
+
@dropped_count += 1
|
|
155
|
+
return nil
|
|
156
|
+
end
|
|
138
157
|
payload = apply_before_send(payload)
|
|
139
|
-
|
|
158
|
+
if payload.nil?
|
|
159
|
+
@dropped_count += 1
|
|
160
|
+
return nil
|
|
161
|
+
end
|
|
140
162
|
|
|
141
163
|
status, _ = @transport.post(PATH, payload)
|
|
142
164
|
status == 202 ? exception_class : nil
|
|
@@ -152,6 +174,21 @@ module AllStak
|
|
|
152
174
|
end
|
|
153
175
|
end
|
|
154
176
|
|
|
177
|
+
def breadcrumb_count
|
|
178
|
+
buffer = Thread.current[BREADCRUMB_TLS_KEY]
|
|
179
|
+
buffer.respond_to?(:length) ? buffer.length : 0
|
|
180
|
+
rescue StandardError
|
|
181
|
+
0
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def captured_count
|
|
185
|
+
@captured_count
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def dropped_count
|
|
189
|
+
@dropped_count
|
|
190
|
+
end
|
|
191
|
+
|
|
155
192
|
private
|
|
156
193
|
|
|
157
194
|
# The current thread's breadcrumb ring buffer (lazily created).
|
|
@@ -12,6 +12,7 @@ module AllStak
|
|
|
12
12
|
@transport = transport
|
|
13
13
|
@config = config
|
|
14
14
|
@logger = logger
|
|
15
|
+
@captured_count = 0
|
|
15
16
|
@buffer = Transport::FlushBuffer.new(
|
|
16
17
|
name: "http",
|
|
17
18
|
max_size: config.buffer_size,
|
|
@@ -45,6 +46,7 @@ module AllStak
|
|
|
45
46
|
environment: @config.environment,
|
|
46
47
|
release: @config.release
|
|
47
48
|
}.compact
|
|
49
|
+
@captured_count += 1
|
|
48
50
|
@buffer.push(item)
|
|
49
51
|
end
|
|
50
52
|
|
|
@@ -56,6 +58,18 @@ module AllStak
|
|
|
56
58
|
@buffer.shutdown
|
|
57
59
|
end
|
|
58
60
|
|
|
61
|
+
def buffer_count
|
|
62
|
+
@buffer.count
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def dropped_count
|
|
66
|
+
@buffer.dropped_count
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def captured_count
|
|
70
|
+
@captured_count
|
|
71
|
+
end
|
|
72
|
+
|
|
59
73
|
private
|
|
60
74
|
|
|
61
75
|
def strip_query(path)
|
data/lib/allstak/modules/logs.rb
CHANGED
|
@@ -13,6 +13,7 @@ module AllStak
|
|
|
13
13
|
# entries also surface as breadcrumbs on the next captured exception.
|
|
14
14
|
# Injected by the client; nil keeps Logs standalone (and recursion-free).
|
|
15
15
|
@breadcrumb_sink = breadcrumb_sink
|
|
16
|
+
@captured_count = 0
|
|
16
17
|
@buffer = Transport::FlushBuffer.new(
|
|
17
18
|
name: "logs",
|
|
18
19
|
max_size: config.buffer_size,
|
|
@@ -40,6 +41,7 @@ module AllStak
|
|
|
40
41
|
errorId: error_id,
|
|
41
42
|
metadata: @config.release_tags.merge(metadata || {})
|
|
42
43
|
}.compact
|
|
44
|
+
@captured_count += 1
|
|
43
45
|
@buffer.push(payload)
|
|
44
46
|
emit_breadcrumb(level, message, trace_id: trace_id, span_id: span_id)
|
|
45
47
|
nil
|
|
@@ -59,6 +61,18 @@ module AllStak
|
|
|
59
61
|
@buffer.shutdown
|
|
60
62
|
end
|
|
61
63
|
|
|
64
|
+
def buffer_count
|
|
65
|
+
@buffer.count
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def dropped_count
|
|
69
|
+
@buffer.dropped_count
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def captured_count
|
|
73
|
+
@captured_count
|
|
74
|
+
end
|
|
75
|
+
|
|
62
76
|
private
|
|
63
77
|
|
|
64
78
|
# Bridge an accepted log into a breadcrumb via the injected sink. The
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require "securerandom"
|
|
2
|
+
require_relative "../propagation"
|
|
2
3
|
require_relative "../sampling"
|
|
3
4
|
|
|
4
5
|
module AllStak
|
|
@@ -12,6 +13,8 @@ module AllStak
|
|
|
12
13
|
@transport = transport
|
|
13
14
|
@config = config
|
|
14
15
|
@logger = logger
|
|
16
|
+
@captured_count = 0
|
|
17
|
+
@dropped_count = 0
|
|
15
18
|
@buffer = Transport::FlushBuffer.new(
|
|
16
19
|
name: "tracing",
|
|
17
20
|
max_size: config.buffer_size,
|
|
@@ -26,7 +29,12 @@ module AllStak
|
|
|
26
29
|
end
|
|
27
30
|
|
|
28
31
|
def set_trace_id(trace_id)
|
|
29
|
-
Thread.current[:allstak_trace_id] = trace_id
|
|
32
|
+
Thread.current[:allstak_trace_id] = AllStak::Propagation.normalize_trace_id(trace_id)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def set_parent_span_id(span_id)
|
|
36
|
+
return unless AllStak::Propagation.valid_span_id?(span_id)
|
|
37
|
+
Thread.current[:allstak_span_stack] = [span_id.to_s.downcase]
|
|
30
38
|
end
|
|
31
39
|
|
|
32
40
|
def current_span_id
|
|
@@ -101,6 +109,31 @@ module AllStak
|
|
|
101
109
|
@buffer.shutdown
|
|
102
110
|
end
|
|
103
111
|
|
|
112
|
+
def buffer_count
|
|
113
|
+
@buffer.count
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def dropped_count
|
|
117
|
+
@dropped_count + @buffer.dropped_count
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def captured_count
|
|
121
|
+
@captured_count
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def active_span_count
|
|
125
|
+
stack = Thread.current[:allstak_span_stack]
|
|
126
|
+
stack.respond_to?(:length) ? stack.length : 0
|
|
127
|
+
rescue StandardError
|
|
128
|
+
0
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def active_trace_count
|
|
132
|
+
Thread.current[:allstak_trace_id].nil? ? 0 : 1
|
|
133
|
+
rescue StandardError
|
|
134
|
+
0
|
|
135
|
+
end
|
|
136
|
+
|
|
104
137
|
private
|
|
105
138
|
|
|
106
139
|
def on_span_finish(span)
|
|
@@ -109,7 +142,11 @@ module AllStak
|
|
|
109
142
|
# Drop unsampled spans: they were never meant to be sent. The span
|
|
110
143
|
# still ran (timing/finish semantics intact) so block-form `in_span`
|
|
111
144
|
# control flow is unaffected.
|
|
112
|
-
|
|
145
|
+
unless span.sampled?
|
|
146
|
+
@dropped_count += 1
|
|
147
|
+
return
|
|
148
|
+
end
|
|
149
|
+
@captured_count += 1
|
|
113
150
|
@buffer.push(span.to_h)
|
|
114
151
|
end
|
|
115
152
|
|
data/lib/allstak/propagation.rb
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
|
+
require "securerandom"
|
|
2
|
+
|
|
1
3
|
module AllStak
|
|
2
4
|
module Propagation
|
|
3
5
|
module_function
|
|
4
6
|
|
|
7
|
+
TRACE_ID_RE = /\A[0-9a-f]{32}\z/.freeze
|
|
8
|
+
SPAN_ID_RE = /\A[0-9a-f]{16}\z/.freeze
|
|
9
|
+
ZERO_TRACE_ID_RE = /\A0{32}\z/.freeze
|
|
10
|
+
ZERO_SPAN_ID_RE = /\A0{16}\z/.freeze
|
|
11
|
+
|
|
5
12
|
def baggage(trace_id:, request_id: nil, span_id: nil)
|
|
6
13
|
parts = ["allstak-trace_id=#{trace_id}"]
|
|
7
14
|
parts << "allstak-request_id=#{request_id}" if request_id && !request_id.to_s.empty?
|
|
@@ -23,26 +30,77 @@ module AllStak
|
|
|
23
30
|
sampled == false ? "00" : "01"
|
|
24
31
|
end
|
|
25
32
|
|
|
33
|
+
def valid_trace_id?(trace_id)
|
|
34
|
+
value = trace_id.to_s.downcase
|
|
35
|
+
value.match?(TRACE_ID_RE) && !value.match?(ZERO_TRACE_ID_RE)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def valid_span_id?(span_id)
|
|
39
|
+
value = span_id.to_s.downcase
|
|
40
|
+
value.match?(SPAN_ID_RE) && !value.match?(ZERO_SPAN_ID_RE)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def normalize_trace_id(trace_id)
|
|
44
|
+
hex = trace_id.to_s.gsub(/[^0-9a-f]/i, "").downcase
|
|
45
|
+
candidate =
|
|
46
|
+
if hex.length >= 32
|
|
47
|
+
hex[0, 32]
|
|
48
|
+
elsif !hex.empty?
|
|
49
|
+
hex.ljust(32, "0")
|
|
50
|
+
end
|
|
51
|
+
return candidate if candidate && valid_trace_id?(candidate)
|
|
52
|
+
SecureRandom.hex(16)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def normalize_span_id(span_id)
|
|
56
|
+
hex = span_id.to_s.gsub(/[^0-9a-f]/i, "").downcase
|
|
57
|
+
candidate =
|
|
58
|
+
if hex.length >= 16
|
|
59
|
+
hex[0, 16]
|
|
60
|
+
elsif !hex.empty?
|
|
61
|
+
hex.ljust(16, "0")
|
|
62
|
+
end
|
|
63
|
+
return candidate if candidate && valid_span_id?(candidate)
|
|
64
|
+
SecureRandom.hex(8)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def parse_traceparent(header)
|
|
68
|
+
match = /\A00-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})\z/i.match(header.to_s.strip)
|
|
69
|
+
return nil unless match
|
|
70
|
+
trace_id = match[1].downcase
|
|
71
|
+
parent_span_id = match[2].downcase
|
|
72
|
+
return nil unless valid_trace_id?(trace_id) && valid_span_id?(parent_span_id)
|
|
73
|
+
{
|
|
74
|
+
trace_id: trace_id,
|
|
75
|
+
parent_span_id: parent_span_id,
|
|
76
|
+
sampled: (match[3].to_i(16) & 1) == 1
|
|
77
|
+
}
|
|
78
|
+
end
|
|
79
|
+
|
|
26
80
|
def apply_headers(headers, trace_id:, request_id: nil, span_id: nil, sampled: true)
|
|
27
|
-
|
|
81
|
+
wire_trace_id = normalize_trace_id(trace_id)
|
|
82
|
+
wire_span_id = span_id && !span_id.to_s.empty? ? normalize_span_id(span_id) : nil
|
|
83
|
+
headers["X-AllStak-Trace-Id"] = wire_trace_id
|
|
28
84
|
headers["X-AllStak-Request-Id"] = request_id if request_id && !request_id.to_s.empty?
|
|
29
|
-
if
|
|
30
|
-
headers["X-AllStak-Span-Id"] =
|
|
31
|
-
headers["traceparent"] = "00-#{
|
|
85
|
+
if wire_span_id
|
|
86
|
+
headers["X-AllStak-Span-Id"] = wire_span_id
|
|
87
|
+
headers["traceparent"] = "00-#{wire_trace_id}-#{wire_span_id}-#{trace_flags(sampled)}"
|
|
32
88
|
end
|
|
33
|
-
headers["baggage"] = merge_baggage(headers["baggage"], trace_id:
|
|
34
|
-
headers["AllStak-Baggage"] = baggage(trace_id:
|
|
89
|
+
headers["baggage"] = merge_baggage(headers["baggage"], trace_id: wire_trace_id, request_id: request_id, span_id: wire_span_id)
|
|
90
|
+
headers["AllStak-Baggage"] = baggage(trace_id: wire_trace_id, request_id: request_id, span_id: wire_span_id)
|
|
35
91
|
end
|
|
36
92
|
|
|
37
93
|
def apply_request_headers(req, trace_id:, request_id: nil, span_id: nil, sampled: true)
|
|
38
|
-
|
|
94
|
+
wire_trace_id = normalize_trace_id(trace_id)
|
|
95
|
+
wire_span_id = span_id && !span_id.to_s.empty? ? normalize_span_id(span_id) : nil
|
|
96
|
+
req["X-AllStak-Trace-Id"] ||= wire_trace_id
|
|
39
97
|
req["X-AllStak-Request-Id"] ||= request_id if request_id && !request_id.to_s.empty?
|
|
40
|
-
if
|
|
41
|
-
req["X-AllStak-Span-Id"] ||=
|
|
42
|
-
req["traceparent"] ||= "00-#{
|
|
98
|
+
if wire_span_id
|
|
99
|
+
req["X-AllStak-Span-Id"] ||= wire_span_id
|
|
100
|
+
req["traceparent"] ||= "00-#{wire_trace_id}-#{wire_span_id}-#{trace_flags(sampled)}"
|
|
43
101
|
end
|
|
44
|
-
req["baggage"] = merge_baggage(req["baggage"], trace_id:
|
|
45
|
-
req["AllStak-Baggage"] = baggage(trace_id:
|
|
102
|
+
req["baggage"] = merge_baggage(req["baggage"], trace_id: wire_trace_id, request_id: request_id, span_id: wire_span_id)
|
|
103
|
+
req["AllStak-Baggage"] = baggage(trace_id: wire_trace_id, request_id: request_id, span_id: wire_span_id)
|
|
46
104
|
end
|
|
47
105
|
end
|
|
48
106
|
end
|
data/lib/allstak/sanitizer.rb
CHANGED
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
# falls back to the key-redacted-but-not-value-scrubbed structure.
|
|
34
34
|
|
|
35
35
|
require "set"
|
|
36
|
+
require "thread"
|
|
36
37
|
|
|
37
38
|
module AllStak
|
|
38
39
|
module Sanitizer
|
|
@@ -166,6 +167,28 @@ module AllStak
|
|
|
166
167
|
|
|
167
168
|
module_function
|
|
168
169
|
|
|
170
|
+
@redaction_count = 0
|
|
171
|
+
@redaction_mutex = Mutex.new
|
|
172
|
+
|
|
173
|
+
def redaction_count
|
|
174
|
+
@redaction_mutex.synchronize { @redaction_count }
|
|
175
|
+
rescue StandardError
|
|
176
|
+
0
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def reset_redaction_count!
|
|
180
|
+
@redaction_mutex.synchronize { @redaction_count = 0 }
|
|
181
|
+
nil
|
|
182
|
+
rescue StandardError
|
|
183
|
+
nil
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def record_redaction
|
|
187
|
+
@redaction_mutex.synchronize { @redaction_count += 1 }
|
|
188
|
+
rescue StandardError
|
|
189
|
+
nil
|
|
190
|
+
end
|
|
191
|
+
|
|
169
192
|
# Returns a sanitized deep copy of `payload`.
|
|
170
193
|
#
|
|
171
194
|
# @param extra_denylist [Array<String>, nil] additional key terms to redact;
|
|
@@ -207,6 +230,7 @@ module AllStak
|
|
|
207
230
|
value.each_with_object({}) do |(k, v), out|
|
|
208
231
|
out[k] =
|
|
209
232
|
if sensitive?(k, denylist)
|
|
233
|
+
record_redaction
|
|
210
234
|
REDACTED
|
|
211
235
|
elsif skip_subtree?(k)
|
|
212
236
|
# Explicit user object / stack frames: deep-copy with key-name
|
|
@@ -241,7 +265,12 @@ module AllStak
|
|
|
241
265
|
|
|
242
266
|
seen.add(value.object_id)
|
|
243
267
|
value.each_with_object({}) do |(k, v), out|
|
|
244
|
-
|
|
268
|
+
if sensitive?(k, denylist)
|
|
269
|
+
record_redaction
|
|
270
|
+
out[k] = REDACTED
|
|
271
|
+
else
|
|
272
|
+
out[k] = walk_keys_only(v, denylist, seen)
|
|
273
|
+
end
|
|
245
274
|
end
|
|
246
275
|
when Array
|
|
247
276
|
return REDACTED if seen.include?(value.object_id)
|
|
@@ -282,6 +311,7 @@ module AllStak
|
|
|
282
311
|
out = out.gsub(IPV6_REGEX, REDACTED)
|
|
283
312
|
end
|
|
284
313
|
|
|
314
|
+
record_redaction if out != str
|
|
285
315
|
out
|
|
286
316
|
rescue StandardError
|
|
287
317
|
str
|
|
@@ -107,6 +107,10 @@ module AllStak
|
|
|
107
107
|
@mutex.synchronize { (@started && !@ended) ? @session_id : nil }
|
|
108
108
|
end
|
|
109
109
|
|
|
110
|
+
def recovery_count
|
|
111
|
+
0
|
|
112
|
+
end
|
|
113
|
+
|
|
110
114
|
# Record a HANDLED error: bump status ok -> errored (never downgrades a
|
|
111
115
|
# terminal crash). No I/O.
|
|
112
116
|
def record_error
|
|
@@ -21,6 +21,7 @@ module AllStak
|
|
|
21
21
|
@queue = []
|
|
22
22
|
@stopped = false
|
|
23
23
|
@overflow_warned = false
|
|
24
|
+
@dropped_count = 0
|
|
24
25
|
@flushing_mutex = Mutex.new
|
|
25
26
|
start_timer
|
|
26
27
|
end
|
|
@@ -29,6 +30,7 @@ module AllStak
|
|
|
29
30
|
synchronize do
|
|
30
31
|
if @queue.length >= @max_size
|
|
31
32
|
@queue.shift
|
|
33
|
+
@dropped_count += 1
|
|
32
34
|
unless @overflow_warned
|
|
33
35
|
@overflow_warned = true
|
|
34
36
|
@logger.warn("[AllStak] Buffer #{@name} full (#{@max_size}); oldest events dropped")
|
|
@@ -45,6 +47,10 @@ module AllStak
|
|
|
45
47
|
synchronize { @queue.length }
|
|
46
48
|
end
|
|
47
49
|
|
|
50
|
+
def dropped_count
|
|
51
|
+
synchronize { @dropped_count }
|
|
52
|
+
end
|
|
53
|
+
|
|
48
54
|
def flush
|
|
49
55
|
@flushing_mutex.synchronize do
|
|
50
56
|
drained = synchronize do
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
require "net/http"
|
|
2
2
|
require "uri"
|
|
3
3
|
require "json"
|
|
4
|
+
require "thread"
|
|
5
|
+
require "zlib"
|
|
4
6
|
require_relative "../sanitizer"
|
|
5
7
|
require_relative "event_spool"
|
|
6
8
|
|
|
@@ -16,7 +18,7 @@ module AllStak
|
|
|
16
18
|
# backoff = 1s → 2s → 4s → 8s (+ jitter 0-500ms)
|
|
17
19
|
# max attempts = 5
|
|
18
20
|
# 401 → disable SDK
|
|
19
|
-
# 4xx
|
|
21
|
+
# 4xx except 429 → no retry
|
|
20
22
|
# 5xx / network → retry
|
|
21
23
|
class HttpTransport
|
|
22
24
|
NON_RETRYABLE_STATUSES = [400, 401, 403, 404, 422].freeze
|
|
@@ -25,6 +27,7 @@ module AllStak
|
|
|
25
27
|
RETRY_AFTER_STATUSES = [429, 503].freeze
|
|
26
28
|
# Upper bound on any honored Retry-After delay, in seconds.
|
|
27
29
|
MAX_RETRY_AFTER = 300.0
|
|
30
|
+
COMPRESSION_THRESHOLD_BYTES = 1024
|
|
28
31
|
|
|
29
32
|
# Session lifecycle calls are best-effort LIVE-only — a replayed stale
|
|
30
33
|
# session would skew durations, so they are NEVER spooled to disk.
|
|
@@ -42,6 +45,17 @@ module AllStak
|
|
|
42
45
|
@api_key = config.api_key
|
|
43
46
|
@disabled = false
|
|
44
47
|
@spool = build_spool(config, logger)
|
|
48
|
+
@stats_mutex = Mutex.new
|
|
49
|
+
@sent_count = 0
|
|
50
|
+
@failed_count = 0
|
|
51
|
+
@dropped_count = 0
|
|
52
|
+
@persisted_count = 0
|
|
53
|
+
@replayed_count = 0
|
|
54
|
+
@retry_attempt_count = 0
|
|
55
|
+
@rate_limited_count = 0
|
|
56
|
+
@compressed_count = 0
|
|
57
|
+
@uncompressed_count = 0
|
|
58
|
+
@compression_bytes_saved = 0
|
|
45
59
|
end
|
|
46
60
|
|
|
47
61
|
# The offline spool, or nil when disabled / unavailable. Exposed for
|
|
@@ -68,11 +82,15 @@ module AllStak
|
|
|
68
82
|
AllStak::Sanitizer.scrub(parsed, **scrub_options)
|
|
69
83
|
rescue StandardError => e
|
|
70
84
|
@logger.debug("[AllStak] spool scrub failed; not persisting: #{e.class}: #{e.message}") if @config.debug
|
|
85
|
+
increment(:@dropped_count)
|
|
71
86
|
return false
|
|
72
87
|
end
|
|
73
|
-
@spool.persist(path, scrubbed)
|
|
88
|
+
persisted = @spool.persist(path, scrubbed)
|
|
89
|
+
increment(persisted ? :@persisted_count : :@dropped_count)
|
|
90
|
+
persisted
|
|
74
91
|
rescue StandardError => e
|
|
75
92
|
@logger.debug("[AllStak] persist_failed swallowed: #{e.class}: #{e.message}") if @config.debug
|
|
93
|
+
increment(:@dropped_count)
|
|
76
94
|
false
|
|
77
95
|
end
|
|
78
96
|
|
|
@@ -89,6 +107,8 @@ module AllStak
|
|
|
89
107
|
status, _ = post(path, payload)
|
|
90
108
|
# post() returns for 2xx and non-retryable 4xx; raises otherwise.
|
|
91
109
|
if status && (status < 400 || (status >= 400 && status != 429))
|
|
110
|
+
increment(:@replayed_count) if status < 400
|
|
111
|
+
increment(:@dropped_count) if status >= 400
|
|
92
112
|
@spool.remove(handle)
|
|
93
113
|
end
|
|
94
114
|
rescue AllStakAuthError
|
|
@@ -113,9 +133,26 @@ module AllStak
|
|
|
113
133
|
end
|
|
114
134
|
|
|
115
135
|
def post(path, payload)
|
|
116
|
-
|
|
136
|
+
if @disabled
|
|
137
|
+
increment(:@dropped_count)
|
|
138
|
+
raise AllStakAuthError, "SDK disabled"
|
|
139
|
+
end
|
|
117
140
|
|
|
118
|
-
wire_payload =
|
|
141
|
+
wire_payload =
|
|
142
|
+
begin
|
|
143
|
+
serialize_payload(payload)
|
|
144
|
+
rescue AllStakTransportError
|
|
145
|
+
increment(:@failed_count)
|
|
146
|
+
increment(:@dropped_count)
|
|
147
|
+
raise
|
|
148
|
+
end
|
|
149
|
+
wire_body, compressed, bytes_saved = prepare_body(wire_payload)
|
|
150
|
+
if compressed
|
|
151
|
+
increment(:@compressed_count)
|
|
152
|
+
increment(:@compression_bytes_saved, bytes_saved)
|
|
153
|
+
else
|
|
154
|
+
increment(:@uncompressed_count)
|
|
155
|
+
end
|
|
119
156
|
|
|
120
157
|
uri = URI.parse("#{@base_url}#{path}")
|
|
121
158
|
http = Net::HTTP.new(uri.host, uri.port)
|
|
@@ -136,7 +173,8 @@ module AllStak
|
|
|
136
173
|
"X-AllStak-Key" => @api_key,
|
|
137
174
|
"User-Agent" => "allstak-ruby/#{AllStak::VERSION}"
|
|
138
175
|
})
|
|
139
|
-
req
|
|
176
|
+
req["Content-Encoding"] = "gzip" if compressed
|
|
177
|
+
req.body = wire_body
|
|
140
178
|
@logger.debug("[AllStak] POST #{path} attempt=#{attempt}") if @config.debug
|
|
141
179
|
|
|
142
180
|
resp = http.request(req)
|
|
@@ -145,15 +183,25 @@ module AllStak
|
|
|
145
183
|
|
|
146
184
|
if last_status == 401
|
|
147
185
|
@disabled = true
|
|
186
|
+
increment(:@failed_count)
|
|
187
|
+
increment(:@dropped_count)
|
|
148
188
|
@logger.warn("[AllStak] SDK disabled: invalid API key (401). No further events will be sent.")
|
|
149
189
|
raise AllStakAuthError, "Invalid API key"
|
|
150
190
|
end
|
|
151
191
|
|
|
152
|
-
|
|
153
|
-
|
|
192
|
+
if NON_RETRYABLE_STATUSES.include?(last_status) ||
|
|
193
|
+
(last_status >= 400 && last_status < 500 && last_status != 429)
|
|
194
|
+
increment(:@dropped_count) if last_status >= 400
|
|
195
|
+
return [last_status, body]
|
|
196
|
+
end
|
|
197
|
+
if last_status < 400
|
|
198
|
+
increment(:@sent_count)
|
|
199
|
+
return [last_status, body]
|
|
200
|
+
end
|
|
154
201
|
|
|
155
202
|
# 429 (rate limited) / 503 (unavailable) → honor Retry-After when present.
|
|
156
203
|
if RETRY_AFTER_STATUSES.include?(last_status)
|
|
204
|
+
increment(:@rate_limited_count) if last_status == 429
|
|
157
205
|
parsed = parse_retry_after(resp["Retry-After"])
|
|
158
206
|
retry_after_delay = parsed if parsed > 0
|
|
159
207
|
end
|
|
@@ -171,6 +219,7 @@ module AllStak
|
|
|
171
219
|
end
|
|
172
220
|
|
|
173
221
|
if attempt < max_attempts
|
|
222
|
+
increment(:@retry_attempt_count)
|
|
174
223
|
if retry_after_delay
|
|
175
224
|
# Server told us how long to wait; honor it (already clamped).
|
|
176
225
|
sleep(retry_after_delay)
|
|
@@ -182,10 +231,38 @@ module AllStak
|
|
|
182
231
|
end
|
|
183
232
|
end
|
|
184
233
|
|
|
234
|
+
increment(:@failed_count)
|
|
185
235
|
raise AllStakTransportError,
|
|
186
236
|
"All #{max_attempts} attempts failed for POST #{path}. last_status=#{last_status} last_error=#{last_exc&.message}"
|
|
187
237
|
end
|
|
188
238
|
|
|
239
|
+
def diagnostics
|
|
240
|
+
snapshot = @stats_mutex.synchronize do
|
|
241
|
+
{
|
|
242
|
+
sent: @sent_count,
|
|
243
|
+
failed: @failed_count,
|
|
244
|
+
dropped: @dropped_count,
|
|
245
|
+
persisted: @persisted_count,
|
|
246
|
+
replayed: @replayed_count,
|
|
247
|
+
retry_attempts: @retry_attempt_count,
|
|
248
|
+
rate_limited: @rate_limited_count,
|
|
249
|
+
compressed: @compressed_count,
|
|
250
|
+
uncompressed: @uncompressed_count,
|
|
251
|
+
compression_bytes_saved: @compression_bytes_saved
|
|
252
|
+
}
|
|
253
|
+
end
|
|
254
|
+
snapshot[:queue_size] = @spool&.size.to_i
|
|
255
|
+
snapshot[:disabled] = disabled?
|
|
256
|
+
snapshot
|
|
257
|
+
rescue StandardError
|
|
258
|
+
{
|
|
259
|
+
sent: 0, failed: 0, dropped: 0, persisted: 0, replayed: 0,
|
|
260
|
+
retry_attempts: 0, rate_limited: 0, queue_size: 0,
|
|
261
|
+
compressed: 0, uncompressed: 0, compression_bytes_saved: 0,
|
|
262
|
+
disabled: disabled?
|
|
263
|
+
}
|
|
264
|
+
end
|
|
265
|
+
|
|
189
266
|
def serialize_payload(payload)
|
|
190
267
|
parsed = payload.is_a?(String) ? JSON.parse(payload) : payload
|
|
191
268
|
JSON.generate(AllStak::Sanitizer.scrub(parsed, **scrub_options))
|
|
@@ -238,6 +315,26 @@ module AllStak
|
|
|
238
315
|
|
|
239
316
|
private
|
|
240
317
|
|
|
318
|
+
def prepare_body(wire_payload)
|
|
319
|
+
return [wire_payload, false, 0] if wire_payload.bytesize < COMPRESSION_THRESHOLD_BYTES
|
|
320
|
+
|
|
321
|
+
compressed = Zlib.gzip(wire_payload)
|
|
322
|
+
return [wire_payload, false, 0] if compressed.bytesize >= wire_payload.bytesize
|
|
323
|
+
|
|
324
|
+
[compressed, true, wire_payload.bytesize - compressed.bytesize]
|
|
325
|
+
rescue StandardError
|
|
326
|
+
[wire_payload, false, 0]
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def increment(counter, by = 1)
|
|
330
|
+
@stats_mutex.synchronize do
|
|
331
|
+
value = instance_variable_get(counter).to_i
|
|
332
|
+
instance_variable_set(counter, value + by.to_i)
|
|
333
|
+
end
|
|
334
|
+
rescue StandardError
|
|
335
|
+
nil
|
|
336
|
+
end
|
|
337
|
+
|
|
241
338
|
# Build the offline spool from config, or nil when opted out. The spool
|
|
242
339
|
# itself fails open (an unwritable dir leaves it `available? == false`),
|
|
243
340
|
# so a non-nil-but-unavailable spool is fine — every public method guards
|
data/lib/allstak/version.rb
CHANGED
data/lib/allstak.rb
CHANGED
|
@@ -3,6 +3,7 @@ require "time"
|
|
|
3
3
|
|
|
4
4
|
require_relative "allstak/version"
|
|
5
5
|
require_relative "allstak/config"
|
|
6
|
+
require_relative "allstak/diagnostics"
|
|
6
7
|
require_relative "allstak/transport/http_transport"
|
|
7
8
|
require_relative "allstak/transport/flush_buffer"
|
|
8
9
|
require_relative "allstak/models/user_context"
|
|
@@ -228,6 +229,14 @@ module AllStak
|
|
|
228
229
|
@client&.shutdown
|
|
229
230
|
end
|
|
230
231
|
|
|
232
|
+
# Privacy-safe SDK diagnostics for the active client. Returns a disabled
|
|
233
|
+
# zero snapshot before configure or when configuration is invalid.
|
|
234
|
+
def get_diagnostics
|
|
235
|
+
@client&.diagnostics || Diagnostics.new(disabled: true)
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
alias diagnostics get_diagnostics
|
|
239
|
+
|
|
231
240
|
# Test helper.
|
|
232
241
|
def reset!
|
|
233
242
|
@mutex.synchronize do
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: allstak
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- AllStak
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-06-06 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rake
|
|
@@ -68,6 +68,7 @@ files:
|
|
|
68
68
|
- lib/allstak.rb
|
|
69
69
|
- lib/allstak/client.rb
|
|
70
70
|
- lib/allstak/config.rb
|
|
71
|
+
- lib/allstak/diagnostics.rb
|
|
71
72
|
- lib/allstak/global_handler.rb
|
|
72
73
|
- lib/allstak/integrations/active_record.rb
|
|
73
74
|
- lib/allstak/integrations/logger.rb
|