allstak 0.1.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 +7 -0
- data/CHANGELOG.md +71 -0
- data/LICENSE +21 -0
- data/README.md +292 -0
- data/allstak.gemspec +40 -0
- data/lib/allstak/client.rb +98 -0
- data/lib/allstak/config.rb +36 -0
- data/lib/allstak/integrations/active_record.rb +78 -0
- data/lib/allstak/integrations/net_http.rb +87 -0
- data/lib/allstak/integrations/rack.rb +136 -0
- data/lib/allstak/models/user_context.rb +43 -0
- data/lib/allstak/modules/cron.rb +54 -0
- data/lib/allstak/modules/database.rb +89 -0
- data/lib/allstak/modules/errors.rb +111 -0
- data/lib/allstak/modules/http_monitor.rb +79 -0
- data/lib/allstak/modules/logs.rb +79 -0
- data/lib/allstak/modules/tracing.rb +170 -0
- data/lib/allstak/transport/flush_buffer.rb +91 -0
- data/lib/allstak/transport/http_transport.rb +97 -0
- data/lib/allstak/version.rb +3 -0
- data/lib/allstak.rb +151 -0
- metadata +128 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 943cc01f8f95ed1cabef20b1c88b50e462b7be871c0a962a46c225e85902e355
|
|
4
|
+
data.tar.gz: f61664c5d936089b368e22c3c71d32f58f05081a35b478814a5b4f041b799133
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 7262178e8689bed7b675cc0fd60c494978ce73f826eac9566fc063054d33043c8475350e0780acadfd81c23a626c30f3ab24e1b5ca2e3191b83898b81e473f82
|
|
7
|
+
data.tar.gz: e41f14091f0e5abde8930f421394a36bdaf12e617d5376c4126f86cb700c16de740f6fdb632bd60ddc0f3dd15ba655e6c75d532b43b3bfe297b8f04ff03bfc43
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to the AllStak Ruby SDK.
|
|
4
|
+
This project follows [Semantic Versioning](https://semver.org/).
|
|
5
|
+
|
|
6
|
+
## 0.1.0 — 2026-04-11
|
|
7
|
+
|
|
8
|
+
First public release. Driven end-to-end through a real Sinatra + ActiveRecord
|
|
9
|
+
+ SQLite + JWT application (TaskFlow API) with full auth, CRUD, validation,
|
|
10
|
+
outbound HTTP, cron, and real exceptions — verified in the AllStak dashboard
|
|
11
|
+
against every feature page via Chrome DevTools MCP.
|
|
12
|
+
|
|
13
|
+
### Highlights
|
|
14
|
+
|
|
15
|
+
- **Rack middleware** that auto-captures inbound HTTP telemetry, unhandled
|
|
16
|
+
exceptions, request context, user context (env-standard + session), and
|
|
17
|
+
per-request trace lifecycle in one line: `use AllStak::Integrations::Rack::Middleware`.
|
|
18
|
+
- **ActiveRecord instrumentation** via `ActiveSupport::Notifications`
|
|
19
|
+
(`sql.active_record` subscriber) — zero-config, one event per query,
|
|
20
|
+
no duplication.
|
|
21
|
+
- **Net::HTTP instrumentation** via `Module#prepend` on `Net::HTTP#request` —
|
|
22
|
+
all convenience methods (`get`, `post`, etc.) funnel through `#request`, so
|
|
23
|
+
there is no duplication, and calls to the AllStak ingest host are filtered
|
|
24
|
+
out to avoid recursive instrumentation.
|
|
25
|
+
- **Distributed tracing** with `AllStak.tracing.in_span(...)` block form that
|
|
26
|
+
uses `ensure` to finish the span even on non-local flow (`throw :halt`,
|
|
27
|
+
early returns, etc.) — so Sinatra's `halt` does not orphan spans.
|
|
28
|
+
|
|
29
|
+
### Added
|
|
30
|
+
|
|
31
|
+
- `AllStak::Config` — all config via ENV or block form, with sensible defaults.
|
|
32
|
+
- `AllStak::Transport::HttpTransport` — retry/backoff (1s → 2s → 4s → 8s
|
|
33
|
+
+ jitter, max 5), 401 disable, 4xx no-retry, thread-safe.
|
|
34
|
+
- `AllStak::Transport::FlushBuffer` — bounded ring buffer with background
|
|
35
|
+
timer thread, 80% early-flush, overflow warning, single-flight drain.
|
|
36
|
+
- `AllStak::Modules::Errors` — `capture_exception`, `capture_error`,
|
|
37
|
+
breadcrumbs, user context, full `RequestContext` + trace ID serialization.
|
|
38
|
+
- `AllStak::Modules::Logs` — buffered structured logs with
|
|
39
|
+
`debug | info | warn | error | fatal` levels (normalizes `"warning"` → `"warn"`).
|
|
40
|
+
- `AllStak::Modules::HttpMonitor` — inbound + outbound HTTP telemetry,
|
|
41
|
+
batched up to 100 per POST, query-string stripping.
|
|
42
|
+
- `AllStak::Modules::Tracing` — span hierarchy with thread-local parent
|
|
43
|
+
tracking, `finish`-on-`ensure` block helper.
|
|
44
|
+
- `AllStak::Modules::Database` — normalized SQL, MD5 query hash, query-type
|
|
45
|
+
detection, status + error + row count, batched up to 100.
|
|
46
|
+
- `AllStak::Modules::Cron` — `job(slug) { ... }` block helper with success
|
|
47
|
+
and failure heartbeat, plus direct `ping`.
|
|
48
|
+
- `AllStak::Integrations::Rack::Middleware` — Rack 3-compatible middleware
|
|
49
|
+
with trace adoption (`X-AllStak-Trace-Id` / `traceparent`).
|
|
50
|
+
- `AllStak::Integrations::ActiveRecordIntegration::Subscriber` —
|
|
51
|
+
`sql.active_record` subscriber, auto-installed by `AllStak.configure`.
|
|
52
|
+
- `AllStak::Integrations::NetHTTP` — `Net::HTTP#request` patch,
|
|
53
|
+
auto-installed by `AllStak.configure`.
|
|
54
|
+
- `AllStak::Models::UserContext` / `RequestContext` — serialized objects
|
|
55
|
+
attached to error events.
|
|
56
|
+
|
|
57
|
+
### Verified production surface
|
|
58
|
+
|
|
59
|
+
- Real register/login/logout/JWT flow ✔
|
|
60
|
+
- Real CRUD with ownership / forbidden / 404 / state-transition guards ✔
|
|
61
|
+
- Real ActiveRecord validation failures ✔
|
|
62
|
+
- Real unhandled exceptions with full stack, user, request context, trace ✔
|
|
63
|
+
- 89 logs across `taskflow-ruby-api` service ✔
|
|
64
|
+
- 97 inbound + 3 outbound HTTP requests (success + failure) ✔
|
|
65
|
+
- 349 ActiveRecord queries (SELECT / INSERT / UPDATE / DELETE) grouped ✔
|
|
66
|
+
- Distributed tracing with span linking on error detail ✔
|
|
67
|
+
- 2 cron monitors (healthy + failed) auto-created ✔
|
|
68
|
+
|
|
69
|
+
### Breaking changes
|
|
70
|
+
|
|
71
|
+
None — initial public release.
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) AllStak
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
# AllStak Ruby SDK
|
|
2
|
+
|
|
3
|
+
Official Ruby SDK for [AllStak](https://allstak.dev) — error tracking,
|
|
4
|
+
structured logs, HTTP + ActiveRecord monitoring, distributed tracing, and cron
|
|
5
|
+
monitoring for Rack-based Ruby applications (Rails, Sinatra, Roda, Hanami).
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
gem "allstak"
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
bundle install
|
|
13
|
+
# or
|
|
14
|
+
gem install allstak
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## 60-second setup
|
|
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.
|
|
49
|
+
|
|
50
|
+
```ruby
|
|
51
|
+
AllStak.configure { |c| ... } # once at bootstrap
|
|
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
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
`AllStak.capture_message`, `AllStak.set_tag`, and `AllStak.set_context`
|
|
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
|
|
80
|
+
|
|
81
|
+
```ruby
|
|
82
|
+
# config/initializers/allstak.rb
|
|
83
|
+
require "allstak"
|
|
84
|
+
|
|
85
|
+
AllStak.configure do |c|
|
|
86
|
+
c.api_key = ENV["ALLSTAK_API_KEY"]
|
|
87
|
+
c.environment = Rails.env
|
|
88
|
+
c.release = "myapp@#{ENV['GIT_SHA'] || 'dev'}"
|
|
89
|
+
c.service_name = "myapp-api"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
Rails.application.config.middleware.use AllStak::Integrations::Rack::Middleware
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Sinatra
|
|
96
|
+
|
|
97
|
+
```ruby
|
|
98
|
+
require "sinatra/base"
|
|
99
|
+
require "allstak"
|
|
100
|
+
|
|
101
|
+
AllStak.configure { |c| c.api_key = ENV["ALLSTAK_API_KEY"] }
|
|
102
|
+
|
|
103
|
+
class MyApp < Sinatra::Base
|
|
104
|
+
use AllStak::Integrations::Rack::Middleware
|
|
105
|
+
# ...
|
|
106
|
+
end
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## What gets captured automatically
|
|
110
|
+
|
|
111
|
+
| What | How |
|
|
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 |
|
|
119
|
+
|
|
120
|
+
The ActiveRecord subscriber and Net::HTTP patch are installed automatically
|
|
121
|
+
by `AllStak.configure` — no extra setup needed.
|
|
122
|
+
|
|
123
|
+
## ActiveRecord
|
|
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.
|
|
130
|
+
|
|
131
|
+
```ruby
|
|
132
|
+
# Nothing to do — this just works:
|
|
133
|
+
User.where(email: "alice@example.com").first
|
|
134
|
+
# → captured as: SELECT "users".* FROM "users" WHERE "users"."email" = ? LIMIT ?
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Outbound HTTP (Net::HTTP)
|
|
138
|
+
|
|
139
|
+
```ruby
|
|
140
|
+
# Also nothing to do:
|
|
141
|
+
Net::HTTP.get(URI("https://api.example.com/v1/data"))
|
|
142
|
+
# → captured as outbound HTTP telemetry with method, host, path, status, duration
|
|
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.
|
|
149
|
+
|
|
150
|
+
## Manual capture cheat sheet
|
|
151
|
+
|
|
152
|
+
```ruby
|
|
153
|
+
# Errors
|
|
154
|
+
AllStak.capture_exception(exc, metadata: { order_id: "ORD-123" })
|
|
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
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Cron monitoring — slug auto-created on first ping
|
|
170
|
+
AllStak.cron.job("daily-report") do
|
|
171
|
+
generate_report
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# User context (for events that should show who was affected)
|
|
175
|
+
AllStak.set_user(id: "u-1", email: "alice@example.com")
|
|
176
|
+
AllStak.clear_user
|
|
177
|
+
|
|
178
|
+
# Graceful flush
|
|
179
|
+
AllStak.flush
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## Dashboard mapping
|
|
183
|
+
|
|
184
|
+
| Your code | Dashboard page |
|
|
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.
|
|
232
|
+
|
|
233
|
+
## Troubleshooting
|
|
234
|
+
|
|
235
|
+
| Symptom | Fix |
|
|
236
|
+
| ------------------------------------ | ------------------------------------------------ |
|
|
237
|
+
| No events in dashboard | Check `host` and `api_key`. Set `debug = true`. |
|
|
238
|
+
| 401 warning | Invalid API key. Create a new one in Settings. |
|
|
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. |
|
|
243
|
+
|
|
244
|
+
## Full Sinatra + ActiveRecord example
|
|
245
|
+
|
|
246
|
+
```ruby
|
|
247
|
+
require "sinatra/base"
|
|
248
|
+
require "active_record"
|
|
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
|
+
```
|
|
289
|
+
|
|
290
|
+
## License
|
|
291
|
+
|
|
292
|
+
MIT
|
data/allstak.gemspec
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
require_relative "lib/allstak/version"
|
|
2
|
+
|
|
3
|
+
Gem::Specification.new do |spec|
|
|
4
|
+
spec.name = "allstak"
|
|
5
|
+
spec.version = AllStak::VERSION
|
|
6
|
+
spec.authors = ["AllStak"]
|
|
7
|
+
spec.email = ["sdk@allstak.dev"]
|
|
8
|
+
|
|
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: Rack/Rails middleware, ActiveRecord instrumentation, outbound HTTP capture, distributed tracing, cron monitoring, and structured logs."
|
|
11
|
+
spec.homepage = "https://allstak.dev"
|
|
12
|
+
spec.license = "MIT"
|
|
13
|
+
spec.required_ruby_version = ">= 3.0.0"
|
|
14
|
+
|
|
15
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
|
16
|
+
spec.metadata["source_code_uri"] = "https://github.com/allstak-io/allstak-ruby"
|
|
17
|
+
spec.metadata["changelog_uri"] = "https://github.com/allstak-io/allstak-ruby/blob/main/CHANGELOG.md"
|
|
18
|
+
spec.metadata["bug_tracker_uri"] = "https://github.com/allstak-io/allstak-ruby/issues"
|
|
19
|
+
spec.metadata["documentation_uri"] = "https://allstak.dev/docs/sdks/ruby"
|
|
20
|
+
spec.metadata["rubygems_mfa_required"] = "true"
|
|
21
|
+
|
|
22
|
+
spec.files = Dir[
|
|
23
|
+
"lib/**/*.rb",
|
|
24
|
+
"README.md",
|
|
25
|
+
"CHANGELOG.md",
|
|
26
|
+
"LICENSE",
|
|
27
|
+
"allstak.gemspec"
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
spec.require_paths = ["lib"]
|
|
31
|
+
|
|
32
|
+
# No runtime dependencies — the SDK uses only the Ruby standard library.
|
|
33
|
+
# Framework integrations (Rack, Rails, ActiveRecord, Net::HTTP) are loaded
|
|
34
|
+
# lazily and only activate when the host app has them available.
|
|
35
|
+
|
|
36
|
+
spec.add_development_dependency "rspec", "~> 3.12"
|
|
37
|
+
spec.add_development_dependency "webmock", "~> 3.19"
|
|
38
|
+
spec.add_development_dependency "rack", "~> 3.0"
|
|
39
|
+
spec.add_development_dependency "activerecord", "~> 8.0"
|
|
40
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
module AllStak
|
|
2
|
+
# The AllStak SDK client. Create once via {AllStak.configure}.
|
|
3
|
+
class Client
|
|
4
|
+
attr_reader :config, :logger, :errors, :logs, :http, :tracing, :database, :cron, :tags, :contexts
|
|
5
|
+
|
|
6
|
+
def initialize(config, logger)
|
|
7
|
+
@config = config
|
|
8
|
+
@logger = logger
|
|
9
|
+
@transport = Transport::HttpTransport.new(config, logger)
|
|
10
|
+
|
|
11
|
+
@errors = Modules::Errors.new(@transport, config, logger)
|
|
12
|
+
@logs = Modules::Logs.new(@transport, config, logger)
|
|
13
|
+
@http = Modules::HttpMonitor.new(@transport, config, logger)
|
|
14
|
+
@tracing = Modules::Tracing.new(@transport, config, logger)
|
|
15
|
+
@database = Modules::Database.new(@transport, config, logger)
|
|
16
|
+
@cron = Modules::Cron.new(@transport, logger, config)
|
|
17
|
+
@tags = {}
|
|
18
|
+
@contexts = {}
|
|
19
|
+
|
|
20
|
+
at_exit { shutdown rescue nil }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def set_user(id: nil, email: nil, ip: nil)
|
|
24
|
+
@errors.set_user(id: id, email: email, ip: ip)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def clear_user
|
|
28
|
+
@errors.clear_user
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def capture_exception(exc, **kw)
|
|
32
|
+
kw = merge_default_metadata(kw)
|
|
33
|
+
@errors.capture_exception(exc, **kw)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def capture_error(exception_class, message, **kw)
|
|
37
|
+
kw = merge_default_metadata(kw)
|
|
38
|
+
@errors.capture_error(exception_class, message, **kw)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Capture a standalone string as an "error group" at the given level.
|
|
42
|
+
# Cross-SDK parity with JS/Python/PHP/Java `captureMessage`.
|
|
43
|
+
# Implemented on top of capture_error so the dashboard surfaces it as
|
|
44
|
+
# an "info"/"warning"/"error" level entry in the Errors list.
|
|
45
|
+
def capture_message(message, level: "info", **kw)
|
|
46
|
+
kw = merge_default_metadata(kw)
|
|
47
|
+
@errors.capture_error("Message", message.to_s, level: level.to_s, **kw)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Attach a key/value tag to every subsequent event sent by the SDK.
|
|
51
|
+
# Cross-SDK parity with JS `setTag` and Python `set_tag`.
|
|
52
|
+
def set_tag(key, value)
|
|
53
|
+
@tags[key.to_s] = value.to_s
|
|
54
|
+
self
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Bulk-set tags.
|
|
58
|
+
def set_tags(pairs)
|
|
59
|
+
pairs.each { |k, v| set_tag(k, v) }
|
|
60
|
+
self
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Attach a key/value context entry (goes into metadata on every event).
|
|
64
|
+
# Cross-SDK parity with JS/Python `setContext`.
|
|
65
|
+
def set_context(key, value)
|
|
66
|
+
@contexts[key.to_s] = value
|
|
67
|
+
self
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def flush
|
|
71
|
+
@logs.flush
|
|
72
|
+
@http.flush
|
|
73
|
+
@tracing.flush
|
|
74
|
+
@database.flush
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def shutdown
|
|
78
|
+
flush
|
|
79
|
+
@logs.shutdown
|
|
80
|
+
@http.shutdown
|
|
81
|
+
@tracing.shutdown
|
|
82
|
+
@database.shutdown
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
# Fold the persistent tags + contexts into any explicit metadata caller
|
|
88
|
+
# passed. Caller-supplied keys win on conflict.
|
|
89
|
+
def merge_default_metadata(kw)
|
|
90
|
+
return kw if @tags.empty? && @contexts.empty?
|
|
91
|
+
base = {}
|
|
92
|
+
base.merge!(@tags) unless @tags.empty?
|
|
93
|
+
base.merge!(@contexts) unless @contexts.empty?
|
|
94
|
+
existing = kw[:metadata] || {}
|
|
95
|
+
kw.merge(metadata: base.merge(existing))
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
module AllStak
|
|
2
|
+
# SDK configuration. Populated via {AllStak.configure}.
|
|
3
|
+
class Config
|
|
4
|
+
attr_accessor :api_key, :host, :environment, :release, :service_name,
|
|
5
|
+
:flush_interval_ms, :buffer_size, :debug,
|
|
6
|
+
:connect_timeout, :read_timeout, :max_retries,
|
|
7
|
+
:capture_unhandled_exceptions, :capture_http_requests,
|
|
8
|
+
:capture_user_context, :capture_sql
|
|
9
|
+
|
|
10
|
+
def initialize
|
|
11
|
+
@api_key = ENV["ALLSTAK_API_KEY"].to_s
|
|
12
|
+
@host = ENV["ALLSTAK_HOST"] || "https://api.allstak.sa"
|
|
13
|
+
@environment = ENV["ALLSTAK_ENVIRONMENT"]
|
|
14
|
+
@release = ENV["ALLSTAK_RELEASE"]
|
|
15
|
+
@service_name = ENV["ALLSTAK_SERVICE"] || "ruby-service"
|
|
16
|
+
@flush_interval_ms = 2_000
|
|
17
|
+
@buffer_size = 500
|
|
18
|
+
@debug = !ENV["ALLSTAK_DEBUG"].to_s.empty?
|
|
19
|
+
@connect_timeout = 3
|
|
20
|
+
@read_timeout = 3
|
|
21
|
+
@max_retries = 5
|
|
22
|
+
@capture_unhandled_exceptions = true
|
|
23
|
+
@capture_http_requests = true
|
|
24
|
+
@capture_user_context = true
|
|
25
|
+
@capture_sql = true
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def valid?
|
|
29
|
+
!@api_key.to_s.empty?
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def host=(value)
|
|
33
|
+
@host = value.to_s.sub(%r{/+\z}, "")
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
module AllStak
|
|
2
|
+
module Integrations
|
|
3
|
+
module ActiveRecordIntegration
|
|
4
|
+
# Subscribes to `sql.active_record` via ActiveSupport::Notifications and records
|
|
5
|
+
# every ORM-level query with timing, status, and connection metadata.
|
|
6
|
+
#
|
|
7
|
+
# Skips:
|
|
8
|
+
# * schema / EXPLAIN / SAVEPOINT / TRANSACTION / nil SQL
|
|
9
|
+
# * our own AllStak internal transport (there is none on AR side, but guarded)
|
|
10
|
+
#
|
|
11
|
+
# Duplicates are avoided because ActiveRecord fires a single `sql.active_record`
|
|
12
|
+
# event per executed command — whether it's from an ORM query, a `find_by_sql`,
|
|
13
|
+
# or `connection.execute`. Raw-SQL executions done through the AR connection
|
|
14
|
+
# are therefore captured via the same subscriber without double-counting.
|
|
15
|
+
class Subscriber
|
|
16
|
+
IGNORED_NAMES = [
|
|
17
|
+
"SCHEMA", "EXPLAIN", "TRANSACTION", "SAVEPOINT", "RELEASE SAVEPOINT"
|
|
18
|
+
].freeze
|
|
19
|
+
|
|
20
|
+
def self.install!
|
|
21
|
+
return if @installed
|
|
22
|
+
return unless defined?(::ActiveSupport::Notifications)
|
|
23
|
+
|
|
24
|
+
::ActiveSupport::Notifications.subscribe("sql.active_record") do |*args|
|
|
25
|
+
begin
|
|
26
|
+
event = ::ActiveSupport::Notifications::Event.new(*args)
|
|
27
|
+
name = event.payload[:name].to_s
|
|
28
|
+
sql = event.payload[:sql].to_s
|
|
29
|
+
next if sql.empty?
|
|
30
|
+
next if IGNORED_NAMES.include?(name)
|
|
31
|
+
next unless AllStak.initialized?
|
|
32
|
+
|
|
33
|
+
client = AllStak.client
|
|
34
|
+
config = client.config
|
|
35
|
+
next unless config.capture_sql
|
|
36
|
+
|
|
37
|
+
status = event.payload[:exception] ? "error" : "success"
|
|
38
|
+
error_message = event.payload[:exception].is_a?(Array) ? event.payload[:exception].last.to_s : nil
|
|
39
|
+
rows = event.payload[:row_count].to_i rescue -1
|
|
40
|
+
|
|
41
|
+
db_name = nil
|
|
42
|
+
db_type = nil
|
|
43
|
+
if defined?(::ActiveRecord::Base) && ::ActiveRecord::Base.respond_to?(:connection_db_config)
|
|
44
|
+
begin
|
|
45
|
+
cfg = ::ActiveRecord::Base.connection_db_config
|
|
46
|
+
db_name = cfg.database rescue nil
|
|
47
|
+
db_type = cfg.adapter rescue nil
|
|
48
|
+
rescue
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
client.database.record(
|
|
53
|
+
sql: sql,
|
|
54
|
+
duration_ms: event.duration.to_i,
|
|
55
|
+
status: status,
|
|
56
|
+
error_message: error_message,
|
|
57
|
+
database_name: db_name,
|
|
58
|
+
database_type: db_type,
|
|
59
|
+
rows_affected: rows >= 0 ? rows : -1,
|
|
60
|
+
trace_id: client.tracing.current_trace_id,
|
|
61
|
+
span_id: client.tracing.current_span_id
|
|
62
|
+
)
|
|
63
|
+
rescue => e
|
|
64
|
+
# never raise into host
|
|
65
|
+
AllStak.logger.debug("[AllStak] AR subscriber error: #{e.message}") rescue nil
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
@installed = true
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def self.installed?
|
|
73
|
+
@installed == true
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|