allstak 0.2.1 → 0.3.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/CHANGELOG.md +37 -5
- data/README.md +45 -0
- data/lib/allstak/client.rb +8 -1
- data/lib/allstak/config.rb +19 -6
- data/lib/allstak/integrations/active_record.rb +18 -0
- data/lib/allstak/integrations/logger.rb +201 -0
- data/lib/allstak/integrations/net_http.rb +18 -0
- data/lib/allstak/integrations/rack.rb +18 -1
- data/lib/allstak/integrations/sidekiq.rb +2 -1
- data/lib/allstak/modules/errors.rb +88 -27
- data/lib/allstak/modules/logs.rb +23 -1
- data/lib/allstak/sanitizer.rb +4 -4
- data/lib/allstak/session_tracker.rb +1 -1
- data/lib/allstak/transport/event_spool.rb +2 -3
- data/lib/allstak/transport/http_transport.rb +1 -1
- data/lib/allstak/version.rb +1 -1
- data/lib/allstak.rb +13 -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: ea16fc5869d09516a29301382b4d59d9e3f33018226516beb208aa1c826a77f3
|
|
4
|
+
data.tar.gz: 6b73ffb1e1319585c2be4c7398181cf9beae32a4ea17914501df6ae1a5df2a66
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1a21a2da36cf561691221369b9cf25fdfc4063300fc3f32452652d6f0987044b9121d719a597db9d6b18d617583e0253e8ee59f08539c8f2ed3c1efe5ffd543a
|
|
7
|
+
data.tar.gz: 43e2c1d26dd502e77fe498e4850352235a63be28fa33ac5c2d61a4ee7171cafda55bc11285d781de96664cdb962becf4df387c83573ccdfa495de3e80fe05695
|
data/CHANGELOG.md
CHANGED
|
@@ -5,11 +5,43 @@ This project follows [Semantic Versioning](https://semver.org/).
|
|
|
5
5
|
|
|
6
6
|
## Unreleased
|
|
7
7
|
|
|
8
|
+
## [0.3.0] — 2026-05-30
|
|
9
|
+
|
|
10
|
+
### Added — Automatic breadcrumbs across all instrumentation
|
|
11
|
+
- Breadcrumbs are now collected automatically from every auto-instrumentation
|
|
12
|
+
layer, not just Sidekiq. The inbound Rack middleware (request method/path/
|
|
13
|
+
status/duration), the outbound `Net::HTTP` patch (method/host/path/status/
|
|
14
|
+
duration), and the ActiveRecord subscriber (truncated SQL/duration/status)
|
|
15
|
+
each emit a breadcrumb, and every `AllStak.log.*` call is bridged into a
|
|
16
|
+
`log` breadcrumb. The trail is attached to the next captured exception with
|
|
17
|
+
no per-call developer code.
|
|
18
|
+
- The breadcrumb ring buffer is now **per-thread** (50 entries), so one
|
|
19
|
+
request/job's trail never leaks into a concurrent request's captured
|
|
20
|
+
exception. Breadcrumb collection is drained on capture as before.
|
|
21
|
+
- New `config.enable_auto_breadcrumbs` (default `true`; env
|
|
22
|
+
`ALLSTAK_AUTO_BREADCRUMBS=0/false` to disable) gates only the automatic
|
|
23
|
+
layers. New top-level `AllStak.add_breadcrumb(...)` records a manual
|
|
24
|
+
breadcrumb and always works regardless of the toggle.
|
|
25
|
+
|
|
26
|
+
### Added — Optional structured-log adapter
|
|
27
|
+
- `AllStak::Integrations::Logger` — a `::Logger`-compatible sink that forwards
|
|
28
|
+
records to `/ingest/v1/logs` via `AllStak.log`. Compose it alongside your
|
|
29
|
+
existing logger (`AllStak::Integrations::Logger.attach_to_rails!` on Rails
|
|
30
|
+
7.1+ BroadcastLogger, or `AllStak::Integrations::Logger.broadcast(logger)`)
|
|
31
|
+
so existing log destinations are preserved and app logs ship automatically.
|
|
32
|
+
- ERROR PROMOTION: records at/above `ERROR` (configurable via
|
|
33
|
+
`error_promotion_level:`) are additionally captured as message error-group
|
|
34
|
+
entries so they surface in the Errors list. Opt out with
|
|
35
|
+
`error_promotion: false`. Opt-in by design and fully fail-open — the adapter
|
|
36
|
+
never raises into the host's logging path and is a no-op until the SDK is
|
|
37
|
+
configured. Defined on require but never auto-attached, so default logging
|
|
38
|
+
behavior is unchanged.
|
|
39
|
+
|
|
8
40
|
## [0.2.0] — 2026-05-29
|
|
9
41
|
|
|
10
42
|
### Added — Release-health session tracking
|
|
11
43
|
- `AllStak::SessionTracker` — server-mode single-session release-health
|
|
12
|
-
lifecycle that mirrors the AllStak
|
|
44
|
+
lifecycle that mirrors the AllStak SDKs. On boot it POSTs
|
|
13
45
|
`/ingest/v1/sessions/start` (off the hot path, on a daemon thread) with a
|
|
14
46
|
per-process `sessionId`, the resolved release, environment, and SDK identity;
|
|
15
47
|
on shutdown it POSTs `/ingest/v1/sessions/end` (synchronous, best-effort)
|
|
@@ -46,11 +78,11 @@ This project follows [Semantic Versioning](https://semver.org/).
|
|
|
46
78
|
### Added — Value-pattern PII scrubbing + sendDefaultPii
|
|
47
79
|
- `AllStak::Sanitizer` gains a second scrubbing layer alongside the existing
|
|
48
80
|
key-name denylist: VALUE-PATTERN redaction that scans free-text string values
|
|
49
|
-
for PII regardless of key name (
|
|
81
|
+
for PII regardless of key name (AllStak data-scrubbing). Tier A is
|
|
50
82
|
ALWAYS scrubbed — Luhn-validated credit-card numbers (13–19 digits, optional
|
|
51
83
|
space/hyphen separators) and hyphenated US SSNs. Tier B — email addresses and
|
|
52
84
|
IPv4/IPv6 — is scrubbed UNLESS `send_default_pii` is enabled.
|
|
53
|
-
- `config.send_default_pii` (default `false`,
|
|
85
|
+
- `config.send_default_pii` (default `false`, ; env
|
|
54
86
|
`ALLSTAK_SEND_DEFAULT_PII`) opts into shipping email/IP values. The
|
|
55
87
|
always-on financial/identity scrubbers are not affected by this flag.
|
|
56
88
|
- Structural exemptions prevent over-redaction: the explicit `user` object and
|
|
@@ -128,8 +160,8 @@ This project follows [Semantic Versioning](https://semver.org/).
|
|
|
128
160
|
logs at Warning level and sends raw — telemetry is never blocked.
|
|
129
161
|
|
|
130
162
|
### Live canary E2E
|
|
131
|
-
-
|
|
132
|
-
-
|
|
163
|
+
- Verified end-to-end against the AllStak ingest API at `api.allstak.sa`.
|
|
164
|
+
- Ingested event confirmed `leak_pos = 0` across `metadata`, `stack_trace`,
|
|
133
165
|
`breadcrumbs`, `message`. Canary `should_not_leak_ruby` planted in
|
|
134
166
|
`password`, `authorization`, `cookie`, `Bearer`, `api_key`, request
|
|
135
167
|
headers / body, `credit_card`, `ssn`, and a 3-level-nested `token`.
|
data/README.md
CHANGED
|
@@ -76,6 +76,50 @@ AllStak.tracing.in_span("checkout.authorize") do
|
|
|
76
76
|
end
|
|
77
77
|
```
|
|
78
78
|
|
|
79
|
+
## Breadcrumbs
|
|
80
|
+
|
|
81
|
+
Breadcrumbs are collected **automatically** — no per-call code. After
|
|
82
|
+
`AllStak.configure`, the SDK records a trail of recent events on a 50-entry
|
|
83
|
+
**per-thread** ring buffer and attaches it to the next captured exception on
|
|
84
|
+
that thread. Auto breadcrumbs come from:
|
|
85
|
+
|
|
86
|
+
- inbound Rack requests (method, path, status, duration),
|
|
87
|
+
- outbound `Net::HTTP` calls (method, host, path, status, duration),
|
|
88
|
+
- ActiveRecord queries (truncated SQL, duration, status),
|
|
89
|
+
- Sidekiq job processing,
|
|
90
|
+
- and every `AllStak.log.*` call.
|
|
91
|
+
|
|
92
|
+
Because the buffer is per-thread, one request's trail never bleeds into a
|
|
93
|
+
concurrent request's exception. You can also add your own:
|
|
94
|
+
|
|
95
|
+
```ruby
|
|
96
|
+
AllStak.add_breadcrumb(type: "auth", message: "password reset requested",
|
|
97
|
+
data: { "userId" => current_user.id })
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Disable automatic collection (manual `add_breadcrumb` still works) with
|
|
101
|
+
`enable_auto_breadcrumbs = false` or `ALLSTAK_AUTO_BREADCRUMBS=0`.
|
|
102
|
+
|
|
103
|
+
## Structured logs (optional)
|
|
104
|
+
|
|
105
|
+
Ship your application logs to AllStak by composing the optional log adapter
|
|
106
|
+
with your existing logger — existing log destinations are preserved:
|
|
107
|
+
|
|
108
|
+
```ruby
|
|
109
|
+
# Rails 7.1+ (broadcast alongside the current logger):
|
|
110
|
+
AllStak::Integrations::Logger.attach_to_rails!
|
|
111
|
+
|
|
112
|
+
# Any Ruby logger:
|
|
113
|
+
logger = AllStak::Integrations::Logger.broadcast(logger)
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Once attached, `logger.info / .warn / .error` ship to `/ingest/v1/logs`
|
|
117
|
+
automatically. Records at `ERROR`/`FATAL` are additionally promoted to error
|
|
118
|
+
entries so they surface in the Errors list. Disable promotion with
|
|
119
|
+
`AllStak::Integrations::Logger.new(error_promotion: false)`, or change the
|
|
120
|
+
threshold with `error_promotion_level:`. The adapter is opt-in and fail-open —
|
|
121
|
+
it never raises into your logging path.
|
|
122
|
+
|
|
79
123
|
## Configuration
|
|
80
124
|
|
|
81
125
|
| Option | Description |
|
|
@@ -86,6 +130,7 @@ end
|
|
|
86
130
|
| `service_name` | Logical service name. |
|
|
87
131
|
| `flush_interval_ms` | Background flush interval. |
|
|
88
132
|
| `buffer_size` | Max buffered events. |
|
|
133
|
+
| `enable_auto_breadcrumbs` | Collect breadcrumbs automatically from the Rack/Net::HTTP/ActiveRecord/Sidekiq instrumentation and the `AllStak.log.*` bridge (default `true`; env `ALLSTAK_AUTO_BREADCRUMBS=0` to disable). Manual `AllStak.add_breadcrumb` is unaffected. |
|
|
89
134
|
| `install_at_exit_handler` | Install a process-wide `at_exit` hook that captures the exception terminating the process as an unhandled event (default `true`). |
|
|
90
135
|
| `before_send` | Callable invoked with the event hash just before transport. Return a modified hash, or `nil` to drop the event. Fails open (sends the original) if it raises. |
|
|
91
136
|
| `sample_rate` | Float in `[0.0, 1.0]` head-sampling rate for error/message events (default `1.0` = keep all). |
|
data/lib/allstak/client.rb
CHANGED
|
@@ -14,7 +14,14 @@ module AllStak
|
|
|
14
14
|
|
|
15
15
|
@errors = Modules::Errors.new(@transport, config, logger,
|
|
16
16
|
session_id_provider: -> { @session_tracker&.current_session_id })
|
|
17
|
-
|
|
17
|
+
# Bridge AllStak.log.* into auto breadcrumbs (gated by the errors module
|
|
18
|
+
# on config.enable_auto_breadcrumbs). `auto: true` so it respects the
|
|
19
|
+
# toggle and never duplicates a manually-added breadcrumb.
|
|
20
|
+
errors = @errors
|
|
21
|
+
@logs = Modules::Logs.new(@transport, config, logger,
|
|
22
|
+
breadcrumb_sink: lambda do |**kw|
|
|
23
|
+
errors.add_breadcrumb(**kw, auto: true)
|
|
24
|
+
end)
|
|
18
25
|
@http = Modules::HttpMonitor.new(@transport, config, logger)
|
|
19
26
|
@tracing = Modules::Tracing.new(@transport, config, logger)
|
|
20
27
|
@database = Modules::Database.new(@transport, config, logger)
|
data/lib/allstak/config.rb
CHANGED
|
@@ -73,15 +73,25 @@ module AllStak
|
|
|
73
73
|
:connect_timeout, :read_timeout, :max_retries,
|
|
74
74
|
:capture_unhandled_exceptions, :capture_http_requests,
|
|
75
75
|
:capture_user_context, :capture_sql,
|
|
76
|
+
# When true (default), the auto-instrumentation layers
|
|
77
|
+
# (inbound Rack requests, outbound Net::HTTP calls,
|
|
78
|
+
# ActiveRecord queries) and the AllStak.log.* bridge emit
|
|
79
|
+
# breadcrumbs into a 50-entry per-thread ring buffer that is
|
|
80
|
+
# attached to the next captured exception. Set false to
|
|
81
|
+
# disable automatic breadcrumb collection entirely (manual
|
|
82
|
+
# AllStak.add_breadcrumb still works). Honors
|
|
83
|
+
# ALLSTAK_AUTO_BREADCRUMBS=0/false.
|
|
84
|
+
:enable_auto_breadcrumbs,
|
|
76
85
|
# Install a process-wide `at_exit` hook that captures the
|
|
77
86
|
# exception terminating the process (global uncaught
|
|
78
87
|
# handler). Opt out by setting this false.
|
|
79
88
|
:install_at_exit_handler,
|
|
80
89
|
# Event pre-processing hook. A callable (proc/lambda) invoked
|
|
81
|
-
# once just before transport with
|
|
82
|
-
# (possibly modified) event Hash, or nil to DROP the
|
|
83
|
-
# Runs for exception + message capture.
|
|
84
|
-
#
|
|
90
|
+
# once just before transport with a sanitized event Hash;
|
|
91
|
+
# returns a (possibly modified) event Hash, or nil to DROP the
|
|
92
|
+
# event. Runs for exception + message capture. The transport
|
|
93
|
+
# sanitizes again after the hook; fail-open sends the
|
|
94
|
+
# sanitized pre-callback event if the hook raises.
|
|
85
95
|
:before_send,
|
|
86
96
|
# Deterministic head-sampling for error/message events in
|
|
87
97
|
# [0.0, 1.0]. 1.0 keeps everything (default); 0.0 drops all.
|
|
@@ -124,7 +134,7 @@ module AllStak
|
|
|
124
134
|
:offline_queue_max_entries,
|
|
125
135
|
:offline_queue_max_bytes,
|
|
126
136
|
:offline_queue_max_age_s,
|
|
127
|
-
#
|
|
137
|
+
# PII control. When FALSE (default), value-pattern
|
|
128
138
|
# scrubbing strips email + IP addresses from free-text values
|
|
129
139
|
# before they hit the wire, and any auto-collected client IP
|
|
130
140
|
# the SDK attaches is dropped. When TRUE, the caller has opted
|
|
@@ -197,7 +207,7 @@ module AllStak
|
|
|
197
207
|
@offline_queue_max_entries = nil
|
|
198
208
|
@offline_queue_max_bytes = nil
|
|
199
209
|
@offline_queue_max_age_s = nil
|
|
200
|
-
#
|
|
210
|
+
# Default FALSE. Opt in via ALLSTAK_SEND_DEFAULT_PII.
|
|
201
211
|
@send_default_pii = %w[1 true yes on].include?(ENV["ALLSTAK_SEND_DEFAULT_PII"].to_s.strip.downcase)
|
|
202
212
|
@extra_denylist = parse_extra_denylist(ENV["ALLSTAK_EXTRA_DENYLIST"])
|
|
203
213
|
@service_name = ENV["ALLSTAK_SERVICE"] || "ruby-service"
|
|
@@ -211,6 +221,9 @@ module AllStak
|
|
|
211
221
|
@capture_http_requests = true
|
|
212
222
|
@capture_user_context = true
|
|
213
223
|
@capture_sql = true
|
|
224
|
+
# Auto breadcrumbs: default ON for server runtimes. Honors
|
|
225
|
+
# ALLSTAK_AUTO_BREADCRUMBS=0/false.
|
|
226
|
+
@enable_auto_breadcrumbs = !%w[0 false no off].include?(ENV["ALLSTAK_AUTO_BREADCRUMBS"].to_s.strip.downcase)
|
|
214
227
|
@install_at_exit_handler = true
|
|
215
228
|
@before_send = nil
|
|
216
229
|
@sample_rate = 1.0
|
|
@@ -60,6 +60,24 @@ module AllStak
|
|
|
60
60
|
trace_id: client.tracing.current_trace_id,
|
|
61
61
|
span_id: client.tracing.current_span_id
|
|
62
62
|
)
|
|
63
|
+
|
|
64
|
+
# Query breadcrumb so the recent SQL trail lands on the next
|
|
65
|
+
# captured exception in this thread. The SQL is truncated for the
|
|
66
|
+
# breadcrumb message; value-pattern PII scrubbing still runs on the
|
|
67
|
+
# wire path. Auto-gated via config.enable_auto_breadcrumbs.
|
|
68
|
+
client.errors.add_breadcrumb(
|
|
69
|
+
type: "query",
|
|
70
|
+
message: sql.length > 300 ? "#{sql[0, 300]}…" : sql,
|
|
71
|
+
level: status == "error" ? "error" : "info",
|
|
72
|
+
data: {
|
|
73
|
+
"name" => name.empty? ? nil : name,
|
|
74
|
+
"durationMs" => event.duration.to_i,
|
|
75
|
+
"status" => status,
|
|
76
|
+
"db" => db_name,
|
|
77
|
+
"dbType" => db_type
|
|
78
|
+
}.reject { |_, v| v.nil? },
|
|
79
|
+
auto: true
|
|
80
|
+
)
|
|
63
81
|
rescue => e
|
|
64
82
|
# never raise into host
|
|
65
83
|
AllStak.logger.debug("[AllStak] AR subscriber error: #{e.message}") rescue nil
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
require "logger"
|
|
2
|
+
|
|
3
|
+
module AllStak
|
|
4
|
+
module Integrations
|
|
5
|
+
# Optional structured-log adapter.
|
|
6
|
+
#
|
|
7
|
+
# A drop-in {::Logger}-compatible sink that forwards every log record to
|
|
8
|
+
# AllStak's `/ingest/v1/logs` endpoint (via {AllStak.log}). It is intended
|
|
9
|
+
# to be *broadcast alongside* your existing logger so app logs keep going to
|
|
10
|
+
# STDOUT/file unchanged while also flowing into AllStak — no per-call code.
|
|
11
|
+
#
|
|
12
|
+
# ERROR PROMOTION: records at or above {#error_promotion_level} (default
|
|
13
|
+
# ERROR) are additionally captured as AllStak "message" error-group entries
|
|
14
|
+
# (via {AllStak.capture_message}) so error-level logs surface in the Errors
|
|
15
|
+
# list, not just the log stream. Set `error_promotion: false` to disable.
|
|
16
|
+
#
|
|
17
|
+
# OPT-IN by design — adding this adapter is the only manual step; after that
|
|
18
|
+
# `Rails.logger.error(...)` / `logger.warn(...)` ship automatically.
|
|
19
|
+
#
|
|
20
|
+
# # Rails 7.1+ (BroadcastLogger) — keep existing logging, add AllStak:
|
|
21
|
+
# AllStak::Integrations::Logger.attach_to_rails!
|
|
22
|
+
#
|
|
23
|
+
# # Or compose manually with any Ruby Logger:
|
|
24
|
+
# sink = AllStak::Integrations::Logger.new
|
|
25
|
+
# Rails.logger.broadcast_to(sink) # Rails 7.1+
|
|
26
|
+
# # ...or wrap a single logger so both get every line:
|
|
27
|
+
# Rails.logger = AllStak::Integrations::Logger.broadcast(Rails.logger)
|
|
28
|
+
#
|
|
29
|
+
# Fully fail-open: a transport/SDK error never propagates into the host's
|
|
30
|
+
# logging path, and the adapter is a graceful no-op when the SDK is not
|
|
31
|
+
# configured.
|
|
32
|
+
class Logger < ::Logger
|
|
33
|
+
# Map Ruby Logger severities to AllStak log levels.
|
|
34
|
+
SEVERITY_TO_LEVEL = {
|
|
35
|
+
::Logger::DEBUG => "debug",
|
|
36
|
+
::Logger::INFO => "info",
|
|
37
|
+
::Logger::WARN => "warn",
|
|
38
|
+
::Logger::ERROR => "error",
|
|
39
|
+
::Logger::FATAL => "fatal",
|
|
40
|
+
::Logger::UNKNOWN => "error"
|
|
41
|
+
}.freeze
|
|
42
|
+
|
|
43
|
+
attr_reader :error_promotion_level
|
|
44
|
+
|
|
45
|
+
# @param level [Integer] minimum severity to forward (default DEBUG).
|
|
46
|
+
# @param error_promotion [Boolean] capture >= error_promotion_level
|
|
47
|
+
# records as AllStak message events too (default true).
|
|
48
|
+
# @param error_promotion_level [Integer] severity threshold for promotion
|
|
49
|
+
# (default ::Logger::ERROR).
|
|
50
|
+
def initialize(level: ::Logger::DEBUG, error_promotion: true,
|
|
51
|
+
error_promotion_level: ::Logger::ERROR)
|
|
52
|
+
# logdev=nil: this sink does not write to any device of its own; it only
|
|
53
|
+
# forwards into AllStak. Composition (broadcast) keeps the real device.
|
|
54
|
+
super(nil)
|
|
55
|
+
self.level = level
|
|
56
|
+
@error_promotion = error_promotion != false
|
|
57
|
+
@error_promotion_level = error_promotion_level
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Core Logger entry point. Every severity helper (#debug/#info/#warn/
|
|
61
|
+
# #error/#fatal/#unknown) and `<<` funnel through #add, so overriding it
|
|
62
|
+
# captures the whole surface. Returns true (Logger#add contract) and never
|
|
63
|
+
# raises into the host.
|
|
64
|
+
def add(severity, message = nil, progname = nil, &block)
|
|
65
|
+
severity ||= ::Logger::UNKNOWN
|
|
66
|
+
return true if severity < level
|
|
67
|
+
|
|
68
|
+
text = resolve_message(message, progname, &block)
|
|
69
|
+
forward(severity, text, progname)
|
|
70
|
+
true
|
|
71
|
+
rescue StandardError
|
|
72
|
+
true
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# `logger << "raw"` writes an UNKNOWN-severity record in Ruby Logger.
|
|
76
|
+
def <<(msg)
|
|
77
|
+
add(::Logger::UNKNOWN, msg.to_s)
|
|
78
|
+
msg
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
class << self
|
|
82
|
+
# Compose `existing` with an AllStak sink so both receive every record.
|
|
83
|
+
# Prefers Rails' BroadcastLogger when available; otherwise returns a
|
|
84
|
+
# {BroadcastLogger} shim. Returns `existing` unchanged on any failure.
|
|
85
|
+
def broadcast(existing, **opts)
|
|
86
|
+
sink = new(**opts)
|
|
87
|
+
if defined?(::ActiveSupport::BroadcastLogger)
|
|
88
|
+
::ActiveSupport::BroadcastLogger.new(existing, sink)
|
|
89
|
+
else
|
|
90
|
+
BroadcastLogger.new([existing, sink])
|
|
91
|
+
end
|
|
92
|
+
rescue StandardError
|
|
93
|
+
existing
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Attach an AllStak sink to the current Rails.logger without replacing
|
|
97
|
+
# the existing logging destinations. Idempotent and fail-open. Returns
|
|
98
|
+
# true when (now) attached, false otherwise (no Rails / no logger).
|
|
99
|
+
def attach_to_rails!(**opts)
|
|
100
|
+
return false unless defined?(::Rails) && ::Rails.respond_to?(:logger)
|
|
101
|
+
current = ::Rails.logger
|
|
102
|
+
return false if current.nil?
|
|
103
|
+
return true if @attached_to_rails
|
|
104
|
+
|
|
105
|
+
sink = new(**opts)
|
|
106
|
+
if current.respond_to?(:broadcast_to)
|
|
107
|
+
# Rails 7.1+ BroadcastLogger — add without disturbing existing sinks.
|
|
108
|
+
current.broadcast_to(sink)
|
|
109
|
+
else
|
|
110
|
+
::Rails.logger = broadcast(current, **opts)
|
|
111
|
+
end
|
|
112
|
+
@attached_to_rails = true
|
|
113
|
+
true
|
|
114
|
+
rescue StandardError
|
|
115
|
+
false
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Test seam.
|
|
119
|
+
def reset_attached!
|
|
120
|
+
@attached_to_rails = false
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
private
|
|
125
|
+
|
|
126
|
+
# Ruby Logger's message-resolution rules: an explicit message wins; else a
|
|
127
|
+
# block's value; else the progname is treated as the message.
|
|
128
|
+
def resolve_message(message, progname, &block)
|
|
129
|
+
if message.nil?
|
|
130
|
+
block ? block.call : progname
|
|
131
|
+
else
|
|
132
|
+
message
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def forward(severity, text, progname)
|
|
137
|
+
return if text.nil?
|
|
138
|
+
return unless AllStak.respond_to?(:initialized?) && AllStak.initialized?
|
|
139
|
+
|
|
140
|
+
level = SEVERITY_TO_LEVEL.fetch(severity, "info")
|
|
141
|
+
msg = text.to_s
|
|
142
|
+
return if msg.empty?
|
|
143
|
+
|
|
144
|
+
metadata = {}
|
|
145
|
+
metadata["logger"] = progname.to_s unless progname.nil? || progname.to_s.empty?
|
|
146
|
+
|
|
147
|
+
sink = AllStak.log
|
|
148
|
+
sink&.log(level, msg, metadata: metadata.empty? ? nil : metadata)
|
|
149
|
+
|
|
150
|
+
# ERROR PROMOTION: surface error/fatal logs as message error-group
|
|
151
|
+
# entries too, so they appear in the Errors list. Best-effort.
|
|
152
|
+
if @error_promotion && severity >= @error_promotion_level
|
|
153
|
+
AllStak.capture_message(msg, level: level, metadata: metadata.empty? ? nil : metadata)
|
|
154
|
+
end
|
|
155
|
+
rescue StandardError
|
|
156
|
+
nil
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Minimal broadcast shim for runtimes without ActiveSupport::BroadcastLogger
|
|
160
|
+
# (plain Ruby, older Rails). Fans every public Logger call out to each
|
|
161
|
+
# delegate; reads (e.g. #level) come from the first. Fail-open per call.
|
|
162
|
+
class BroadcastLogger
|
|
163
|
+
def initialize(loggers)
|
|
164
|
+
@loggers = Array(loggers)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
%i[debug info warn error fatal unknown add log <<].each do |m|
|
|
168
|
+
define_method(m) do |*args, &block|
|
|
169
|
+
@loggers.each do |l|
|
|
170
|
+
begin
|
|
171
|
+
l.public_send(m, *args, &block) if l.respond_to?(m)
|
|
172
|
+
rescue StandardError
|
|
173
|
+
nil
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
true
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Read-ish delegations resolve against the first logger.
|
|
181
|
+
def level
|
|
182
|
+
@loggers.first&.level
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def level=(value)
|
|
186
|
+
@loggers.each { |l| l.level = value if l.respond_to?(:level=) }
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def respond_to_missing?(name, include_private = false)
|
|
190
|
+
@loggers.any? { |l| l.respond_to?(name, include_private) } || super
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def method_missing(name, *args, &block)
|
|
194
|
+
target = @loggers.find { |l| l.respond_to?(name) }
|
|
195
|
+
return super unless target
|
|
196
|
+
target.public_send(name, *args, &block)
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
@@ -70,6 +70,24 @@ module AllStak
|
|
|
70
70
|
span_id: span_id,
|
|
71
71
|
error_fingerprint: error_fp
|
|
72
72
|
)
|
|
73
|
+
|
|
74
|
+
# Outbound-call breadcrumb so it lands on the trail of any
|
|
75
|
+
# exception captured later in the same thread. Auto-gated.
|
|
76
|
+
client.errors.add_breadcrumb(
|
|
77
|
+
type: "http",
|
|
78
|
+
message: "#{method} #{host}#{path} #{status}",
|
|
79
|
+
level: (error_fp || status >= 500) ? "error" : "info",
|
|
80
|
+
data: {
|
|
81
|
+
"direction" => "outbound",
|
|
82
|
+
"method" => method,
|
|
83
|
+
"host" => host,
|
|
84
|
+
"path" => path,
|
|
85
|
+
"status" => status,
|
|
86
|
+
"durationMs" => duration,
|
|
87
|
+
"error" => error_fp
|
|
88
|
+
}.reject { |_, v| v.nil? },
|
|
89
|
+
auto: true
|
|
90
|
+
)
|
|
73
91
|
rescue
|
|
74
92
|
# never raise into host
|
|
75
93
|
end
|
|
@@ -80,6 +80,23 @@ module AllStak
|
|
|
80
80
|
)
|
|
81
81
|
span.set_tag("http.status_code", status.to_i.to_s)
|
|
82
82
|
span.finish(status.to_i >= 500 || captured ? "error" : "ok")
|
|
83
|
+
|
|
84
|
+
# Inbound-request breadcrumb so it lands on the trail of any
|
|
85
|
+
# exception captured later in the same thread. Auto-gated.
|
|
86
|
+
client.errors.add_breadcrumb(
|
|
87
|
+
type: "http",
|
|
88
|
+
message: "#{env["REQUEST_METHOD"] || "GET"} #{path} #{status.to_i}",
|
|
89
|
+
level: (status.to_i >= 500 || captured) ? "error" : "info",
|
|
90
|
+
data: {
|
|
91
|
+
"direction" => "inbound",
|
|
92
|
+
"method" => env["REQUEST_METHOD"] || "GET",
|
|
93
|
+
"host" => env["HTTP_HOST"] || "localhost",
|
|
94
|
+
"path" => path,
|
|
95
|
+
"status" => status.to_i,
|
|
96
|
+
"durationMs" => duration
|
|
97
|
+
},
|
|
98
|
+
auto: true
|
|
99
|
+
)
|
|
83
100
|
rescue => err
|
|
84
101
|
# never raise into host
|
|
85
102
|
config.debug && warn("[AllStak] rack request capture failed: #{err.message}")
|
|
@@ -167,7 +184,7 @@ module AllStak
|
|
|
167
184
|
email = env["allstak.user_email"]
|
|
168
185
|
return nil if id.nil? && email.nil?
|
|
169
186
|
# The client IP here is AUTO-collected by the middleware (not set
|
|
170
|
-
# explicitly by the app via setUser).
|
|
187
|
+
# explicitly by the app via setUser). Privacy default: drop it unless
|
|
171
188
|
# the caller opted into PII via send_default_pii. Guarded so a nil/old
|
|
172
189
|
# config defaults to the privacy-preserving behavior.
|
|
173
190
|
send_pii = config.respond_to?(:send_default_pii?) ? config.send_default_pii? : false
|
|
@@ -117,7 +117,8 @@ module AllStak
|
|
|
117
117
|
client.errors.add_breadcrumb(
|
|
118
118
|
type: "sidekiq",
|
|
119
119
|
message: "process #{worker_class}",
|
|
120
|
-
data: { "queue" => job_queue, "jid" => jid }.reject { |_, v| v.nil? }
|
|
120
|
+
data: { "queue" => job_queue, "jid" => jid }.reject { |_, v| v.nil? },
|
|
121
|
+
auto: true
|
|
121
122
|
)
|
|
122
123
|
|
|
123
124
|
span = client.tracing.start_span(
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
require "json"
|
|
2
2
|
require_relative "../sampling"
|
|
3
|
+
require_relative "../sanitizer"
|
|
3
4
|
|
|
4
5
|
module AllStak
|
|
5
6
|
module Modules
|
|
@@ -8,13 +9,17 @@ module AllStak
|
|
|
8
9
|
PATH = "/ingest/v1/errors".freeze
|
|
9
10
|
MAX_BREADCRUMBS = 50
|
|
10
11
|
|
|
12
|
+
# Thread-local key for the per-thread breadcrumb ring buffer. Each
|
|
13
|
+
# request/job runs on its own thread, so a per-thread buffer keeps one
|
|
14
|
+
# thread's breadcrumb trail from bleeding into another concurrent
|
|
15
|
+
# request's captured exception.
|
|
16
|
+
BREADCRUMB_TLS_KEY = :allstak_breadcrumbs
|
|
17
|
+
|
|
11
18
|
def initialize(transport, config, logger, session_id_provider: nil)
|
|
12
19
|
@transport = transport
|
|
13
20
|
@config = config
|
|
14
21
|
@logger = logger
|
|
15
22
|
@current_user = nil
|
|
16
|
-
@breadcrumbs = []
|
|
17
|
-
@breadcrumb_mutex = Mutex.new
|
|
18
23
|
# Optional callable returning the active release-health session id, so
|
|
19
24
|
# the backend's error consumer can mark the session errored/crashed.
|
|
20
25
|
@session_id_provider = session_id_provider
|
|
@@ -28,28 +33,34 @@ module AllStak
|
|
|
28
33
|
@current_user = nil
|
|
29
34
|
end
|
|
30
35
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
36
|
+
# Append a breadcrumb to the current thread's ring buffer.
|
|
37
|
+
#
|
|
38
|
+
# `auto: true` marks the crumb as produced by an auto-instrumentation
|
|
39
|
+
# layer (Rack/Net::HTTP/ActiveRecord/log bridge); those are suppressed
|
|
40
|
+
# when `config.enable_auto_breadcrumbs` is false. Manual breadcrumbs
|
|
41
|
+
# (`auto: false`, the default) are always recorded so existing callers
|
|
42
|
+
# (e.g. the Sidekiq middleware) keep working unchanged. Fail-open: a
|
|
43
|
+
# malformed crumb never raises into the host.
|
|
44
|
+
def add_breadcrumb(type:, message:, level: "info", data: nil, auto: false)
|
|
45
|
+
return if auto && !auto_breadcrumbs_enabled?
|
|
46
|
+
buffer = thread_breadcrumbs
|
|
47
|
+
buffer.shift if buffer.length >= MAX_BREADCRUMBS
|
|
48
|
+
buffer << {
|
|
49
|
+
timestamp: Time.now.utc.iso8601(6),
|
|
50
|
+
type: type,
|
|
51
|
+
message: message,
|
|
52
|
+
level: level,
|
|
53
|
+
data: data
|
|
54
|
+
}.compact
|
|
55
|
+
nil
|
|
56
|
+
rescue StandardError
|
|
57
|
+
nil
|
|
42
58
|
end
|
|
43
59
|
|
|
44
60
|
def capture_exception(exc, level: "error", user: nil, request_context: nil, trace_id: nil, metadata: nil)
|
|
45
61
|
return nil if @transport.disabled?
|
|
46
62
|
begin
|
|
47
|
-
crumbs =
|
|
48
|
-
next nil if @breadcrumbs.empty?
|
|
49
|
-
out = @breadcrumbs.dup
|
|
50
|
-
@breadcrumbs.clear
|
|
51
|
-
out
|
|
52
|
-
end
|
|
63
|
+
crumbs = drain_breadcrumbs
|
|
53
64
|
|
|
54
65
|
payload = {
|
|
55
66
|
exceptionClass: exc.class.name,
|
|
@@ -76,7 +87,9 @@ module AllStak
|
|
|
76
87
|
payload.delete(:user) if payload[:user]&.empty?
|
|
77
88
|
payload.delete(:requestContext) if payload[:requestContext]&.empty?
|
|
78
89
|
|
|
79
|
-
# Sampling first, then before_send,
|
|
90
|
+
# Sampling first, then pre-hook scrub, before_send, and final
|
|
91
|
+
# transport scrub. Hooks never see raw secrets and cannot reintroduce
|
|
92
|
+
# values that escape the wire-path sanitizer.
|
|
80
93
|
return nil unless Sampling.sampled?(@config.sample_rate)
|
|
81
94
|
payload = apply_before_send(payload)
|
|
82
95
|
return nil if payload.nil?
|
|
@@ -119,7 +132,8 @@ module AllStak
|
|
|
119
132
|
payload.delete(:user) if payload[:user]&.empty?
|
|
120
133
|
payload.delete(:requestContext) if payload[:requestContext]&.empty?
|
|
121
134
|
|
|
122
|
-
# Sampling first, then before_send,
|
|
135
|
+
# Sampling first, then pre-hook scrub, before_send, and final
|
|
136
|
+
# transport scrub.
|
|
123
137
|
return nil unless Sampling.sampled?(@config.sample_rate)
|
|
124
138
|
payload = apply_before_send(payload)
|
|
125
139
|
return nil if payload.nil?
|
|
@@ -140,6 +154,28 @@ module AllStak
|
|
|
140
154
|
|
|
141
155
|
private
|
|
142
156
|
|
|
157
|
+
# The current thread's breadcrumb ring buffer (lazily created).
|
|
158
|
+
def thread_breadcrumbs
|
|
159
|
+
Thread.current[BREADCRUMB_TLS_KEY] ||= []
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Snapshot + clear the current thread's breadcrumbs for attachment to an
|
|
163
|
+
# outgoing event. Returns nil when empty so the payload omits the key.
|
|
164
|
+
def drain_breadcrumbs
|
|
165
|
+
buffer = Thread.current[BREADCRUMB_TLS_KEY]
|
|
166
|
+
return nil if buffer.nil? || buffer.empty?
|
|
167
|
+
out = buffer.dup
|
|
168
|
+
buffer.clear
|
|
169
|
+
out
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Whether auto-emitted breadcrumbs are enabled. Defaults to true if the
|
|
173
|
+
# (possibly older/stubbed) config doesn't expose the flag.
|
|
174
|
+
def auto_breadcrumbs_enabled?
|
|
175
|
+
return true unless @config.respond_to?(:enable_auto_breadcrumbs)
|
|
176
|
+
@config.enable_auto_breadcrumbs != false
|
|
177
|
+
end
|
|
178
|
+
|
|
143
179
|
# Resolve the active release-health session id via the injected provider.
|
|
144
180
|
# Fail-open: any error yields nil so capture is never blocked.
|
|
145
181
|
def current_session_id
|
|
@@ -151,19 +187,44 @@ module AllStak
|
|
|
151
187
|
end
|
|
152
188
|
|
|
153
189
|
# Run the user-supplied before_send hook. Returns the (possibly modified)
|
|
154
|
-
# event, or nil to drop.
|
|
155
|
-
#
|
|
190
|
+
# event, or nil to drop. The hook receives a sanitized copy and the
|
|
191
|
+
# transport sanitizes again after the hook. Fail-open: if the hook raises,
|
|
192
|
+
# log and return the sanitized event so telemetry is never lost to a buggy
|
|
193
|
+
# hook and raw secrets are not exposed.
|
|
156
194
|
def apply_before_send(payload)
|
|
157
195
|
hook = @config.before_send
|
|
158
|
-
|
|
196
|
+
sanitized = sanitize_before_send(payload)
|
|
197
|
+
return sanitized unless hook.respond_to?(:call)
|
|
159
198
|
begin
|
|
160
|
-
hook.call(
|
|
199
|
+
hook.call(sanitized)
|
|
161
200
|
rescue => e
|
|
162
|
-
@logger.warn("[AllStak] before_send raised; sending
|
|
163
|
-
|
|
201
|
+
@logger.warn("[AllStak] before_send raised; sending sanitized event: #{e.class}: #{e.message}")
|
|
202
|
+
sanitized
|
|
164
203
|
end
|
|
165
204
|
end
|
|
166
205
|
|
|
206
|
+
def sanitize_before_send(payload)
|
|
207
|
+
AllStak::Sanitizer.scrub(payload, **sanitizer_options)
|
|
208
|
+
rescue => e
|
|
209
|
+
@logger.warn("[AllStak] pre-before_send sanitizer failed; sending redacted event: #{e.class}: #{e.message}")
|
|
210
|
+
redacted_payload(payload)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def sanitizer_options
|
|
214
|
+
{
|
|
215
|
+
extra_denylist: @config.respond_to?(:extra_denylist) ? @config.extra_denylist : nil,
|
|
216
|
+
send_default_pii: @config.respond_to?(:send_default_pii?) ? @config.send_default_pii? : false
|
|
217
|
+
}
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def redacted_payload(payload)
|
|
221
|
+
out = payload.dup
|
|
222
|
+
out[:message] = AllStak::Sanitizer::REDACTED
|
|
223
|
+
out[:metadata] = { "redacted" => true }
|
|
224
|
+
out.delete(:breadcrumbs)
|
|
225
|
+
out
|
|
226
|
+
end
|
|
227
|
+
|
|
167
228
|
def extract_frames(exc)
|
|
168
229
|
return [] unless exc.backtrace.is_a?(Array)
|
|
169
230
|
exc.backtrace.first(50)
|
data/lib/allstak/modules/logs.rb
CHANGED
|
@@ -5,10 +5,14 @@ module AllStak
|
|
|
5
5
|
PATH = "/ingest/v1/logs".freeze
|
|
6
6
|
VALID_LEVELS = %w[debug info warn error fatal].freeze
|
|
7
7
|
|
|
8
|
-
def initialize(transport, config, logger)
|
|
8
|
+
def initialize(transport, config, logger, breadcrumb_sink: nil)
|
|
9
9
|
@transport = transport
|
|
10
10
|
@config = config
|
|
11
11
|
@logger = logger
|
|
12
|
+
# Optional callable invoked for each accepted log so AllStak.log.*
|
|
13
|
+
# entries also surface as breadcrumbs on the next captured exception.
|
|
14
|
+
# Injected by the client; nil keeps Logs standalone (and recursion-free).
|
|
15
|
+
@breadcrumb_sink = breadcrumb_sink
|
|
12
16
|
@buffer = Transport::FlushBuffer.new(
|
|
13
17
|
name: "logs",
|
|
14
18
|
max_size: config.buffer_size,
|
|
@@ -37,6 +41,8 @@ module AllStak
|
|
|
37
41
|
metadata: @config.release_tags.merge(metadata || {})
|
|
38
42
|
}.compact
|
|
39
43
|
@buffer.push(payload)
|
|
44
|
+
emit_breadcrumb(level, message, trace_id: trace_id, span_id: span_id)
|
|
45
|
+
nil
|
|
40
46
|
end
|
|
41
47
|
|
|
42
48
|
def debug(msg, **kw); log("debug", msg, **kw); end
|
|
@@ -55,6 +61,22 @@ module AllStak
|
|
|
55
61
|
|
|
56
62
|
private
|
|
57
63
|
|
|
64
|
+
# Bridge an accepted log into a breadcrumb via the injected sink. The
|
|
65
|
+
# sink is the errors module's add_breadcrumb (auto-gated), so this is a
|
|
66
|
+
# no-op when breadcrumbs are disabled or no sink was wired. Fail-open.
|
|
67
|
+
def emit_breadcrumb(level, message, trace_id: nil, span_id: nil)
|
|
68
|
+
sink = @breadcrumb_sink
|
|
69
|
+
return unless sink.respond_to?(:call)
|
|
70
|
+
sink.call(
|
|
71
|
+
type: "log",
|
|
72
|
+
message: message.to_s,
|
|
73
|
+
level: level,
|
|
74
|
+
data: { "traceId" => trace_id, "spanId" => span_id }.reject { |_, v| v.nil? }
|
|
75
|
+
)
|
|
76
|
+
rescue StandardError
|
|
77
|
+
nil
|
|
78
|
+
end
|
|
79
|
+
|
|
58
80
|
def normalize_level(level)
|
|
59
81
|
lv = level.to_s.downcase
|
|
60
82
|
lv = "warn" if lv == "warning"
|
data/lib/allstak/sanitizer.rb
CHANGED
|
@@ -11,13 +11,13 @@
|
|
|
11
11
|
# Hash keys against the canonical denylist. Conforms to the canonical
|
|
12
12
|
# AllStak SDK denylist defined in docs/standards/sdk-platform-standards.md.
|
|
13
13
|
#
|
|
14
|
-
# 2. VALUE-PATTERN redaction (
|
|
14
|
+
# 2. VALUE-PATTERN redaction (data scrubbing): scans free-text
|
|
15
15
|
# *string values* for PII that leaks regardless of key name. Two tiers:
|
|
16
16
|
# A) ALWAYS scrubbed — credit-card numbers that pass the Luhn checksum,
|
|
17
17
|
# and US SSNs written with hyphens. High-risk financial/identity data
|
|
18
18
|
# never legitimately wanted in telemetry.
|
|
19
19
|
# B) Scrubbed UNLESS send_default_pii — email addresses and IPv4
|
|
20
|
-
# addresses. Default send_default_pii=false
|
|
20
|
+
# addresses. Default send_default_pii=false (privacy-safe).
|
|
21
21
|
#
|
|
22
22
|
# Semantics:
|
|
23
23
|
# - Key match: case-insensitive substring match on Hash keys.
|
|
@@ -133,7 +133,7 @@ module AllStak
|
|
|
133
133
|
|
|
134
134
|
# Top-level subtrees that are never value-scrubbed. `user` holds data the
|
|
135
135
|
# caller explicitly set via setUser (intentional identification — ships as
|
|
136
|
-
# before
|
|
136
|
+
# before). `frames`/`stackTrace` hold structured stack
|
|
137
137
|
# frames whose filenames/functions must not be corrupted.
|
|
138
138
|
VALUE_SCRUB_SKIP_SUBTREES = %w[
|
|
139
139
|
user
|
|
@@ -172,7 +172,7 @@ module AllStak
|
|
|
172
172
|
# may extend but not narrow the canonical list.
|
|
173
173
|
# @param send_default_pii [Boolean] when true, the tier-B value scrubbers
|
|
174
174
|
# (email, IPv4/IPv6) are disabled — the caller has opted into PII. Tier-A
|
|
175
|
-
# (credit card, SSN) is ALWAYS applied. Default false (
|
|
175
|
+
# (credit card, SSN) is ALWAYS applied. Default false (privacy-safe).
|
|
176
176
|
# @param values [Boolean] when false, only key-name redaction runs (no
|
|
177
177
|
# value-pattern scrubbing). Useful for an intermediate pre-scrub (e.g.
|
|
178
178
|
# Sidekiq job args) where the wire-path scrub will value-scrub later with
|
|
@@ -23,7 +23,7 @@ module AllStak
|
|
|
23
23
|
PATH_END = "/ingest/v1/sessions/end".freeze
|
|
24
24
|
|
|
25
25
|
# Lifecycle status. Vocabulary matches the backend `/sessions/end` contract
|
|
26
|
-
# and
|
|
26
|
+
# and standard release-health conventions:
|
|
27
27
|
# ok — ended normally, at most non-fatal logs.
|
|
28
28
|
# errored — at least one HANDLED error captured; process kept running.
|
|
29
29
|
# crashed — an UNHANDLED/fatal exception ended the process.
|
|
@@ -15,9 +15,8 @@ module AllStak
|
|
|
15
15
|
# { "v" => 1, "path" => "/ingest/v1/errors", "payload" => {...},
|
|
16
16
|
# "ts" => 1700000000.123 }
|
|
17
17
|
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
# and replayed on the next init.
|
|
18
|
+
# Offline durability: an on-disk transport cache — events that cannot be
|
|
19
|
+
# delivered are persisted (PII-scrubbed) and replayed on the next init.
|
|
21
20
|
#
|
|
22
21
|
# HARD invariants:
|
|
23
22
|
# * Fail-open everywhere. A read-only FS, a sandboxed/serverless runtime,
|
|
@@ -196,7 +196,7 @@ module AllStak
|
|
|
196
196
|
|
|
197
197
|
# Sanitizer options derived from config. Guarded with respond_to? so a
|
|
198
198
|
# bare/stub config (some transport unit tests) still scrubs with safe
|
|
199
|
-
# defaults: PII off (
|
|
199
|
+
# defaults: PII off (privacy-safe), no extra denylist.
|
|
200
200
|
def scrub_options
|
|
201
201
|
{
|
|
202
202
|
send_default_pii: @config.respond_to?(:send_default_pii?) ? @config.send_default_pii? : false,
|
data/lib/allstak/version.rb
CHANGED
data/lib/allstak.rb
CHANGED
|
@@ -21,6 +21,10 @@ require_relative "allstak/integrations/rack"
|
|
|
21
21
|
require_relative "allstak/integrations/active_record"
|
|
22
22
|
require_relative "allstak/integrations/net_http"
|
|
23
23
|
require_relative "allstak/integrations/sidekiq"
|
|
24
|
+
# Optional structured-log adapter. Defined on require but NOT auto-attached —
|
|
25
|
+
# it only ships logs once the app opts in via Logger.attach_to_rails! /
|
|
26
|
+
# Logger.broadcast, so existing logging behavior is preserved by default.
|
|
27
|
+
require_relative "allstak/integrations/logger"
|
|
24
28
|
# The Rails Railtie self-installs on require when Rails is present, and is a
|
|
25
29
|
# guarded no-op otherwise. Loading it here means Rails apps that `require
|
|
26
30
|
# "allstak"` get the Rack middleware auto-wired without manual setup.
|
|
@@ -163,6 +167,15 @@ module AllStak
|
|
|
163
167
|
AllStak::GlobalHandler.capture_unhandled(exc)
|
|
164
168
|
end
|
|
165
169
|
|
|
170
|
+
# Manually record a breadcrumb on the current thread's ring buffer. It is
|
|
171
|
+
# attached to the next captured exception on this thread. Cross-SDK parity
|
|
172
|
+
# with JS addBreadcrumb / Python add_breadcrumb. Safe no-op when the SDK is
|
|
173
|
+
# not configured. Manual breadcrumbs are always recorded (independent of
|
|
174
|
+
# the enable_auto_breadcrumbs toggle, which only gates auto-instrumentation).
|
|
175
|
+
def add_breadcrumb(type:, message:, level: "info", data: nil)
|
|
176
|
+
@client&.errors&.add_breadcrumb(type: type, message: message, level: level, data: data)
|
|
177
|
+
end
|
|
178
|
+
|
|
166
179
|
def set_user(**kw)
|
|
167
180
|
@client&.set_user(**kw)
|
|
168
181
|
end
|
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.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- AllStak
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-05-
|
|
11
|
+
date: 2026-05-30 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rake
|
|
@@ -70,6 +70,7 @@ files:
|
|
|
70
70
|
- lib/allstak/config.rb
|
|
71
71
|
- lib/allstak/global_handler.rb
|
|
72
72
|
- lib/allstak/integrations/active_record.rb
|
|
73
|
+
- lib/allstak/integrations/logger.rb
|
|
73
74
|
- lib/allstak/integrations/net_http.rb
|
|
74
75
|
- lib/allstak/integrations/rack.rb
|
|
75
76
|
- lib/allstak/integrations/rails.rb
|