apm_bro 0.1.11 → 0.1.13

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: 1763fe75b6723866fbf061cdc8a978105e3a8bd1be612c2dc0937865c576a1da
4
- data.tar.gz: e1bad4ee3231e2a0783994983986e040cec36314912e30f402c044a41f47bdea
3
+ metadata.gz: 1104f9fc59e6d12b981c8c9dbd646ccf0e4199afaf2a5792a40e7a928ebc2ac9
4
+ data.tar.gz: b6798ce81a4e61cfd7e05cdf5974cbf0475610b0355686fbd5fad538eb5a0ea6
5
5
  SHA512:
6
- metadata.gz: 070b54479382d040aebd53e2927d0ebd0aff1a0e0e3a8b4ad4045cc674cec680fd64f2cb279c18412aeff347a34eb098de6bb4fcf227094d2a11e61c12b33b67
7
- data.tar.gz: 17894ab0bb4446d2fb90848ee0f1996683390710e6f81c198fa357f31822a6577fae98b70749feb46f1e2fbba70e71dd38bbb4886252114164b5d8dcac96a652
6
+ metadata.gz: 233bdd3ec6fe51292f331da900f3fbde862a4e78db6a3bceb1d6b2caeeef28bf9611f382ca0f0052baaa255a39f1fb2f3da57dc915c7def3a42d974df4f451e5
7
+ data.tar.gz: edff2e3c6da3985dab9b176387dafd65c315b7428e29e72231191c2d495831ba6ca69b74e085a882c614a712a9576b72c8f7fa757edfa5ebcd50ed417cf367ab
@@ -51,7 +51,8 @@ module ApmBro
51
51
  },
52
52
  rails_env: safe_rails_env,
53
53
  app: safe_app_name,
54
- pid: Process.pid
54
+ pid: Process.pid,
55
+ logs: ApmBro.logger.logs
55
56
  }
56
57
  end
57
58
 
@@ -5,7 +5,19 @@ module ApmBro
5
5
  def self.subscribe!
6
6
  # Start SQL tracking when a job begins - use the start event, not the complete event
7
7
  ActiveSupport::Notifications.subscribe("perform_start.active_job") do |name, started, finished, _unique_id, data|
8
+ # Clear logs for this job
9
+ ApmBro.logger.clear
8
10
  ApmBro::SqlSubscriber.start_request_tracking
11
+
12
+ # Start lightweight memory tracking for this job
13
+ if defined?(ApmBro::LightweightMemoryTracker)
14
+ ApmBro::LightweightMemoryTracker.start_request_tracking
15
+ end
16
+
17
+ # Start detailed memory tracking when allocation tracking is enabled
18
+ if ApmBro.configuration.allocation_tracking_enabled && defined?(ApmBro::MemoryTrackingSubscriber)
19
+ ApmBro::MemoryTrackingSubscriber.start_request_tracking
20
+ end
9
21
  end
10
22
  rescue StandardError
11
23
  # Never raise from instrumentation install
@@ -23,6 +23,34 @@ module ApmBro
23
23
  # Get SQL queries executed during this job
24
24
  sql_queries = ApmBro::SqlSubscriber.stop_request_tracking
25
25
 
26
+ # Stop memory tracking and get collected memory data
27
+ if ApmBro.configuration.allocation_tracking_enabled && defined?(ApmBro::MemoryTrackingSubscriber)
28
+ detailed_memory = ApmBro::MemoryTrackingSubscriber.stop_request_tracking
29
+ memory_performance = ApmBro::MemoryTrackingSubscriber.analyze_memory_performance(detailed_memory)
30
+ # Keep memory_events compact and user-friendly (no large raw arrays)
31
+ memory_events = {
32
+ memory_before: detailed_memory[:memory_before],
33
+ memory_after: detailed_memory[:memory_after],
34
+ duration_seconds: detailed_memory[:duration_seconds],
35
+ allocations_count: (detailed_memory[:allocations] || []).length,
36
+ memory_snapshots_count: (detailed_memory[:memory_snapshots] || []).length,
37
+ large_objects_count: (detailed_memory[:large_objects] || []).length
38
+ }
39
+ else
40
+ lightweight_memory = ApmBro::LightweightMemoryTracker.stop_request_tracking
41
+ # Separate raw readings from derived performance metrics to avoid duplicating data
42
+ memory_events = {
43
+ memory_before: lightweight_memory[:memory_before],
44
+ memory_after: lightweight_memory[:memory_after]
45
+ }
46
+ memory_performance = {
47
+ memory_growth_mb: lightweight_memory[:memory_growth_mb],
48
+ gc_count_increase: lightweight_memory[:gc_count_increase],
49
+ heap_pages_increase: lightweight_memory[:heap_pages_increase],
50
+ duration_seconds: lightweight_memory[:duration_seconds]
51
+ }
52
+ end
53
+
26
54
  payload = {
27
55
  job_class: data[:job].class.name,
28
56
  job_id: data[:job].job_id,
@@ -34,7 +62,10 @@ module ApmBro
34
62
  rails_env: safe_rails_env,
35
63
  host: safe_host,
36
64
  memory_usage: memory_usage_mb,
37
- gc_stats: gc_stats
65
+ gc_stats: gc_stats,
66
+ memory_events: memory_events,
67
+ memory_performance: memory_performance,
68
+ logs: ApmBro.logger.logs
38
69
  }
39
70
 
40
71
  client.post_metric(event_name: name, payload: payload)
@@ -56,6 +87,34 @@ module ApmBro
56
87
  # Get SQL queries executed during this job
57
88
  sql_queries = ApmBro::SqlSubscriber.stop_request_tracking
58
89
 
90
+ # Stop memory tracking and get collected memory data
91
+ if ApmBro.configuration.allocation_tracking_enabled && defined?(ApmBro::MemoryTrackingSubscriber)
92
+ detailed_memory = ApmBro::MemoryTrackingSubscriber.stop_request_tracking
93
+ memory_performance = ApmBro::MemoryTrackingSubscriber.analyze_memory_performance(detailed_memory)
94
+ # Keep memory_events compact and user-friendly (no large raw arrays)
95
+ memory_events = {
96
+ memory_before: detailed_memory[:memory_before],
97
+ memory_after: detailed_memory[:memory_after],
98
+ duration_seconds: detailed_memory[:duration_seconds],
99
+ allocations_count: (detailed_memory[:allocations] || []).length,
100
+ memory_snapshots_count: (detailed_memory[:memory_snapshots] || []).length,
101
+ large_objects_count: (detailed_memory[:large_objects] || []).length
102
+ }
103
+ else
104
+ lightweight_memory = ApmBro::LightweightMemoryTracker.stop_request_tracking
105
+ # Separate raw readings from derived performance metrics to avoid duplicating data
106
+ memory_events = {
107
+ memory_before: lightweight_memory[:memory_before],
108
+ memory_after: lightweight_memory[:memory_after]
109
+ }
110
+ memory_performance = {
111
+ memory_growth_mb: lightweight_memory[:memory_growth_mb],
112
+ gc_count_increase: lightweight_memory[:gc_count_increase],
113
+ heap_pages_increase: lightweight_memory[:heap_pages_increase],
114
+ duration_seconds: lightweight_memory[:duration_seconds]
115
+ }
116
+ end
117
+
59
118
  payload = {
60
119
  job_class: data[:job].class.name,
61
120
  job_id: data[:job].job_id,
@@ -70,7 +129,10 @@ module ApmBro
70
129
  rails_env: safe_rails_env,
71
130
  host: safe_host,
72
131
  memory_usage: memory_usage_mb,
73
- gc_stats: gc_stats
132
+ gc_stats: gc_stats,
133
+ memory_events: memory_events,
134
+ memory_performance: memory_performance,
135
+ logs: ApmBro.logger.logs
74
136
  }
75
137
 
76
138
  event_name = exception&.class&.name || "ActiveJob::Exception"
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ApmBro
4
+ class Logger
5
+ SEVERITY_LEVELS = {
6
+ debug: 0,
7
+ info: 1,
8
+ warn: 2,
9
+ error: 3,
10
+ fatal: 4
11
+ }.freeze
12
+
13
+ # ANSI color codes
14
+ COLOR_RESET = "\033[0m".freeze
15
+ COLOR_DEBUG = "\033[36m".freeze # Cyan
16
+ COLOR_INFO = "\033[32m".freeze # Green
17
+ COLOR_WARN = "\033[33m".freeze # Yellow
18
+ COLOR_ERROR = "\033[31m".freeze # Red
19
+ COLOR_FATAL = "\033[35m".freeze # Magenta
20
+
21
+ def initialize
22
+ @thread_logs_key = :apm_bro_logs
23
+ end
24
+
25
+ def debug(message)
26
+ log(:debug, message)
27
+ end
28
+
29
+ def info(message)
30
+ log(:info, message)
31
+ end
32
+
33
+ def warn(message)
34
+ log(:warn, message)
35
+ end
36
+
37
+ def error(message)
38
+ log(:error, message)
39
+ end
40
+
41
+ def fatal(message)
42
+ log(:fatal, message)
43
+ end
44
+
45
+ # Get all logs for the current thread
46
+ def logs
47
+ Thread.current[@thread_logs_key] || []
48
+ end
49
+
50
+ # Clear logs for the current thread
51
+ def clear
52
+ Thread.current[@thread_logs_key] = []
53
+ end
54
+
55
+ private
56
+
57
+ def log(severity, message)
58
+ timestamp = Time.now.utc
59
+ log_entry = {
60
+ sev: severity.to_s,
61
+ msg: message.to_s,
62
+ time: timestamp.iso8601(3) # Include milliseconds for better precision
63
+ }
64
+
65
+ # Store in thread-local storage
66
+ Thread.current[@thread_logs_key] ||= []
67
+ Thread.current[@thread_logs_key] << log_entry
68
+
69
+ # Print the message immediately
70
+ print_log(severity, message, timestamp)
71
+ end
72
+
73
+ def print_log(severity, message, timestamp)
74
+ formatted_message = format_log_message(severity, message, timestamp)
75
+
76
+ if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
77
+ # Use Rails logger if available (Rails handles its own color formatting)
78
+ case severity
79
+ when :debug
80
+ Rails.logger.debug(formatted_message)
81
+ when :info
82
+ Rails.logger.info(formatted_message)
83
+ when :warn
84
+ Rails.logger.warn(formatted_message)
85
+ when :error
86
+ Rails.logger.error(formatted_message)
87
+ when :fatal
88
+ Rails.logger.fatal(formatted_message)
89
+ end
90
+ else
91
+ # Fallback to stdout with colors
92
+ colored_message = format_log_message_with_color(severity, message, timestamp)
93
+ $stdout.puts(colored_message)
94
+ end
95
+ rescue StandardError
96
+ # Never let logging break the application
97
+ $stdout.puts("[ApmBro] #{severity.to_s.upcase}: #{message}")
98
+ end
99
+
100
+ def format_log_message(severity, message, timestamp)
101
+ "[ApmBro] #{timestamp.iso8601(3)} #{severity.to_s.upcase}: #{message}"
102
+ end
103
+
104
+ def format_log_message_with_color(severity, message, timestamp)
105
+ color = color_for_severity(severity)
106
+ severity_str = severity.to_s.upcase
107
+ "#{color}[ApmBro] #{timestamp.iso8601(3)} #{severity_str}: #{message}#{COLOR_RESET}"
108
+ end
109
+
110
+ def color_for_severity(severity)
111
+ case severity
112
+ when :debug
113
+ COLOR_DEBUG
114
+ when :info
115
+ COLOR_INFO
116
+ when :warn
117
+ COLOR_WARN
118
+ when :error
119
+ COLOR_ERROR
120
+ when :fatal
121
+ COLOR_FATAL
122
+ else
123
+ COLOR_RESET
124
+ end
125
+ end
126
+ end
127
+ end
128
+
@@ -9,6 +9,7 @@ module ApmBro
9
9
  THREAD_LOCAL_KEY = :apm_bro_sql_queries
10
10
  THREAD_LOCAL_ALLOC_START_KEY = :apm_bro_sql_alloc_start
11
11
  THREAD_LOCAL_ALLOC_RESULTS_KEY = :apm_bro_sql_alloc_results
12
+ THREAD_LOCAL_BACKTRACE_KEY = :apm_bro_sql_backtraces
12
13
 
13
14
  def self.subscribe!
14
15
  # Subscribe with a start/finish listener to measure allocations per query
@@ -20,13 +21,19 @@ module ApmBro
20
21
  end
21
22
 
22
23
  ActiveSupport::Notifications.subscribe(SQL_EVENT_NAME) do |name, started, finished, _unique_id, data|
24
+ next if data[:name] == "SCHEMA"
23
25
  # Only track queries that are part of the current request
24
26
  next unless Thread.current[THREAD_LOCAL_KEY]
25
27
  unique_id = _unique_id
26
28
  allocations = nil
29
+ captured_backtrace = nil
27
30
  begin
28
31
  alloc_results = Thread.current[THREAD_LOCAL_ALLOC_RESULTS_KEY]
29
32
  allocations = alloc_results && alloc_results.delete(unique_id)
33
+
34
+ # Get the captured backtrace from when the query started
35
+ backtrace_map = Thread.current[THREAD_LOCAL_BACKTRACE_KEY]
36
+ captured_backtrace = backtrace_map && backtrace_map.delete(unique_id)
30
37
  rescue StandardError
31
38
  end
32
39
 
@@ -36,7 +43,7 @@ module ApmBro
36
43
  duration_ms: ((finished - started) * 1000.0).round(2),
37
44
  cached: data[:cached] || false,
38
45
  connection_id: data[:connection_id],
39
- trace: safe_query_trace(data),
46
+ trace: safe_query_trace(data, captured_backtrace),
40
47
  allocations: allocations
41
48
  }
42
49
  # Add to thread-local storage
@@ -49,6 +56,7 @@ module ApmBro
49
56
  Thread.current[THREAD_LOCAL_KEY] = []
50
57
  Thread.current[THREAD_LOCAL_ALLOC_START_KEY] = {}
51
58
  Thread.current[THREAD_LOCAL_ALLOC_RESULTS_KEY] = {}
59
+ Thread.current[THREAD_LOCAL_BACKTRACE_KEY] = {}
52
60
  end
53
61
 
54
62
  def self.stop_request_tracking
@@ -56,6 +64,7 @@ module ApmBro
56
64
  Thread.current[THREAD_LOCAL_KEY] = nil
57
65
  Thread.current[THREAD_LOCAL_ALLOC_START_KEY] = nil
58
66
  Thread.current[THREAD_LOCAL_ALLOC_RESULTS_KEY] = nil
67
+ Thread.current[THREAD_LOCAL_BACKTRACE_KEY] = nil
59
68
  queries || []
60
69
  end
61
70
 
@@ -75,7 +84,7 @@ module ApmBro
75
84
  sql.length > 1000 ? sql[0..1000] + "..." : sql
76
85
  end
77
86
 
78
- def self.safe_query_trace(data)
87
+ def self.safe_query_trace(data, captured_backtrace = nil)
79
88
  return [] unless data.is_a?(Hash)
80
89
 
81
90
  # Build trace from available data fields
@@ -86,24 +95,64 @@ module ApmBro
86
95
  trace << "#{data[:filename]}:#{data[:line]}:in `#{data[:method]}'"
87
96
  end
88
97
 
89
- # Always try to get the full call stack for better trace information
90
- begin
91
- # Get the current call stack, skip the first few frames (our own code)
92
- caller_stack = caller(3, 0) # Skip 3 frames, get up to 1
93
- caller_trace = caller_stack.map do |line|
98
+ # Use the captured backtrace from when the query started (most accurate)
99
+ if captured_backtrace && captured_backtrace.is_a?(Array) && !captured_backtrace.empty?
100
+ # Filter to only include frames that contain "app/" (application code)
101
+ app_frames = captured_backtrace.select do |frame|
102
+ frame.include?('/app/')
103
+ end
104
+
105
+ caller_trace = app_frames.map do |line|
94
106
  # Remove any potential sensitive information from file paths
95
107
  line.gsub(/\/[^\/]*(password|secret|key|token)[^\/]*\//i, '/[FILTERED]/')
96
108
  end
97
109
 
98
- # Combine the immediate location with the call stack
99
110
  trace.concat(caller_trace)
100
- rescue StandardError
101
- # If caller fails, we still have the immediate location
111
+ else
112
+ # Fallback: try to get backtrace from current context
113
+ begin
114
+ # Get all available frames - we'll filter to find application code
115
+ all_frames = Thread.current.backtrace || []
116
+
117
+ if all_frames.empty?
118
+ # Fallback to caller_locations if backtrace is empty
119
+ locations = caller_locations(1, 50)
120
+ all_frames = locations.map { |loc| "#{loc.path}:#{loc.lineno}:in `#{loc.label}'" } if locations
121
+ end
122
+
123
+ # Filter to only include frames that contain "app/" (application code)
124
+ app_frames = all_frames.select do |frame|
125
+ frame.include?('/app/')
126
+ end
127
+
128
+ caller_trace = app_frames.map do |line|
129
+ line.gsub(/\/[^\/]*(password|secret|key|token)[^\/]*\//i, '/[FILTERED]/')
130
+ end
131
+
132
+ trace.concat(caller_trace)
133
+ rescue StandardError
134
+ # If backtrace fails, try caller as fallback
135
+ begin
136
+ caller_stack = caller(20, 50) # Get more frames to find app/ frames
137
+ app_frames = caller_stack.select { |frame| frame.include?('/app/') }
138
+ caller_trace = app_frames.map do |line|
139
+ line.gsub(/\/[^\/]*(password|secret|key|token)[^\/]*\//i, '/[FILTERED]/')
140
+ end
141
+ trace.concat(caller_trace)
142
+ rescue StandardError
143
+ # If caller also fails, we still have the immediate location
144
+ end
145
+ end
102
146
  end
103
147
 
104
- # If we have a backtrace, use it (but it's usually nil for SQL events)
148
+ # If we have a backtrace in the data, use it (but it's usually nil for SQL events)
105
149
  if data[:backtrace] && data[:backtrace].is_a?(Array)
106
- backtrace_trace = data[:backtrace].first(5).map do |line|
150
+ # Filter to only include frames that contain "app/"
151
+ app_backtrace = data[:backtrace].select do |line|
152
+ line.is_a?(String) && line.include?('/app/')
153
+ end
154
+
155
+ backtrace_trace = app_backtrace.map do |line|
107
156
  case line
108
157
  when String
109
158
  line.gsub(/\/[^\/]*(password|secret|key|token)[^\/]*\//i, '/[FILTERED]/')
@@ -114,8 +163,8 @@ module ApmBro
114
163
  trace.concat(backtrace_trace)
115
164
  end
116
165
 
117
- # Remove duplicates and limit the number of frames
118
- trace.uniq.first(10).map do |line|
166
+ # Remove duplicates and return all app/ frames (no limit)
167
+ trace.uniq.map do |line|
119
168
  case line
120
169
  when String
121
170
  # Remove any potential sensitive information from file paths
@@ -138,6 +187,15 @@ module ApmBro
138
187
  begin
139
188
  map = (Thread.current[ApmBro::SqlSubscriber::THREAD_LOCAL_ALLOC_START_KEY] ||= {})
140
189
  map[id] = GC.stat[:total_allocated_objects] if defined?(GC) && GC.respond_to?(:stat)
190
+
191
+ # Capture the backtrace at query start time (before notification system processes it)
192
+ # This gives us the actual call stack where the SQL was executed
193
+ backtrace_map = (Thread.current[ApmBro::SqlSubscriber::THREAD_LOCAL_BACKTRACE_KEY] ||= {})
194
+ captured_backtrace = Thread.current.backtrace
195
+ if captured_backtrace && captured_backtrace.is_a?(Array)
196
+ # Skip the first few frames (our listener code) to get to the actual query execution
197
+ backtrace_map[id] = captured_backtrace[5..-1] || captured_backtrace
198
+ end
141
199
  rescue StandardError
142
200
  end
143
201
  end
@@ -7,6 +7,9 @@ module ApmBro
7
7
  end
8
8
 
9
9
  def call(env)
10
+ # Clear logs for this request
11
+ ApmBro.logger.clear
12
+
10
13
  # Start SQL tracking for this request
11
14
  if defined?(ApmBro::SqlSubscriber)
12
15
  puts "Starting SQL tracking for request: #{env['REQUEST_METHOD']} #{env['PATH_INFO']}"
@@ -95,7 +95,8 @@ module ApmBro
95
95
  exception_class: (exception_class || exception_obj&.class&.name),
96
96
  message: (exception_message || exception_obj&.message).to_s[0, 1000],
97
97
  backtrace: backtrace,
98
- error: true
98
+ error: true,
99
+ logs: ApmBro.logger.logs
99
100
  }
100
101
 
101
102
  event_name = (exception_class || exception_obj&.class&.name || "exception").to_s
@@ -134,7 +135,8 @@ module ApmBro
134
135
  view_events: view_events,
135
136
  view_performance: view_performance,
136
137
  memory_events: memory_events,
137
- memory_performance: memory_performance
138
+ memory_performance: memory_performance,
139
+ logs: ApmBro.logger.logs
138
140
  }
139
141
  client.post_metric(event_name: name, payload: payload)
140
142
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ApmBro
4
- VERSION = "0.1.11"
4
+ VERSION = "0.1.13"
5
5
  end
data/lib/apm_bro.rb CHANGED
@@ -18,6 +18,7 @@ module ApmBro
18
18
  autoload :MemoryHelpers, "apm_bro/memory_helpers"
19
19
  autoload :JobSubscriber, "apm_bro/job_subscriber"
20
20
  autoload :JobSqlTrackingMiddleware, "apm_bro/job_sql_tracking_middleware"
21
+ autoload :Logger, "apm_bro/logger"
21
22
  begin
22
23
  require "apm_bro/railtie"
23
24
  rescue LoadError
@@ -45,4 +46,9 @@ module ApmBro
45
46
  SecureRandom.uuid
46
47
  end
47
48
  end
49
+
50
+ # Returns the logger instance for storing and retrieving log messages
51
+ def self.logger
52
+ @logger ||= Logger.new
53
+ end
48
54
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: apm_bro
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.11
4
+ version: 0.1.13
5
5
  platform: ruby
6
6
  authors:
7
7
  - Emanuel Comsa
@@ -29,6 +29,7 @@ files:
29
29
  - lib/apm_bro/job_sql_tracking_middleware.rb
30
30
  - lib/apm_bro/job_subscriber.rb
31
31
  - lib/apm_bro/lightweight_memory_tracker.rb
32
+ - lib/apm_bro/logger.rb
32
33
  - lib/apm_bro/memory_helpers.rb
33
34
  - lib/apm_bro/memory_leak_detector.rb
34
35
  - lib/apm_bro/memory_tracking_subscriber.rb