dead_bro 0.2.28 → 0.2.30

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6b96d1dee673157c1b4c46ad86f7e0d4a4feb51774038de260c5464353ff292e
4
- data.tar.gz: f922d71e3c1f132642bf5756aff381a5d372538672c3e0affd010cb31f30b397
3
+ metadata.gz: cccae4ac857f38e889e12c292943b25fad11f7e372d7f13e4eb838d13c6330ee
4
+ data.tar.gz: 4d459c4d049219747ed5c1b6f10e1417648282eca2e3e81c7eeef132e8a347bd
5
5
  SHA512:
6
- metadata.gz: 20f9d81a03bb468cc4ded983ad77b1ad9191dde3ba5c5320b7dd1e11bb5295b5d90289f597df036770cb0d6740ec10f426cf75ba5c941a1922cce8785ee21fe8
7
- data.tar.gz: c5325f9e3c8126236fb4386b9214d9637a4086176ae6e2675d4344958bc09d16bbb5718f6461f8bb8eaf75da4ef7e295d3ae012b15a5bb0ab1fbdea8ef0b8f48
6
+ metadata.gz: d12c33c0902c95f318cd716d7c88c78925543b235729ee90525d0453b16ed46521e2b00cc1daa5131b673abb7ee7e74e0968f299d56da055fd36452c2e314b90
7
+ data.tar.gz: e04ca78806e4d9a07757c0fedc177c2e4b77b295437d415fab94546d84add717ceab84791e34991f4955197ab1d8036ad498d679b089e795e5356beb0662d286
data/CHANGELOG.md CHANGED
@@ -3,6 +3,11 @@
3
3
  ### Added
4
4
  - Monitor thread now sends a synchronous heartbeat on startup before the first collection tick. This ensures remote settings — including `monitor_enabled` — are applied from the very first reporting cycle, so Sidekiq workers and other non-web processes that have not yet sent any metrics still receive the correct configuration immediately on boot rather than waiting up to 60 seconds for the first scheduled tick.
5
5
 
6
+ ## [0.2.30] - 2026-06-19
7
+
8
+ ### Added
9
+ - **`DeadBro.watch(label, **tags) { ... }`**: opt-in scoped timing for arbitrary code during instrumented web requests and background jobs. Records elapsed time, `start_offset_ms`, nesting depth, SQL count/duration attributable to the block, optional tags, and exception metadata (then re-raises). Spans are sent as `watch_events` in the normal APM payload for the Request Trace waterfall. Disabled by default; enable locally via `DeadBro.configure { |c| c.watch_enabled = true }` or remotely via the `watch_enabled` setting. Guardrails: max depth 10, max 50 spans per request, label length 200 chars, max 5 tags per span with tag values truncated to 100 chars. **Nested SQL:** each span diffs the request-wide SQL counter at enter/exit; parent spans include SQL from nested child spans (inclusive totals — use inner spans for per-block SQL, do not sum parent + child counts).
10
+
6
11
  ## [0.2.25] - 2026-06-14
7
12
 
8
13
  ### Added
@@ -17,7 +17,7 @@ module DeadBro
17
17
 
18
18
  # Remote-managed settings (overwritten by backend JSON `settings` on successful API responses)
19
19
  attr_accessor :memory_tracking_enabled, :allocation_tracking_enabled, :allocation_sample_rate,
20
- :sample_rate, :slow_query_threshold_ms,
20
+ :sample_rate, :slow_query_threshold_ms, :watch_enabled,
21
21
  :monitor_enabled, :enable_db_stats, :enable_process_stats, :enable_system_stats,
22
22
  :max_sql_queries_to_send, :max_logs_to_send
23
23
 
@@ -67,7 +67,7 @@ module DeadBro
67
67
  REMOTE_SETTING_KEYS = %w[
68
68
  enabled sample_rate memory_tracking_enabled allocation_tracking_enabled allocation_sample_rate
69
69
  explain_analyze_enabled slow_query_threshold_ms max_sql_queries_to_send max_logs_to_send
70
- excluded_controllers excluded_jobs exclusive_controllers exclusive_jobs
70
+ watch_enabled excluded_controllers excluded_jobs exclusive_controllers exclusive_jobs
71
71
  monitor_enabled enable_db_stats enable_process_stats enable_system_stats
72
72
  sample_rates_by_type
73
73
  ].freeze
@@ -102,6 +102,7 @@ module DeadBro
102
102
  @slow_query_threshold_ms = 500
103
103
  @max_sql_queries_to_send = 500
104
104
  @max_logs_to_send = 100
105
+ @watch_enabled = false
105
106
  self.excluded_controllers = []
106
107
  self.excluded_jobs = []
107
108
  self.exclusive_controllers = []
@@ -178,7 +179,7 @@ module DeadBro
178
179
  # must never be able to switch it on — only off. The local opt-in
179
180
  # (explain_analyze_enabled) stays untouched; see #explain_analyze_active?
180
181
  @remote_explain_analyze_enabled = !!value
181
- when "enabled", "memory_tracking_enabled", "allocation_tracking_enabled",
182
+ when "enabled", "memory_tracking_enabled", "allocation_tracking_enabled", "watch_enabled",
182
183
  "monitor_enabled", "enable_db_stats", "enable_process_stats", "enable_system_stats"
183
184
  send(:"#{k}=", !!value)
184
185
  when "excluded_controllers", "excluded_jobs", "exclusive_controllers", "exclusive_jobs"
@@ -208,6 +209,10 @@ module DeadBro
208
209
  Time.now.utc < t
209
210
  end
210
211
 
212
+ def watch_enabled?
213
+ !!@watch_enabled
214
+ end
215
+
211
216
  def resolve_deploy_id
212
217
  explicit = @explicit_deploy_revision&.to_s&.strip
213
218
  return explicit unless explicit.nil? || explicit.empty?
@@ -15,6 +15,17 @@ module DeadBro
15
15
 
16
16
  DeadBro::SqlSubscriber.start_request_tracking
17
17
 
18
+ # Start dependency tracking so HTTP / Redis / cache / view / Elasticsearch time
19
+ # spent inside the job is captured — mirrors the web SqlTrackingMiddleware. Each
20
+ # of those subscribers only records when its thread-local has been initialized;
21
+ # without this, they silently drop every event during a job and all that time
22
+ # collapses into the "Active Job" residual of the performance breakdown.
23
+ Thread.current[:dead_bro_http_events] = []
24
+ DeadBro::CacheSubscriber.start_request_tracking if defined?(DeadBro::CacheSubscriber)
25
+ DeadBro::RedisSubscriber.start_request_tracking if defined?(DeadBro::RedisSubscriber)
26
+ DeadBro::ElasticsearchSubscriber.start_request_tracking if defined?(DeadBro::ElasticsearchSubscriber)
27
+ DeadBro::ViewRenderingSubscriber.start_request_tracking if defined?(DeadBro::ViewRenderingSubscriber)
28
+
18
29
  # Start lightweight memory tracking for this job
19
30
  if defined?(DeadBro::LightweightMemoryTracker)
20
31
  DeadBro::LightweightMemoryTracker.start_request_tracking
@@ -29,6 +40,8 @@ module DeadBro
29
40
  if defined?(DeadBro::DbConnectionSubscriber)
30
41
  DeadBro::DbConnectionSubscriber.start_request_tracking
31
42
  end
43
+
44
+ DeadBro::WatchTracker.start_request_tracking if defined?(DeadBro::WatchTracker)
32
45
  end
33
46
  rescue
34
47
  # Never raise from instrumentation install
@@ -59,7 +59,9 @@ module DeadBro
59
59
  DeadBro.logger.clear
60
60
  Thread.current[DeadBro::TRACKING_START_TIME_KEY] = Time.now
61
61
  DeadBro::SqlSubscriber.start_request_tracking
62
+ start_job_dependency_tracking
62
63
  DeadBro::DbConnectionSubscriber.start_request_tracking if defined?(DeadBro::DbConnectionSubscriber)
64
+ DeadBro::WatchTracker.start_request_tracking if defined?(DeadBro::WatchTracker)
63
65
  if DeadBro.configuration.allocation_tracking_enabled && defined?(DeadBro::MemoryTrackingSubscriber)
64
66
  DeadBro::MemoryTrackingSubscriber.start_request_tracking
65
67
  else
@@ -69,9 +71,11 @@ module DeadBro
69
71
 
70
72
  # Get SQL queries executed during this job
71
73
  sql_queries = DeadBro::SqlSubscriber.stop_request_tracking
74
+ dependency_events = job_dependency_payload
72
75
  db_connection_stats = defined?(DeadBro::DbConnectionSubscriber) ? DeadBro::DbConnectionSubscriber.stop_request_tracking : {}
73
76
  gc_pressure = defined?(DeadBro::GcTracker) ? DeadBro::GcTracker.stop_request_tracking : {}
74
77
  ar_instantiation_count = defined?(DeadBro::ArObjectTracker) ? DeadBro::ArObjectTracker.stop_request_tracking : nil
78
+ watch_events = defined?(DeadBro::WatchTracker) ? DeadBro::WatchTracker.stop_request_tracking : []
75
79
 
76
80
  # Stop memory tracking and get collected memory data
77
81
  if DeadBro.configuration.allocation_tracking_enabled && defined?(DeadBro::MemoryTrackingSubscriber)
@@ -122,8 +126,9 @@ module DeadBro
122
126
  gc_stats: gc_stats,
123
127
  memory_events: memory_events,
124
128
  memory_performance: memory_performance,
129
+ watch_events: watch_events,
125
130
  logs: DeadBro.logger.logs
126
- }
131
+ }.merge(dependency_events)
127
132
 
128
133
  # force: true — the sampling decision above already accounted for any
129
134
  # per-job-type override; client#post_metric must not re-roll it globally.
@@ -158,7 +163,9 @@ module DeadBro
158
163
  DeadBro.logger.clear
159
164
  Thread.current[DeadBro::TRACKING_START_TIME_KEY] = Time.now
160
165
  DeadBro::SqlSubscriber.start_request_tracking
166
+ start_job_dependency_tracking
161
167
  DeadBro::DbConnectionSubscriber.start_request_tracking if defined?(DeadBro::DbConnectionSubscriber)
168
+ DeadBro::WatchTracker.start_request_tracking if defined?(DeadBro::WatchTracker)
162
169
  if DeadBro.configuration.allocation_tracking_enabled && defined?(DeadBro::MemoryTrackingSubscriber)
163
170
  DeadBro::MemoryTrackingSubscriber.start_request_tracking
164
171
  else
@@ -168,9 +175,11 @@ module DeadBro
168
175
 
169
176
  # Get SQL queries executed during this job
170
177
  sql_queries = DeadBro::SqlSubscriber.stop_request_tracking
178
+ dependency_events = job_dependency_payload
171
179
  db_connection_stats = defined?(DeadBro::DbConnectionSubscriber) ? DeadBro::DbConnectionSubscriber.stop_request_tracking : {}
172
180
  gc_pressure = defined?(DeadBro::GcTracker) ? DeadBro::GcTracker.stop_request_tracking : {}
173
181
  ar_instantiation_count = defined?(DeadBro::ArObjectTracker) ? DeadBro::ArObjectTracker.stop_request_tracking : nil
182
+ watch_events = defined?(DeadBro::WatchTracker) ? DeadBro::WatchTracker.stop_request_tracking : []
174
183
 
175
184
  # Stop memory tracking and get collected memory data
176
185
  if DeadBro.configuration.allocation_tracking_enabled && defined?(DeadBro::MemoryTrackingSubscriber)
@@ -224,8 +233,9 @@ module DeadBro
224
233
  gc_stats: gc_stats,
225
234
  memory_events: memory_events,
226
235
  memory_performance: memory_performance,
236
+ watch_events: watch_events,
227
237
  logs: DeadBro.logger.logs
228
- }
238
+ }.merge(dependency_events)
229
239
 
230
240
  event_name = exception&.class&.name || "ActiveJob::Exception"
231
241
  client.post_metric(event_name: event_name, payload: payload, force: true)
@@ -239,6 +249,11 @@ module DeadBro
239
249
  def self.drain_job_tracking
240
250
  # wait_for_explains: false — result is discarded, don't block on pending plans.
241
251
  DeadBro::SqlSubscriber.stop_request_tracking(wait_for_explains: false) if defined?(DeadBro::SqlSubscriber)
252
+ Thread.current[:dead_bro_http_events] = nil
253
+ DeadBro::CacheSubscriber.stop_request_tracking if defined?(DeadBro::CacheSubscriber)
254
+ DeadBro::RedisSubscriber.stop_request_tracking if defined?(DeadBro::RedisSubscriber)
255
+ DeadBro::ElasticsearchSubscriber.stop_request_tracking if defined?(DeadBro::ElasticsearchSubscriber)
256
+ DeadBro::ViewRenderingSubscriber.stop_request_tracking if defined?(DeadBro::ViewRenderingSubscriber)
242
257
  DeadBro::DbConnectionSubscriber.stop_request_tracking if defined?(DeadBro::DbConnectionSubscriber)
243
258
  DeadBro::GcTracker.stop_request_tracking if defined?(DeadBro::GcTracker)
244
259
  DeadBro::ArObjectTracker.stop_request_tracking if defined?(DeadBro::ArObjectTracker)
@@ -246,10 +261,59 @@ module DeadBro
246
261
  if DeadBro.configuration.allocation_tracking_enabled && defined?(DeadBro::MemoryTrackingSubscriber)
247
262
  DeadBro::MemoryTrackingSubscriber.stop_request_tracking
248
263
  end
264
+ DeadBro::WatchTracker.stop_request_tracking if defined?(DeadBro::WatchTracker)
249
265
  rescue
250
266
  # Best effort
251
267
  end
252
268
 
269
+ # Start HTTP/Redis/cache/view/Elasticsearch tracking for job backends that never
270
+ # emit perform_start.active_job (so JobSqlTrackingMiddleware didn't run). Mirrors the
271
+ # dependency tracking the middleware normally arms — see its comment for why each
272
+ # thread-local must be initialized before its subscriber will record anything.
273
+ def self.start_job_dependency_tracking
274
+ Thread.current[:dead_bro_http_events] = []
275
+ DeadBro::CacheSubscriber.start_request_tracking if defined?(DeadBro::CacheSubscriber)
276
+ DeadBro::RedisSubscriber.start_request_tracking if defined?(DeadBro::RedisSubscriber)
277
+ DeadBro::ElasticsearchSubscriber.start_request_tracking if defined?(DeadBro::ElasticsearchSubscriber)
278
+ DeadBro::ViewRenderingSubscriber.start_request_tracking if defined?(DeadBro::ViewRenderingSubscriber)
279
+ rescue
280
+ end
281
+
282
+ # Snapshot and clear the per-job dependency events, returning the payload slice that
283
+ # mirrors what the web Subscriber sends. These feed the performance breakdown (the app
284
+ # derives http/redis/es duration columns from them) and the trace timeline.
285
+ def self.job_dependency_payload
286
+ http_outgoing = Thread.current[:dead_bro_http_events] || []
287
+ Thread.current[:dead_bro_http_events] = nil
288
+ cache_events = defined?(DeadBro::CacheSubscriber) ? DeadBro::CacheSubscriber.stop_request_tracking : []
289
+ redis_events = defined?(DeadBro::RedisSubscriber) ? DeadBro::RedisSubscriber.stop_request_tracking : []
290
+ elasticsearch_events = defined?(DeadBro::ElasticsearchSubscriber) ? DeadBro::ElasticsearchSubscriber.stop_request_tracking : []
291
+ view_events = defined?(DeadBro::ViewRenderingSubscriber) ? DeadBro::ViewRenderingSubscriber.stop_request_tracking : []
292
+ view_performance = defined?(DeadBro::ViewRenderingSubscriber) ? DeadBro::ViewRenderingSubscriber.analyze_view_performance(view_events) : {}
293
+
294
+ {
295
+ http_outgoing: http_outgoing,
296
+ cache_events: cache_events,
297
+ redis_events: redis_events,
298
+ elasticsearch_events: elasticsearch_events,
299
+ view_events: view_events,
300
+ view_performance: view_performance,
301
+ view_runtime_ms: sum_view_runtime_ms(view_events)
302
+ }
303
+ rescue
304
+ {}
305
+ end
306
+
307
+ def self.sum_view_runtime_ms(view_events)
308
+ return nil unless view_events.is_a?(Array) && view_events.any?
309
+ view_events.sum do |e|
310
+ next 0 unless e.is_a?(Hash)
311
+ (e[:total_duration_ms] || e["total_duration_ms"] || e[:duration_ms] || e["duration_ms"] || 0).to_f
312
+ end.round(2)
313
+ rescue
314
+ nil
315
+ end
316
+
253
317
  private
254
318
 
255
319
  def self.job_queue_duration_ms(job, perform_started)
@@ -50,6 +50,21 @@ module DeadBro
50
50
  stack.last
51
51
  end
52
52
 
53
+ # Sum of SQL counts and durations recorded so far in the active tracking
54
+ # context. Used by WatchTracker to attribute SQL to a DeadBro.watch block.
55
+ def self.current_sql_metrics
56
+ agg_stack = Thread.current[THREAD_LOCAL_AGGREGATES_KEY]
57
+ aggregates_h = (agg_stack.is_a?(Array) && agg_stack.any?) ? agg_stack.last : {}
58
+ return {count: 0, duration_ms: 0.0} unless aggregates_h.is_a?(Hash) && aggregates_h.any?
59
+
60
+ {
61
+ count: aggregates_h.values.sum { |a| (a[:count] || 0).to_i },
62
+ duration_ms: aggregates_h.values.sum { |a| (a[:total_duration_ms] || 0.0).to_f }.round(2)
63
+ }
64
+ rescue StandardError
65
+ {count: 0, duration_ms: 0.0}
66
+ end
67
+
53
68
  # Check if we should continue tracking based on count and time limits
54
69
  def self.should_continue_tracking?(current_queries_array, max_count)
55
70
  return false unless current_queries_array.is_a?(Array)
@@ -82,6 +82,9 @@ module DeadBro
82
82
  # Start CPU time tracking for this request (thread-local clock)
83
83
  DeadBro::CpuTracker.start_request_tracking if defined?(DeadBro::CpuTracker)
84
84
 
85
+ # Start user-defined watch block tracking for this request
86
+ DeadBro::WatchTracker.start_request_tracking if defined?(DeadBro::WatchTracker)
87
+
85
88
  # Start outgoing HTTP accumulation for this request
86
89
  Thread.current[:dead_bro_http_events] = []
87
90
 
@@ -80,6 +80,8 @@ module DeadBro
80
80
  view_events = DeadBro::ViewRenderingSubscriber.stop_request_tracking
81
81
  view_performance = DeadBro::ViewRenderingSubscriber.analyze_view_performance(view_events)
82
82
 
83
+ watch_events = defined?(DeadBro::WatchTracker) ? DeadBro::WatchTracker.stop_request_tracking : []
84
+
83
85
  # Per-phase allocation attribution (under memory tracking) — which phase
84
86
  # allocated the request's objects (sql / view / elasticsearch).
85
87
  allocation_phases = if DeadBro.configuration.memory_tracking_enabled && defined?(DeadBro::MemoryPhaseTracker)
@@ -222,6 +224,7 @@ module DeadBro
222
224
  gc_pressure: gc_pressure,
223
225
  ar_instantiation_count: ar_instantiation_count,
224
226
  cpu_time_ms: cpu_time_ms,
227
+ watch_events: watch_events,
225
228
  logs: DeadBro.logger.logs
226
229
  }
227
230
  # force: true — the sampling decision (global or per-request-type) was
@@ -254,6 +257,7 @@ module DeadBro
254
257
  DeadBro::GcTracker.stop_request_tracking if defined?(DeadBro::GcTracker)
255
258
  DeadBro::ArObjectTracker.stop_request_tracking if defined?(DeadBro::ArObjectTracker)
256
259
  DeadBro::CpuTracker.stop_request_tracking if defined?(DeadBro::CpuTracker)
260
+ DeadBro::WatchTracker.stop_request_tracking if defined?(DeadBro::WatchTracker)
257
261
  rescue
258
262
  # Best effort — draining must never raise from the notifications callback.
259
263
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DeadBro
4
- VERSION = "0.2.28"
4
+ VERSION = "0.2.30"
5
5
  end
@@ -0,0 +1,189 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DeadBro
4
+ # Records user-defined timed blocks (DeadBro.watch) during an instrumented
5
+ # request or background job. Spans include wall duration, nesting depth, and
6
+ # SQL attributable to the block (delta from SqlSubscriber aggregates).
7
+ #
8
+ # SQL attribution: each span diffs the global SQL counter at block entry vs
9
+ # exit. Nested blocks each get their own diff; a parent span's sql_count /
10
+ # sql_duration_ms therefore includes queries run inside nested child spans
11
+ # (inclusive totals, not exclusive). Compare inner spans when you need
12
+ # per-block SQL; do not sum parent and child counts.
13
+ #
14
+ # Unlike DeadBro.analyze, watch spans are sent to the DeadBro backend as part
15
+ # of the normal request/job payload when watch_enabled is on.
16
+ module WatchTracker
17
+ THREAD_LOCAL_EVENTS_KEY = :dead_bro_watch_events
18
+ THREAD_LOCAL_ACTIVE_STACK_KEY = :dead_bro_watch_active_stack
19
+
20
+ # Deepest depth index still recorded — depths 0..MAX_DEPTH are kept, anything
21
+ # nested below that yields untracked.
22
+ MAX_DEPTH = 10
23
+ MAX_SPANS_PER_REQUEST = 50
24
+ MAX_LABEL_LENGTH = 200
25
+ MAX_TAGS = 5
26
+ MAX_TAG_VALUE_LENGTH = 100
27
+
28
+ module_function
29
+
30
+ def enabled?
31
+ DeadBro.configuration.watch_enabled?
32
+ rescue StandardError
33
+ false
34
+ end
35
+
36
+ def tracking_active?
37
+ Thread.current[THREAD_LOCAL_EVENTS_KEY].is_a?(Array)
38
+ rescue StandardError
39
+ false
40
+ end
41
+
42
+ def start_request_tracking
43
+ Thread.current[THREAD_LOCAL_EVENTS_KEY] = []
44
+ Thread.current[THREAD_LOCAL_ACTIVE_STACK_KEY] = []
45
+ end
46
+
47
+ def stop_request_tracking
48
+ events = Thread.current[THREAD_LOCAL_EVENTS_KEY]
49
+ Thread.current[THREAD_LOCAL_EVENTS_KEY] = nil
50
+ Thread.current[THREAD_LOCAL_ACTIVE_STACK_KEY] = nil
51
+ # compact drops reserved slots whose block never completed — can't happen
52
+ # for a lexical block, but keeps a nil out of the payload if it ever does.
53
+ events.is_a?(Array) ? events.compact : []
54
+ rescue StandardError
55
+ []
56
+ end
57
+
58
+ def watch(label = nil, **tags)
59
+ return yield unless block_given?
60
+ return yield unless enabled?
61
+ return yield unless tracking_active?
62
+
63
+ events = Thread.current[THREAD_LOCAL_EVENTS_KEY]
64
+ active_stack = Thread.current[THREAD_LOCAL_ACTIVE_STACK_KEY]
65
+ return yield unless events.is_a?(Array) && active_stack.is_a?(Array)
66
+
67
+ depth = active_stack.length
68
+ return yield if depth > MAX_DEPTH
69
+ return yield if events.length >= MAX_SPANS_PER_REQUEST
70
+
71
+ sanitized_label = sanitize_label(label)
72
+ sanitized_tags = sanitize_tags(tags)
73
+ start_offset_ms = compute_start_offset_ms
74
+ sql_before = sql_metrics_snapshot
75
+ block_start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
76
+
77
+ frame = {
78
+ label: sanitized_label,
79
+ depth: depth,
80
+ tags: sanitized_tags,
81
+ start_offset_ms: start_offset_ms,
82
+ sql_before: sql_before,
83
+ block_start: block_start,
84
+ error: false,
85
+ exception_class: nil
86
+ }
87
+ # Reserve the slot on entry so spans come out in start order. Blocks only
88
+ # complete inner-first, so appending on completion would emit a nested span
89
+ # ahead of its own parent; the placeholder keeps parents before children.
90
+ # It also makes MAX_SPANS_PER_REQUEST count blocks still in flight.
91
+ frame[:slot] = events.length
92
+ events << nil
93
+ active_stack << frame
94
+
95
+ begin
96
+ yield
97
+ rescue StandardError => e
98
+ frame[:error] = true
99
+ # Anonymous classes (Class.new(StandardError)) have a nil name — leave the
100
+ # field unset rather than shipping an empty string the dashboard would
101
+ # render as a blank exception.
102
+ class_name = e.class.name
103
+ frame[:exception_class] = class_name[0, 200] if class_name
104
+ raise
105
+ ensure
106
+ active_stack.pop if active_stack.last.equal?(frame)
107
+ append_completed_span(events, frame)
108
+ end
109
+ end
110
+
111
+ def sanitize_label(label)
112
+ text = label.to_s.strip
113
+ text = "block" if text.empty?
114
+ (text.length > MAX_LABEL_LENGTH) ? text[0, MAX_LABEL_LENGTH] + "..." : text
115
+ rescue StandardError
116
+ "block"
117
+ end
118
+
119
+ def sanitize_tags(tags)
120
+ return {} unless tags.is_a?(Hash) && tags.any?
121
+
122
+ tags.first(MAX_TAGS).each_with_object({}) do |(key, value), out|
123
+ k = key.to_s.strip[0, 50]
124
+ next if k.empty?
125
+
126
+ v = value.to_s.strip
127
+ v = v[0, MAX_TAG_VALUE_LENGTH] + "..." if v.length > MAX_TAG_VALUE_LENGTH
128
+ out[k] = v
129
+ end
130
+ rescue StandardError
131
+ {}
132
+ end
133
+
134
+ def compute_start_offset_ms
135
+ tracking_start = Thread.current[DeadBro::TRACKING_START_TIME_KEY]
136
+ return 0.0 unless tracking_start
137
+
138
+ ((Time.now - tracking_start) * 1000.0).round(2)
139
+ rescue StandardError
140
+ 0.0
141
+ end
142
+
143
+ def sql_metrics_snapshot
144
+ return {count: 0, duration_ms: 0.0} unless defined?(DeadBro::SqlSubscriber)
145
+
146
+ DeadBro::SqlSubscriber.current_sql_metrics
147
+ rescue StandardError
148
+ {count: 0, duration_ms: 0.0}
149
+ end
150
+
151
+ def append_completed_span(events, frame)
152
+ return unless events.is_a?(Array)
153
+
154
+ slot = frame[:slot]
155
+ return unless slot.is_a?(Integer) && slot < events.length
156
+
157
+ elapsed_ms = begin
158
+ ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - frame[:block_start]) * 1000.0).round(2)
159
+ rescue StandardError
160
+ 0.0
161
+ end
162
+
163
+ sql_after = sql_metrics_snapshot
164
+ sql_before = frame[:sql_before] || {count: 0, duration_ms: 0.0}
165
+ sql_count = [(sql_after[:count] || 0).to_i - (sql_before[:count] || 0).to_i, 0].max
166
+ sql_duration_ms = [
167
+ (sql_after[:duration_ms] || 0.0).to_f - (sql_before[:duration_ms] || 0.0).to_f,
168
+ 0.0
169
+ ].max.round(2)
170
+
171
+ span = {
172
+ label: frame[:label],
173
+ duration_ms: elapsed_ms,
174
+ start_offset_ms: frame[:start_offset_ms],
175
+ depth: frame[:depth],
176
+ sql_count: sql_count,
177
+ sql_duration_ms: sql_duration_ms,
178
+ error: frame[:error] == true
179
+ }
180
+ span[:exception_class] = frame[:exception_class] if frame[:exception_class]
181
+ tags = frame[:tags]
182
+ span[:tags] = tags if tags.is_a?(Hash) && tags.any?
183
+
184
+ events[slot] = span
185
+ rescue StandardError
186
+ # Never raise from instrumentation
187
+ end
188
+ end
189
+ end
data/lib/dead_bro.rb CHANGED
@@ -29,6 +29,7 @@ module DeadBro
29
29
  autoload :Monitor, "dead_bro/monitor"
30
30
  autoload :MemoryDetails, "dead_bro/memory_details"
31
31
  autoload :Logger, "dead_bro/logger"
32
+ autoload :WatchTracker, "dead_bro/watch_tracker"
32
33
  begin
33
34
  require "dead_bro/railtie"
34
35
  rescue LoadError
@@ -458,4 +459,28 @@ module DeadBro
458
459
  raise error if error
459
460
  analysis_result
460
461
  end
462
+
463
+ # Time a named block during an instrumented request or background job.
464
+ #
465
+ # When watch_enabled is on and tracking is active (web request or job),
466
+ # records elapsed time, optional tags, nested SQL count/duration, and errors
467
+ # as watch_events in the APM payload for the Request Trace waterfall.
468
+ #
469
+ # Nested watch blocks: each span's sql_count/sql_duration_ms is the SQL that
470
+ # ran while that block was active. Parent spans include SQL from nested child
471
+ # spans (inclusive). Inner spans show their own slice; do not add parent and
472
+ # child SQL counts together.
473
+ #
474
+ # When disabled or outside an instrumented context, yields with no overhead
475
+ # beyond a single config check.
476
+ #
477
+ # Usage:
478
+ # DeadBro.watch("load users") { User.where(active: true).load }
479
+ # DeadBro.watch("sync billing", customer_id: user.id) { sync!(user) }
480
+ #
481
+ # Compare to DeadBro.analyze — analyze is local-only console profiling;
482
+ # watch spans are sent to the DeadBro dashboard when enabled.
483
+ def self.watch(label = nil, **tags, &block)
484
+ WatchTracker.watch(label, **tags, &block)
485
+ end
461
486
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dead_bro
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.28
4
+ version: 0.2.30
5
5
  platform: ruby
6
6
  authors:
7
7
  - Emanuel Comsa
@@ -18,7 +18,6 @@ extensions: []
18
18
  extra_rdoc_files: []
19
19
  files:
20
20
  - CHANGELOG.md
21
- - FEATURES.md
22
21
  - README.md
23
22
  - lib/dead_bro.rb
24
23
  - lib/dead_bro/allocation_source_sampler.rb
@@ -59,6 +58,7 @@ files:
59
58
  - lib/dead_bro/subscriber.rb
60
59
  - lib/dead_bro/version.rb
61
60
  - lib/dead_bro/view_rendering_subscriber.rb
61
+ - lib/dead_bro/watch_tracker.rb
62
62
  - lib/generators/dead_bro/install/install_generator.rb
63
63
  - lib/generators/dead_bro/install/templates/dead_bro.rb
64
64
  homepage: https://www.deadbro.com
@@ -80,7 +80,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
80
80
  - !ruby/object:Gem::Version
81
81
  version: '0'
82
82
  requirements: []
83
- rubygems_version: 4.0.9
83
+ rubygems_version: 4.0.10
84
84
  specification_version: 4
85
85
  summary: Minimal APM for Rails apps.
86
86
  test_files: []
data/FEATURES.md DELETED
@@ -1,333 +0,0 @@
1
- # ApmBro Feature List
2
-
3
- A comprehensive feature list for comparing ApmBro with other APM (Application Performance Monitoring) tools.
4
-
5
- ## Core Architecture
6
-
7
- - **Rails Integration**: Automatic subscription to Rails events via ActiveSupport::Notifications
8
- - **Zero-Configuration Setup**: Works out of the box with minimal configuration
9
- - **Asynchronous Metrics Posting**: Non-blocking HTTP requests using background threads
10
- - **Thread-Local Storage**: Per-request metric collection using thread-local variables
11
- - **Circuit Breaker Pattern**: Built-in circuit breaker to prevent cascading failures when APM endpoint is down
12
- - **Deploy Tracking**: Automatic deploy ID resolution from multiple sources (Rails settings, ENV vars, Heroku, Git)
13
-
14
- ## Request Tracking
15
-
16
- ### Controller Action Monitoring
17
- - **Automatic Tracking**: Tracks all controller actions automatically
18
- - **Request Duration**: Measures total request processing time
19
- - **HTTP Method & Path**: Captures HTTP method and request path
20
- - **Status Codes**: Tracks HTTP response status codes
21
- - **View Runtime**: Separate tracking of view rendering time
22
- - **Database Runtime**: Separate tracking of database query time
23
- - **Request Parameters**: Captures request parameters (with sensitive data filtering)
24
- - **User Agent**: Tracks user agent strings
25
- - **User ID Extraction**: Extracts authenticated user ID (supports Warden)
26
- - **Environment Context**: Tracks Rails environment (development, staging, production)
27
-
28
- ### Request Sampling
29
- - **Configurable Sample Rate**: Percentage-based sampling (1-100%)
30
- - **Random Sampling**: Each request has random chance of being tracked
31
- - **Consistent Per-Request**: Sampling decision applies to all metrics for a request
32
- - **Error Override**: Errors are always tracked regardless of sampling
33
- - **Cost Optimization**: Reduces data volume and costs for high-traffic applications
34
-
35
- ### Exclusion Rules
36
- - **Controller Exclusion**: Exclude entire controllers from tracking
37
- - **Action Exclusion**: Exclude specific controller#action combinations
38
- - **Wildcard Support**: Pattern matching with `*` wildcards (e.g., `Admin::*`, `Admin::*#*`)
39
- - **Job Exclusion**: Exclude specific background jobs from tracking
40
- - **Flexible Configuration**: Configure via initializer, Rails settings, or environment variables
41
-
42
- ## SQL Query Tracking
43
-
44
- ### Query Details
45
- - **Full SQL Tracking**: Captures all SQL queries executed during requests and jobs
46
- - **Query Sanitization**: Automatically sanitizes SQL to remove sensitive data
47
- - **Query Name**: Tracks query names (e.g., "User Load", "User Update")
48
- - **Duration Measurement**: Precise query execution time in milliseconds
49
- - **Cache Detection**: Identifies cached queries
50
- - **Connection ID**: Tracks database connection ID
51
- - **Call Stack Traces**: Full backtrace showing where queries were executed
52
- - **Object Allocations**: Optional tracking of object allocations per query
53
-
54
- ### Query Performance Analysis
55
- - **Slow Query Detection**: Configurable threshold for identifying slow queries
56
- - **EXPLAIN ANALYZE**: Automatic execution plan capture for slow queries
57
- - **Background Execution**: EXPLAIN ANALYZE runs in separate thread (non-blocking)
58
- - **Multi-Database Support**: Works with PostgreSQL, MySQL, SQLite, and others
59
- - **Smart Filtering**: Automatically skips transaction queries (BEGIN, COMMIT, ROLLBACK)
60
- - **Execution Plan Details**:
61
- - PostgreSQL: Full EXPLAIN ANALYZE with buffer usage statistics
62
- - MySQL: EXPLAIN ANALYZE with actual execution times
63
- - SQLite: EXPLAIN QUERY PLAN output
64
- - **Query Optimization Insights**: Helps identify missing indexes, full table scans, JOIN issues
65
-
66
- ## View Rendering Tracking
67
-
68
- ### View Performance
69
- - **Template Rendering**: Tracks main template rendering
70
- - **Partial Rendering**: Tracks partial template rendering with cache key information
71
- - **Collection Rendering**: Tracks collection rendering (partials in loops)
72
- - **Rendering Duration**: Precise timing for each view component
73
- - **Virtual Path Tracking**: Tracks view virtual paths
74
- - **Layout Information**: Captures layout usage
75
-
76
- ### View Analysis
77
- - **Slow View Detection**: Identifies the slowest rendering views
78
- - **Frequency Analysis**: Tracks most frequently rendered views
79
- - **Cache Hit Rate**: Calculates cache hit rates for partials
80
- - **Collection Cache Analysis**: Tracks cache hit rates for collection rendering
81
- - **Performance Metrics**:
82
- - Total views rendered per request
83
- - Total view rendering duration
84
- - Average view rendering duration
85
- - Breakdown by view type (template, partial, collection)
86
-
87
- ## Memory Tracking & Leak Detection
88
-
89
- ### Lightweight Memory Tracking (Default)
90
- - **Memory Usage Monitoring**: Tracks memory consumption per request using GC stats
91
- - **Memory Growth Tracking**: Measures memory growth during request processing
92
- - **GC Statistics**: Tracks garbage collection count and heap pages
93
- - **Minimal Performance Impact**: ~0.1ms overhead per request
94
- - **Memory Before/After**: Captures memory state at request start and end
95
-
96
- ### Detailed Allocation Tracking (Optional)
97
- - **Object Allocation Tracking**: Detailed tracking of object allocations (disabled by default)
98
- - **Allocation Sampling**: Configurable sampling rate for allocations
99
- - **Large Object Detection**: Identifies objects larger than 1MB threshold
100
- - **Memory Snapshots**: Periodic memory snapshots during request processing
101
- - **Object Count Tracking**: Tracks object counts before and after requests
102
- - **Performance Impact**: ~2-5ms overhead per request (only when enabled)
103
-
104
- ### Memory Leak Detection
105
- - **Pattern Detection**: Detects growing memory patterns over time
106
- - **GC Efficiency Analysis**: Monitors garbage collection effectiveness
107
- - **Heap Page Tracking**: Tracks heap page growth
108
- - **Request Correlation**: Correlates memory growth with specific controllers/actions
109
-
110
- ## Background Job Tracking
111
-
112
- ### Job Execution Monitoring
113
- - **ActiveJob Integration**: Automatic tracking when ActiveJob is available
114
- - **Job Class Tracking**: Tracks job class names
115
- - **Job ID**: Captures unique job identifiers
116
- - **Queue Name**: Tracks which queue processed the job
117
- - **Job Arguments**: Captures job arguments (with sensitive data filtering)
118
- - **Duration Measurement**: Precise job execution time in milliseconds
119
- - **Status Tracking**: Tracks job status (completed or failed)
120
-
121
- ### Job Error Tracking
122
- - **Exception Capture**: Captures exceptions from failed jobs
123
- - **Exception Class**: Tracks exception class names
124
- - **Exception Messages**: Captures exception messages (truncated to 1000 chars)
125
- - **Backtraces**: Full exception backtraces (first 50 lines)
126
- - **SQL Query Context**: Includes SQL queries executed during failed jobs
127
- - **Memory Context**: Includes memory usage during job execution
128
-
129
- ### Job SQL Tracking
130
- - **SQL Query Tracking**: Tracks all SQL queries executed during job processing
131
- - **Query Details**: Same detailed SQL tracking as request tracking
132
- - **Query Context**: Full context of database operations in background jobs
133
-
134
- ## Cache Tracking
135
-
136
- ### Cache Operations
137
- - **Read Operations**: Tracks cache read operations
138
- - **Write Operations**: Tracks cache write operations
139
- - **Delete Operations**: Tracks cache delete operations
140
- - **Existence Checks**: Tracks cache existence checks
141
- - **Fetch Operations**: Tracks cache fetch with hit/miss detection
142
- - **Multi-Read Operations**: Tracks cache read_multi operations
143
- - **Multi-Write Operations**: Tracks cache write_multi operations
144
- - **Cache Generation**: Tracks cache generation events
145
-
146
- ### Cache Analysis
147
- - **Cache Hit Detection**: Identifies cache hits vs misses
148
- - **Cache Key Tracking**: Tracks cache keys (truncated to 200 chars)
149
- - **Store Information**: Identifies which cache store was used
150
- - **Namespace Tracking**: Tracks cache namespaces
151
- - **Duration Measurement**: Precise timing for each cache operation
152
- - **Hit Rate Calculation**: Calculates cache hit rates per request
153
-
154
- ## Redis Tracking
155
-
156
- ### Redis Command Tracking
157
- - **Command Monitoring**: Tracks all Redis commands executed
158
- - **Command Name**: Captures Redis command names (GET, SET, etc.)
159
- - **Key Tracking**: Tracks Redis keys (truncated to 200 chars)
160
- - **Argument Count**: Tracks number of arguments per command
161
- - **Database Selection**: Tracks which Redis database is used
162
- - **Duration Measurement**: Precise timing for each Redis command
163
- - **Error Tracking**: Captures Redis command errors
164
-
165
- ### Advanced Redis Features
166
- - **Pipeline Support**: Tracks Redis pipeline operations with command counts
167
- - **Multi/Transaction Support**: Tracks Redis MULTI/EXEC transactions
168
- - **ActiveSupport Integration**: Subscribes to ActiveSupport::Notifications for Redis events
169
- - **Client Instrumentation**: Direct instrumentation of Redis::Client for comprehensive coverage
170
-
171
- ## Error Tracking
172
-
173
- ### Exception Handling
174
- - **Automatic Exception Capture**: Captures exceptions from controller actions
175
- - **Exception Class**: Tracks exception class names
176
- - **Exception Messages**: Captures exception messages (truncated to 1000 chars)
177
- - **Full Backtraces**: Captures complete exception backtraces (first 50 lines)
178
- - **Request Context**: Includes full request context with exceptions
179
- - **Error Flagging**: Errors are marked and always tracked (even with sampling)
180
-
181
- ### Error Context
182
- - **Controller/Action**: Identifies where the error occurred
183
- - **Request Parameters**: Includes request parameters at time of error
184
- - **User Information**: Includes user ID if available
185
- - **SQL Queries**: Includes SQL queries executed before error
186
- - **Memory State**: Includes memory usage at time of error
187
- - **Log Messages**: Includes application logs captured during request
188
-
189
- ## HTTP Instrumentation
190
-
191
- ### Outgoing HTTP Tracking
192
- - **HTTP Request Tracking**: Tracks outgoing HTTP requests (via middleware)
193
- - **Request Context**: Captures HTTP request details
194
- - **Response Context**: Captures HTTP response details
195
- - **Duration Measurement**: Tracks HTTP request duration
196
-
197
- ## Configuration & Flexibility
198
-
199
- ### Configuration Options
200
- - **API Key Management**: Multiple sources (config, Rails credentials, ENV)
201
- - **Endpoint Configuration**: Configurable endpoint URL
202
- - **Timeout Settings**: Configurable open and read timeouts
203
- - **Enable/Disable Toggle**: Can be enabled/disabled via configuration
204
- - **Environment Detection**: Automatic Rails environment detection
205
-
206
- ### Circuit Breaker Configuration
207
- - **Failure Threshold**: Configurable failure threshold (default: 3)
208
- - **Recovery Timeout**: Configurable recovery timeout (default: 60 seconds)
209
- - **Retry Timeout**: Configurable retry timeout (default: 300 seconds)
210
- - **Enable/Disable**: Can enable/disable circuit breaker
211
-
212
- ### Memory Tracking Configuration
213
- - **Memory Tracking Toggle**: Enable/disable memory tracking
214
- - **Allocation Tracking Toggle**: Enable/disable detailed allocation tracking
215
- - **Sampling Configuration**: Configurable request sampling rate
216
-
217
- ### Query Analysis Configuration
218
- - **Slow Query Threshold**: Configurable threshold in milliseconds (default: 500ms)
219
- - **EXPLAIN ANALYZE Toggle**: Enable/disable automatic EXPLAIN ANALYZE
220
-
221
- ## Data Safety & Privacy
222
-
223
- ### Data Sanitization
224
- - **SQL Sanitization**: Automatically sanitizes SQL queries
225
- - **Parameter Filtering**: Filters sensitive parameters (password, token, secret, key)
226
- - **Argument Truncation**: Limits and truncates job arguments
227
- - **Key Truncation**: Truncates cache and Redis keys to 200 characters
228
- - **Value Truncation**: Recursively truncates nested values to prevent huge payloads
229
- - **String Limits**: Limits string values (e.g., user agent to 200 chars, messages to 1000 chars)
230
-
231
- ### Data Limits
232
- - **Array Limits**: Limits array sizes (e.g., first 10 job arguments, first 5 array elements)
233
- - **Hash Limits**: Limits hash key counts (e.g., first 20 hash keys, first 30 params)
234
- - **Backtrace Limits**: Limits backtraces to first 50 lines
235
- - **Allocation Limits**: Limits allocations tracked per request (max 1000)
236
-
237
- ## Performance & Reliability
238
-
239
- ### Performance Optimizations
240
- - **Asynchronous Posting**: Non-blocking HTTP requests
241
- - **Lightweight Default Mode**: Minimal overhead in default configuration
242
- - **Sampling Support**: Reduces data volume for high-traffic applications
243
- - **Thread-Local Storage**: Efficient per-request data collection
244
- - **Background EXPLAIN**: EXPLAIN ANALYZE runs in background thread
245
-
246
- ### Reliability Features
247
- - **Circuit Breaker**: Prevents cascading failures
248
- - **Error Handling**: Comprehensive error handling to prevent instrumentation failures
249
- - **Graceful Degradation**: Continues working even if some features fail
250
- - **Timeout Protection**: Configurable timeouts prevent hanging requests
251
-
252
- ## Integration & Compatibility
253
-
254
- ### Framework Support
255
- - **Rails Integration**: Full Rails integration via Railtie
256
- - **ActiveSupport Notifications**: Uses ActiveSupport::Notifications for event subscription
257
- - **ActiveRecord Integration**: Tracks ActiveRecord SQL queries
258
- - **ActiveJob Integration**: Tracks ActiveJob background jobs
259
- - **ActionView Integration**: Tracks ActionView rendering
260
-
261
- ### Database Support
262
- - **PostgreSQL**: Full support with EXPLAIN ANALYZE
263
- - **MySQL**: Full support with EXPLAIN ANALYZE
264
- - **SQLite**: Full support with EXPLAIN QUERY PLAN
265
- - **Other Databases**: Basic support with standard EXPLAIN
266
-
267
- ### Cache Store Support
268
- - **All Cache Stores**: Works with any Rails cache store
269
- - **Multi-Store Support**: Tracks cache operations across different stores
270
-
271
- ### Redis Support
272
- - **Redis Gem**: Works with redis gem
273
- - **Client Instrumentation**: Direct instrumentation of Redis::Client
274
- - **Pipeline Support**: Tracks Redis pipelines
275
- - **Transaction Support**: Tracks Redis MULTI/EXEC transactions
276
-
277
- ## Logging & Debugging
278
-
279
- ### Application Logging
280
- - **Log Capture**: Captures application logs during request processing
281
- - **Log Context**: Includes logs in metric payloads
282
- - **Debug Logging**: Optional debug logging for skipped requests
283
-
284
- ## Deployment & Environment
285
-
286
- ### Deploy Tracking
287
- - **Deploy ID Resolution**: Multiple sources for deploy identification (`Configuration#deploy_id=` wins when set, then ENV in `Configuration::DEPLOY_REVISION_ENV_KEYS` order—including `DEAD_BRO_DEPLOY_ID`, git/CI vars, `DD_VERSION`, etc.), otherwise a **per-process UUID** (fine for single dyno/process; unusable alone for fleets like ECS replicas)
288
- - **Revision Tracking**: Includes deploy/revision ID in all metric payloads
289
-
290
- ### Environment Support
291
- - **Rails Environment**: Automatic Rails environment detection
292
- - **Rack Environment**: Fallback to RACK_ENV or RAILS_ENV
293
- - **Environment Context**: Includes environment in all metric payloads
294
-
295
- ## Data Collection & Transmission
296
-
297
- ### Metric Payload Structure
298
- - **Structured Data**: Well-structured JSON payloads
299
- - **Event Names**: Descriptive event names for different metric types
300
- - **Timestamp Tracking**: ISO8601 timestamps for all metrics
301
- - **Metadata**: Rich metadata including environment, host, deploy ID
302
-
303
- ### HTTP Client
304
- - **HTTPS Support**: Secure HTTPS communication
305
- - **Bearer Token Auth**: API key authentication via Bearer tokens
306
- - **JSON Encoding**: JSON-encoded payloads
307
- - **Custom Headers**: Proper Content-Type and Authorization headers
308
-
309
- ## Comparison-Ready Features
310
-
311
- ### Unique Differentiators
312
- 1. **Automatic EXPLAIN ANALYZE**: Background execution plan capture for slow queries
313
- 2. **Lightweight Memory Tracking**: Low-overhead memory monitoring by default
314
- 3. **Comprehensive Cache Tracking**: Detailed cache operation tracking
315
- 4. **Redis Instrumentation**: Full Redis command tracking including pipelines
316
- 5. **View Rendering Analysis**: Detailed view performance analysis with cache hit rates
317
- 6. **Flexible Exclusion Rules**: Wildcard support for controller/job exclusion
318
- 7. **Request Sampling**: Configurable percentage-based sampling
319
- 8. **Circuit Breaker**: Built-in resilience for APM endpoint failures
320
- 9. **Multi-Source Configuration**: Flexible configuration from multiple sources
321
- 10. **Deploy Tracking**: Automatic deploy ID resolution from multiple sources
322
-
323
- ### Standard APM Features
324
- - Request/response tracking
325
- - SQL query tracking
326
- - Error tracking
327
- - Background job tracking
328
- - Memory tracking
329
- - Performance metrics
330
- - Exception handling
331
- - User context
332
- - Environment tracking
333
-