forge_ops_tracker 0.5.0 → 0.7.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 +21 -0
- data/README.md +141 -1
- data/lib/forge_ops_tracker/client.rb +20 -0
- data/lib/forge_ops_tracker/configuration.rb +56 -0
- data/lib/forge_ops_tracker/error_subscriber.rb +6 -1
- data/lib/forge_ops_tracker/event_builder.rb +7 -5
- data/lib/forge_ops_tracker/infrastructure_metric_buffer.rb +69 -0
- data/lib/forge_ops_tracker/integrations/net_http.rb +32 -0
- data/lib/forge_ops_tracker/integrations/puma.rb +55 -0
- data/lib/forge_ops_tracker/integrations/sidekiq.rb +69 -0
- data/lib/forge_ops_tracker/integrations/solid_queue.rb +37 -0
- data/lib/forge_ops_tracker/metric_buffer.rb +80 -0
- data/lib/forge_ops_tracker/middleware/user_context.rb +52 -0
- data/lib/forge_ops_tracker/performance_flusher.rb +101 -0
- data/lib/forge_ops_tracker/performance_instrumentation.rb +60 -0
- data/lib/forge_ops_tracker/periodic_poller.rb +50 -0
- data/lib/forge_ops_tracker/railtie.rb +119 -0
- data/lib/forge_ops_tracker/version.rb +1 -1
- data/lib/forge_ops_tracker.rb +50 -0
- metadata +53 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 72692319cfdf068129d7d005773e21a3b6b78cb9f7816cf0e7c8e6409c9704f1
|
|
4
|
+
data.tar.gz: 713f244a7ab71a005e66b7f00d1dff3e1e3ffc44e304d5a36f79c911e508c74b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 57f5372fc1aa6ac8f8830804003c00d68d0633d4f330dc8f6c678faa1c1dbc3f09ad1da0f29c180c63a90dc41f2d0f6acb008e4a726d59ba6b806298af3d6d8f
|
|
7
|
+
data.tar.gz: 2b21f12200f99202c5f26cc739104021d5eaf1cfe16a01b075fe4bc3418ada28cf7befb91470fb1ebd8bb18865cd52e61a705decc5f511c2c497a79c74c36016
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.7.0
|
|
4
|
+
|
|
5
|
+
- Automatically associates a reported error with the current signed-in user, for any Warden-based
|
|
6
|
+
auth setup (Devise included; Devise just mounts Warden automatically, this isn't Devise-
|
|
7
|
+
specific). On by default, same as every other automatic instrumentation this gem does; opt out
|
|
8
|
+
with `config.track_current_user = false`. `ForgeOpsTracker.set_user(id:, email:, username:)`
|
|
9
|
+
manually sets or overrides it, for a custom auth setup or outside a request entirely (a
|
|
10
|
+
background job, a console session).
|
|
11
|
+
|
|
12
|
+
## 0.6.0
|
|
13
|
+
|
|
14
|
+
- Performance monitoring now also covers database queries (`sql.active_record`, bucketed by
|
|
15
|
+
Rails' own query name, never raw SQL text), background jobs (`perform.active_job`, any backend,
|
|
16
|
+
Solid Queue included, plus raw Sidekiq workers via a server middleware, deduplicated against
|
|
17
|
+
jobs dispatched through Active Job), and outbound `Net::HTTP` calls, alongside the existing
|
|
18
|
+
controller/action timing. Each shows up as its own `kind` on the same `performance` dashboard
|
|
19
|
+
dataset. Also reports Sidekiq's, Solid Queue's, and Puma's own operational gauges (queue depth,
|
|
20
|
+
worker/dispatcher counts, Puma's thread pool) as infrastructure metrics on a periodic timer
|
|
21
|
+
(`config.gauge_poll_interval`, default 60 seconds), whenever the corresponding library is
|
|
22
|
+
already loaded. All on the same `track_performance` flag as before; no new flags to opt into.
|
|
23
|
+
|
|
3
24
|
## 0.5.0
|
|
4
25
|
|
|
5
26
|
- Every reported event now carries `sdk_name` ("ruby"), so a project's language on the ForgeOps
|
data/README.md
CHANGED
|
@@ -67,13 +67,41 @@ Bottom line: if an exception would otherwise crash something, you're already cov
|
|
|
67
67
|
code already catches and handles it, route that specific `rescue` through `Rails.error.handle`/
|
|
68
68
|
`.record` instead of a bare one wherever you want ForgeOps to know about it.
|
|
69
69
|
|
|
70
|
+
## Identifying users
|
|
71
|
+
|
|
72
|
+
If you're using Devise, or any other Warden-based authentication, the currently signed-in user is
|
|
73
|
+
already associated with every error automatically, on by default, no setup needed:
|
|
74
|
+
|
|
75
|
+
```ruby
|
|
76
|
+
ForgeOpsTracker.configure do |config|
|
|
77
|
+
config.track_current_user = false # opt out entirely
|
|
78
|
+
end
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
For a custom auth setup this can't detect, or to override its guess, call
|
|
82
|
+
`ForgeOpsTracker.set_user` yourself:
|
|
83
|
+
|
|
84
|
+
```ruby
|
|
85
|
+
before_action do
|
|
86
|
+
ForgeOpsTracker.set_user(id: current_user&.id, email: current_user&.email)
|
|
87
|
+
end
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Also usable outside a request entirely, a background job or a console session, for the rest of
|
|
91
|
+
whichever thread calls it. `id`/`email`/`username` are all independently optional; call it with
|
|
92
|
+
none of them (or with a signed-out `current_user`, as the `&.` above already handles) to clear
|
|
93
|
+
whatever was set. Shows up on an issue's own detail page, and as its own `affected_users_count`
|
|
94
|
+
alongside the regular event count.
|
|
95
|
+
|
|
70
96
|
## PII scrubbing
|
|
71
97
|
|
|
72
98
|
By default, the message, backtrace, and any context/tags you attach are scanned for likely
|
|
73
99
|
personal data: email addresses, formatted SSNs/credit cards, known API key/token formats, and
|
|
74
100
|
anything under a suspiciously-named key (`password`, `api_key`, `ssn`, and similar); redacted
|
|
75
101
|
before the payload ever leaves this process. ForgeOps itself scrubs again on arrival regardless, so
|
|
76
|
-
this is a second, earlier layer, not the only one.
|
|
102
|
+
this is a second, earlier layer, not the only one. The user identified via automatic Warden
|
|
103
|
+
detection or `ForgeOpsTracker.set_user` above is a deliberate exception: it's never scrubbed,
|
|
104
|
+
since redacting it would defeat the whole point of identifying users in the first place.
|
|
77
105
|
|
|
78
106
|
To disable it (e.g. if your app already scrubs its own error context, or you have your own reasons
|
|
79
107
|
to want the raw payload):
|
|
@@ -124,3 +152,115 @@ end
|
|
|
124
152
|
|
|
125
153
|
Requires a ForgeOps plan that includes release health; on a plan that doesn't, the periodic
|
|
126
154
|
flushes are simply rejected server-side and dropped, exactly like any other delivery failure.
|
|
155
|
+
|
|
156
|
+
## Performance monitoring
|
|
157
|
+
|
|
158
|
+
By default, every request's controller/action duration is timed (via Rails' own
|
|
159
|
+
`process_action.action_controller` instrumentation, no extra middleware needed) so a dashboard
|
|
160
|
+
widget on ForgeOps can show which parts of your app are actually slow, not just which ones raise.
|
|
161
|
+
Bucketed by transaction ("PostsController#show") and flushed as a small periodic aggregate per
|
|
162
|
+
transaction on a background thread, the same delivery philosophy as session tracking above: a
|
|
163
|
+
broken or unreachable tracker never affects the host app either way.
|
|
164
|
+
|
|
165
|
+
```ruby
|
|
166
|
+
ForgeOpsTracker.configure do |config|
|
|
167
|
+
config.track_performance = false # opt out entirely
|
|
168
|
+
config.performance_flush_interval = 30 # seconds; default 60
|
|
169
|
+
end
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Requires a ForgeOps plan that includes performance monitoring; on a plan that doesn't, the
|
|
173
|
+
periodic flushes are simply rejected server-side and dropped, exactly like any other delivery
|
|
174
|
+
failure.
|
|
175
|
+
|
|
176
|
+
### Database queries, background jobs, and outbound HTTP calls
|
|
177
|
+
|
|
178
|
+
The same automatic instrumentation, on the same `track_performance` flag, also covers:
|
|
179
|
+
|
|
180
|
+
- **Database queries**, via `sql.active_record`: bucketed by Rails' own auto-generated query name
|
|
181
|
+
("User Load", "Order Create"), not the raw SQL text. Internal schema-introspection queries
|
|
182
|
+
("SCHEMA") and cached reads (never a real round trip to the database) are skipped.
|
|
183
|
+
- **Background jobs**, via `perform.active_job`: bucketed by job class. Covers any Active Job
|
|
184
|
+
backend, Solid Queue included, with no backend-specific code needed.
|
|
185
|
+
- **Raw Sidekiq workers** (a `Sidekiq::Worker`/`Sidekiq::Job` not dispatched through Active Job):
|
|
186
|
+
a server middleware, registered automatically when Sidekiq is already loaded. A job dispatched
|
|
187
|
+
through Active Job and run on Sidekiq is still only counted once, by the Active Job hook above,
|
|
188
|
+
not twice.
|
|
189
|
+
- **Outbound `Net::HTTP` calls**, bucketed by `"<method> <host>"`, never the full URL (a path or
|
|
190
|
+
query string could carry an id or a token). Most other Ruby HTTP client libraries (Faraday's own
|
|
191
|
+
`net_http` adapter, HTTParty, RestClient) ultimately call through `Net::HTTP`, so this covers
|
|
192
|
+
those too as a side effect.
|
|
193
|
+
|
|
194
|
+
Every one of these shows up as its own `kind` ("controller", "job", "query", "http") on the same
|
|
195
|
+
`performance` dashboard dataset, so "slowest jobs" and "slowest queries" are just a filtered
|
|
196
|
+
version of the same widget builder "slowest transactions" already uses.
|
|
197
|
+
|
|
198
|
+
### Sidekiq, Solid Queue, and Puma gauges
|
|
199
|
+
|
|
200
|
+
Also on by default whenever the corresponding library is already loaded: Sidekiq's own aggregate
|
|
201
|
+
stats (`Sidekiq::Stats`: processed/failed/scheduled/retry/dead counts, plus a queue-depth reading
|
|
202
|
+
per queue), Solid Queue's own state (active workers, active dispatchers, failed/scheduled/blocked
|
|
203
|
+
counts, a queue-depth reading per queue), and Puma's own thread pool (backlog, running threads,
|
|
204
|
+
pool capacity). Reported as ordinary infrastructure metrics (`sidekiq.queue_depth.default`,
|
|
205
|
+
`puma.backlog`, and so on) via the same `capture_infrastructure_metric` call your own scripts use,
|
|
206
|
+
on a periodic timer:
|
|
207
|
+
|
|
208
|
+
```ruby
|
|
209
|
+
ForgeOpsTracker.configure do |config|
|
|
210
|
+
config.gauge_poll_interval = 30 # seconds; default 60
|
|
211
|
+
end
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
No separate opt-out: these are gated by the same `track_performance` flag as everything else in
|
|
215
|
+
this section, not a flag of their own.
|
|
216
|
+
|
|
217
|
+
## Custom metrics
|
|
218
|
+
|
|
219
|
+
Unlike session/performance tracking above, there's no automatic instrumentation here at all: a
|
|
220
|
+
signup or a payment isn't something this gem could ever detect on its own, so this is an explicit
|
|
221
|
+
call your own code makes. `value` defaults to `1.0` so a bare counter-style call needs no
|
|
222
|
+
argument; pass one for a metric with a real amount. Buffered and flushed as a batch on a
|
|
223
|
+
background thread, same delivery philosophy as everything else here, so this is safe to call from
|
|
224
|
+
inside a request (right after a signup completes, say) without adding network latency there.
|
|
225
|
+
|
|
226
|
+
```ruby
|
|
227
|
+
ForgeOpsTracker.capture_metric("signups")
|
|
228
|
+
ForgeOpsTracker.capture_metric("revenue", value: 49.00)
|
|
229
|
+
|
|
230
|
+
ForgeOpsTracker.configure do |config|
|
|
231
|
+
config.metric_flush_interval = 30 # seconds; default 60
|
|
232
|
+
end
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Requires a ForgeOps plan that includes custom metrics; on a plan that doesn't, the periodic
|
|
236
|
+
flushes are simply rejected server-side and dropped, exactly like any other delivery failure.
|
|
237
|
+
|
|
238
|
+
## Infrastructure monitoring
|
|
239
|
+
|
|
240
|
+
A deliberately different shape from everything above: there's no automatic instrumentation and no
|
|
241
|
+
ForgeOps-built agent. Run your own short-lived script on a cron entry or a systemd timer, reading
|
|
242
|
+
your own host's own stats; `hostname` defaults to the box the script is actually running on.
|
|
243
|
+
Buffered and flushed on exit, so a handful of capture calls in one short-lived process still cost
|
|
244
|
+
one network request, not several.
|
|
245
|
+
|
|
246
|
+
```ruby
|
|
247
|
+
# A cron entry or systemd timer runs this periodically, not your web app itself.
|
|
248
|
+
require "forge_ops_tracker"
|
|
249
|
+
ForgeOpsTracker.configure { |config| config.dsn = ENV["FORGE_OPS_DSN"] }
|
|
250
|
+
|
|
251
|
+
load_average = File.read("/proc/loadavg").split.first.to_f
|
|
252
|
+
ForgeOpsTracker.capture_infrastructure_metric("load_average", value: load_average)
|
|
253
|
+
|
|
254
|
+
meminfo = File.read("/proc/meminfo").lines.to_h { |line| line.split(":").map(&:strip) }
|
|
255
|
+
total_kb, available_kb = meminfo["MemTotal"].to_i, meminfo["MemAvailable"].to_i
|
|
256
|
+
ForgeOpsTracker.capture_infrastructure_metric("memory_used_percent",
|
|
257
|
+
value: 100.0 * (total_kb - available_kb) / total_kb)
|
|
258
|
+
|
|
259
|
+
disk_used_percent = `df --output=pcent / | tail -1`.strip.delete("%").to_f
|
|
260
|
+
ForgeOpsTracker.capture_infrastructure_metric("disk_used_percent", value: disk_used_percent)
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
The example above is Linux-specific (`/proc/loadavg`, `/proc/meminfo`); on another OS, read that
|
|
264
|
+
platform's own equivalents instead. Requires a ForgeOps plan that includes infrastructure
|
|
265
|
+
monitoring; on a plan that doesn't, the periodic flushes are simply rejected server-side and
|
|
266
|
+
dropped, exactly like any other delivery failure.
|
|
@@ -23,6 +23,26 @@ module ForgeOpsTracker
|
|
|
23
23
|
post(configuration.session_checkins_uri, payload)
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
+
# Same delivery contract again; see Configuration#performance_samples_uri. payload here is a
|
|
27
|
+
# batch (one entry per distinct transaction a flush interval saw), not a single aggregate the
|
|
28
|
+
# way deliver_session_checkin's own payload is, so this posts { samples: [...] } rather than
|
|
29
|
+
# the array bare, matching what Api::V1::PerformanceSamplesController expects.
|
|
30
|
+
def deliver_performance_samples(samples)
|
|
31
|
+
post(configuration.performance_samples_uri, { samples: samples })
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Same delivery contract again; see Configuration#custom_metrics_uri. payload is a batch of
|
|
35
|
+
# individual capture_metric calls (see MetricBuffer), matching what
|
|
36
|
+
# Api::V1::CustomMetricsController expects.
|
|
37
|
+
def deliver_metrics(entries)
|
|
38
|
+
post(configuration.custom_metrics_uri, { metrics: entries })
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Same again; see Configuration#infrastructure_metrics_uri and InfrastructureMetricBuffer.
|
|
42
|
+
def deliver_infrastructure_metrics(entries)
|
|
43
|
+
post(configuration.infrastructure_metrics_uri, { metrics: entries })
|
|
44
|
+
end
|
|
45
|
+
|
|
26
46
|
private
|
|
27
47
|
attr_reader :configuration
|
|
28
48
|
|
|
@@ -9,6 +9,10 @@ module ForgeOpsTracker
|
|
|
9
9
|
attr_accessor :enabled_environments, :queue_size, :open_timeout, :read_timeout, :scrub_pii
|
|
10
10
|
attr_accessor :capture_source_context
|
|
11
11
|
attr_accessor :track_sessions, :session_flush_interval
|
|
12
|
+
attr_accessor :track_performance, :performance_flush_interval
|
|
13
|
+
attr_accessor :track_current_user
|
|
14
|
+
attr_accessor :metric_flush_interval, :infrastructure_metric_flush_interval
|
|
15
|
+
attr_accessor :gauge_poll_interval
|
|
12
16
|
|
|
13
17
|
def initialize
|
|
14
18
|
@dsn = ENV["FORGE_OPS_DSN"]
|
|
@@ -43,6 +47,30 @@ module ForgeOpsTracker
|
|
|
43
47
|
# ForgeOpsTracker::Middleware::SessionTracking for what this actually wraps.
|
|
44
48
|
@track_sessions = true
|
|
45
49
|
@session_flush_interval = 60
|
|
50
|
+
# Auto-instruments every request the same "on unless you turn it off" default as
|
|
51
|
+
# track_sessions above; see Railtie's own comment for exactly what this subscribes to.
|
|
52
|
+
@track_performance = true
|
|
53
|
+
@performance_flush_interval = 60
|
|
54
|
+
# Auto-detects the current user via Warden (env["warden"].user; Devise mounts Warden
|
|
55
|
+
# automatically, so this covers Devise apps too, but works for any Warden-based auth, not
|
|
56
|
+
# just Devise specifically) the same "on unless you turn it off" default every other
|
|
57
|
+
# automatic instrumentation flag above already has. See
|
|
58
|
+
# ForgeOpsTracker::Middleware::UserContext for how, and ForgeOpsTracker.set_user for the
|
|
59
|
+
# manual override/fallback when there's no Warden at all or its guess isn't right.
|
|
60
|
+
@track_current_user = true
|
|
61
|
+
# No track_metrics/track_infrastructure boolean the way track_sessions/track_performance
|
|
62
|
+
# each have one: those gate automatic instrumentation that's on unless you turn it off;
|
|
63
|
+
# capture_metric/capture_infrastructure_metric are explicit calls the customer's own code
|
|
64
|
+
# chooses to make at all, so there's no "automatic behavior" for a flag to disable. Only
|
|
65
|
+
# the flush interval needs a knob.
|
|
66
|
+
@metric_flush_interval = 60
|
|
67
|
+
@infrastructure_metric_flush_interval = 60
|
|
68
|
+
# How often the Sidekiq/Solid Queue/Puma stats reporters poll and report their own gauge
|
|
69
|
+
# readings (queue depth, worker counts, Puma backlog/threads/pool capacity). Gated by
|
|
70
|
+
# track_performance, same as controller/job/query/http timing: one flag for "automatically
|
|
71
|
+
# instrument this app's own operational data," reused rather than adding a separate boolean
|
|
72
|
+
# per new automatic instrumentation source this gem grows.
|
|
73
|
+
@gauge_poll_interval = 60
|
|
46
74
|
end
|
|
47
75
|
|
|
48
76
|
def api_key
|
|
@@ -72,6 +100,34 @@ module ForgeOpsTracker
|
|
|
72
100
|
uri
|
|
73
101
|
end
|
|
74
102
|
|
|
103
|
+
# Same substitution as session_checkins_uri above, its own sibling path under the same DSN.
|
|
104
|
+
def performance_samples_uri
|
|
105
|
+
uri = ingestion_uri
|
|
106
|
+
return nil unless uri
|
|
107
|
+
|
|
108
|
+
uri = uri.dup
|
|
109
|
+
uri.path = uri.path.sub(%r{/events\z}, "/performance_samples")
|
|
110
|
+
uri
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def custom_metrics_uri
|
|
114
|
+
uri = ingestion_uri
|
|
115
|
+
return nil unless uri
|
|
116
|
+
|
|
117
|
+
uri = uri.dup
|
|
118
|
+
uri.path = uri.path.sub(%r{/events\z}, "/custom_metrics")
|
|
119
|
+
uri
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def infrastructure_metrics_uri
|
|
123
|
+
uri = ingestion_uri
|
|
124
|
+
return nil unless uri
|
|
125
|
+
|
|
126
|
+
uri = uri.dup
|
|
127
|
+
uri.path = uri.path.sub(%r{/events\z}, "/infrastructure_metrics")
|
|
128
|
+
uri
|
|
129
|
+
end
|
|
130
|
+
|
|
75
131
|
def enabled?
|
|
76
132
|
!blank?(dsn) && !blank?(api_key) && enabled_environments.map(&:to_s).include?(environment.to_s)
|
|
77
133
|
end
|
|
@@ -14,7 +14,12 @@ module ForgeOpsTracker
|
|
|
14
14
|
def report(error, handled: true, severity: nil, context: {}, source: nil)
|
|
15
15
|
return unless configuration.enabled?
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
# Read here, not inside EventBuilder: Rails.error.subscribe's own #report interface never
|
|
18
|
+
# hands this class the Rack env, so ForgeOpsTracker::Middleware::UserContext (Warden
|
|
19
|
+
# auto-detection) and ForgeOpsTracker.set_user (the manual override/fallback) both
|
|
20
|
+
# communicate with this exact same thread-local instead, the only channel available here.
|
|
21
|
+
user = Thread.current[:forge_ops_tracker_current_user]
|
|
22
|
+
delivery_queue.push(event_builder.build(error, context: context, user: user))
|
|
18
23
|
nil
|
|
19
24
|
rescue StandardError => e
|
|
20
25
|
configuration.logger&.debug { "[ForgeOpsTracker] report failed: #{e.class}: #{e.message}" }
|
|
@@ -27,7 +27,7 @@ module ForgeOpsTracker
|
|
|
27
27
|
@configuration = configuration
|
|
28
28
|
end
|
|
29
29
|
|
|
30
|
-
def build(error, context: {})
|
|
30
|
+
def build(error, context: {}, user: nil)
|
|
31
31
|
payload = {
|
|
32
32
|
exception_class: error.class.name,
|
|
33
33
|
message: error.message.to_s,
|
|
@@ -40,16 +40,18 @@ module ForgeOpsTracker
|
|
|
40
40
|
tags: {},
|
|
41
41
|
sdk_name: SDK_NAME
|
|
42
42
|
}
|
|
43
|
+
payload[:user] = user if user && !user.empty?
|
|
43
44
|
scrub(payload)
|
|
44
45
|
end
|
|
45
46
|
|
|
46
47
|
private
|
|
47
48
|
attr_reader :configuration
|
|
48
49
|
|
|
49
|
-
# exception_class/occurred_at/environment/release/server_name/sdk_name are
|
|
50
|
-
#
|
|
51
|
-
#
|
|
52
|
-
#
|
|
50
|
+
# exception_class/occurred_at/environment/release/server_name/sdk_name/user are left alone;
|
|
51
|
+
# structured fields this gem or the host app sets deliberately, not free text an exception
|
|
52
|
+
# or its context could accidentally spill sensitive data into. user specifically is a
|
|
53
|
+
# deliberate exemption, not an oversight: the server's own PiiScrubber-equivalent would
|
|
54
|
+
# otherwise redact the exact email address this field exists to carry.
|
|
53
55
|
def scrub(payload)
|
|
54
56
|
return payload unless configuration.scrub_pii
|
|
55
57
|
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
require "thread"
|
|
2
|
+
|
|
3
|
+
module ForgeOpsTracker
|
|
4
|
+
# Collects individual ForgeOpsTracker.capture_infrastructure_metric readings in-process and
|
|
5
|
+
# periodically flushes them as one batch, same shape as MetricBuffer (a sibling class, not a
|
|
6
|
+
# shared base -- this gem keeps SessionFlusher/PerformanceFlusher as separate, independently-
|
|
7
|
+
# readable classes despite their own similar shape too, and these two follow that precedent).
|
|
8
|
+
# Its main real caller is a short-lived cron/script process that exits right after a handful of
|
|
9
|
+
# capture calls, so the at_exit flush below is what actually matters in practice; the periodic
|
|
10
|
+
# background-thread timer still runs for a long-lived process that chooses to call this
|
|
11
|
+
# continuously instead, but rarely gets the chance to fire in the cron case.
|
|
12
|
+
class InfrastructureMetricBuffer
|
|
13
|
+
def initialize(configuration, client: Client.new(configuration))
|
|
14
|
+
@configuration = configuration
|
|
15
|
+
@client = client
|
|
16
|
+
@mutex = Mutex.new
|
|
17
|
+
@entries = []
|
|
18
|
+
@thread = nil
|
|
19
|
+
|
|
20
|
+
at_exit { flush }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def record(metric_name:, value:, hostname:)
|
|
24
|
+
ensure_worker_started
|
|
25
|
+
|
|
26
|
+
@mutex.synchronize do
|
|
27
|
+
@entries << { metric_name: metric_name, value: value, hostname: hostname, recorded_at: Time.now.utc.iso8601 }
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Same reset-on-success/keep-on-failure contract as MetricBuffer#flush; see that class's own
|
|
32
|
+
# comment for the accepted race this shares with it.
|
|
33
|
+
def flush
|
|
34
|
+
snapshot = nil
|
|
35
|
+
|
|
36
|
+
@mutex.synchronize do
|
|
37
|
+
return if @entries.empty?
|
|
38
|
+
snapshot = @entries
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
return unless client.deliver_infrastructure_metrics(snapshot)
|
|
42
|
+
|
|
43
|
+
@mutex.synchronize { @entries = [] }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
attr_reader :configuration, :client
|
|
48
|
+
|
|
49
|
+
def ensure_worker_started
|
|
50
|
+
return if @thread&.alive?
|
|
51
|
+
|
|
52
|
+
@mutex.synchronize do
|
|
53
|
+
return if @thread&.alive?
|
|
54
|
+
|
|
55
|
+
@thread = Thread.new { run }
|
|
56
|
+
@thread.abort_on_exception = false
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def run
|
|
61
|
+
loop do
|
|
62
|
+
sleep configuration.infrastructure_metric_flush_interval
|
|
63
|
+
flush
|
|
64
|
+
rescue StandardError => e
|
|
65
|
+
configuration.logger&.debug { "[ForgeOpsTracker] infrastructure metric flush thread error: #{e.class}: #{e.message}" }
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
module ForgeOpsTracker
|
|
2
|
+
module Integrations
|
|
3
|
+
module NetHTTP
|
|
4
|
+
# Times every outbound Net::HTTP call, applied once via Net::HTTP.prepend(Timing) from
|
|
5
|
+
# Railtie. Ruby's standard library has no ActiveSupport::Notifications instrumentation of
|
|
6
|
+
# its own for outbound HTTP the way it does for ActiveRecord/ActionController, so wrapping
|
|
7
|
+
# #request directly is the only hook available; most other HTTP client libraries in the Ruby
|
|
8
|
+
# ecosystem (Faraday's own net_http adapter, HTTParty, RestClient) ultimately call through
|
|
9
|
+
# Net::HTTP too, so this covers those as a side effect without needing a wrapper per library.
|
|
10
|
+
#
|
|
11
|
+
# transaction_name is "<HTTP method> <host>", not the full URL: a request's own path or
|
|
12
|
+
# query string could carry an id or a token, the same low-cardinality/no-secrets-in-a-label
|
|
13
|
+
# reasoning every other transaction_name in this system already follows.
|
|
14
|
+
module Timing
|
|
15
|
+
# The guard (return super unless ...) skips timing, but the ensure block below still runs
|
|
16
|
+
# on every exit path regardless, same as any ensure; start staying nil is what actually
|
|
17
|
+
# skips recording there, not a second guard duplicating this one.
|
|
18
|
+
def request(req, body = nil, &block)
|
|
19
|
+
return super unless ForgeOpsTracker.configuration.track_performance && ForgeOpsTracker.configuration.enabled?
|
|
20
|
+
|
|
21
|
+
start = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
|
22
|
+
super
|
|
23
|
+
ensure
|
|
24
|
+
if start
|
|
25
|
+
duration_ms = (::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - start) * 1000.0
|
|
26
|
+
Railtie.performance_flusher.record(transaction_name: "#{req.method} #{address}", duration_ms: duration_ms, kind: "http")
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
module ForgeOpsTracker
|
|
2
|
+
module Integrations
|
|
3
|
+
module Puma
|
|
4
|
+
# Reports Puma's own backlog/running-threads/pool-capacity as gauge-style infrastructure
|
|
5
|
+
# metrics on a periodic timer (see PeriodicPoller). Only ever loaded when Puma is already
|
|
6
|
+
# present (checked with defined? in Railtie).
|
|
7
|
+
#
|
|
8
|
+
# Puma.stats_hash's own shape genuinely differs by deployment mode, confirmed directly
|
|
9
|
+
# against Puma 6.6.1's real source rather than assumed: single-mode (Puma::Single, no
|
|
10
|
+
# `workers` config) has backlog/running/pool_capacity at the top level (Server#stats merged
|
|
11
|
+
# with ThreadPool#stats); clustered mode (Puma::Cluster) has none of those at the top level
|
|
12
|
+
# at all, only a worker_status array, one entry per forked worker, each carrying its own
|
|
13
|
+
# last_status hash with the identical three keys (parsed from that worker's own periodic
|
|
14
|
+
# IPC ping, see Puma::Cluster::WorkerHandle's own STATUS_PATTERN), summed across workers
|
|
15
|
+
# here for one whole-process reading, rather than reported per worker, since ForgeOps'
|
|
16
|
+
# infrastructure dataset already aggregates by hostname, not by worker index. Puma.stats_object
|
|
17
|
+
# is set unconditionally by Puma::Launcher on every normal boot (not just when a control/
|
|
18
|
+
# stats app is configured), so Puma.stats_hash is populated by the time this ever runs; still
|
|
19
|
+
# guarded against nil/an unrecognized shape, both because that wiring could change in a
|
|
20
|
+
# future Puma version and because this must never be the thing that crashes a poll tick.
|
|
21
|
+
class StatsReporter
|
|
22
|
+
def self.start(configuration: ForgeOpsTracker.configuration)
|
|
23
|
+
return unless configuration.track_performance && configuration.enabled?
|
|
24
|
+
|
|
25
|
+
PeriodicPoller.start(configuration.gauge_poll_interval, logger: configuration.logger) { new.report }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def report
|
|
29
|
+
stats = ::Puma.stats_hash
|
|
30
|
+
return unless stats
|
|
31
|
+
|
|
32
|
+
if stats[:backlog] && stats[:running]
|
|
33
|
+
report_metrics(stats[:backlog], stats[:running], stats[:pool_capacity])
|
|
34
|
+
elsif stats[:worker_status]
|
|
35
|
+
statuses = stats[:worker_status].filter_map { |worker| worker[:last_status] }.reject(&:empty?)
|
|
36
|
+
return if statuses.empty?
|
|
37
|
+
|
|
38
|
+
report_metrics(
|
|
39
|
+
statuses.sum { |s| s[:backlog] || 0 },
|
|
40
|
+
statuses.sum { |s| s[:running] || 0 },
|
|
41
|
+
statuses.sum { |s| s[:pool_capacity] || 0 }
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
def report_metrics(backlog, running, pool_capacity)
|
|
48
|
+
ForgeOpsTracker.capture_infrastructure_metric("puma.backlog", value: backlog)
|
|
49
|
+
ForgeOpsTracker.capture_infrastructure_metric("puma.running_threads", value: running)
|
|
50
|
+
ForgeOpsTracker.capture_infrastructure_metric("puma.pool_capacity", value: pool_capacity)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Sidekiq::Stats/Sidekiq::Queue (used by StatsReporter below) live in sidekiq/api, not
|
|
2
|
+
# autoloaded by a bare "sidekiq" require; a real Sidekiq server process almost always already has
|
|
3
|
+
# this loaded some other way, but requiring it explicitly here doesn't depend on that happening to
|
|
4
|
+
# be true.
|
|
5
|
+
require "sidekiq/api"
|
|
6
|
+
|
|
7
|
+
module ForgeOpsTracker
|
|
8
|
+
module Integrations
|
|
9
|
+
module Sidekiq
|
|
10
|
+
# Times a raw (non-ActiveJob) Sidekiq::Worker's own #perform. Registered via
|
|
11
|
+
# Sidekiq.configure_server { |c| c.server_middleware { |chain| chain.add ... } } (see
|
|
12
|
+
# Railtie); only ever loaded when Sidekiq is already present (checked with defined? there),
|
|
13
|
+
# matching every other optional integration in this gem.
|
|
14
|
+
#
|
|
15
|
+
# Skips (yields, but doesn't record) any job whose worker is ActiveJob's own Sidekiq wrapper:
|
|
16
|
+
# see PerformanceInstrumentation.sidekiq_worker_is_active_job_wrapper?'s own comment for why
|
|
17
|
+
# that job is already counted by the perform.active_job subscription instead, and would be
|
|
18
|
+
# double-counted here otherwise.
|
|
19
|
+
class Middleware
|
|
20
|
+
include ::Sidekiq::ServerMiddleware
|
|
21
|
+
|
|
22
|
+
def initialize(configuration: ForgeOpsTracker.configuration, flusher: Railtie.performance_flusher)
|
|
23
|
+
@configuration = configuration
|
|
24
|
+
@flusher = flusher
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def call(job_instance, _msg, _queue)
|
|
28
|
+
skip = !@configuration.track_performance || !@configuration.enabled? ||
|
|
29
|
+
PerformanceInstrumentation.sidekiq_worker_is_active_job_wrapper?(job_instance)
|
|
30
|
+
start = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) unless skip
|
|
31
|
+
|
|
32
|
+
yield
|
|
33
|
+
ensure
|
|
34
|
+
unless skip
|
|
35
|
+
duration_ms = (::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - start) * 1000.0
|
|
36
|
+
@flusher.record(transaction_name: job_instance.class.name, duration_ms: duration_ms, kind: "job")
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Reports Sidekiq's own aggregate stats (Sidekiq::Stats, Sidekiq::Queue) as gauge-style
|
|
42
|
+
# infrastructure metrics on a periodic timer (see PeriodicPoller), via the same public
|
|
43
|
+
# ForgeOpsTracker.capture_infrastructure_metric every customer's own manual infrastructure-
|
|
44
|
+
# monitoring script already uses; no new ingestion endpoint or dashboard dataset needed.
|
|
45
|
+
class StatsReporter
|
|
46
|
+
def self.start(configuration: ForgeOpsTracker.configuration)
|
|
47
|
+
return unless configuration.track_performance && configuration.enabled?
|
|
48
|
+
|
|
49
|
+
PeriodicPoller.start(configuration.gauge_poll_interval, logger: configuration.logger) { new.report }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def report
|
|
53
|
+
stats = ::Sidekiq::Stats.new
|
|
54
|
+
|
|
55
|
+
ForgeOpsTracker.capture_infrastructure_metric("sidekiq.processed", value: stats.processed)
|
|
56
|
+
ForgeOpsTracker.capture_infrastructure_metric("sidekiq.failed", value: stats.failed)
|
|
57
|
+
ForgeOpsTracker.capture_infrastructure_metric("sidekiq.scheduled_size", value: stats.scheduled_size)
|
|
58
|
+
ForgeOpsTracker.capture_infrastructure_metric("sidekiq.retry_size", value: stats.retry_size)
|
|
59
|
+
ForgeOpsTracker.capture_infrastructure_metric("sidekiq.dead_size", value: stats.dead_size)
|
|
60
|
+
ForgeOpsTracker.capture_infrastructure_metric("sidekiq.workers_size", value: stats.workers_size)
|
|
61
|
+
|
|
62
|
+
::Sidekiq::Queue.all.each do |queue|
|
|
63
|
+
ForgeOpsTracker.capture_infrastructure_metric("sidekiq.queue_depth.#{queue.name}", value: queue.size)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
module ForgeOpsTracker
|
|
2
|
+
module Integrations
|
|
3
|
+
module SolidQueue
|
|
4
|
+
# Reports Solid Queue's own gauge-style state as infrastructure metrics on a periodic timer
|
|
5
|
+
# (see PeriodicPoller and Sidekiq::StatsReporter's own comment, the same shape). Only ever
|
|
6
|
+
# loaded when Solid Queue is already present (checked with defined? in Railtie).
|
|
7
|
+
#
|
|
8
|
+
# No job *duration*/*count* reporting here: Solid Queue jobs are ActiveJob jobs, already
|
|
9
|
+
# covered end to end by the perform.active_job subscription in Railtie regardless of which
|
|
10
|
+
# backend actually runs them, so there's nothing left for this integration to time.
|
|
11
|
+
#
|
|
12
|
+
# kind is Solid Queue's own attribute on a registered process (a plain String, "Worker" or
|
|
13
|
+
# "Dispatcher": confirmed directly against solid_queue's own Processes::Base#kind, which is
|
|
14
|
+
# literally self.class.name.demodulize, not assumed from its docs), so this reads it exactly
|
|
15
|
+
# as Solid Queue itself defines it rather than re-deriving it a different way.
|
|
16
|
+
class StatsReporter
|
|
17
|
+
def self.start(configuration: ForgeOpsTracker.configuration)
|
|
18
|
+
return unless configuration.track_performance && configuration.enabled?
|
|
19
|
+
|
|
20
|
+
PeriodicPoller.start(configuration.gauge_poll_interval, logger: configuration.logger) { new.report }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def report
|
|
24
|
+
ForgeOpsTracker.capture_infrastructure_metric("solid_queue.active_workers", value: ::SolidQueue::Process.where(kind: "Worker").count)
|
|
25
|
+
ForgeOpsTracker.capture_infrastructure_metric("solid_queue.active_dispatchers", value: ::SolidQueue::Process.where(kind: "Dispatcher").count)
|
|
26
|
+
ForgeOpsTracker.capture_infrastructure_metric("solid_queue.failed", value: ::SolidQueue::FailedExecution.count)
|
|
27
|
+
ForgeOpsTracker.capture_infrastructure_metric("solid_queue.scheduled", value: ::SolidQueue::ScheduledExecution.count)
|
|
28
|
+
ForgeOpsTracker.capture_infrastructure_metric("solid_queue.blocked", value: ::SolidQueue::BlockedExecution.count)
|
|
29
|
+
|
|
30
|
+
::SolidQueue::Queue.all.each do |queue|
|
|
31
|
+
ForgeOpsTracker.capture_infrastructure_metric("solid_queue.queue_depth.#{queue.name}", value: queue.size)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
require "thread"
|
|
2
|
+
|
|
3
|
+
module ForgeOpsTracker
|
|
4
|
+
# Collects individual ForgeOpsTracker.capture_metric calls in-process and periodically flushes
|
|
5
|
+
# them as one batch, rather than one network call per capture -- the same "lazily start a
|
|
6
|
+
# background thread on first use" pattern SessionFlusher/PerformanceFlusher already use, so each
|
|
7
|
+
# forked Puma/Passenger worker gets its own fresh thread instead of inheriting a dead one across
|
|
8
|
+
# fork. Unlike those two, this collects a *list* of individually-meaningful entries rather than
|
|
9
|
+
# summing them into buckets: a customer's own signup or payment is exactly the kind of thing
|
|
10
|
+
# they'll want a genuinely accurate count/sum of later, not something to pre-aggregate away
|
|
11
|
+
# client-side, so CustomMetricsController stores one row per entry as-is.
|
|
12
|
+
class MetricBuffer
|
|
13
|
+
def initialize(configuration, client: Client.new(configuration))
|
|
14
|
+
@configuration = configuration
|
|
15
|
+
@client = client
|
|
16
|
+
@mutex = Mutex.new
|
|
17
|
+
@entries = []
|
|
18
|
+
@thread = nil
|
|
19
|
+
|
|
20
|
+
# Flushes whatever's already buffered on a normal process exit, so the last partial window
|
|
21
|
+
# isn't silently dropped; same reasoning every other buffer/flusher in this gem documents.
|
|
22
|
+
at_exit { flush }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def record(metric_name:, value:)
|
|
26
|
+
ensure_worker_started
|
|
27
|
+
|
|
28
|
+
@mutex.synchronize do
|
|
29
|
+
@entries << {
|
|
30
|
+
metric_name: metric_name, value: value,
|
|
31
|
+
environment: configuration.environment.to_s, release: configuration.release,
|
|
32
|
+
recorded_at: Time.now.utc.iso8601
|
|
33
|
+
}
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Snapshots and resets the buffered entries, then delivers them as one batch. A failed
|
|
38
|
+
# delivery keeps every entry where it is rather than resetting, so the next flush's batch
|
|
39
|
+
# just grows instead of losing what was already buffered; same reasoning SessionFlusher#flush
|
|
40
|
+
# already documents, including the same small, accepted race (an entry recorded during the
|
|
41
|
+
# in-flight HTTP request gets folded into this snapshot's own reset rather than kept for the
|
|
42
|
+
# next one) that class already lives with, for the same reason: simple and consistent beats a
|
|
43
|
+
# cleverer dedup that would only trade one rare edge case for a subtler one.
|
|
44
|
+
def flush
|
|
45
|
+
snapshot = nil
|
|
46
|
+
|
|
47
|
+
@mutex.synchronize do
|
|
48
|
+
return if @entries.empty?
|
|
49
|
+
snapshot = @entries
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
return unless client.deliver_metrics(snapshot)
|
|
53
|
+
|
|
54
|
+
@mutex.synchronize { @entries = [] }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
attr_reader :configuration, :client
|
|
59
|
+
|
|
60
|
+
def ensure_worker_started
|
|
61
|
+
return if @thread&.alive?
|
|
62
|
+
|
|
63
|
+
@mutex.synchronize do
|
|
64
|
+
return if @thread&.alive?
|
|
65
|
+
|
|
66
|
+
@thread = Thread.new { run }
|
|
67
|
+
@thread.abort_on_exception = false
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def run
|
|
72
|
+
loop do
|
|
73
|
+
sleep configuration.metric_flush_interval
|
|
74
|
+
flush
|
|
75
|
+
rescue StandardError => e
|
|
76
|
+
configuration.logger&.debug { "[ForgeOpsTracker] metric flush thread error: #{e.class}: #{e.message}" }
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
module ForgeOpsTracker
|
|
2
|
+
module Middleware
|
|
3
|
+
# Wraps every request: if Warden is mounted (env["warden"] present, whether or not the app
|
|
4
|
+
# uses Devise specifically, since Devise just mounts Warden automatically, but any Warden-
|
|
5
|
+
# based auth setup works here, confirmed directly against the installed warden gem's own
|
|
6
|
+
# Warden::Manager/Warden::Proxy source) and it has a signed-in user, stashes a serialized
|
|
7
|
+
# version of that user in a thread-local for the duration of this request so
|
|
8
|
+
# ErrorSubscriber#report (which never receives the Rack env at all: Rails.error.subscribe's
|
|
9
|
+
# own interface hands it error/context/severity, nothing request-shaped) can read it back and
|
|
10
|
+
# attach it to whatever gets reported. A sibling to SessionTracking, not folded into it:
|
|
11
|
+
# distinct concern, same "own file, own class" shape every middleware in this gem already
|
|
12
|
+
# takes.
|
|
13
|
+
#
|
|
14
|
+
# The ensure-clear is load-bearing, not optional: Puma reuses threads across requests, so
|
|
15
|
+
# leaving this set would leak one request's user into a later, unrelated request handled on
|
|
16
|
+
# the same thread.
|
|
17
|
+
class UserContext
|
|
18
|
+
def initialize(app, configuration: ForgeOpsTracker.configuration)
|
|
19
|
+
@app = app
|
|
20
|
+
@configuration = configuration
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def call(env)
|
|
24
|
+
return app.call(env) unless configuration.track_current_user && configuration.enabled?
|
|
25
|
+
|
|
26
|
+
if env.key?("warden") && (user = env["warden"].user)
|
|
27
|
+
serialized = serialize(user)
|
|
28
|
+
Thread.current[:forge_ops_tracker_current_user] = serialized unless serialized.empty?
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
app.call(env)
|
|
32
|
+
ensure
|
|
33
|
+
Thread.current[:forge_ops_tracker_current_user] = nil
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
attr_reader :app, :configuration
|
|
38
|
+
|
|
39
|
+
# Duck-types rather than assuming a Devise-shaped model: id if the object responds to it,
|
|
40
|
+
# email if it responds to it, username else name if either responds to it. A custom
|
|
41
|
+
# Warden setup with a differently-shaped user object just gets whatever subset applies
|
|
42
|
+
# here; ForgeOpsTracker.set_user covers anything this can't infer.
|
|
43
|
+
def serialize(user)
|
|
44
|
+
{
|
|
45
|
+
id: (user.id if user.respond_to?(:id)),
|
|
46
|
+
email: (user.email if user.respond_to?(:email)),
|
|
47
|
+
username: (user.username if user.respond_to?(:username)) || (user.name if user.respond_to?(:name))
|
|
48
|
+
}.compact
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
require "thread"
|
|
2
|
+
|
|
3
|
+
module ForgeOpsTracker
|
|
4
|
+
# Times every request/job/query/outbound call in-process, bucketed by [transaction_name, kind]
|
|
5
|
+
# (see Railtie's several ActiveSupport::Notifications subscriptions, each a different kind), and
|
|
6
|
+
# periodically flushes each distinct bucket as one small aggregate report, rather than one
|
|
7
|
+
# network call per event. One shared flusher for every kind, not one per kind: the aggregation
|
|
8
|
+
# shape (count + duration sum + max over a period) is identical regardless of what's actually
|
|
9
|
+
# being timed. Same "lazily start a background thread on first use, not at load time" pattern
|
|
10
|
+
# SessionFlusher already uses, so each forked Puma/Passenger worker gets its own fresh thread
|
|
11
|
+
# instead of inheriting a dead one across fork.
|
|
12
|
+
class PerformanceFlusher
|
|
13
|
+
def initialize(configuration, client: Client.new(configuration))
|
|
14
|
+
@configuration = configuration
|
|
15
|
+
@client = client
|
|
16
|
+
@mutex = Mutex.new
|
|
17
|
+
@buckets = {}
|
|
18
|
+
@period_started_at = Time.now.utc
|
|
19
|
+
@thread = nil
|
|
20
|
+
|
|
21
|
+
# Flushes whatever's already tallied on a normal process exit, so the last partial window
|
|
22
|
+
# (anything shorter than a full performance_flush_interval) isn't silently dropped; same
|
|
23
|
+
# reasoning SessionFlusher's own at_exit hook documents.
|
|
24
|
+
at_exit { flush }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# kind ("controller"/"job"/"query"/"http", see Railtie's own subscriptions) is part of the
|
|
28
|
+
# bucket key alongside transaction_name, not folded into transaction_name itself: a job and a
|
|
29
|
+
# controller action (or, less likely but still real, a query and a job) can share a name
|
|
30
|
+
# without colliding into the same bucket.
|
|
31
|
+
def record(transaction_name:, duration_ms:, kind: "controller")
|
|
32
|
+
ensure_worker_started
|
|
33
|
+
|
|
34
|
+
@mutex.synchronize do
|
|
35
|
+
bucket = (@buckets[[ transaction_name, kind ]] ||= { count: 0, duration_sum_ms: 0.0, max_duration_ms: 0.0 })
|
|
36
|
+
bucket[:count] += 1
|
|
37
|
+
bucket[:duration_sum_ms] += duration_ms
|
|
38
|
+
bucket[:max_duration_ms] = duration_ms if duration_ms > bucket[:max_duration_ms]
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Snapshots and resets the in-process buckets, then delivers them as one batch. A failed
|
|
43
|
+
# delivery keeps every bucket where it is rather than resetting, so the next flush's window
|
|
44
|
+
# just grows instead of losing what was already tallied; same reasoning SessionFlusher#flush
|
|
45
|
+
# already documents, and the same reason PerformanceSamplesController accepts a batch rather
|
|
46
|
+
# than a single row: a failed flush shouldn't have to re-deliver by transaction one at a time.
|
|
47
|
+
def flush
|
|
48
|
+
snapshot = nil
|
|
49
|
+
period_started_at = nil
|
|
50
|
+
|
|
51
|
+
@mutex.synchronize do
|
|
52
|
+
return if @buckets.empty?
|
|
53
|
+
|
|
54
|
+
period_started_at = @period_started_at
|
|
55
|
+
snapshot = @buckets.map do |(transaction_name, kind), bucket|
|
|
56
|
+
{
|
|
57
|
+
transaction_name: transaction_name,
|
|
58
|
+
kind: kind,
|
|
59
|
+
environment: configuration.environment.to_s,
|
|
60
|
+
release: configuration.release,
|
|
61
|
+
period_started_at: period_started_at.iso8601,
|
|
62
|
+
period_ended_at: Time.now.utc.iso8601,
|
|
63
|
+
request_count: bucket[:count],
|
|
64
|
+
duration_sum_ms: bucket[:duration_sum_ms],
|
|
65
|
+
max_duration_ms: bucket[:max_duration_ms]
|
|
66
|
+
}
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
return unless snapshot && client.deliver_performance_samples(snapshot)
|
|
71
|
+
|
|
72
|
+
@mutex.synchronize do
|
|
73
|
+
@buckets = {}
|
|
74
|
+
@period_started_at = Time.now.utc
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
attr_reader :configuration, :client
|
|
80
|
+
|
|
81
|
+
def ensure_worker_started
|
|
82
|
+
return if @thread&.alive?
|
|
83
|
+
|
|
84
|
+
@mutex.synchronize do
|
|
85
|
+
return if @thread&.alive?
|
|
86
|
+
|
|
87
|
+
@thread = Thread.new { run }
|
|
88
|
+
@thread.abort_on_exception = false
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def run
|
|
93
|
+
loop do
|
|
94
|
+
sleep configuration.performance_flush_interval
|
|
95
|
+
flush
|
|
96
|
+
rescue StandardError => e
|
|
97
|
+
configuration.logger&.debug { "[ForgeOpsTracker] performance flush thread error: #{e.class}: #{e.message}" }
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
module ForgeOpsTracker
|
|
2
|
+
# The actual "what do we call this, and should we even record it" decisions Railtie's
|
|
3
|
+
# ActiveSupport::Notifications subscriptions make, pulled out into plain module-function methods
|
|
4
|
+
# rather than left inline in each subscribe block. Railtie itself only ever loads when
|
|
5
|
+
# Rails::Railtie is already defined (see lib/forge_ops_tracker.rb's own final line), so nothing
|
|
6
|
+
# in that file can be exercised by this gem's own test suite without adding Rails itself as a
|
|
7
|
+
# development dependency just to fire a notification event by hand; every method here takes a
|
|
8
|
+
# plain payload Hash instead of a real ActiveSupport::Notifications::Event, so the decision logic
|
|
9
|
+
# itself (the SCHEMA/cached skip, the ActiveJob-wrapper dedup) is fully unit-testable with no
|
|
10
|
+
# Rails/Sidekiq/ActiveJob dependency at all. Railtie's own subscribe blocks stay thin, calling
|
|
11
|
+
# straight into these.
|
|
12
|
+
module PerformanceInstrumentation
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
def controller_transaction_name(payload)
|
|
16
|
+
"#{payload[:controller]}##{payload[:action]}"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# nil means "don't record this one": Rails' own sql.active_record fires for two kinds of
|
|
20
|
+
# noise that would drown out real query timing otherwise. "SCHEMA" is Rails' own internal
|
|
21
|
+
# introspection (column lookups, etc.), never something a customer's own code triggered;
|
|
22
|
+
# payload[:cached] is a genuinely instant hash lookup off Rails' own query cache, not a real
|
|
23
|
+
# round trip to the database, so timing it would understate every cached query's own real
|
|
24
|
+
# cost the next time it isn't cached.
|
|
25
|
+
#
|
|
26
|
+
# payload[:name] (Rails' own auto-generated label, e.g. "User Load", "Order Create"), not
|
|
27
|
+
# payload[:sql] (the literal query text): a customer's own query text could carry a literal
|
|
28
|
+
# value in some edge cases despite ActiveRecord's own parameterization, and even where it
|
|
29
|
+
# doesn't, raw SQL text is far higher cardinality than this system's transaction_name is
|
|
30
|
+
# designed for everywhere else. Falls back to the literal string "SQL" for the rare query
|
|
31
|
+
# with no name at all, rather than skipping it outright.
|
|
32
|
+
def query_transaction_name(payload)
|
|
33
|
+
return nil if payload[:name] == "SCHEMA" || payload[:cached]
|
|
34
|
+
|
|
35
|
+
# A plain nil-or-empty check, not ActiveSupport's #presence: this module is unit-tested
|
|
36
|
+
# without Rails/ActiveSupport loaded at all (see this file's own top comment), so it can't
|
|
37
|
+
# rely on a method ActiveSupport adds to String/NilClass.
|
|
38
|
+
name = payload[:name]
|
|
39
|
+
name.nil? || name.empty? ? "SQL" : name
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def job_transaction_name(payload)
|
|
43
|
+
payload[:job].class.name
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# True for a job dispatched through ActiveJob but actually run on Sidekiq: Sidekiq's own
|
|
47
|
+
# server middleware chain wraps every job it runs, ActiveJob-dispatched or not, so without
|
|
48
|
+
# this check a job on the Sidekiq/ActiveJob combination would be recorded twice: once here
|
|
49
|
+
# (kind: "job", via the raw Sidekiq middleware) and once already by the perform.active_job
|
|
50
|
+
# subscription that already covers every ActiveJob job regardless of which queue backend
|
|
51
|
+
# actually runs it (Solid Queue, Sidekiq, or anything else). The class name itself is
|
|
52
|
+
# ActiveJob's own, not this gem's: confirmed directly against how ActiveJob wraps a job for
|
|
53
|
+
# Sidekiq, not assumed from either project's docs alone.
|
|
54
|
+
def sidekiq_worker_is_active_job_wrapper?(worker)
|
|
55
|
+
return false unless defined?(::ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper)
|
|
56
|
+
|
|
57
|
+
worker.is_a?(::ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
module ForgeOpsTracker
|
|
2
|
+
# A small shared background-thread loop for the Sidekiq/Solid Queue/Puma stats reporters (see
|
|
3
|
+
# integrations/): unlike PerformanceFlusher and the two metric buffers, which genuinely differ
|
|
4
|
+
# in what they buffer and are kept as separate sibling classes on purpose (see
|
|
5
|
+
# InfrastructureMetricBuffer's own comment), these three reporters only differ in *what they
|
|
6
|
+
# poll*, not *how they poll it*: lazily start a thread on first use (so a forked worker gets
|
|
7
|
+
# its own live thread regardless of when it forked relative to load, same reasoning
|
|
8
|
+
# PerformanceFlusher's own lazy start documents), sleep the interval, call the block, rescue and
|
|
9
|
+
# log so one bad tick never kills the loop, forever. Genuinely mechanical enough across all
|
|
10
|
+
# three to share, unlike those.
|
|
11
|
+
#
|
|
12
|
+
# No at_exit flush: unlike the flushers/buffers, which hold real accumulated data that would
|
|
13
|
+
# otherwise be lost, a gauge reading skipped on process exit is simply the same as one skipped
|
|
14
|
+
# because the interval hadn't ticked yet: nothing to lose.
|
|
15
|
+
class PeriodicPoller
|
|
16
|
+
def self.start(interval, logger: nil, &block)
|
|
17
|
+
new(interval, logger: logger, &block).tap(&:start)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def initialize(interval, logger: nil, &block)
|
|
21
|
+
@interval = interval
|
|
22
|
+
@logger = logger
|
|
23
|
+
@block = block
|
|
24
|
+
@mutex = Mutex.new
|
|
25
|
+
@thread = nil
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def start
|
|
29
|
+
return if @thread&.alive?
|
|
30
|
+
|
|
31
|
+
@mutex.synchronize do
|
|
32
|
+
return if @thread&.alive?
|
|
33
|
+
|
|
34
|
+
@thread = Thread.new { run }
|
|
35
|
+
@thread.abort_on_exception = false
|
|
36
|
+
end
|
|
37
|
+
self
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
def run
|
|
42
|
+
loop do
|
|
43
|
+
sleep @interval
|
|
44
|
+
@block.call
|
|
45
|
+
rescue StandardError => e
|
|
46
|
+
@logger&.debug { "[ForgeOpsTracker] periodic poller error: #{e.class}: #{e.message}" }
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -2,6 +2,20 @@ require "rails/railtie"
|
|
|
2
2
|
|
|
3
3
|
module ForgeOpsTracker
|
|
4
4
|
class Railtie < ::Rails::Railtie
|
|
5
|
+
# Shared across every performance-timing subscription/integration below (controller, query,
|
|
6
|
+
# job, raw Sidekiq, outbound Net::HTTP): one flusher, one background thread, one set of
|
|
7
|
+
# buckets, since PerformanceFlusher already buckets on [transaction_name, kind] and safely
|
|
8
|
+
# handles concurrent calls from several sources at once. Exposed as a class method, not a
|
|
9
|
+
# local variable inside one initializer, specifically so the Sidekiq/Net::HTTP integrations
|
|
10
|
+
# (each guarded by their own separate initializer, since each only loads if its own optional
|
|
11
|
+
# library is present) can record into the exact same instance rather than each starting a
|
|
12
|
+
# redundant flusher/thread of their own.
|
|
13
|
+
class << self
|
|
14
|
+
def performance_flusher
|
|
15
|
+
@performance_flusher ||= ForgeOpsTracker::PerformanceFlusher.new(ForgeOpsTracker.configuration)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
5
19
|
initializer "forge_ops_tracker.subscribe_error_reporter" do |app|
|
|
6
20
|
configuration = ForgeOpsTracker.configuration
|
|
7
21
|
configuration.app_root ||= app.root.to_s
|
|
@@ -14,5 +28,110 @@ module ForgeOpsTracker
|
|
|
14
28
|
initializer "forge_ops_tracker.track_sessions" do |app|
|
|
15
29
|
app.middleware.use ForgeOpsTracker::Middleware::SessionTracking, configuration: ForgeOpsTracker.configuration
|
|
16
30
|
end
|
|
31
|
+
|
|
32
|
+
# Warden::Manager (mounted automatically by Devise, or directly by any other Warden-based
|
|
33
|
+
# auth setup) sets env["warden"] itself; this only ever reads it, so it works regardless of
|
|
34
|
+
# exactly where in the middleware stack this ends up relative to Warden::Manager, as long as
|
|
35
|
+
# that key exists in env by the time this middleware's own #call runs, which is true for any
|
|
36
|
+
# Warden-based auth setup: it mounts its own middleware once, for the whole app, long before
|
|
37
|
+
# any individual request begins.
|
|
38
|
+
initializer "forge_ops_tracker.track_current_user" do |app|
|
|
39
|
+
app.middleware.use ForgeOpsTracker::Middleware::UserContext, configuration: ForgeOpsTracker.configuration
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Rails already fires "process_action.action_controller" after every request with the
|
|
43
|
+
# controller/action and how long it took (event.duration, in ms) built in, so unlike session
|
|
44
|
+
# tracking above (which needed a Rack middleware because there's no single existing hook for
|
|
45
|
+
# "a request finished"), this needs no middleware of its own at all, just a subscriber.
|
|
46
|
+
# Guarded the same way the error subscriber and session-tracking middleware both are: on by
|
|
47
|
+
# default, off entirely if track_performance is turned off or the gem itself isn't enabled
|
|
48
|
+
# (no DSN configured, or the current environment isn't in enabled_environments).
|
|
49
|
+
#
|
|
50
|
+
# sql.active_record/perform.active_job are subscribed here too, not in their own separate
|
|
51
|
+
# initializers: all three (plus Sidekiq/Net::HTTP below) are genuinely the same concern
|
|
52
|
+
# (automatic performance timing), sharing the one performance_flusher above; see
|
|
53
|
+
# PerformanceInstrumentation for the actual transaction_name/skip decisions each one makes,
|
|
54
|
+
# kept out of these blocks specifically so that decision logic is unit-testable without a real
|
|
55
|
+
# Rails/ActiveJob boot (this gem's own test suite doesn't require Rails at all; see that
|
|
56
|
+
# file's own comment).
|
|
57
|
+
initializer "forge_ops_tracker.track_performance" do
|
|
58
|
+
configuration = ForgeOpsTracker.configuration
|
|
59
|
+
flusher = Railtie.performance_flusher
|
|
60
|
+
|
|
61
|
+
ActiveSupport::Notifications.subscribe("process_action.action_controller") do |*args|
|
|
62
|
+
next unless configuration.track_performance && configuration.enabled?
|
|
63
|
+
|
|
64
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
|
65
|
+
transaction_name = PerformanceInstrumentation.controller_transaction_name(event.payload)
|
|
66
|
+
flusher.record(transaction_name: transaction_name, duration_ms: event.duration, kind: "controller")
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
ActiveSupport::Notifications.subscribe("sql.active_record") do |*args|
|
|
70
|
+
next unless configuration.track_performance && configuration.enabled?
|
|
71
|
+
|
|
72
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
|
73
|
+
transaction_name = PerformanceInstrumentation.query_transaction_name(event.payload)
|
|
74
|
+
next if transaction_name.nil? # SCHEMA/cached, see PerformanceInstrumentation's own comment
|
|
75
|
+
|
|
76
|
+
flusher.record(transaction_name: transaction_name, duration_ms: event.duration, kind: "query")
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
ActiveSupport::Notifications.subscribe("perform.active_job") do |*args|
|
|
80
|
+
next unless configuration.track_performance && configuration.enabled?
|
|
81
|
+
|
|
82
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
|
83
|
+
transaction_name = PerformanceInstrumentation.job_transaction_name(event.payload)
|
|
84
|
+
flusher.record(transaction_name: transaction_name, duration_ms: event.duration, kind: "job")
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Raw (non-ActiveJob) Sidekiq::Worker job timing, plus Sidekiq's own queue-depth/processed/
|
|
89
|
+
# failed/etc. gauges: a separate initializer, since unlike ActiveRecord/ActionController
|
|
90
|
+
# (always present in any Rails app this gem's own gemspec already implies), Sidekiq is
|
|
91
|
+
# genuinely optional and isn't a declared dependency of this gem at all.
|
|
92
|
+
initializer "forge_ops_tracker.track_sidekiq" do
|
|
93
|
+
next unless defined?(::Sidekiq)
|
|
94
|
+
|
|
95
|
+
require "forge_ops_tracker/integrations/sidekiq"
|
|
96
|
+
|
|
97
|
+
configuration = ForgeOpsTracker.configuration
|
|
98
|
+
::Sidekiq.configure_server do |config|
|
|
99
|
+
config.server_middleware do |chain|
|
|
100
|
+
# No extra args to chain.add: Sidekiq calls Middleware.new(*args) itself for every job,
|
|
101
|
+
# splatting whatever was passed here; the middleware's own keyword-argument defaults
|
|
102
|
+
# (ForgeOpsTracker.configuration, Railtie.performance_flusher) already resolve correctly
|
|
103
|
+
# when called with none, and passing a keyword hash through *args here is genuinely
|
|
104
|
+
# ambiguous (Sidekiq's own chain.add doc example passes a literal positional Hash, not
|
|
105
|
+
# keyword syntax, for exactly this reason).
|
|
106
|
+
chain.add ForgeOpsTracker::Integrations::Sidekiq::Middleware
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
ForgeOpsTracker::Integrations::Sidekiq::StatsReporter.start(configuration: configuration)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
initializer "forge_ops_tracker.track_solid_queue" do
|
|
114
|
+
next unless defined?(::SolidQueue)
|
|
115
|
+
|
|
116
|
+
require "forge_ops_tracker/integrations/solid_queue"
|
|
117
|
+
ForgeOpsTracker::Integrations::SolidQueue::StatsReporter.start(configuration: ForgeOpsTracker.configuration)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
initializer "forge_ops_tracker.track_puma" do
|
|
121
|
+
next unless defined?(::Puma)
|
|
122
|
+
|
|
123
|
+
require "forge_ops_tracker/integrations/puma"
|
|
124
|
+
ForgeOpsTracker::Integrations::Puma::StatsReporter.start(configuration: ForgeOpsTracker.configuration)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Net::HTTP is always present (Ruby's own standard library), so this always prepends; the
|
|
128
|
+
# actual on/off decision happens at call time inside Timing#request itself (checking
|
|
129
|
+
# configuration.track_performance/enabled? the same way the subscriptions above do), so
|
|
130
|
+
# toggling that flag after boot still takes effect immediately, the same as every other
|
|
131
|
+
# automatic instrumentation point in this gem.
|
|
132
|
+
initializer "forge_ops_tracker.track_net_http" do
|
|
133
|
+
require "forge_ops_tracker/integrations/net_http"
|
|
134
|
+
Net::HTTP.prepend(ForgeOpsTracker::Integrations::NetHTTP::Timing)
|
|
135
|
+
end
|
|
17
136
|
end
|
|
18
137
|
end
|
data/lib/forge_ops_tracker.rb
CHANGED
|
@@ -6,7 +6,13 @@ require "forge_ops_tracker/client"
|
|
|
6
6
|
require "forge_ops_tracker/delivery_queue"
|
|
7
7
|
require "forge_ops_tracker/error_subscriber"
|
|
8
8
|
require "forge_ops_tracker/session_flusher"
|
|
9
|
+
require "forge_ops_tracker/performance_flusher"
|
|
10
|
+
require "forge_ops_tracker/performance_instrumentation"
|
|
11
|
+
require "forge_ops_tracker/metric_buffer"
|
|
12
|
+
require "forge_ops_tracker/infrastructure_metric_buffer"
|
|
13
|
+
require "forge_ops_tracker/periodic_poller"
|
|
9
14
|
require "forge_ops_tracker/middleware/session_tracking"
|
|
15
|
+
require "forge_ops_tracker/middleware/user_context"
|
|
10
16
|
|
|
11
17
|
module ForgeOpsTracker
|
|
12
18
|
class << self
|
|
@@ -17,6 +23,50 @@ module ForgeOpsTracker
|
|
|
17
23
|
def configure
|
|
18
24
|
yield configuration
|
|
19
25
|
end
|
|
26
|
+
|
|
27
|
+
# Records a named business metric (a signup, a payment, anything a customer wants to name),
|
|
28
|
+
# buffered and flushed periodically as a batch rather than one network call per capture (see
|
|
29
|
+
# MetricBuffer). value defaults to 1.0 so a bare counter-style call ("a signup happened")
|
|
30
|
+
# needs no argument; pass an explicit one for a metric with a real magnitude ("a $49 payment
|
|
31
|
+
# happened"). A no-op, same as every other capture path in this gem, when the gem isn't
|
|
32
|
+
# enabled (no DSN configured, or the current environment isn't in enabled_environments).
|
|
33
|
+
def capture_metric(name, value: 1.0)
|
|
34
|
+
return unless configuration.enabled?
|
|
35
|
+
|
|
36
|
+
metric_buffer.record(metric_name: name, value: value)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Records one infrastructure reading (CPU/memory/disk, or anything else a customer's own
|
|
40
|
+
# script reads) from one of their own hosts. hostname defaults to Configuration#server_name
|
|
41
|
+
# (already derived from Socket.gethostname), so a script running on the box it's reporting
|
|
42
|
+
# about doesn't need to pass one explicitly. Same buffered-batch delivery and no-op-when-
|
|
43
|
+
# disabled contract as capture_metric above.
|
|
44
|
+
def capture_infrastructure_metric(name, value:, hostname: nil)
|
|
45
|
+
return unless configuration.enabled?
|
|
46
|
+
|
|
47
|
+
infrastructure_metric_buffer.record(metric_name: name, value: value, hostname: hostname || configuration.server_name)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Manually attaches an affected user to whatever gets reported for the rest of this request
|
|
51
|
+
# (or, outside a request entirely, in a background job or a console session, for the rest of
|
|
52
|
+
# this thread). Sets the same thread-local ForgeOpsTracker::Middleware::UserContext's own
|
|
53
|
+
# Warden auto-detection uses, so this composes with it rather than being a second, separate
|
|
54
|
+
# mechanism: call this after that middleware ran to override its guess, or call it standalone
|
|
55
|
+
# in an app with no Warden at all. id/email/username are all independently optional; a call
|
|
56
|
+
# with none of them (or all nil) clears whatever was set.
|
|
57
|
+
def set_user(id: nil, email: nil, username: nil)
|
|
58
|
+
user = { id: id, email: email, username: username }.compact
|
|
59
|
+
Thread.current[:forge_ops_tracker_current_user] = user.empty? ? nil : user
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
def metric_buffer
|
|
64
|
+
@metric_buffer ||= MetricBuffer.new(configuration)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def infrastructure_metric_buffer
|
|
68
|
+
@infrastructure_metric_buffer ||= InfrastructureMetricBuffer.new(configuration)
|
|
69
|
+
end
|
|
20
70
|
end
|
|
21
71
|
end
|
|
22
72
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: forge_ops_tracker
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.7.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- ForgeOps
|
|
@@ -23,6 +23,48 @@ dependencies:
|
|
|
23
23
|
- - "~>"
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
25
|
version: '3.13'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: sidekiq
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '7.0'
|
|
33
|
+
type: :development
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '7.0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: puma
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '6.0'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '6.0'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: webrick
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '1.8'
|
|
61
|
+
type: :development
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '1.8'
|
|
26
68
|
description: Hooks Rails' error reporter and reports exceptions to a ForgeOps exception
|
|
27
69
|
tracker instance over HTTP, without ever raising back into the host application.
|
|
28
70
|
executables: []
|
|
@@ -38,7 +80,17 @@ files:
|
|
|
38
80
|
- lib/forge_ops_tracker/delivery_queue.rb
|
|
39
81
|
- lib/forge_ops_tracker/error_subscriber.rb
|
|
40
82
|
- lib/forge_ops_tracker/event_builder.rb
|
|
83
|
+
- lib/forge_ops_tracker/infrastructure_metric_buffer.rb
|
|
84
|
+
- lib/forge_ops_tracker/integrations/net_http.rb
|
|
85
|
+
- lib/forge_ops_tracker/integrations/puma.rb
|
|
86
|
+
- lib/forge_ops_tracker/integrations/sidekiq.rb
|
|
87
|
+
- lib/forge_ops_tracker/integrations/solid_queue.rb
|
|
88
|
+
- lib/forge_ops_tracker/metric_buffer.rb
|
|
41
89
|
- lib/forge_ops_tracker/middleware/session_tracking.rb
|
|
90
|
+
- lib/forge_ops_tracker/middleware/user_context.rb
|
|
91
|
+
- lib/forge_ops_tracker/performance_flusher.rb
|
|
92
|
+
- lib/forge_ops_tracker/performance_instrumentation.rb
|
|
93
|
+
- lib/forge_ops_tracker/periodic_poller.rb
|
|
42
94
|
- lib/forge_ops_tracker/pii_scrubber.rb
|
|
43
95
|
- lib/forge_ops_tracker/railtie.rb
|
|
44
96
|
- lib/forge_ops_tracker/session_flusher.rb
|