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 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