allstak 0.1.1 → 0.2.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 +135 -0
- data/README.md +72 -240
- data/allstak.gemspec +10 -9
- data/lib/allstak/client.rb +58 -2
- data/lib/allstak/config.rb +246 -3
- data/lib/allstak/global_handler.rb +100 -0
- data/lib/allstak/integrations/net_http.rb +9 -1
- data/lib/allstak/integrations/rack.rb +54 -10
- data/lib/allstak/integrations/rails.rb +59 -0
- data/lib/allstak/integrations/sidekiq.rb +183 -0
- data/lib/allstak/modules/database.rb +4 -1
- data/lib/allstak/modules/errors.rb +84 -3
- data/lib/allstak/modules/http_monitor.rb +7 -2
- data/lib/allstak/modules/logs.rb +5 -2
- data/lib/allstak/modules/tracing.rb +33 -2
- data/lib/allstak/propagation.rb +48 -0
- data/lib/allstak/sampling.rb +38 -0
- data/lib/allstak/sanitizer.rb +322 -0
- data/lib/allstak/session_tracker.rb +216 -0
- data/lib/allstak/transport/event_spool.rb +228 -0
- data/lib/allstak/transport/http_transport.rb +168 -5
- data/lib/allstak/version.rb +1 -1
- data/lib/allstak.rb +77 -1
- metadata +23 -29
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f2487acc9b1a4a0491b0a86d246b62ffcdb3a02499d728c8eaf01af50c9779d3
|
|
4
|
+
data.tar.gz: 411db967c468cb7c1793ade463b314cb44169b19071a3ed1d90fdf5bc3a582dc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9c91a85d0c9208021a2ed16ad0c3fa9a8d701b313d3dcd7e127b3d3a87c7d454cdd007dc65e948dec85fdf2f1a4cffaa4fc62367d13a23553df7cb7770566f55
|
|
7
|
+
data.tar.gz: 6e597806d72c3da0692ff4c14eea6aef1a09d7c6c83bbc917015fa925e9e55e82e9eebc5e91b6a9c232f0c40ce64f4b603870da485ec8526876710b34f0f938d
|
data/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,141 @@
|
|
|
3
3
|
All notable changes to the AllStak Ruby SDK.
|
|
4
4
|
This project follows [Semantic Versioning](https://semver.org/).
|
|
5
5
|
|
|
6
|
+
## Unreleased
|
|
7
|
+
|
|
8
|
+
## [0.2.0] — 2026-05-29
|
|
9
|
+
|
|
10
|
+
### Added — Release-health session tracking
|
|
11
|
+
- `AllStak::SessionTracker` — server-mode single-session release-health
|
|
12
|
+
lifecycle that mirrors the AllStak Java SDK. On boot it POSTs
|
|
13
|
+
`/ingest/v1/sessions/start` (off the hot path, on a daemon thread) with a
|
|
14
|
+
per-process `sessionId`, the resolved release, environment, and SDK identity;
|
|
15
|
+
on shutdown it POSTs `/ingest/v1/sessions/end` (synchronous, best-effort)
|
|
16
|
+
with `durationMs` + final status. Status model `ok | errored | crashed |
|
|
17
|
+
abnormal`: handled errors transition `ok -> errored`, an unhandled/fatal
|
|
18
|
+
exception sets `crashed` (terminal). Status transitions are in-memory only —
|
|
19
|
+
only `start`/`end` perform network I/O, so per-error latency is unaffected,
|
|
20
|
+
enabling crash-free session/user rates on the backend.
|
|
21
|
+
- Idempotent and re-entrancy safe (double `start`/`end` are no-ops). Sessions
|
|
22
|
+
are never sampled. Fully fail-open: a network error or read-only runtime must
|
|
23
|
+
never crash app boot or shutdown.
|
|
24
|
+
- Opt-out via `config.enable_auto_session_tracking = false`. Auto-disables under
|
|
25
|
+
a unit-test runtime so the suite never emits session traffic. The active
|
|
26
|
+
`sessionId` is attached to error/event payloads (and is allowlisted in the
|
|
27
|
+
sanitizer so it survives PII scrubbing).
|
|
28
|
+
|
|
29
|
+
### Added — Offline / persistent transport queue
|
|
30
|
+
- `AllStak::Transport::EventSpool` — a filesystem spool for un-sent telemetry
|
|
31
|
+
envelopes so events survive a process restart, network outage, or shutdown.
|
|
32
|
+
One JSON file per envelope (atomic temp-write + rename) so a partially written
|
|
33
|
+
file is discarded without losing the rest of the queue. Persisted bytes are
|
|
34
|
+
**already PII-scrubbed** by the caller before they hit disk.
|
|
35
|
+
- Bounded by COUNT (default 100), BYTES (default 4 MiB), and AGE (default 48h),
|
|
36
|
+
evicting OLDEST-first via an embedded millis+seq ordering. Stale entries past
|
|
37
|
+
max-age are dropped on drain. On the next SDK init the queue is replayed and
|
|
38
|
+
delivered entries are removed; unreadable/oversized entries are dropped.
|
|
39
|
+
- Default ON for server runtimes (`config.enable_offline_queue`), spool dir
|
|
40
|
+
`<tmpdir>/allstak-spool` (override via `config.offline_queue_dir`), with
|
|
41
|
+
configurable `offline_queue_max_entries` / `_max_bytes` / `_max_age_s`. Env
|
|
42
|
+
overrides: `ALLSTAK_OFFLINE_QUEUE`, `ALLSTAK_OFFLINE_QUEUE_DIR`. Fully
|
|
43
|
+
fail-open: a read-only / sandboxed / serverless FS degrades silently to
|
|
44
|
+
in-memory behavior and never blocks capture or init.
|
|
45
|
+
|
|
46
|
+
### Added — Value-pattern PII scrubbing + sendDefaultPii
|
|
47
|
+
- `AllStak::Sanitizer` gains a second scrubbing layer alongside the existing
|
|
48
|
+
key-name denylist: VALUE-PATTERN redaction that scans free-text string values
|
|
49
|
+
for PII regardless of key name (Sentry data-scrubbing parity). Tier A is
|
|
50
|
+
ALWAYS scrubbed — Luhn-validated credit-card numbers (13–19 digits, optional
|
|
51
|
+
space/hyphen separators) and hyphenated US SSNs. Tier B — email addresses and
|
|
52
|
+
IPv4/IPv6 — is scrubbed UNLESS `send_default_pii` is enabled.
|
|
53
|
+
- `config.send_default_pii` (default `false`, Sentry parity; env
|
|
54
|
+
`ALLSTAK_SEND_DEFAULT_PII`) opts into shipping email/IP values. The
|
|
55
|
+
always-on financial/identity scrubbers are not affected by this flag.
|
|
56
|
+
- Structural exemptions prevent over-redaction: the explicit `user` object and
|
|
57
|
+
`stackTrace`/`frames` subtrees are key-name-redacted but never value-scrubbed,
|
|
58
|
+
and structured scalar keys (release, sdk/version, url/path/host, span/trace
|
|
59
|
+
ids, `sessionId`, timestamp) are skipped. Strings over 16 KiB are passed
|
|
60
|
+
through. Value scrubbing is fail-open: any error falls back to the
|
|
61
|
+
key-redacted (non-value-scrubbed) structure.
|
|
62
|
+
|
|
63
|
+
### Added — Global capture, sampling, and release auto-detection
|
|
64
|
+
- `AllStak::GlobalHandler` — global `at_exit` hook captures uncaught exceptions
|
|
65
|
+
(mechanism `at_exit`), feeding session crash status, before re-raising.
|
|
66
|
+
- `before_send` callback + client-side event/trace sampling hooks.
|
|
67
|
+
- Runtime release auto-detection from local git (`AllStak.register_runtime_release`
|
|
68
|
+
/ `config.detect_release`) so events are attributed to a release without
|
|
69
|
+
manual configuration; auto-disabled under a test runtime.
|
|
70
|
+
- Transport hardening: every wire payload is sanitized at the single
|
|
71
|
+
`HttpTransport#post` chokepoint, the `sdk.version` wire value is corrected,
|
|
72
|
+
and `Retry-After` is honored on 429/503.
|
|
73
|
+
|
|
74
|
+
### Added — Sidekiq integration
|
|
75
|
+
- `AllStak::Integrations::Sidekiq::Middleware` — Sidekiq **server** middleware
|
|
76
|
+
that starts a fresh trace per job, wraps execution in a `queue.process`
|
|
77
|
+
span + breadcrumb, and auto-captures job failures with worker class, jid,
|
|
78
|
+
queue, and (PII-sanitized via `Sanitizer`) args, re-raising so Sidekiq's
|
|
79
|
+
retry machinery still runs.
|
|
80
|
+
- `death_handler` registration captures jobs that exhaust their retries
|
|
81
|
+
(`mechanism=sidekiq.death`).
|
|
82
|
+
- Auto-registered by `AllStak.configure` via `Sidekiq.configure_server` when
|
|
83
|
+
Sidekiq is present; a graceful, idempotent no-op otherwise.
|
|
84
|
+
|
|
85
|
+
### Added — Rails Railtie
|
|
86
|
+
- `AllStak::Integrations::Rails::Railtie` — a real `Rails::Railtie` that
|
|
87
|
+
auto-inserts the Rack middleware into the app's middleware stack via an
|
|
88
|
+
initializer. Rails apps are now instrumented **without manual wiring**,
|
|
89
|
+
matching the prior README claim. Guarded (only loads when `Rails::Railtie`
|
|
90
|
+
is defined) and idempotent.
|
|
91
|
+
|
|
92
|
+
### Docs
|
|
93
|
+
- README + gemspec description corrected to reflect real auto-install behavior
|
|
94
|
+
(Railtie + Sidekiq server middleware) instead of the previously overstated
|
|
95
|
+
"Rails middleware" claim. Fixed stale `AllStak::Rack::Middleware` and
|
|
96
|
+
`AllStak.start_span` references.
|
|
97
|
+
|
|
98
|
+
### Tests / tooling
|
|
99
|
+
- New minitest coverage for the Sidekiq middleware (capture + context + span +
|
|
100
|
+
no-op without Sidekiq), the Railtie (middleware insertion + guarded without
|
|
101
|
+
Rails), and backfill for `Client`, `HttpTransport`, and `Modules::Errors`.
|
|
102
|
+
- Removed unused `rspec` / `webmock` dev-dependencies (the suite is minitest
|
|
103
|
+
only; transport tests stub `Net::HTTP` directly). CI workflow simplified
|
|
104
|
+
accordingly.
|
|
105
|
+
- Added minitest coverage for the new waves: `test_session_tracker`,
|
|
106
|
+
`test_session_error_wiring`, `test_event_spool`, `test_offline_queue`,
|
|
107
|
+
`test_send_default_pii`, plus value-pattern cases in `test_sanitizer` and
|
|
108
|
+
`test_global_handler` / `test_release_autodetect` / `test_retry_after`.
|
|
109
|
+
Full suite: 177 runs, 448 assertions, 0 failures.
|
|
110
|
+
|
|
111
|
+
### Repo hygiene
|
|
112
|
+
- Untracked stray build/vendor artifacts that the `.gitignore` already covers
|
|
113
|
+
but were committed before the ignore rules existed: the root `*.gem` packs
|
|
114
|
+
(`allstak-0.1.0.gem`, `allstak-0.1.1.gem`) and the committed
|
|
115
|
+
`vendor/bundle/` dependency cache. These are not part of the published gem
|
|
116
|
+
(`spec.files` ships only `lib/**/*.rb`, README, CHANGELOG, LICENSE, gemspec).
|
|
117
|
+
|
|
118
|
+
## 0.1.2 — 2026-05-18
|
|
119
|
+
|
|
120
|
+
### Added — Recursive payload sanitizer
|
|
121
|
+
- New `AllStak::Sanitizer` module with `scrub(payload, extra_denylist: nil)`.
|
|
122
|
+
25-term canonical denylist, recursive over `Hash` / `Array`, `[REDACTED]`
|
|
123
|
+
substitution, `object_id` Set cycle protection, pure (no caller mutation).
|
|
124
|
+
- Wired into `AllStak::Transport::HttpTransport#post` — every wire payload
|
|
125
|
+
is scrubbed before serialization. One chokepoint protects errors, logs,
|
|
126
|
+
http, db, traces.
|
|
127
|
+
- Fail-open: if the sanitizer raises on a pathological payload, the SDK
|
|
128
|
+
logs at Warning level and sends raw — telemetry is never blocked.
|
|
129
|
+
|
|
130
|
+
### Live canary E2E
|
|
131
|
+
- Event `03188061-8054-439a-8784-a3b52436549e` against `api.allstak.sa`.
|
|
132
|
+
- ClickHouse confirmed `leak_pos = 0` across `metadata`, `stack_trace`,
|
|
133
|
+
`breadcrumbs`, `message`. Canary `should_not_leak_ruby` planted in
|
|
134
|
+
`password`, `authorization`, `cookie`, `Bearer`, `api_key`, request
|
|
135
|
+
headers / body, `credit_card`, `ssn`, and a 3-level-nested `token`.
|
|
136
|
+
All scrubbed.
|
|
137
|
+
|
|
138
|
+
### Tests
|
|
139
|
+
- `test/test_sanitizer.rb` — 12 runs, 51 assertions, 0 failures.
|
|
140
|
+
|
|
6
141
|
## 0.1.0 — 2026-04-11
|
|
7
142
|
|
|
8
143
|
First public release. Driven end-to-end through a real Sinatra + ActiveRecord
|
data/README.md
CHANGED
|
@@ -1,291 +1,123 @@
|
|
|
1
|
-
#
|
|
1
|
+
# allstak
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
structured logs, HTTP + ActiveRecord monitoring, distributed tracing, and cron
|
|
5
|
-
monitoring for Rack-based Ruby applications (Rails, Sinatra, Roda, Hanami).
|
|
3
|
+
AllStak SDK for Ruby, Rails, Rack, and Sidekiq. Captures exceptions, logs, inbound and outbound HTTP requests, ActiveRecord queries, Sidekiq job failures, spans, and cron heartbeats. Rails apps are auto-instrumented via a Railtie; Sidekiq via a server middleware.
|
|
6
4
|
|
|
7
|
-
|
|
8
|
-
gem "allstak"
|
|
9
|
-
```
|
|
5
|
+
## Install
|
|
10
6
|
|
|
11
7
|
```bash
|
|
12
|
-
bundle install
|
|
13
|
-
# or
|
|
14
8
|
gem install allstak
|
|
15
9
|
```
|
|
16
10
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
```ruby
|
|
20
|
-
require "allstak"
|
|
21
|
-
|
|
22
|
-
AllStak.configure do |c|
|
|
23
|
-
c.api_key = ENV["ALLSTAK_API_KEY"]
|
|
24
|
-
c.environment = "production"
|
|
25
|
-
c.release = "myapp@1.2.3"
|
|
26
|
-
c.service_name = "myapp-api"
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
# Rack / Sinatra / Rails:
|
|
30
|
-
use AllStak::Integrations::Rack::Middleware
|
|
31
|
-
|
|
32
|
-
# Manual capture:
|
|
33
|
-
begin
|
|
34
|
-
risky!
|
|
35
|
-
rescue => e
|
|
36
|
-
AllStak.capture_exception(e)
|
|
37
|
-
end
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
That is the whole setup. Every request, every unhandled exception, every
|
|
41
|
-
ActiveRecord query, every outbound Net::HTTP call, and every trace are
|
|
42
|
-
captured automatically.
|
|
43
|
-
|
|
44
|
-
## Public API (cross-SDK consistent)
|
|
45
|
-
|
|
46
|
-
Every method below is a module-level method on `AllStak`, matching the
|
|
47
|
-
names used by the JS, Python, Java, Go, PHP, and .NET SDKs so docs carry
|
|
48
|
-
across languages.
|
|
11
|
+
Or add it to your Gemfile:
|
|
49
12
|
|
|
50
13
|
```ruby
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
AllStak.set_user(id: "42", email: "alice@example.com") # user context
|
|
54
|
-
AllStak.clear_user
|
|
55
|
-
|
|
56
|
-
AllStak.set_tag("service", "checkout") # sticky tag
|
|
57
|
-
AllStak.set_tags(region: "us-east-1", tier: "web") # bulk
|
|
58
|
-
AllStak.set_context("deployment", "canary") # sticky context
|
|
59
|
-
|
|
60
|
-
AllStak.capture_exception(exc) # preferred for errors
|
|
61
|
-
AllStak.capture_error("DomainError", "bad input") # without a throwable
|
|
62
|
-
AllStak.capture_message("hello", level: "info") # plain string event
|
|
63
|
-
|
|
64
|
-
AllStak.log.info("request started", metadata: {...}) # structured logs
|
|
65
|
-
AllStak.tracing # Tracing module
|
|
66
|
-
AllStak.http # HTTP monitor
|
|
67
|
-
AllStak.database # DB query monitor
|
|
68
|
-
AllStak.cron.job("daily-report") { run_job } # cron heartbeats
|
|
69
|
-
|
|
70
|
-
AllStak.flush # drain buffers
|
|
71
|
-
AllStak.shutdown # drain + close
|
|
14
|
+
gem "allstak"
|
|
72
15
|
```
|
|
73
16
|
|
|
74
|
-
|
|
75
|
-
landed in 0.1.1 as cross-SDK parity additions. Older 0.1.0 code that only
|
|
76
|
-
used `capture_exception` / `capture_error` keeps working — no breaking
|
|
77
|
-
changes.
|
|
78
|
-
|
|
79
|
-
## Rails
|
|
17
|
+
## Setup
|
|
80
18
|
|
|
81
19
|
```ruby
|
|
82
|
-
# config/initializers/allstak.rb
|
|
83
20
|
require "allstak"
|
|
84
21
|
|
|
85
|
-
AllStak.configure do |
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
22
|
+
AllStak.configure do |config|
|
|
23
|
+
config.api_key = ENV["ALLSTAK_API_KEY"]
|
|
24
|
+
config.environment = ENV.fetch("APP_ENV", "production")
|
|
25
|
+
config.release = ENV["ALLSTAK_RELEASE"]
|
|
26
|
+
config.service_name = "checkout-api"
|
|
90
27
|
end
|
|
91
28
|
|
|
92
|
-
|
|
29
|
+
AllStak.capture_exception(StandardError.new("checkout failed"))
|
|
30
|
+
AllStak.log.info("payment retry", metadata: { order_id: "ord_123" })
|
|
93
31
|
```
|
|
94
32
|
|
|
95
|
-
##
|
|
33
|
+
## Rails
|
|
34
|
+
|
|
35
|
+
For Rails apps there is **no manual wiring**. Requiring the gem loads a
|
|
36
|
+
`Railtie` that auto-inserts the AllStak Rack middleware into your application's
|
|
37
|
+
middleware stack during boot. Just configure the SDK during app initialization
|
|
38
|
+
(e.g. in `config/initializers/allstak.rb`):
|
|
96
39
|
|
|
97
40
|
```ruby
|
|
98
|
-
require "sinatra/base"
|
|
99
41
|
require "allstak"
|
|
100
42
|
|
|
101
|
-
AllStak.configure
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
# ...
|
|
43
|
+
AllStak.configure do |config|
|
|
44
|
+
config.api_key = ENV["ALLSTAK_API_KEY"]
|
|
45
|
+
config.environment = Rails.env
|
|
46
|
+
config.service_name = "checkout-api"
|
|
106
47
|
end
|
|
107
48
|
```
|
|
108
49
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
| ------------------------------------- | ------------------------------------------ |
|
|
113
|
-
| Unhandled exceptions | Rack middleware |
|
|
114
|
-
| Inbound HTTP requests | Rack middleware |
|
|
115
|
-
| Per-request trace ID | Rack middleware |
|
|
116
|
-
| User context (from env/session) | Rack middleware |
|
|
117
|
-
| ActiveRecord SQL queries | `sql.active_record` subscriber |
|
|
118
|
-
| Outbound HTTP via `Net::HTTP` | `Net::HTTP#request` patched |
|
|
50
|
+
You then get inbound request telemetry, trace propagation, unhandled-exception
|
|
51
|
+
capture, ActiveRecord query instrumentation, and outbound `Net::HTTP` capture
|
|
52
|
+
automatically. The middleware insertion is idempotent.
|
|
119
53
|
|
|
120
|
-
|
|
121
|
-
by `AllStak.configure` — no extra setup needed.
|
|
54
|
+
## Rack (non-Rails)
|
|
122
55
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
`AllStak.configure` installs an `ActiveSupport::Notifications` subscriber on
|
|
126
|
-
`sql.active_record`, which gives you normalized SQL, duration, row counts,
|
|
127
|
-
status, and error messages for every query — whether it's from an Active Record
|
|
128
|
-
relation, a `find_by_sql`, or a raw `connection.execute`. No duplication,
|
|
129
|
-
because every AR query fires exactly one `sql.active_record` event.
|
|
56
|
+
For plain Rack apps (Sinatra, Roda, etc.), add the middleware yourself:
|
|
130
57
|
|
|
131
58
|
```ruby
|
|
132
|
-
|
|
133
|
-
User.where(email: "alice@example.com").first
|
|
134
|
-
# → captured as: SELECT "users".* FROM "users" WHERE "users"."email" = ? LIMIT ?
|
|
59
|
+
use AllStak::Integrations::Rack::Middleware
|
|
135
60
|
```
|
|
136
61
|
|
|
137
|
-
##
|
|
62
|
+
## Sidekiq
|
|
138
63
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
The SDK patches `Net::HTTP#request` at `configure` time. Since every
|
|
146
|
-
convenience method (`get`, `post`, `post_form`, etc.) funnels through
|
|
147
|
-
`#request`, there is no duplication. Calls to your AllStak ingest host are
|
|
148
|
-
skipped to avoid recursive instrumentation.
|
|
64
|
+
When Sidekiq is present, `AllStak.configure` registers a Sidekiq **server
|
|
65
|
+
middleware** automatically — no manual setup. It wraps each job in a span and
|
|
66
|
+
breadcrumb, and captures job failures with worker class, jid, queue, and
|
|
67
|
+
(PII-sanitized) args as context, then re-raises so Sidekiq's retry machinery
|
|
68
|
+
still runs. Jobs that exhaust their retries are also captured via a death
|
|
69
|
+
handler. When Sidekiq is not installed, this is a graceful no-op.
|
|
149
70
|
|
|
150
|
-
##
|
|
71
|
+
## Spans
|
|
151
72
|
|
|
152
73
|
```ruby
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
AllStak.capture_error("StripeTimeout", "Stripe /v1/charges timed out after 30s", level: "error")
|
|
156
|
-
|
|
157
|
-
# Logs (buffered, flushed in background)
|
|
158
|
-
AllStak.log.info("Order placed", metadata: { id: "ORD-1" })
|
|
159
|
-
AllStak.log.warn("Slow query", metadata: { ms: 4500 })
|
|
160
|
-
AllStak.log.error("Payment failed", metadata: { gateway: "stripe" })
|
|
161
|
-
# valid levels: debug | info | warn | error | fatal
|
|
162
|
-
|
|
163
|
-
# Distributed tracing (block-form)
|
|
164
|
-
AllStak.tracing.in_span("db.query", description: "SELECT users") do |span|
|
|
165
|
-
span.set_tag("db.type", "postgresql")
|
|
166
|
-
rows = User.all.to_a
|
|
74
|
+
AllStak.tracing.in_span("checkout.authorize") do
|
|
75
|
+
authorize_payment
|
|
167
76
|
end
|
|
77
|
+
```
|
|
168
78
|
|
|
169
|
-
|
|
170
|
-
AllStak.cron.job("daily-report") do
|
|
171
|
-
generate_report
|
|
172
|
-
end
|
|
79
|
+
## Configuration
|
|
173
80
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
81
|
+
| Option | Description |
|
|
82
|
+
| --- | --- |
|
|
83
|
+
| `api_key` | Project API key. |
|
|
84
|
+
| `environment` | Deployment environment. |
|
|
85
|
+
| `release` | App version or commit SHA. |
|
|
86
|
+
| `service_name` | Logical service name. |
|
|
87
|
+
| `flush_interval_ms` | Background flush interval. |
|
|
88
|
+
| `buffer_size` | Max buffered events. |
|
|
89
|
+
| `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
|
+
| `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
|
+
| `sample_rate` | Float in `[0.0, 1.0]` head-sampling rate for error/message events (default `1.0` = keep all). |
|
|
92
|
+
| `traces_sample_rate` | Float in `[0.0, 1.0]` span sampling rate. `nil` (default) keeps every span and the `traceparent` sampled flag; when set, span creation is sampled and the propagated `traceparent` flag reflects the decision. |
|
|
93
|
+
|
|
94
|
+
For top-level uncaught exceptions outside Rack (workers, threads, rake tasks), the `at_exit` handler catches genuine unhandled terminations automatically. You can also report manually at a boundary:
|
|
177
95
|
|
|
178
|
-
|
|
179
|
-
|
|
96
|
+
```ruby
|
|
97
|
+
begin
|
|
98
|
+
run_worker
|
|
99
|
+
rescue => e
|
|
100
|
+
AllStak.capture_unhandled(e) # mechanism=at_exit, handled=false
|
|
101
|
+
raise
|
|
102
|
+
end
|
|
180
103
|
```
|
|
181
104
|
|
|
182
|
-
##
|
|
105
|
+
## Privacy
|
|
183
106
|
|
|
184
|
-
|
|
185
|
-
| ----------------------------------------- | --------------------- |
|
|
186
|
-
| `AllStak.capture_exception` / middleware | **Errors**, **Incidents** |
|
|
187
|
-
| `AllStak.log.*` | **Logs** |
|
|
188
|
-
| Rack middleware (inbound) | **Requests** |
|
|
189
|
-
| `Net::HTTP` (outbound, auto) | **Requests** (outbound) |
|
|
190
|
-
| ActiveRecord queries (auto) | **Database** |
|
|
191
|
-
| `AllStak.tracing.in_span` | **Traces** |
|
|
192
|
-
| `AllStak.cron.job` / `cron.ping` | **Cron Jobs** |
|
|
193
|
-
|
|
194
|
-
## Configuration
|
|
195
|
-
|
|
196
|
-
| Option | Default | Notes |
|
|
197
|
-
| ------------------------- | ------------------------- | ----- |
|
|
198
|
-
| `api_key` | `ENV["ALLSTAK_API_KEY"]` | Your `ask_live_...` key. |
|
|
199
|
-
| `host` | `https://api.allstak.sa` | Override with your AllStak ingest host. |
|
|
200
|
-
| `environment` | `nil` | e.g. `"production"` |
|
|
201
|
-
| `release` | `nil` | e.g. `"myapp@1.2.3"` |
|
|
202
|
-
| `service_name` | `"ruby-service"` | Shown on spans and logs. |
|
|
203
|
-
| `flush_interval_ms` | `2000` | Background flush interval. |
|
|
204
|
-
| `buffer_size` | `500` | Max buffered items per feature. |
|
|
205
|
-
| `debug` | `false` | Verbose SDK logging. |
|
|
206
|
-
| `connect_timeout` | `3` | Transport connect timeout (seconds). |
|
|
207
|
-
| `read_timeout` | `3` | Transport read timeout (seconds). |
|
|
208
|
-
| `max_retries` | `5` | Retry 5xx with exponential backoff. |
|
|
209
|
-
| `capture_unhandled_exceptions` | `true` | Auto-capture from middleware. |
|
|
210
|
-
| `capture_http_requests` | `true` | Auto-capture inbound HTTP. |
|
|
211
|
-
| `capture_user_context` | `true` | Attach user claims to errors. |
|
|
212
|
-
| `capture_sql` | `true` | Auto-capture AR queries. |
|
|
213
|
-
|
|
214
|
-
Environment variables: `ALLSTAK_API_KEY`, `ALLSTAK_HOST`, `ALLSTAK_ENVIRONMENT`,
|
|
215
|
-
`ALLSTAK_RELEASE`, `ALLSTAK_SERVICE`, `ALLSTAK_DEBUG`.
|
|
216
|
-
|
|
217
|
-
## Production notes
|
|
218
|
-
|
|
219
|
-
- **Never crashes your app.** Every integration catches its own exceptions
|
|
220
|
-
and logs at debug level. The middleware re-raises so your framework's
|
|
221
|
-
exception handler still runs.
|
|
222
|
-
- **Retries.** 5xx and network errors retry with exponential backoff
|
|
223
|
-
(1s → 2s → 4s → 8s, +jitter, max 5 attempts). 4xx are not retried.
|
|
224
|
-
- **401 disables the SDK.** An invalid API key disables the SDK for the
|
|
225
|
-
rest of the process — no further events are sent, a warning is logged
|
|
226
|
-
once, and your app keeps running.
|
|
227
|
-
- **Flush on shutdown.** `at_exit` triggers a best-effort flush.
|
|
228
|
-
- **Thread-safe.** All public APIs are safe to call from any thread.
|
|
229
|
-
Trace context uses Ruby's thread-local storage.
|
|
230
|
-
- **Non-blocking.** Telemetry is buffered and flushed on background threads.
|
|
231
|
-
Your request pipeline is never blocked by SDK work.
|
|
107
|
+
The SDK redacts common sensitive headers and fields. Avoid putting secrets in custom metadata.
|
|
232
108
|
|
|
233
109
|
## Troubleshooting
|
|
234
110
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
| Inbound requests missing | Make sure `use AllStak::Integrations::Rack::Middleware`. |
|
|
240
|
-
| DB queries missing | Make sure `AllStak.configure` runs BEFORE your first AR query. |
|
|
241
|
-
| Outbound HTTP missing | Same — `configure` must run before the first `Net::HTTP` call. |
|
|
242
|
-
| Cron monitor not appearing | Auto-created on first ping; check the slug matches. |
|
|
111
|
+
- No events: confirm `ALLSTAK_API_KEY` is available before configuration.
|
|
112
|
+
- Missing request telemetry (Rails): confirm `require "allstak"` runs at boot so the Railtie can auto-insert the middleware.
|
|
113
|
+
- Missing request telemetry (plain Rack): confirm `use AllStak::Integrations::Rack::Middleware` is in your Rack stack.
|
|
114
|
+
- Short-lived job: call `AllStak.flush` before exit when possible.
|
|
243
115
|
|
|
244
|
-
##
|
|
116
|
+
## Contributing and Support
|
|
245
117
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
require "allstak"
|
|
250
|
-
|
|
251
|
-
AllStak.configure do |c|
|
|
252
|
-
c.api_key = ENV["ALLSTAK_API_KEY"]
|
|
253
|
-
c.environment = "production"
|
|
254
|
-
c.release = "taskflow@1.4.2"
|
|
255
|
-
c.service_name = "taskflow-api"
|
|
256
|
-
end
|
|
257
|
-
|
|
258
|
-
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: "app.db")
|
|
259
|
-
|
|
260
|
-
class Task < ActiveRecord::Base; end
|
|
261
|
-
|
|
262
|
-
class TaskFlow < Sinatra::Base
|
|
263
|
-
use AllStak::Integrations::Rack::Middleware
|
|
264
|
-
|
|
265
|
-
get "/tasks" do
|
|
266
|
-
Task.all.to_json
|
|
267
|
-
end
|
|
268
|
-
|
|
269
|
-
post "/tasks/:id/notify" do
|
|
270
|
-
task = Task.find(params[:id])
|
|
271
|
-
AllStak.tracing.in_span("http.notify", description: "POST httpbin.org/post") do |span|
|
|
272
|
-
span.set_tag("task.id", task.id.to_s)
|
|
273
|
-
uri = URI("https://httpbin.org/post")
|
|
274
|
-
Net::HTTP.post(uri, { task_id: task.id }.to_json, "Content-Type" => "application/json")
|
|
275
|
-
end
|
|
276
|
-
{ ok: true }.to_json
|
|
277
|
-
end
|
|
278
|
-
|
|
279
|
-
error do
|
|
280
|
-
# Framework-level rescue. Sinatra handles the exception before Rack middleware
|
|
281
|
-
# sees it, so forward manually:
|
|
282
|
-
e = env["sinatra.error"]
|
|
283
|
-
AllStak.capture_exception(e) if e
|
|
284
|
-
status 500
|
|
285
|
-
{ error: e.class.name, message: e.message }.to_json
|
|
286
|
-
end
|
|
287
|
-
end
|
|
288
|
-
```
|
|
118
|
+
- Report bugs with the GitHub bug report template: https://github.com/AllStak/allstak-ruby/issues/new/choose
|
|
119
|
+
- Open pull requests using the checklist in [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
120
|
+
- Report security vulnerabilities privately through [SECURITY.md](SECURITY.md).
|
|
289
121
|
|
|
290
122
|
## License
|
|
291
123
|
|
data/allstak.gemspec
CHANGED
|
@@ -4,19 +4,19 @@ Gem::Specification.new do |spec|
|
|
|
4
4
|
spec.name = "allstak"
|
|
5
5
|
spec.version = AllStak::VERSION
|
|
6
6
|
spec.authors = ["AllStak"]
|
|
7
|
-
spec.email = ["sdk@allstak.
|
|
7
|
+
spec.email = ["sdk@allstak.sa"]
|
|
8
8
|
|
|
9
9
|
spec.summary = "Official AllStak Ruby SDK — error tracking, logs, HTTP + ActiveRecord monitoring, tracing, and cron monitoring"
|
|
10
|
-
spec.description = "Production-ready Ruby SDK for AllStak observability:
|
|
11
|
-
spec.homepage = "https://allstak.
|
|
10
|
+
spec.description = "Production-ready Ruby SDK for AllStak observability: auto-installing Rails Railtie and Rack middleware, a Sidekiq server middleware, ActiveRecord instrumentation, outbound HTTP capture, distributed tracing, cron monitoring, and structured logs."
|
|
11
|
+
spec.homepage = "https://allstak.sa"
|
|
12
12
|
spec.license = "MIT"
|
|
13
13
|
spec.required_ruby_version = ">= 3.0.0"
|
|
14
14
|
|
|
15
15
|
spec.metadata["homepage_uri"] = spec.homepage
|
|
16
|
-
spec.metadata["source_code_uri"] = "https://github.com/
|
|
17
|
-
spec.metadata["changelog_uri"] = "https://github.com/
|
|
18
|
-
spec.metadata["bug_tracker_uri"] = "https://github.com/
|
|
19
|
-
spec.metadata["documentation_uri"] = "https://allstak.
|
|
16
|
+
spec.metadata["source_code_uri"] = "https://github.com/AllStak/allstak-ruby"
|
|
17
|
+
spec.metadata["changelog_uri"] = "https://github.com/AllStak/allstak-ruby/blob/main/CHANGELOG.md"
|
|
18
|
+
spec.metadata["bug_tracker_uri"] = "https://github.com/AllStak/allstak-ruby/issues"
|
|
19
|
+
spec.metadata["documentation_uri"] = "https://allstak.sa/docs/sdks/ruby"
|
|
20
20
|
spec.metadata["rubygems_mfa_required"] = "true"
|
|
21
21
|
|
|
22
22
|
spec.files = Dir[
|
|
@@ -33,8 +33,9 @@ Gem::Specification.new do |spec|
|
|
|
33
33
|
# Framework integrations (Rack, Rails, ActiveRecord, Net::HTTP) are loaded
|
|
34
34
|
# lazily and only activate when the host app has them available.
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
# Tests use minitest only (Ruby's bundled stdlib test framework); the
|
|
37
|
+
# transport tests stub Net::HTTP directly, so no rspec/webmock is needed.
|
|
38
|
+
spec.add_development_dependency "rake", "~> 13.0"
|
|
38
39
|
spec.add_development_dependency "rack", "~> 3.0"
|
|
39
40
|
spec.add_development_dependency "activerecord", "~> 8.0"
|
|
40
41
|
end
|
data/lib/allstak/client.rb
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
module AllStak
|
|
2
2
|
# The AllStak SDK client. Create once via {AllStak.configure}.
|
|
3
3
|
class Client
|
|
4
|
-
attr_reader :config, :logger, :errors, :logs, :http, :tracing, :database, :cron, :tags, :contexts
|
|
4
|
+
attr_reader :config, :logger, :errors, :logs, :http, :tracing, :database, :cron, :tags, :contexts, :session_tracker
|
|
5
5
|
|
|
6
6
|
def initialize(config, logger)
|
|
7
7
|
@config = config
|
|
8
8
|
@logger = logger
|
|
9
9
|
@transport = Transport::HttpTransport.new(config, logger)
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
# Release-health session tracker: one session per process. Started by
|
|
12
|
+
# AllStak.configure after the release is resolved; ended in #shutdown.
|
|
13
|
+
@session_tracker = SessionTracker.new(config, @transport, logger)
|
|
14
|
+
|
|
15
|
+
@errors = Modules::Errors.new(@transport, config, logger,
|
|
16
|
+
session_id_provider: -> { @session_tracker&.current_session_id })
|
|
12
17
|
@logs = Modules::Logs.new(@transport, config, logger)
|
|
13
18
|
@http = Modules::HttpMonitor.new(@transport, config, logger)
|
|
14
19
|
@tracing = Modules::Tracing.new(@transport, config, logger)
|
|
@@ -20,6 +25,37 @@ module AllStak
|
|
|
20
25
|
at_exit { shutdown rescue nil }
|
|
21
26
|
end
|
|
22
27
|
|
|
28
|
+
# The active release-health session id (nil before start / after shutdown).
|
|
29
|
+
def current_session_id
|
|
30
|
+
@session_tracker&.current_session_id
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Begin the release-health session. Idempotent + fail-open. Called by
|
|
34
|
+
# AllStak.configure once the release has been finalized.
|
|
35
|
+
def start_session
|
|
36
|
+
@session_tracker&.start
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Replay any telemetry persisted by a previous process/outage. Runs on a
|
|
40
|
+
# daemon thread so init never blocks on the network; fully fail-open. Called
|
|
41
|
+
# by AllStak.configure after the client is built. No-op when the offline
|
|
42
|
+
# queue is disabled/unavailable.
|
|
43
|
+
def drain_offline_queue
|
|
44
|
+
transport = @transport
|
|
45
|
+
return unless transport.respond_to?(:drain_spool)
|
|
46
|
+
thread = Thread.new do
|
|
47
|
+
begin
|
|
48
|
+
transport.drain_spool
|
|
49
|
+
rescue StandardError => e
|
|
50
|
+
@logger&.debug("[AllStak] offline drain failed: #{e.class}: #{e.message}")
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
thread.abort_on_exception = false
|
|
54
|
+
self
|
|
55
|
+
rescue StandardError
|
|
56
|
+
self
|
|
57
|
+
end
|
|
58
|
+
|
|
23
59
|
def set_user(id: nil, email: nil, ip: nil)
|
|
24
60
|
@errors.set_user(id: id, email: email, ip: ip)
|
|
25
61
|
end
|
|
@@ -30,11 +66,13 @@ module AllStak
|
|
|
30
66
|
|
|
31
67
|
def capture_exception(exc, **kw)
|
|
32
68
|
kw = merge_default_metadata(kw)
|
|
69
|
+
mark_session_for(kw)
|
|
33
70
|
@errors.capture_exception(exc, **kw)
|
|
34
71
|
end
|
|
35
72
|
|
|
36
73
|
def capture_error(exception_class, message, **kw)
|
|
37
74
|
kw = merge_default_metadata(kw)
|
|
75
|
+
mark_session_for(kw)
|
|
38
76
|
@errors.capture_error(exception_class, message, **kw)
|
|
39
77
|
end
|
|
40
78
|
|
|
@@ -80,10 +118,28 @@ module AllStak
|
|
|
80
118
|
@http.shutdown
|
|
81
119
|
@tracing.shutdown
|
|
82
120
|
@database.shutdown
|
|
121
|
+
# Graceful shutdown: end the release-health session last so its
|
|
122
|
+
# /sessions/end carries the final accumulated status. Best-effort.
|
|
123
|
+
@session_tracker&.end rescue nil
|
|
83
124
|
end
|
|
84
125
|
|
|
85
126
|
private
|
|
86
127
|
|
|
128
|
+
# Mark the active session errored/crashed based on the captured event's
|
|
129
|
+
# mechanism. An at_exit/unhandled event (handled=false) is a crash;
|
|
130
|
+
# everything else is a handled error. Fail-open — never raises.
|
|
131
|
+
def mark_session_for(kw)
|
|
132
|
+
tracker = @session_tracker
|
|
133
|
+
return unless tracker
|
|
134
|
+
meta = kw[:metadata]
|
|
135
|
+
handled_false =
|
|
136
|
+
meta.is_a?(Hash) &&
|
|
137
|
+
(meta["handled"] == false || meta[:handled] == false)
|
|
138
|
+
handled_false ? tracker.record_crash : tracker.record_error
|
|
139
|
+
rescue StandardError
|
|
140
|
+
nil
|
|
141
|
+
end
|
|
142
|
+
|
|
87
143
|
# Fold the persistent tags + contexts into any explicit metadata caller
|
|
88
144
|
# passed. Caller-supplied keys win on conflict.
|
|
89
145
|
def merge_default_metadata(kw)
|