dead_bro 0.2.9 → 0.2.10

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: 8badc23e17a92bca8e8057c8c49c6ba7a1535975a7da9f61a43fd0e2b80f56b5
4
- data.tar.gz: 32ed00be83e65d97b89e3467129f95986d0417ec27839066a83b7706182a28ba
3
+ metadata.gz: 1ac04c555f3f82572e94327d09b90672869eb17c129deb87c78f746625cb5afd
4
+ data.tar.gz: 4639f701e7bd5dff8b36933e85b405dbbe43a2ccc664c94f16b6e2ea98987fda
5
5
  SHA512:
6
- metadata.gz: becf47113fbfb03c8f851f54e3e518588e8ebc963f762d68b699b3a01dad7bab381c6d07c323abeb1d1414f20a24d949bf4cc245821ea96c4ee38e8e155f82ad
7
- data.tar.gz: 1071fe9473be03be215822ab0bc9349ba1569a03f6d01fca372911f390c584227ebcd4c42272da8dd66198b560c5f09de83c42b6238dd8710241889c9e2cdb55
6
+ metadata.gz: fee0d59a0362226babd057547b8138078d5ce3da4a75a3c5a2bbd175ee21b3043485e9f6c8e0b0a27a18c2373f10827fc7804f64c96888a41eea8d3c8eb8ea74
7
+ data.tar.gz: 1e044e7a275a5e52843119e00488a8a458e6d6f95420543014edd1074985bcdeebe1cbeef579e436a4eb31836eb6f9e24961c039851c4c82b2b0a76184cf23d2
data/README.md CHANGED
@@ -388,6 +388,69 @@ The control plane job sends a single JSON payload roughly shaped like:
388
388
  Not all fields will be present in all environments; unsupported or unavailable metrics may be `null` or omitted, and any hard failures are captured in `error_class` / `error_message` fields per section.
389
389
 
390
390
 
391
+ ## One-Off Analysis with `DeadBro.analyze`
392
+
393
+ Use `DeadBro.analyze` to profile any block of code inline — useful in the Rails console, rake tasks, or debug sessions. It tracks execution time, SQL queries, and memory usage without sending data to the DeadBro backend.
394
+
395
+ ```ruby
396
+ result = DeadBro.analyze("load active users") do
397
+ User.where(active: true).includes(:profile).to_a
398
+ end
399
+ ```
400
+
401
+ Returns a `DeadBro::AnalysisResult` struct.
402
+
403
+ ### Return Value
404
+
405
+ `DeadBro::AnalysisResult` exposes:
406
+
407
+ | Member | Type | Description |
408
+ |---|---|---|
409
+ | `label` | String | Label passed to the block |
410
+ | `total_time_ms` | Float | Wall time of the block |
411
+ | `sql_count` | Integer | Number of SQL queries executed |
412
+ | `sql_time_ms` | Float | Total SQL execution time |
413
+ | `sql_queries` | Array | Per-query breakdown (see below) |
414
+ | `memory_before_mb` | Float | RSS before block |
415
+ | `memory_after_mb` | Float | RSS after block |
416
+ | `memory_delta_mb` | Float | Memory change |
417
+ | `memory_details` | Hash | GC stats, new objects, heap pages |
418
+
419
+ **`sql_queries` is intentionally excluded from `inspect`/`to_s`** to avoid flooding the console with a long array. Access it explicitly when needed:
420
+
421
+ ```ruby
422
+ result # => #<DeadBro::AnalysisResult label="load active users" total_time_ms=42.3 ...>
423
+ result.sql_queries # => [{sql: "SELECT ...", query_type: "SELECT", count: 3, total_time_ms: 12.1}, ...]
424
+ ```
425
+
426
+ ### Options
427
+
428
+ ```ruby
429
+ DeadBro.analyze("my block", verbose: true) do
430
+ # verbose: true lowers the Rails log level to DEBUG and enables
431
+ # ActiveRecord.verbose_query_logs for the duration of the block
432
+ MyService.call
433
+ end
434
+ ```
435
+
436
+ ### Helper methods
437
+
438
+ ```ruby
439
+ result.most_queries # top 5 query patterns by execution count
440
+ result.longest_queries # top 5 query patterns by total_time_ms
441
+ ```
442
+
443
+ Both return an array of query hashes (same shape as `sql_queries`):
444
+
445
+ - `sql` — normalized SQL (literals replaced, whitespace collapsed)
446
+ - `query_type` — e.g. `"SELECT"`, `"INSERT"`, `"UPDATE"`
447
+ - `count` — how many times this pattern ran
448
+ - `total_time_ms` — combined time across all occurrences
449
+
450
+ ### `sql_queries` fields
451
+
452
+ `result.sql_queries` returns the full list. Each entry is a hash with the same fields as above.
453
+
391
454
  ## Development
392
455
 
393
456
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DeadBro
4
+ class ElasticsearchSubscriber
5
+ THREAD_LOCAL_KEY = :dead_bro_elasticsearch_events
6
+ MAX_TRACKED_EVENTS = 500
7
+
8
+ # Install gem-based notification subscriber (request.elasticsearch / request.elastic_transport).
9
+ # The Net::HTTP path is handled by HttpInstrumentation, which calls .record directly.
10
+ def self.subscribe!
11
+ install_notifications_subscription!
12
+ rescue
13
+ end
14
+
15
+ # Called by HttpInstrumentation when it detects a Net::HTTP request to an ES host.
16
+ def self.record(method:, path:, status:, duration_ms:)
17
+ events = Thread.current[THREAD_LOCAL_KEY]
18
+ return unless events
19
+ return unless should_continue_tracking?
20
+
21
+ events << build_event(method, path, status, duration_ms)
22
+ rescue
23
+ end
24
+
25
+ def self.start_request_tracking
26
+ Thread.current[THREAD_LOCAL_KEY] = []
27
+ end
28
+
29
+ def self.stop_request_tracking
30
+ events = Thread.current[THREAD_LOCAL_KEY]
31
+ Thread.current[THREAD_LOCAL_KEY] = nil
32
+ events || []
33
+ end
34
+
35
+ def self.should_continue_tracking?
36
+ events = Thread.current[THREAD_LOCAL_KEY]
37
+ return false unless events
38
+ return false if events.length >= MAX_TRACKED_EVENTS
39
+
40
+ start_time = Thread.current[DeadBro::TRACKING_START_TIME_KEY]
41
+ if start_time
42
+ elapsed_seconds = Time.now - start_time
43
+ return false if elapsed_seconds >= DeadBro::MAX_TRACKING_DURATION_SECONDS
44
+ end
45
+
46
+ true
47
+ end
48
+
49
+ def self.extract_operation(method, path)
50
+ return "unknown" if path.nil?
51
+
52
+ clean = path.to_s.split("?").first.to_s
53
+ m = method.to_s.upcase
54
+
55
+ if clean =~ /_search\z/i
56
+ "search"
57
+ elsif clean =~ /_msearch\z/i
58
+ "msearch"
59
+ elsif clean =~ /_bulk\z/i
60
+ "bulk"
61
+ elsif clean =~ /_doc\/[^\/]+\/_update\z/i
62
+ "update"
63
+ elsif clean =~ /_update\/[^\/]+\z/i
64
+ "update"
65
+ elsif clean =~ /_delete_by_query\z/i
66
+ "delete_by_query"
67
+ elsif clean =~ /_count\z/i
68
+ "count"
69
+ elsif clean =~ /_mapping\z/i
70
+ m == "GET" ? "get_mapping" : "put_mapping"
71
+ elsif clean =~ /_doc\/[^\/]+\z/i
72
+ case m
73
+ when "GET" then "get"
74
+ when "DELETE" then "delete"
75
+ when "POST", "PUT" then "index"
76
+ else "doc"
77
+ end
78
+ elsif clean =~ /_doc\z/i
79
+ "index"
80
+ elsif clean =~ /\A\/_cluster\//i
81
+ "cluster"
82
+ elsif clean =~ /\A\/_cat\//i
83
+ "cat"
84
+ elsif clean =~ /\A\/[^\/]+\z/
85
+ case m
86
+ when "PUT" then "create_index"
87
+ when "DELETE" then "delete_index"
88
+ when "HEAD" then "index_exists"
89
+ when "GET" then "get_index"
90
+ else "index_op"
91
+ end
92
+ else
93
+ m.downcase
94
+ end
95
+ rescue
96
+ "unknown"
97
+ end
98
+
99
+ def self.sanitize_path(path)
100
+ return "" if path.nil?
101
+ path.to_s
102
+ .gsub(/\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i, "/{id}")
103
+ .gsub(/\/\d+(?=\/|\z)/, "/{id}")
104
+ rescue
105
+ path.to_s
106
+ end
107
+
108
+ class << self
109
+ private
110
+
111
+ def install_notifications_subscription!
112
+ return unless defined?(::ActiveSupport::Notifications)
113
+
114
+ %w[request.elasticsearch request.elastic_transport].each do |event_name|
115
+ ::ActiveSupport::Notifications.subscribe(event_name) do |_name, started, finished, _id, payload|
116
+ events = Thread.current[THREAD_LOCAL_KEY]
117
+ next unless events
118
+ next unless should_continue_tracking?
119
+
120
+ duration_ms = ((finished - started) * 1000.0).round(2)
121
+ method = payload[:method].to_s.upcase
122
+ path = payload[:path].to_s
123
+ events << build_event(method, path, payload[:status], duration_ms)
124
+ rescue
125
+ end
126
+ end
127
+ rescue
128
+ end
129
+
130
+ def build_event(method, path, status, duration_ms)
131
+ {
132
+ method: method.to_s.upcase,
133
+ path: sanitize_path(path),
134
+ operation: extract_operation(method, path),
135
+ status: status,
136
+ duration_ms: duration_ms
137
+ }
138
+ end
139
+ end
140
+ end
141
+ end
@@ -12,6 +12,7 @@ module DeadBro
12
12
  def self.install!(client: Client.new)
13
13
  install_net_http!(client)
14
14
  install_typhoeus!(client) if defined?(::Typhoeus)
15
+ install_faraday!(client) if defined?(::Faraday)
15
16
  rescue
16
17
  # Never raise from instrumentation install
17
18
  end
@@ -38,23 +39,37 @@ module DeadBro
38
39
  nil
39
40
  end
40
41
 
41
- # Skip instrumentation for our own APM endpoint to prevent infinite loops,
42
- # but do NOT alter the original method's return value/control flow.
43
- skip_instrumentation = uri && (uri.to_s.include?("localhost") || uri.to_s.include?("aberatii.com"))
42
+ host = (uri && uri.host) || @address
43
+ port = (uri && uri.port) || @port
44
+ is_es_host = DeadBro::HttpInstrumentation.elasticsearch_host?(host, port)
45
+ # Skip localhost/internal only for non-ES hosts. ES on localhost (e.g. port 9200)
46
+ # must still be tracked; only skip the deadbro backend itself.
47
+ skip_instrumentation = !is_es_host && uri && (uri.to_s.include?("localhost") || uri.to_s.include?("aberatii.com"))
44
48
 
45
- unless skip_instrumentation
49
+ if is_es_host
50
+ # Route to elasticsearch subscriber instead of http_outgoing
51
+ if Thread.current[DeadBro::ElasticsearchSubscriber::THREAD_LOCAL_KEY]
52
+ path = (uri && uri.path) || req.path
53
+ DeadBro::ElasticsearchSubscriber.record(
54
+ method: req.method,
55
+ path: path,
56
+ status: response && response.code.to_i,
57
+ duration_ms: duration_ms
58
+ )
59
+ end
60
+ elsif !skip_instrumentation
61
+ lib = DeadBro::HttpInstrumentation.typesense_host?(host, port) ? "typesense" : "net_http"
46
62
  payload = {
47
- library: "net_http",
63
+ library: lib,
48
64
  method: req.method,
49
65
  url: uri && uri.to_s,
50
- host: (uri && uri.host) || @address,
66
+ host: host,
51
67
  path: (uri && uri.path) || req.path,
52
68
  status: response && response.code.to_i,
53
69
  duration_ms: duration_ms,
54
70
  exception: error && error.class.name
55
71
  }
56
- # Accumulate per-request; only send with controller metric
57
- if Thread.current[THREAD_LOCAL_KEY] && should_continue_tracking?
72
+ if Thread.current[THREAD_LOCAL_KEY] && DeadBro::HttpInstrumentation.should_continue_tracking?
58
73
  Thread.current[THREAD_LOCAL_KEY] << payload
59
74
  end
60
75
  end
@@ -85,8 +100,6 @@ module DeadBro
85
100
  (respond_to?(:base_url) ? base_url : nil)
86
101
  end
87
102
 
88
- # Skip instrumentation for our own APM endpoint to prevent infinite loops,
89
- # but do NOT alter the original method's return value/control flow.
90
103
  skip_instrumentation = req_url && (req_url.include?("localhost:3100/apm/v1/metrics") || req_url.include?("deadbro.aberatii.com/apm/v1/metrics"))
91
104
 
92
105
  unless skip_instrumentation
@@ -97,8 +110,7 @@ module DeadBro
97
110
  status: response && response.code,
98
111
  duration_ms: duration_ms
99
112
  }
100
- # Accumulate per-request; only send with controller metric
101
- if Thread.current[THREAD_LOCAL_KEY] && should_continue_tracking?
113
+ if Thread.current[THREAD_LOCAL_KEY] && DeadBro::HttpInstrumentation.should_continue_tracking?
102
114
  Thread.current[THREAD_LOCAL_KEY] << payload
103
115
  end
104
116
  end
@@ -112,15 +124,96 @@ module DeadBro
112
124
  rescue
113
125
  end
114
126
 
115
- # Check if we should continue tracking based on count and time limits
127
+ def self.install_faraday!(client)
128
+ return unless defined?(::Faraday)
129
+
130
+ unless defined?(::DeadBro::FaradayMiddleware)
131
+ middleware_klass = Class.new(::Faraday::Middleware) do
132
+ def call(env)
133
+ start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
134
+ response = nil
135
+ begin
136
+ response = @app.call(env)
137
+ ensure
138
+ finish_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
139
+ duration_ms = ((finish_time - start_time) * 1000.0).round(2)
140
+ begin
141
+ url = env.url
142
+ host = url.host.to_s
143
+ port = url.port
144
+ url_str = url.to_s
145
+
146
+ is_es_host = DeadBro::HttpInstrumentation.elasticsearch_host?(host, port)
147
+ skip = !is_es_host && (url_str.include?("localhost") || url_str.include?("aberatii.com"))
148
+
149
+ if is_es_host
150
+ if Thread.current[DeadBro::ElasticsearchSubscriber::THREAD_LOCAL_KEY]
151
+ DeadBro::ElasticsearchSubscriber.record(
152
+ method: env.method.to_s.upcase,
153
+ path: url.path,
154
+ status: response&.status,
155
+ duration_ms: duration_ms
156
+ )
157
+ end
158
+ elsif !skip
159
+ lib = DeadBro::HttpInstrumentation.typesense_host?(host, port) ? "typesense" : "faraday"
160
+ payload = {
161
+ library: lib,
162
+ method: env.method.to_s.upcase,
163
+ url: url_str,
164
+ host: host,
165
+ path: url.path,
166
+ status: response&.status,
167
+ duration_ms: duration_ms
168
+ }
169
+ key = DeadBro::HttpInstrumentation::THREAD_LOCAL_KEY
170
+ if Thread.current[key] && DeadBro::HttpInstrumentation.should_continue_tracking?
171
+ Thread.current[key] << payload
172
+ end
173
+ end
174
+ rescue
175
+ end
176
+ end
177
+ end
178
+ end
179
+ ::DeadBro.const_set(:FaradayMiddleware, middleware_klass)
180
+ end
181
+
182
+ unless defined?(::DeadBro::FaradayInstrumentation)
183
+ instrumentation_mod = Module.new do
184
+ define_method(:initialize) do |url = nil, options = {}, &block|
185
+ super(url, options, &block)
186
+ unless builder.handlers.map(&:klass).include?(::DeadBro::FaradayMiddleware)
187
+ builder.use(::DeadBro::FaradayMiddleware)
188
+ end
189
+ rescue
190
+ end
191
+ end
192
+ ::DeadBro.const_set(:FaradayInstrumentation, instrumentation_mod)
193
+ end
194
+
195
+ ::Faraday::Connection.prepend(::DeadBro::FaradayInstrumentation) unless ::Faraday::Connection.ancestors.include?(::DeadBro::FaradayInstrumentation)
196
+ rescue
197
+ end
198
+
199
+ def self.elasticsearch_host?(host, port)
200
+ return false if host.nil?
201
+ return true if port == 9200
202
+ h = host.to_s
203
+ h.end_with?(".elastic.co") || h.end_with?(".es.amazonaws.com") || h.include?("elasticsearch")
204
+ end
205
+
206
+ def self.typesense_host?(host, port)
207
+ return false if host.nil?
208
+ port == 8108 || host.to_s.end_with?(".typesense.io")
209
+ end
210
+
116
211
  def self.should_continue_tracking?
117
212
  events = Thread.current[THREAD_LOCAL_KEY]
118
213
  return false unless events
119
214
 
120
- # Check count limit
121
215
  return false if events.length >= MAX_TRACKED_EVENTS
122
216
 
123
- # Check time limit
124
217
  start_time = Thread.current[DeadBro::TRACKING_START_TIME_KEY]
125
218
  if start_time
126
219
  elapsed_seconds = Time.now - start_time
@@ -32,6 +32,10 @@ if defined?(Rails) && defined?(Rails::Railtie)
32
32
  require "dead_bro/redis_subscriber"
33
33
  DeadBro::RedisSubscriber.subscribe!
34
34
 
35
+ # Install Elasticsearch / OpenSearch tracking
36
+ require "dead_bro/elasticsearch_subscriber"
37
+ DeadBro::ElasticsearchSubscriber.subscribe!
38
+
35
39
  # Install view rendering tracking
36
40
  require "dead_bro/view_rendering_subscriber"
37
41
  DeadBro::ViewRenderingSubscriber.subscribe!(client: shared_client)
@@ -151,9 +151,8 @@ module DeadBro
151
151
  # storm from spawning unbounded background threads.
152
152
  MAX_PENDING_EXPLAINS = 20
153
153
  # Overall wall-clock we're willing to block the request thread for pending
154
- # EXPLAINs. Dropped from 5s → 1s: if the plan isn't ready by then, skip it
155
- # rather than stall the request.
156
- EXPLAIN_WAIT_TIMEOUT_SECONDS = 1.0
154
+ # EXPLAINs. If the plan isn't ready by then, skip it rather than stall the request.
155
+ EXPLAIN_WAIT_TIMEOUT_SECONDS = 5.0
157
156
 
158
157
  def self.wait_for_pending_explains(timeout_seconds)
159
158
  pending = Thread.current[THREAD_LOCAL_EXPLAIN_PENDING_KEY]
@@ -40,6 +40,11 @@ module DeadBro
40
40
  DeadBro::MemoryTrackingSubscriber.start_request_tracking
41
41
  end
42
42
 
43
+ # Start Elasticsearch tracking for this request
44
+ if defined?(DeadBro::ElasticsearchSubscriber)
45
+ DeadBro::ElasticsearchSubscriber.start_request_tracking
46
+ end
47
+
43
48
  # Start outgoing HTTP accumulation for this request
44
49
  Thread.current[:dead_bro_http_events] = []
45
50
 
@@ -74,7 +79,8 @@ module DeadBro
74
79
  Thread.current[:dead_bro_lightweight_memory] = nil
75
80
  end
76
81
 
77
- # Clean up HTTP events and tracking start time
82
+ # Clean up HTTP events, ES events, and tracking start time
83
+ Thread.current[:dead_bro_elasticsearch_events] = nil
78
84
  Thread.current[:dead_bro_http_events] = nil
79
85
  Thread.current[DeadBro::TRACKING_START_TIME_KEY] = nil
80
86
  end
@@ -46,9 +46,10 @@ module DeadBro
46
46
  # Stop SQL tracking and get collected queries (this was started by the request)
47
47
  sql_queries = DeadBro::SqlSubscriber.stop_request_tracking
48
48
 
49
- # Stop cache and redis tracking
49
+ # Stop cache, redis, and elasticsearch tracking
50
50
  cache_events = defined?(DeadBro::CacheSubscriber) ? DeadBro::CacheSubscriber.stop_request_tracking : []
51
51
  redis_events = defined?(DeadBro::RedisSubscriber) ? DeadBro::RedisSubscriber.stop_request_tracking : []
52
+ elasticsearch_events = defined?(DeadBro::ElasticsearchSubscriber) ? DeadBro::ElasticsearchSubscriber.stop_request_tracking : []
52
53
 
53
54
  # Stop view rendering tracking and get collected view events
54
55
  view_events = DeadBro::ViewRenderingSubscriber.stop_request_tracking
@@ -152,6 +153,7 @@ module DeadBro
152
153
  http_outgoing: Thread.current[:dead_bro_http_events] || [],
153
154
  cache_events: cache_events,
154
155
  redis_events: redis_events,
156
+ elasticsearch_events: elasticsearch_events,
155
157
  cache_hits: cache_hits(data),
156
158
  cache_misses: cache_misses(data),
157
159
  view_events: view_events,
@@ -171,6 +173,7 @@ module DeadBro
171
173
  DeadBro::SqlSubscriber.stop_request_tracking if defined?(DeadBro::SqlSubscriber)
172
174
  DeadBro::CacheSubscriber.stop_request_tracking if defined?(DeadBro::CacheSubscriber)
173
175
  DeadBro::RedisSubscriber.stop_request_tracking if defined?(DeadBro::RedisSubscriber)
176
+ DeadBro::ElasticsearchSubscriber.stop_request_tracking if defined?(DeadBro::ElasticsearchSubscriber)
174
177
  DeadBro::ViewRenderingSubscriber.stop_request_tracking if defined?(DeadBro::ViewRenderingSubscriber)
175
178
  DeadBro::LightweightMemoryTracker.stop_request_tracking if defined?(DeadBro::LightweightMemoryTracker)
176
179
  if DeadBro.configuration.allocation_tracking_enabled && defined?(DeadBro::MemoryTrackingSubscriber)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DeadBro
4
- VERSION = "0.2.9"
4
+ VERSION = "0.2.10"
5
5
  end
data/lib/dead_bro.rb CHANGED
@@ -13,6 +13,7 @@ module DeadBro
13
13
  autoload :SqlTrackingMiddleware, "dead_bro/sql_tracking_middleware"
14
14
  autoload :CacheSubscriber, "dead_bro/cache_subscriber"
15
15
  autoload :RedisSubscriber, "dead_bro/redis_subscriber"
16
+ autoload :ElasticsearchSubscriber, "dead_bro/elasticsearch_subscriber"
16
17
  autoload :ViewRenderingSubscriber, "dead_bro/view_rendering_subscriber"
17
18
  autoload :MemoryTrackingSubscriber, "dead_bro/memory_tracking_subscriber"
18
19
  autoload :MemoryLeakDetector, "dead_bro/memory_leak_detector"
@@ -30,6 +31,57 @@ module DeadBro
30
31
 
31
32
  class Error < StandardError; end
32
33
 
34
+ # Returned by DeadBro.analyze. sql_queries is intentionally omitted from
35
+ # inspect/to_s/pretty_print to avoid bloating console output.
36
+ # Access it explicitly via .sql_queries.
37
+ class AnalysisResult
38
+ attr_reader :label, :total_time_ms, :sql_count, :sql_time_ms,
39
+ :sql_queries, :memory_before_mb, :memory_after_mb,
40
+ :memory_delta_mb, :memory_details, :verbose
41
+
42
+ def initialize(label:, total_time_ms:, sql_count:, sql_time_ms:, sql_queries:,
43
+ memory_before_mb:, memory_after_mb:, memory_delta_mb:,
44
+ memory_details:, verbose:)
45
+ @label = label
46
+ @total_time_ms = total_time_ms
47
+ @sql_count = sql_count
48
+ @sql_time_ms = sql_time_ms
49
+ @sql_queries = sql_queries
50
+ @memory_before_mb = memory_before_mb
51
+ @memory_after_mb = memory_after_mb
52
+ @memory_delta_mb = memory_delta_mb
53
+ @memory_details = memory_details
54
+ @verbose = verbose
55
+ end
56
+
57
+ def inspect
58
+ "#<DeadBro::AnalysisResult label=#{label.inspect} total_time_ms=#{total_time_ms} " \
59
+ "sql_count=#{sql_count} sql_time_ms=#{sql_time_ms} " \
60
+ "memory_before_mb=#{memory_before_mb} memory_after_mb=#{memory_after_mb} " \
61
+ "memory_delta_mb=#{memory_delta_mb}>"
62
+ end
63
+
64
+ alias_method :to_s, :inspect
65
+
66
+ def most_queries
67
+ sql_queries.max_by(5) { |q| q[:count] }
68
+ end
69
+
70
+ def longest_queries
71
+ sql_queries.max_by(5) { |q| q[:total_time_ms] }
72
+ end
73
+
74
+ def pretty_print(pp)
75
+ pp.text(inspect)
76
+ end
77
+
78
+ def [](key)
79
+ public_send(key)
80
+ rescue NoMethodError
81
+ nil
82
+ end
83
+ end
84
+
33
85
  def self.configure
34
86
  yield configuration
35
87
  end
@@ -102,7 +154,8 @@ module DeadBro
102
154
  # - memory before/after and delta
103
155
  # - when detailed memory tracking is enabled, GC and allocation stats
104
156
  #
105
- # The return value of this method is a hash with the following keys:
157
+ # The return value is a DeadBro::AnalysisResult struct. sql_queries is omitted from
158
+ # inspect/to_s but accessible via .sql_queries. Other members:
106
159
  # - :label
107
160
  # - :total_time_ms
108
161
  # - :sql_count
@@ -249,8 +302,6 @@ module DeadBro
249
302
  entry[:type] ||= q[:query_type]
250
303
  end
251
304
 
252
- top_query_signatures = query_signatures.sort_by { |_, data| -data[:count] }.first(3)
253
-
254
305
  # Capture post-block memory state — always, regardless of config.
255
306
  gc_after = begin; GC.stat; rescue; {}; end
256
307
  memory_after_mb = begin; DeadBro::MemoryHelpers.rss_mb; rescue; memory_before_mb; end
@@ -287,38 +338,6 @@ module DeadBro
287
338
  large_objects: large_objects
288
339
  )
289
340
 
290
- sql_queries_segment = ""
291
- unless top_query_signatures.empty?
292
- formatted_queries = top_query_signatures.map do |sig, data|
293
- type = data[:type] || "SQL"
294
- count = data[:count]
295
- total_ms = data[:total_time_ms].round(2)
296
- "#{type} #{sig} (#{count}x, #{total_ms}ms)"
297
- end
298
- sql_queries_segment = ", sql_top_queries=[#{formatted_queries.join(" | ")}]"
299
- end
300
-
301
- warnings = detailed_memory_summary[:warnings]
302
- warnings_segment = warnings.any? ? ", warnings=[#{warnings.join(", ")}]" : ""
303
- summary = "Analysis for #{label} - total_time=#{total_time_ms}ms, " \
304
- "sql_queries=#{sql_count}, sql_time=#{sql_time_ms}ms, " \
305
- "memory_before=#{memory_before_mb.round(2)}MB, " \
306
- "memory_after=#{memory_after_mb.round(2)}MB, " \
307
- "memory_delta=#{memory_delta_mb}MB, " \
308
- "gc_collections=+#{detailed_memory_summary[:gc_collections]}, " \
309
- "heap_pages_added=+#{detailed_memory_summary[:heap_pages_added]}, " \
310
- "new_objects=+#{detailed_memory_summary[:new_objects]}" \
311
- "#{sql_queries_segment}#{warnings_segment}"
312
-
313
- begin
314
- DeadBro.logger.info(summary)
315
- rescue
316
- begin
317
- $stdout.puts("[DeadBro] #{summary}")
318
- rescue
319
- end
320
- end
321
-
322
341
  # Build structured result hash to return to the caller
323
342
  sql_queries_detail = query_signatures.map do |sig, data|
324
343
  {
@@ -329,7 +348,7 @@ module DeadBro
329
348
  }
330
349
  end
331
350
 
332
- analysis_result = {
351
+ analysis_result = AnalysisResult.new(
333
352
  label: label,
334
353
  total_time_ms: total_time_ms,
335
354
  sql_count: sql_count,
@@ -340,7 +359,7 @@ module DeadBro
340
359
  memory_delta_mb: memory_delta_mb,
341
360
  memory_details: detailed_memory_summary,
342
361
  verbose: verbose
343
- }
362
+ )
344
363
  end
345
364
 
346
365
  raise error if error
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.9
4
+ version: 0.2.10
5
5
  platform: ruby
6
6
  authors:
7
7
  - Emanuel Comsa
@@ -34,6 +34,7 @@ files:
34
34
  - lib/dead_bro/collectors/system.rb
35
35
  - lib/dead_bro/configuration.rb
36
36
  - lib/dead_bro/dispatcher.rb
37
+ - lib/dead_bro/elasticsearch_subscriber.rb
37
38
  - lib/dead_bro/error_middleware.rb
38
39
  - lib/dead_bro/http_instrumentation.rb
39
40
  - lib/dead_bro/job_sql_tracking_middleware.rb