dead_bro 0.2.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.
@@ -0,0 +1,357 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/notifications"
4
+
5
+ module DeadBro
6
+ class Subscriber
7
+ EVENT_NAME = "process_action.action_controller"
8
+
9
+ def self.subscribe!(client: Client.new)
10
+ ActiveSupport::Notifications.subscribe(EVENT_NAME) do |name, started, finished, _unique_id, data|
11
+ # Skip excluded controllers or controller#action pairs
12
+ # Also check exclusive_controller_actions - if defined, only track those
13
+ begin
14
+ controller_name = data[:controller].to_s
15
+ action_name = data[:action].to_s
16
+ if DeadBro.configuration.excluded_controller?(controller_name, action_name)
17
+ puts "excluded controller"
18
+ next
19
+ end
20
+ # If exclusive_controller_actions is defined and not empty, only track matching actions
21
+ unless DeadBro.configuration.exclusive_controller?(controller_name, action_name)
22
+ puts "exclusive controller"
23
+ next
24
+ end
25
+ rescue
26
+ end
27
+
28
+ duration_ms = ((finished - started) * 1000.0).round(2)
29
+ # Stop SQL tracking and get collected queries (this was started by the request)
30
+ sql_queries = DeadBro::SqlSubscriber.stop_request_tracking
31
+
32
+ # Stop cache and redis tracking
33
+ cache_events = defined?(DeadBro::CacheSubscriber) ? DeadBro::CacheSubscriber.stop_request_tracking : []
34
+ redis_events = defined?(DeadBro::RedisSubscriber) ? DeadBro::RedisSubscriber.stop_request_tracking : []
35
+
36
+ # Stop view rendering tracking and get collected view events
37
+ view_events = DeadBro::ViewRenderingSubscriber.stop_request_tracking
38
+ view_performance = DeadBro::ViewRenderingSubscriber.analyze_view_performance(view_events)
39
+
40
+ # Stop memory tracking and get collected memory data
41
+ if DeadBro.configuration.allocation_tracking_enabled && defined?(DeadBro::MemoryTrackingSubscriber)
42
+ detailed_memory = DeadBro::MemoryTrackingSubscriber.stop_request_tracking
43
+ memory_performance = DeadBro::MemoryTrackingSubscriber.analyze_memory_performance(detailed_memory)
44
+ # Keep memory_events compact and user-friendly (no large raw arrays)
45
+ memory_events = {
46
+ memory_before: detailed_memory[:memory_before],
47
+ memory_after: detailed_memory[:memory_after],
48
+ duration_seconds: detailed_memory[:duration_seconds],
49
+ allocations_count: (detailed_memory[:allocations] || []).length,
50
+ memory_snapshots_count: (detailed_memory[:memory_snapshots] || []).length,
51
+ large_objects_count: (detailed_memory[:large_objects] || []).length
52
+ }
53
+ else
54
+ lightweight_memory = DeadBro::LightweightMemoryTracker.stop_request_tracking
55
+ # Separate raw readings from derived performance metrics to avoid duplicating data
56
+ memory_events = {
57
+ memory_before: lightweight_memory[:memory_before],
58
+ memory_after: lightweight_memory[:memory_after]
59
+ }
60
+ memory_performance = {
61
+ memory_growth_mb: lightweight_memory[:memory_growth_mb],
62
+ gc_count_increase: lightweight_memory[:gc_count_increase],
63
+ heap_pages_increase: lightweight_memory[:heap_pages_increase],
64
+ duration_seconds: lightweight_memory[:duration_seconds]
65
+ }
66
+ end
67
+
68
+ # Record memory sample for leak detection (only if memory tracking enabled)
69
+ if DeadBro.configuration.memory_tracking_enabled
70
+ DeadBro::MemoryLeakDetector.record_memory_sample({
71
+ memory_usage: memory_usage_mb,
72
+ gc_count: gc_stats[:count],
73
+ heap_pages: gc_stats[:heap_allocated_pages],
74
+ object_count: gc_stats[:heap_live_slots],
75
+ request_id: data[:request_id],
76
+ controller: data[:controller],
77
+ action: data[:action]
78
+ })
79
+ end
80
+
81
+ # Report exceptions attached to this action (e.g. controller/view errors)
82
+ if data[:exception] || data[:exception_object]
83
+ begin
84
+ exception_class, exception_message = data[:exception] if data[:exception]
85
+ exception_obj = data[:exception_object]
86
+ backtrace = Array(exception_obj&.backtrace).first(50)
87
+
88
+ error_payload = {
89
+ controller: data[:controller],
90
+ action: data[:action],
91
+ format: data[:format],
92
+ method: data[:method],
93
+ path: safe_path(data),
94
+ status: data[:status],
95
+ duration_ms: duration_ms,
96
+ rails_env: rails_env,
97
+ host: safe_host,
98
+ params: safe_params(data),
99
+ user_agent: safe_user_agent(data),
100
+ user_id: extract_user_id(data),
101
+ exception_class: exception_class || exception_obj&.class&.name,
102
+ message: (exception_message || exception_obj&.message).to_s[0, 1000],
103
+ backtrace: backtrace,
104
+ error: true,
105
+ logs: DeadBro.logger.logs
106
+ }
107
+
108
+ event_name = (exception_class || exception_obj&.class&.name || "exception").to_s
109
+ client.post_metric(event_name: event_name, payload: error_payload)
110
+ rescue
111
+ ensure
112
+ next
113
+ end
114
+ end
115
+
116
+ payload = {
117
+ controller: data[:controller],
118
+ action: data[:action],
119
+ format: data[:format],
120
+ method: data[:method],
121
+ path: safe_path(data),
122
+ status: data[:status],
123
+ duration_ms: duration_ms,
124
+ view_runtime_ms: data[:view_runtime],
125
+ db_runtime_ms: data[:db_runtime],
126
+ host: safe_host,
127
+ rails_env: rails_env,
128
+ params: safe_params(data),
129
+ user_agent: safe_user_agent(data),
130
+ user_id: extract_user_id(data),
131
+ memory_usage: memory_usage_mb,
132
+ gc_stats: gc_stats,
133
+ sql_count: sql_count(data),
134
+ sql_queries: sql_queries,
135
+ http_outgoing: Thread.current[:dead_bro_http_events] || [],
136
+ cache_events: cache_events,
137
+ redis_events: redis_events,
138
+ cache_hits: cache_hits(data),
139
+ cache_misses: cache_misses(data),
140
+ view_events: view_events,
141
+ view_performance: view_performance,
142
+ memory_events: memory_events,
143
+ memory_performance: memory_performance,
144
+ logs: DeadBro.logger.logs
145
+ }
146
+ client.post_metric(event_name: name, payload: payload)
147
+ end
148
+ end
149
+
150
+ def self.safe_path(data)
151
+ path = data[:path] || (data[:request] && data[:request].path)
152
+ path.to_s
153
+ rescue
154
+ ""
155
+ end
156
+
157
+ def self.safe_host
158
+ if defined?(Rails) && Rails.respond_to?(:application)
159
+ begin
160
+ Rails.application.class.module_parent_name
161
+ rescue
162
+ ""
163
+ end
164
+ else
165
+ ""
166
+ end
167
+ end
168
+
169
+ def self.rails_env
170
+ if defined?(Rails) && Rails.respond_to?(:env)
171
+ Rails.env
172
+ else
173
+ ENV["RACK_ENV"] || ENV["RAILS_ENV"] || "development"
174
+ end
175
+ end
176
+
177
+ def self.safe_params(data)
178
+ return {} unless data[:params]
179
+
180
+ params = data[:params]
181
+ begin
182
+ params = params.to_unsafe_h if params.respond_to?(:to_unsafe_h)
183
+ rescue
184
+ end
185
+
186
+ unless params.is_a?(Hash)
187
+ return {}
188
+ end
189
+
190
+ # Remove router-provided keys that we already send at top-level
191
+ router_keys = %w[controller action format]
192
+
193
+ # Filter out sensitive parameters
194
+ sensitive_keys = %w[password password_confirmation token secret key]
195
+
196
+ filtered = params.dup
197
+ router_keys.each { |k| filtered.delete(k) || filtered.delete(k.to_sym) }
198
+ filtered = filtered.except(*sensitive_keys, *sensitive_keys.map(&:to_sym)) if filtered.respond_to?(:except)
199
+
200
+ # Truncate deeply to keep payload small and safe
201
+ truncate_value(filtered)
202
+ rescue
203
+ {}
204
+ end
205
+
206
+ # Recursively truncate values to reasonable sizes to avoid huge payloads
207
+ def self.truncate_value(value, max_str: 200, max_array: 20, max_hash_keys: 30)
208
+ case value
209
+ when String
210
+ (value.length > max_str) ? value[0, max_str] + "…" : value
211
+ when Numeric, TrueClass, FalseClass, NilClass
212
+ value
213
+ when Array
214
+ value[0, max_array].map { |v| truncate_value(v, max_str: max_str, max_array: max_array, max_hash_keys: max_hash_keys) }
215
+ when Hash
216
+ entries = value.to_a[0, max_hash_keys]
217
+ entries.each_with_object({}) do |(k, v), memo|
218
+ memo[k] = truncate_value(v, max_str: max_str, max_array: max_array, max_hash_keys: max_hash_keys)
219
+ end
220
+ else
221
+ (value.to_s.length > max_str) ? value.to_s[0, max_str] + "…" : value.to_s
222
+ end
223
+ end
224
+
225
+ def self.safe_user_agent(data)
226
+ begin
227
+ # Prefer request object if available
228
+ if data[:request]
229
+ ua = nil
230
+ if data[:request].respond_to?(:user_agent)
231
+ ua = data[:request].user_agent
232
+ elsif data[:request].respond_to?(:env)
233
+ ua = data[:request].env && data[:request].env["HTTP_USER_AGENT"]
234
+ end
235
+ return ua.to_s[0..200]
236
+ end
237
+
238
+ # Fallback to headers object/hash if present in notification data
239
+ if data[:headers]
240
+ headers = data[:headers]
241
+ if headers.respond_to?(:[])
242
+ ua = headers["HTTP_USER_AGENT"] || headers["User-Agent"] || headers["user-agent"]
243
+ return ua.to_s[0..200]
244
+ elsif headers.respond_to?(:to_h)
245
+ h = begin
246
+ headers.to_h
247
+ rescue
248
+ {}
249
+ end
250
+ ua = h["HTTP_USER_AGENT"] || h["User-Agent"] || h["user-agent"]
251
+ return ua.to_s[0..200]
252
+ end
253
+ end
254
+
255
+ # Fallback to env hash if present in notification data
256
+ if data[:env].is_a?(Hash)
257
+ ua = data[:env]["HTTP_USER_AGENT"]
258
+ return ua.to_s[0..200]
259
+ end
260
+
261
+ ""
262
+ rescue
263
+ ""
264
+ end
265
+ rescue
266
+ ""
267
+ end
268
+
269
+ def self.memory_usage_mb
270
+ if defined?(GC) && GC.respond_to?(:stat)
271
+ # Get memory usage in MB
272
+ memory_kb = begin
273
+ `ps -o rss= -p #{Process.pid}`.to_i
274
+ rescue
275
+ 0
276
+ end
277
+ (memory_kb / 1024.0).round(2)
278
+ else
279
+ 0
280
+ end
281
+ rescue
282
+ 0
283
+ end
284
+
285
+ def self.gc_stats
286
+ if defined?(GC) && GC.respond_to?(:stat)
287
+ stats = GC.stat
288
+ {
289
+ count: stats[:count] || 0,
290
+ heap_allocated_pages: stats[:heap_allocated_pages] || 0,
291
+ heap_sorted_pages: stats[:heap_sorted_pages] || 0,
292
+ total_allocated_objects: stats[:total_allocated_objects] || 0
293
+ }
294
+ else
295
+ {}
296
+ end
297
+ rescue
298
+ {}
299
+ end
300
+
301
+ def self.sql_count(data)
302
+ # Count SQL queries from the payload if available
303
+ if data[:sql_count]
304
+ data[:sql_count]
305
+ elsif defined?(ActiveRecord) && ActiveRecord::Base.connection
306
+ # Try to get from ActiveRecord connection
307
+ begin
308
+ ActiveRecord::Base.connection.query_cache.size
309
+ rescue
310
+ 0
311
+ end
312
+ else
313
+ 0
314
+ end
315
+ rescue
316
+ 0
317
+ end
318
+
319
+ def self.cache_hits(data)
320
+ if data[:cache_hits]
321
+ data[:cache_hits]
322
+ elsif defined?(Rails) && Rails.cache.respond_to?(:stats)
323
+ begin
324
+ Rails.cache.stats[:hits]
325
+ rescue
326
+ 0
327
+ end
328
+ else
329
+ 0
330
+ end
331
+ rescue
332
+ 0
333
+ end
334
+
335
+ def self.cache_misses(data)
336
+ if data[:cache_misses]
337
+ data[:cache_misses]
338
+ elsif defined?(Rails) && Rails.cache.respond_to?(:stats)
339
+ begin
340
+ Rails.cache.stats[:misses]
341
+ rescue
342
+ 0
343
+ end
344
+ else
345
+ 0
346
+ end
347
+ rescue
348
+ 0
349
+ end
350
+
351
+ def self.extract_user_id(data)
352
+ data[:headers].env["warden"].user.id
353
+ rescue
354
+ nil
355
+ end
356
+ end
357
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DeadBro
4
+ VERSION = "0.2.0"
5
+ end
@@ -0,0 +1,151 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/notifications"
4
+
5
+ module DeadBro
6
+ class ViewRenderingSubscriber
7
+ # Rails view rendering events
8
+ RENDER_TEMPLATE_EVENT = "render_template.action_view"
9
+ RENDER_PARTIAL_EVENT = "render_partial.action_view"
10
+ RENDER_COLLECTION_EVENT = "render_collection.action_view"
11
+
12
+ THREAD_LOCAL_KEY = :dead_bro_view_events
13
+
14
+ def self.subscribe!(client: Client.new)
15
+ # Track template rendering
16
+ ActiveSupport::Notifications.subscribe(RENDER_TEMPLATE_EVENT) do |name, started, finished, _unique_id, data|
17
+ duration_ms = ((finished - started) * 1000.0).round(2)
18
+
19
+ view_info = {
20
+ type: "template",
21
+ identifier: safe_identifier(data[:identifier]),
22
+ layout: data[:layout],
23
+ duration_ms: duration_ms,
24
+ virtual_path: data[:virtual_path],
25
+ rendered_at: Time.now.utc.to_i
26
+ }
27
+
28
+ add_view_event(view_info)
29
+ end
30
+
31
+ # Track partial rendering
32
+ ActiveSupport::Notifications.subscribe(RENDER_PARTIAL_EVENT) do |name, started, finished, _unique_id, data|
33
+ duration_ms = ((finished - started) * 1000.0).round(2)
34
+
35
+ view_info = {
36
+ type: "partial",
37
+ identifier: safe_identifier(data[:identifier]),
38
+ layout: data[:layout],
39
+ duration_ms: duration_ms,
40
+ virtual_path: data[:virtual_path],
41
+ cache_key: data[:cache_key],
42
+ rendered_at: Time.now.utc.to_i
43
+ }
44
+
45
+ add_view_event(view_info)
46
+ end
47
+
48
+ # Track collection rendering (for partials rendered in loops)
49
+ ActiveSupport::Notifications.subscribe(RENDER_COLLECTION_EVENT) do |name, started, finished, _unique_id, data|
50
+ duration_ms = ((finished - started) * 1000.0).round(2)
51
+
52
+ view_info = {
53
+ type: "collection",
54
+ identifier: safe_identifier(data[:identifier]),
55
+ layout: data[:layout],
56
+ duration_ms: duration_ms,
57
+ virtual_path: data[:virtual_path],
58
+ cache_key: data[:cache_key],
59
+ count: data[:count],
60
+ cached_count: data[:cached_count],
61
+ rendered_at: Time.now.utc.to_i
62
+ }
63
+
64
+ add_view_event(view_info)
65
+ end
66
+ rescue
67
+ # Never raise from instrumentation install
68
+ end
69
+
70
+ def self.start_request_tracking
71
+ Thread.current[THREAD_LOCAL_KEY] = []
72
+ end
73
+
74
+ def self.stop_request_tracking
75
+ events = Thread.current[THREAD_LOCAL_KEY]
76
+ Thread.current[THREAD_LOCAL_KEY] = nil
77
+ events || []
78
+ end
79
+
80
+ def self.add_view_event(view_info)
81
+ if Thread.current[THREAD_LOCAL_KEY]
82
+ Thread.current[THREAD_LOCAL_KEY] << view_info
83
+ end
84
+ end
85
+
86
+ def self.safe_identifier(identifier)
87
+ return "" unless identifier.is_a?(String)
88
+
89
+ # Extract meaningful parts of the file path
90
+ # e.g., "/app/views/users/show.html.erb" -> "users/show.html.erb"
91
+ identifier.split("/").last(3).join("/")
92
+ rescue
93
+ identifier.to_s
94
+ end
95
+
96
+ # Analyze view rendering performance
97
+ def self.analyze_view_performance(view_events)
98
+ return {} if view_events.empty?
99
+
100
+ total_duration = view_events.sum { |event| event[:duration_ms] }
101
+
102
+ # Group by view type
103
+ by_type = view_events.group_by { |event| event[:type] }
104
+
105
+ # Find slowest views
106
+ slowest_views = view_events.sort_by { |event| -event[:duration_ms] }.first(5)
107
+
108
+ # Find most frequently rendered views
109
+ view_frequency = view_events.group_by { |event| event[:identifier] }
110
+ .transform_values(&:count)
111
+ .sort_by { |_, count| -count }
112
+ .first(5)
113
+
114
+ # Calculate cache hit rates for partials
115
+ partials = view_events.select { |event| event[:type] == "partial" }
116
+ cache_hits = partials.count { |event| event[:cache_key] }
117
+ cache_hit_rate = partials.any? ? (cache_hits.to_f / partials.count * 100).round(2) : 0
118
+
119
+ # Collection rendering analysis
120
+ collections = view_events.select { |event| event[:type] == "collection" }
121
+ total_collection_items = collections.sum { |event| event[:count] || 0 }
122
+ total_cached_items = collections.sum { |event| event[:cached_count] || 0 }
123
+ collection_cache_hit_rate = (total_collection_items > 0) ?
124
+ (total_cached_items.to_f / total_collection_items * 100).round(2) : 0
125
+
126
+ {
127
+ total_views_rendered: view_events.count,
128
+ total_view_duration_ms: total_duration.round(2),
129
+ average_view_duration_ms: (total_duration / view_events.count).round(2),
130
+ by_type: by_type.transform_values(&:count),
131
+ slowest_views: slowest_views.map { |view|
132
+ {
133
+ identifier: view[:identifier],
134
+ duration_ms: view[:duration_ms],
135
+ type: view[:type]
136
+ }
137
+ },
138
+ most_frequent_views: view_frequency.map { |identifier, count|
139
+ {
140
+ identifier: identifier,
141
+ count: count
142
+ }
143
+ },
144
+ partial_cache_hit_rate: cache_hit_rate,
145
+ collection_cache_hit_rate: collection_cache_hit_rate,
146
+ total_collection_items: total_collection_items,
147
+ total_cached_collection_items: total_cached_items
148
+ }
149
+ end
150
+ end
151
+ end
data/lib/dead_bro.rb ADDED
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "dead_bro/version"
4
+
5
+ module DeadBro
6
+ autoload :Configuration, "dead_bro/configuration"
7
+ autoload :Client, "dead_bro/client"
8
+ autoload :CircuitBreaker, "dead_bro/circuit_breaker"
9
+ autoload :Subscriber, "dead_bro/subscriber"
10
+ autoload :SqlSubscriber, "dead_bro/sql_subscriber"
11
+ autoload :SqlTrackingMiddleware, "dead_bro/sql_tracking_middleware"
12
+ autoload :CacheSubscriber, "dead_bro/cache_subscriber"
13
+ autoload :RedisSubscriber, "dead_bro/redis_subscriber"
14
+ autoload :ViewRenderingSubscriber, "dead_bro/view_rendering_subscriber"
15
+ autoload :MemoryTrackingSubscriber, "dead_bro/memory_tracking_subscriber"
16
+ autoload :MemoryLeakDetector, "dead_bro/memory_leak_detector"
17
+ autoload :LightweightMemoryTracker, "dead_bro/lightweight_memory_tracker"
18
+ autoload :MemoryHelpers, "dead_bro/memory_helpers"
19
+ autoload :JobSubscriber, "dead_bro/job_subscriber"
20
+ autoload :JobSqlTrackingMiddleware, "dead_bro/job_sql_tracking_middleware"
21
+ autoload :Logger, "dead_bro/logger"
22
+ begin
23
+ require "dead_bro/railtie"
24
+ rescue LoadError
25
+ end
26
+
27
+ class Error < StandardError; end
28
+
29
+ def self.configure
30
+ yield configuration
31
+ end
32
+
33
+ def self.configuration
34
+ @configuration ||= Configuration.new
35
+ end
36
+
37
+ def self.reset_configuration!
38
+ @configuration = nil
39
+ @client = nil
40
+ end
41
+
42
+ # Returns a shared Client instance for use across the application
43
+ def self.client
44
+ @client ||= Client.new
45
+ end
46
+
47
+ # Returns a process-stable deploy identifier used when none is configured.
48
+ # Memoized per-Ruby process to avoid generating a new UUID per request.
49
+ def self.process_deploy_id
50
+ @process_deploy_id ||= begin
51
+ require "securerandom"
52
+ SecureRandom.uuid
53
+ end
54
+ end
55
+
56
+ # Returns the logger instance for storing and retrieving log messages
57
+ def self.logger
58
+ @logger ||= Logger.new
59
+ end
60
+
61
+ # Returns the current environment (Rails.env or ENV fallback)
62
+ def self.env
63
+ if defined?(Rails) && Rails.respond_to?(:env)
64
+ Rails.env
65
+ else
66
+ ENV["RACK_ENV"] || ENV["RAILS_ENV"] || "development"
67
+ end
68
+ end
69
+ end
metadata ADDED
@@ -0,0 +1,66 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: dead_bro
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - Emanuel Comsa
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies: []
12
+ description: Gem used by DeadBro - Rails APM to track performance metrics of Rails
13
+ apps.
14
+ email:
15
+ - office@rubydev.ro
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - CHANGELOG.md
21
+ - FEATURES.md
22
+ - README.md
23
+ - lib/dead_bro.rb
24
+ - lib/dead_bro/cache_subscriber.rb
25
+ - lib/dead_bro/circuit_breaker.rb
26
+ - lib/dead_bro/client.rb
27
+ - lib/dead_bro/configuration.rb
28
+ - lib/dead_bro/error_middleware.rb
29
+ - lib/dead_bro/http_instrumentation.rb
30
+ - lib/dead_bro/job_sql_tracking_middleware.rb
31
+ - lib/dead_bro/job_subscriber.rb
32
+ - lib/dead_bro/lightweight_memory_tracker.rb
33
+ - lib/dead_bro/logger.rb
34
+ - lib/dead_bro/memory_helpers.rb
35
+ - lib/dead_bro/memory_leak_detector.rb
36
+ - lib/dead_bro/memory_tracking_subscriber.rb
37
+ - lib/dead_bro/railtie.rb
38
+ - lib/dead_bro/redis_subscriber.rb
39
+ - lib/dead_bro/sql_subscriber.rb
40
+ - lib/dead_bro/sql_tracking_middleware.rb
41
+ - lib/dead_bro/subscriber.rb
42
+ - lib/dead_bro/version.rb
43
+ - lib/dead_bro/view_rendering_subscriber.rb
44
+ homepage: https://www.deadbro.com
45
+ licenses: []
46
+ metadata:
47
+ homepage_uri: https://www.deadbro.com
48
+ source_code_uri: https://github.com/rubydevro/dead_bro
49
+ rdoc_options: []
50
+ require_paths:
51
+ - lib
52
+ required_ruby_version: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: 3.0.0
57
+ required_rubygems_version: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ requirements: []
63
+ rubygems_version: 3.6.7
64
+ specification_version: 4
65
+ summary: Minimal APM for Rails apps.
66
+ test_files: []