railswatch_gem 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/railswatch_gem/instrumentation/cache_instrumenter.rb +8 -3
- data/lib/railswatch_gem/instrumentation/commands_instrumenter.rb +3 -7
- data/lib/railswatch_gem/instrumentation/errors_instrumenter.rb +19 -17
- data/lib/railswatch_gem/instrumentation/jobs_instrumenter.rb +16 -8
- data/lib/railswatch_gem/instrumentation/logs_instrumenter.rb +2 -19
- data/lib/railswatch_gem/instrumentation/mail_instrumenter.rb +10 -12
- data/lib/railswatch_gem/instrumentation/models_instrumenter.rb +20 -21
- data/lib/railswatch_gem/instrumentation/notifications_instrumenter.rb +6 -14
- data/lib/railswatch_gem/instrumentation/outgoing_requests_instrumenter.rb +6 -13
- data/lib/railswatch_gem/instrumentation/queries_instrumenter.rb +16 -5
- data/lib/railswatch_gem/instrumentation/requests_instrumenter.rb +23 -9
- data/lib/railswatch_gem/instrumentation/scheduled_tasks_instrumenter.rb +10 -7
- data/lib/railswatch_gem/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a4ab7fecdace81bbd2d66eb61b06026abb547c6815fc59c67fb1137bd9aea0bd
|
|
4
|
+
data.tar.gz: 68fccbf28d63b0f133bd9a5d9ef19d93263d913686b7526726aa3b0b170f75aa
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d0bb1ccbf5d4a199990f9a20bb5ebc937a3eeccef63761d54cf301b1d29977672767ee9815cb7309788cfdcf01769118cde1b2528e70f7140d7f47817545c99c
|
|
7
|
+
data.tar.gz: a757ce0625f2a52679d44a93c5b0ee13c188b66f304a76888396ede5e1d3c42f144bd2c813eadcd8aeb7c31593034ddd74c1aad6e113ce73ac50a1d10b4e1a05
|
|
@@ -25,16 +25,21 @@ module RailswatchGem
|
|
|
25
25
|
name = event.name # e.g., "cache_read.active_support"
|
|
26
26
|
|
|
27
27
|
# Extract action from name: cache_read.active_support -> read
|
|
28
|
-
# Common actions: read, write, delete, fetch_hit, generate
|
|
29
28
|
action = name.split(".").first.sub("cache_", "")
|
|
30
29
|
|
|
30
|
+
# Capture source location (Where did we call Rails.cache.read?)
|
|
31
|
+
# We use the cleaner to ignore framework lines and find user code.
|
|
32
|
+
app_trace = ::Rails.backtrace_cleaner.clean(caller)
|
|
33
|
+
|
|
31
34
|
data = {
|
|
32
35
|
event_type: "cache",
|
|
33
|
-
|
|
34
|
-
timestamp: Time.at(event.end).utc.iso8601,
|
|
36
|
+
timestamp: Time.at(event.time).utc.iso8601(6),
|
|
35
37
|
action: action,
|
|
36
38
|
key: payload[:key],
|
|
37
39
|
|
|
40
|
+
# Add Trace for debugging
|
|
41
|
+
backtrace: app_trace,
|
|
42
|
+
|
|
38
43
|
# Performance
|
|
39
44
|
duration_ms: event.duration.round(2)
|
|
40
45
|
}
|
|
@@ -11,7 +11,7 @@ module RailswatchGem
|
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
def start
|
|
14
|
-
# Hook into Thor to capture CLI commands (e.g., rails runner, rails generate
|
|
14
|
+
# Hook into Thor to capture CLI commands (e.g., rails runner, rails generate)
|
|
15
15
|
if defined?(::Thor::Command)
|
|
16
16
|
patch_thor_commands
|
|
17
17
|
|
|
@@ -37,15 +37,14 @@ module RailswatchGem
|
|
|
37
37
|
|
|
38
38
|
data = {
|
|
39
39
|
event_type: "command",
|
|
40
|
-
|
|
41
|
-
timestamp: Time.at(event.end).utc.iso8601,
|
|
40
|
+
timestamp: Time.at(event.time).utc.iso8601(6),
|
|
42
41
|
|
|
43
42
|
# Command Identity
|
|
44
43
|
name: command_name,
|
|
45
44
|
args: payload[:args],
|
|
46
45
|
options: payload[:options],
|
|
47
46
|
|
|
48
|
-
# Context
|
|
47
|
+
# Context
|
|
49
48
|
tool: payload[:tool_class],
|
|
50
49
|
|
|
51
50
|
# Performance
|
|
@@ -71,9 +70,6 @@ module RailswatchGem
|
|
|
71
70
|
# ----------------------------------------------------------------
|
|
72
71
|
module ThorCommandPatch
|
|
73
72
|
def run(instance, args = [])
|
|
74
|
-
# Thor::Command#run(instance, args)
|
|
75
|
-
# 'name' is available on the command object
|
|
76
|
-
|
|
77
73
|
# Capture options if available on the instance (parsed flags)
|
|
78
74
|
opts = instance.respond_to?(:options) ? instance.options : nil
|
|
79
75
|
tool_class = instance.class.name
|
|
@@ -9,9 +9,6 @@ module RailswatchGem
|
|
|
9
9
|
end
|
|
10
10
|
|
|
11
11
|
def start
|
|
12
|
-
# This instrumenter relies on the Rails 7.0+ Error Reporter interface.
|
|
13
|
-
# It allows capturing both unhandled exceptions and manually reported errors
|
|
14
|
-
# (e.g., via Rails.error.handle { ... }).
|
|
15
12
|
if defined?(::Rails) && ::Rails.respond_to?(:error)
|
|
16
13
|
::Rails.error.subscribe(ErrorSubscriber.new(@client))
|
|
17
14
|
end
|
|
@@ -22,42 +19,47 @@ module RailswatchGem
|
|
|
22
19
|
@client = client
|
|
23
20
|
end
|
|
24
21
|
|
|
25
|
-
# The signature required by Rails.error.subscribe
|
|
26
22
|
def report(error, handled:, severity:, context:, source: nil)
|
|
27
|
-
# 1. Infinite Loop Protection
|
|
28
|
-
# Don't report errors originating from our own gem to prevent recursion.
|
|
23
|
+
# 1. Infinite Loop Protection
|
|
29
24
|
return if error.class.name.start_with?("RailswatchGem::")
|
|
30
25
|
|
|
31
|
-
# 2.
|
|
32
|
-
#
|
|
33
|
-
|
|
26
|
+
# 2. Capture Backtrace Smartly
|
|
27
|
+
# If error.backtrace is nil (un-raised exception), use the current caller.
|
|
28
|
+
raw_trace = error.backtrace || caller
|
|
29
|
+
|
|
30
|
+
# Clean the trace to remove framework noise (gems, rails internals)
|
|
31
|
+
# This ensures we see the User's code, not the Gem's code.
|
|
32
|
+
clean_trace = if defined?(::Rails.backtrace_cleaner)
|
|
33
|
+
::Rails.backtrace_cleaner.clean(raw_trace)
|
|
34
|
+
else
|
|
35
|
+
raw_trace
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# 3. Context Correlation
|
|
34
39
|
request_id = context[:request_id] || Thread.current[:railswatch_request_id]
|
|
35
40
|
|
|
36
41
|
data = {
|
|
37
42
|
event_type: "error",
|
|
38
|
-
timestamp: Time.now.utc.iso8601,
|
|
43
|
+
timestamp: Time.now.utc.iso8601(6),
|
|
39
44
|
|
|
40
45
|
# Exception Details
|
|
41
46
|
class: error.class.name,
|
|
42
47
|
message: error.message,
|
|
43
|
-
# Limit backtrace to save bandwidth; the dashboard usually only needs the top frames.
|
|
44
|
-
backtrace: (error.backtrace || []).first(25),
|
|
45
48
|
|
|
46
|
-
|
|
49
|
+
backtrace: clean_trace.first(25),
|
|
50
|
+
|
|
51
|
+
# Rails Context
|
|
47
52
|
handled: handled,
|
|
48
53
|
severity: severity,
|
|
49
|
-
source: source,
|
|
54
|
+
source: source,
|
|
50
55
|
|
|
51
56
|
# Correlation
|
|
52
57
|
request_id: request_id,
|
|
53
|
-
|
|
54
|
-
# Extra context passed to Rails.error.report(e, context: { user_id: 1 })
|
|
55
58
|
context: context
|
|
56
59
|
}
|
|
57
60
|
|
|
58
61
|
@client.record(data)
|
|
59
62
|
rescue => e
|
|
60
|
-
# Absolute safety net: If reporting the error fails, print to stderr and move on.
|
|
61
63
|
warn "RailswatchGem: Failed to report error event: #{e.message}"
|
|
62
64
|
end
|
|
63
65
|
end
|
|
@@ -12,7 +12,6 @@ module RailswatchGem
|
|
|
12
12
|
|
|
13
13
|
def start
|
|
14
14
|
# Subscribe to the execution event.
|
|
15
|
-
# "enqueue.active_job" is also available if you want to track queue depth/latency separately.
|
|
16
15
|
ActiveSupport::Notifications.subscribe("perform.active_job") do |*args|
|
|
17
16
|
event = ActiveSupport::Notifications::Event.new(*args)
|
|
18
17
|
process_event(event)
|
|
@@ -25,13 +24,21 @@ module RailswatchGem
|
|
|
25
24
|
payload = event.payload
|
|
26
25
|
job = payload[:job]
|
|
27
26
|
|
|
28
|
-
# Guard against malformed payloads (though rare in ActiveJob)
|
|
29
27
|
return unless job
|
|
30
28
|
|
|
29
|
+
# Safely serialize arguments (GlobalID or primitives)
|
|
30
|
+
# We use standard ActiveJob serialization to get readable values
|
|
31
|
+
serialized_args = begin
|
|
32
|
+
job.arguments.map do |arg|
|
|
33
|
+
arg.respond_to?(:to_global_id) ? arg.to_global_id.to_s : arg.inspect
|
|
34
|
+
end
|
|
35
|
+
rescue
|
|
36
|
+
["(unserializable args)"]
|
|
37
|
+
end
|
|
38
|
+
|
|
31
39
|
data = {
|
|
32
40
|
event_type: "job",
|
|
33
|
-
|
|
34
|
-
timestamp: Time.at(event.end).utc.iso8601,
|
|
41
|
+
timestamp: Time.at(event.time).utc.iso8601(6),
|
|
35
42
|
|
|
36
43
|
# Job Identity
|
|
37
44
|
job_class: job.class.name,
|
|
@@ -39,6 +46,9 @@ module RailswatchGem
|
|
|
39
46
|
queue_name: job.queue_name,
|
|
40
47
|
priority: job.priority,
|
|
41
48
|
|
|
49
|
+
# Arguments allow debugging "Which user failed?"
|
|
50
|
+
arguments: serialized_args,
|
|
51
|
+
|
|
42
52
|
# Execution Context
|
|
43
53
|
adapter: payload[:adapter]&.class&.name,
|
|
44
54
|
executions: job.executions,
|
|
@@ -47,12 +57,11 @@ module RailswatchGem
|
|
|
47
57
|
duration_ms: event.duration.round(2)
|
|
48
58
|
}
|
|
49
59
|
|
|
50
|
-
# Calculate Queue Latency
|
|
60
|
+
# Calculate Queue Latency
|
|
51
61
|
if job.enqueued_at
|
|
52
62
|
enqueued_time = Time.iso8601(job.enqueued_at) rescue nil
|
|
53
63
|
if enqueued_time
|
|
54
|
-
#
|
|
55
|
-
# FIX: Convert event.time (Float) to Time before subtraction
|
|
64
|
+
# Fix: Ensure event.time is converted to Time object
|
|
56
65
|
latency = (Time.at(event.time) - enqueued_time) * 1000
|
|
57
66
|
data[:queue_latency_ms] = latency.round(2)
|
|
58
67
|
end
|
|
@@ -60,7 +69,6 @@ module RailswatchGem
|
|
|
60
69
|
|
|
61
70
|
# Error handling
|
|
62
71
|
if payload[:exception]
|
|
63
|
-
# payload[:exception] is [ClassName, Message]
|
|
64
72
|
data[:error_class] = payload[:exception].first.to_s
|
|
65
73
|
data[:error_message] = payload[:exception].last.to_s
|
|
66
74
|
end
|
|
@@ -11,20 +11,12 @@ module RailswatchGem
|
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
def start
|
|
14
|
-
# Only attach if Rails logger is available
|
|
15
14
|
return unless defined?(::Rails) && ::Rails.logger
|
|
16
15
|
|
|
17
16
|
target_logger = ::Rails.logger
|
|
18
|
-
|
|
19
|
-
# Avoid double patching if called multiple times
|
|
20
17
|
return if target_logger.respond_to?(:_railswatch_instrumented?)
|
|
21
18
|
|
|
22
|
-
# Create the interception module bound to our client
|
|
23
19
|
interceptor = create_interceptor(@client)
|
|
24
|
-
|
|
25
|
-
# Extend the specific logger instance.
|
|
26
|
-
# This inserts the module into the object's singleton class inheritance chain,
|
|
27
|
-
# effectively wrapping the 'add' method.
|
|
28
20
|
target_logger.extend(interceptor)
|
|
29
21
|
end
|
|
30
22
|
|
|
@@ -32,21 +24,15 @@ module RailswatchGem
|
|
|
32
24
|
|
|
33
25
|
def create_interceptor(client)
|
|
34
26
|
Module.new do
|
|
35
|
-
# FIX: We use define_method here to create a closure over the 'client' variable.
|
|
36
|
-
# Standard 'def' methods create a new scope and cannot access outer local variables.
|
|
37
|
-
# We expose client as a private method so 'add' can access it.
|
|
38
27
|
define_method(:_railswatch_client) { client }
|
|
39
28
|
private :_railswatch_client
|
|
40
29
|
|
|
41
|
-
# The primary entry point for Ruby Logger
|
|
42
30
|
def add(severity, message = nil, progname = nil, &block)
|
|
43
|
-
# PERFORMANCE: Check level first.
|
|
44
31
|
if respond_to?(:level) && severity < level
|
|
45
32
|
return super
|
|
46
33
|
end
|
|
47
34
|
|
|
48
35
|
begin
|
|
49
|
-
# Resolve the message content.
|
|
50
36
|
log_message = message
|
|
51
37
|
if log_message.nil?
|
|
52
38
|
if block_given?
|
|
@@ -58,13 +44,11 @@ module RailswatchGem
|
|
|
58
44
|
|
|
59
45
|
# INFINITE LOOP PROTECTION
|
|
60
46
|
unless log_message.to_s.include?("[Railswatch]")
|
|
61
|
-
|
|
62
47
|
request_id = Thread.current[:railswatch_request_id]
|
|
63
48
|
|
|
64
|
-
# FIX: Access client via the closure-captured helper method
|
|
65
49
|
_railswatch_client.record({
|
|
66
50
|
event_type: "log",
|
|
67
|
-
timestamp: Time.now.utc.iso8601,
|
|
51
|
+
timestamp: Time.now.utc.iso8601(6),
|
|
68
52
|
severity: format_severity_level(severity),
|
|
69
53
|
message: log_message.to_s,
|
|
70
54
|
progname: progname,
|
|
@@ -74,13 +58,12 @@ module RailswatchGem
|
|
|
74
58
|
|
|
75
59
|
super(severity, log_message, nil)
|
|
76
60
|
rescue => e
|
|
77
|
-
#
|
|
61
|
+
# Fail safe: don't crash the app if logging fails
|
|
78
62
|
$stderr.puts "RailswatchGem: LogsInstrumenter error: #{e.message}"
|
|
79
63
|
super
|
|
80
64
|
end
|
|
81
65
|
end
|
|
82
66
|
|
|
83
|
-
# Marker method to prevent double-patching
|
|
84
67
|
def _railswatch_instrumented?
|
|
85
68
|
true
|
|
86
69
|
end
|
|
@@ -11,8 +11,6 @@ module RailswatchGem
|
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
def start
|
|
14
|
-
# Subscribe to the delivery event.
|
|
15
|
-
# "process.action_mailer" is also available if you want to track template rendering time separately.
|
|
16
14
|
ActiveSupport::Notifications.subscribe("deliver.action_mailer") do |*args|
|
|
17
15
|
event = ActiveSupport::Notifications::Event.new(*args)
|
|
18
16
|
process_event(event)
|
|
@@ -24,35 +22,35 @@ module RailswatchGem
|
|
|
24
22
|
def process_event(event)
|
|
25
23
|
payload = event.payload
|
|
26
24
|
|
|
27
|
-
# Payload keys depend on Rails version, but generally include:
|
|
28
|
-
# :mailer, :message_id, :subject, :to, :from, :bcc, :cc, :date
|
|
29
|
-
|
|
30
|
-
# Calculate recipient count for metrics
|
|
31
25
|
to_count = Array(payload[:to]).size
|
|
32
26
|
cc_count = Array(payload[:cc]).size
|
|
33
27
|
bcc_count = Array(payload[:bcc]).size
|
|
34
28
|
total_recipients = to_count + cc_count + bcc_count
|
|
35
29
|
|
|
30
|
+
# Attempt to correlate with parent request (e.g. "Reset Password" controller action)
|
|
31
|
+
request_id = Thread.current[:railswatch_request_id]
|
|
32
|
+
|
|
36
33
|
data = {
|
|
37
34
|
event_type: "mail",
|
|
38
|
-
|
|
39
|
-
timestamp: Time.at(event.end).utc.iso8601,
|
|
35
|
+
timestamp: Time.at(event.time).utc.iso8601(6),
|
|
40
36
|
|
|
41
37
|
# Identity
|
|
42
|
-
mailer: payload[:mailer],
|
|
38
|
+
mailer: payload[:mailer],
|
|
43
39
|
message_id: payload[:message_id],
|
|
44
40
|
|
|
45
|
-
# Content
|
|
41
|
+
# Content
|
|
46
42
|
subject: payload[:subject],
|
|
47
43
|
from: Array(payload[:from]).join(", "),
|
|
48
44
|
to: Array(payload[:to]).join(", "),
|
|
49
45
|
|
|
50
46
|
# Metrics
|
|
51
47
|
recipient_count: total_recipients,
|
|
52
|
-
duration_ms: event.duration.round(2)
|
|
48
|
+
duration_ms: event.duration.round(2),
|
|
49
|
+
|
|
50
|
+
# Correlation
|
|
51
|
+
request_id: request_id
|
|
53
52
|
}
|
|
54
53
|
|
|
55
|
-
# Error handling
|
|
56
54
|
if payload[:exception]
|
|
57
55
|
data[:error_class] = payload[:exception].first.to_s
|
|
58
56
|
data[:error_message] = payload[:exception].last.to_s
|
|
@@ -10,8 +10,6 @@ module RailswatchGem
|
|
|
10
10
|
|
|
11
11
|
def start
|
|
12
12
|
return unless defined?(::ActiveRecord::Base)
|
|
13
|
-
|
|
14
|
-
# Inject our tracking module into ActiveRecord
|
|
15
13
|
::ActiveRecord::Base.include(Tracker)
|
|
16
14
|
end
|
|
17
15
|
|
|
@@ -19,8 +17,6 @@ module RailswatchGem
|
|
|
19
17
|
extend ActiveSupport::Concern
|
|
20
18
|
|
|
21
19
|
included do
|
|
22
|
-
# We use after_commit to ensure we only report data that actually persisted.
|
|
23
|
-
# We pass 'self' to the callbacks.
|
|
24
20
|
after_commit :_railswatch_handle_create, on: :create
|
|
25
21
|
after_commit :_railswatch_handle_update, on: :update
|
|
26
22
|
after_commit :_railswatch_handle_destroy, on: :destroy
|
|
@@ -41,29 +37,25 @@ module RailswatchGem
|
|
|
41
37
|
private
|
|
42
38
|
|
|
43
39
|
def _railswatch_record_event(type)
|
|
44
|
-
# Guard: Avoid tracking internal Rails models or
|
|
45
|
-
return if self.class.name.start_with?("ActiveRecord::", "RailswatchGem::")
|
|
40
|
+
# Guard: Avoid tracking internal Rails models, gem models, or schema migrations
|
|
41
|
+
return if self.class.name.start_with?("ActiveRecord::", "RailswatchGem::", "Ahoy::", "PaperTrail::")
|
|
46
42
|
|
|
47
|
-
# Retrieve Request ID if available for correlation
|
|
48
43
|
request_id = Thread.current[:railswatch_request_id]
|
|
49
|
-
|
|
50
|
-
# Calculate changes.
|
|
51
|
-
# Note: In after_commit, previous_changes is needed because changes is empty.
|
|
52
44
|
changes_hash = previous_changes.dup
|
|
53
45
|
|
|
54
|
-
#
|
|
46
|
+
# Use Rails standard parameter filtering for maximum safety
|
|
55
47
|
_railswatch_filter_attributes!(changes_hash)
|
|
56
48
|
|
|
57
49
|
data = {
|
|
58
50
|
event_type: "model",
|
|
59
|
-
action: type,
|
|
60
|
-
timestamp: Time.now.utc.iso8601,
|
|
51
|
+
action: type,
|
|
52
|
+
timestamp: Time.now.utc.iso8601(6),
|
|
61
53
|
|
|
62
54
|
# Model Identity
|
|
63
55
|
model: self.class.name,
|
|
64
56
|
key: self.id,
|
|
65
57
|
|
|
66
|
-
#
|
|
58
|
+
# Data
|
|
67
59
|
changes: changes_hash,
|
|
68
60
|
|
|
69
61
|
# Correlation
|
|
@@ -72,17 +64,24 @@ module RailswatchGem
|
|
|
72
64
|
|
|
73
65
|
RailswatchGem.record(data)
|
|
74
66
|
rescue => e
|
|
75
|
-
# Swallow errors to prevent model callbacks from aborting transactions
|
|
76
67
|
warn "RailswatchGem: Failed to record model event: #{e.message}"
|
|
77
68
|
end
|
|
78
69
|
|
|
79
70
|
def _railswatch_filter_attributes!(hash)
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
71
|
+
if defined?(::Rails.application) && ::Rails.application
|
|
72
|
+
# Use the app's real configuration
|
|
73
|
+
filter = ActiveSupport::ParameterFilter.new(::Rails.application.config.filter_parameters)
|
|
74
|
+
# Filter keys (parameter filter expects a hash, usually params, but works on attributes too)
|
|
75
|
+
filtered = filter.filter(hash)
|
|
76
|
+
hash.replace(filtered)
|
|
77
|
+
else
|
|
78
|
+
# Fallback
|
|
79
|
+
sensitive = %w[password password_digest token secret credit_card cc_number]
|
|
80
|
+
hash.each_key do |key|
|
|
81
|
+
if sensitive.any? { |s| key.to_s.include?(s) }
|
|
82
|
+
hash[key] = ["[FILTERED]", "[FILTERED]"]
|
|
83
|
+
end
|
|
84
|
+
end
|
|
86
85
|
end
|
|
87
86
|
end
|
|
88
87
|
end
|
|
@@ -11,20 +11,12 @@ module RailswatchGem
|
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
def start
|
|
14
|
-
# This instrumenter allows users to track arbitrary ActiveSupport::Notifications.
|
|
15
|
-
# It relies on the user defining 'tracked_notifications' in the configuration.
|
|
16
|
-
#
|
|
17
|
-
# Example Config usage:
|
|
18
|
-
# config.tracked_notifications = ["billing.checkout", /users\..*/]
|
|
19
|
-
|
|
20
14
|
patterns = if @config.respond_to?(:tracked_notifications)
|
|
21
15
|
@config.tracked_notifications
|
|
22
16
|
else
|
|
23
17
|
[]
|
|
24
18
|
end
|
|
25
19
|
|
|
26
|
-
# FIX: Use .map instead of .each to return the array of subscribers.
|
|
27
|
-
# This allows tests (and apps) to keep track of subscriptions and unsubscribe if needed.
|
|
28
20
|
Array(patterns).map do |pattern|
|
|
29
21
|
ActiveSupport::Notifications.subscribe(pattern) do |*args|
|
|
30
22
|
event = ActiveSupport::Notifications::Event.new(*args)
|
|
@@ -36,27 +28,27 @@ module RailswatchGem
|
|
|
36
28
|
private
|
|
37
29
|
|
|
38
30
|
def process_event(event)
|
|
39
|
-
# Prevent infinite loops
|
|
31
|
+
# Prevent infinite loops
|
|
40
32
|
return if event.name.start_with?("railswatch_gem")
|
|
41
33
|
|
|
42
34
|
payload = event.payload
|
|
35
|
+
request_id = Thread.current[:railswatch_request_id]
|
|
43
36
|
|
|
44
37
|
data = {
|
|
45
38
|
event_type: "notification",
|
|
46
39
|
name: event.name,
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
40
|
+
timestamp: Time.at(event.time).utc.iso8601(6),
|
|
41
|
+
duration_ms: event.duration.round(2),
|
|
42
|
+
request_id: request_id
|
|
50
43
|
}
|
|
51
44
|
|
|
52
|
-
#
|
|
45
|
+
# Safe Payload Merge
|
|
53
46
|
safe_payload = payload.select do |_, v|
|
|
54
47
|
v.is_a?(String) || v.is_a?(Numeric) || v.is_a?(TrueClass) || v.is_a?(FalseClass) || v.nil?
|
|
55
48
|
end
|
|
56
49
|
|
|
57
50
|
data[:payload] = safe_payload
|
|
58
51
|
|
|
59
|
-
# Error handling
|
|
60
52
|
if payload[:exception]
|
|
61
53
|
data[:error_class] = payload[:exception].first.to_s
|
|
62
54
|
data[:error_message] = payload[:exception].last.to_s
|
|
@@ -10,13 +10,10 @@ module RailswatchGem
|
|
|
10
10
|
def initialize(client, config)
|
|
11
11
|
@client = client
|
|
12
12
|
@config = config
|
|
13
|
-
# Cache ingestion URI components for fast comparison
|
|
14
13
|
@ingest_uri = URI(@config.ingest_url) rescue nil
|
|
15
14
|
end
|
|
16
15
|
|
|
17
16
|
def start
|
|
18
|
-
# Monkey-patch Net::HTTP if we haven't already.
|
|
19
|
-
# This wraps the low-level request method to emit an ActiveSupport notification.
|
|
20
17
|
unless Net::HTTP.include?(NetHttpPatch)
|
|
21
18
|
Net::HTTP.prepend(NetHttpPatch)
|
|
22
19
|
end
|
|
@@ -33,17 +30,17 @@ module RailswatchGem
|
|
|
33
30
|
payload = event.payload
|
|
34
31
|
|
|
35
32
|
# CRITICAL: Ignore requests to the ingestion endpoint.
|
|
36
|
-
# This prevents an infinite loop where reporting metrics generates more metrics.
|
|
37
33
|
if @ingest_uri &&
|
|
38
34
|
payload[:host] == @ingest_uri.host &&
|
|
39
35
|
payload[:port] == @ingest_uri.port
|
|
40
36
|
return
|
|
41
37
|
end
|
|
42
38
|
|
|
39
|
+
request_id = Thread.current[:railswatch_request_id]
|
|
40
|
+
|
|
43
41
|
data = {
|
|
44
42
|
event_type: "outgoing_request",
|
|
45
|
-
|
|
46
|
-
timestamp: Time.at(event.end).utc.iso8601,
|
|
43
|
+
timestamp: Time.at(event.time).utc.iso8601(6),
|
|
47
44
|
|
|
48
45
|
# Request Details
|
|
49
46
|
method: payload[:method],
|
|
@@ -53,11 +50,13 @@ module RailswatchGem
|
|
|
53
50
|
scheme: payload[:scheme],
|
|
54
51
|
path: payload[:path],
|
|
55
52
|
|
|
53
|
+
# Correlation
|
|
54
|
+
request_id: request_id,
|
|
55
|
+
|
|
56
56
|
# Performance
|
|
57
57
|
duration_ms: event.duration.round(2)
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
# Error handling
|
|
61
60
|
if payload[:exception]
|
|
62
61
|
data[:error_class] = payload[:exception].first.to_s
|
|
63
62
|
data[:error_message] = payload[:exception].last.to_s
|
|
@@ -68,15 +67,9 @@ module RailswatchGem
|
|
|
68
67
|
warn "RailswatchGem: Failed to process outgoing request event: #{e.message}"
|
|
69
68
|
end
|
|
70
69
|
|
|
71
|
-
# ----------------------------------------------------------------
|
|
72
|
-
# Internal Patch for Net::HTTP
|
|
73
|
-
# ----------------------------------------------------------------
|
|
74
70
|
module NetHttpPatch
|
|
75
71
|
def request(req, body = nil, &block)
|
|
76
72
|
scheme = use_ssl? ? "https" : "http"
|
|
77
|
-
|
|
78
|
-
# Reconstruct basic URI for context
|
|
79
|
-
# Note: address and port are methods on the Net::HTTP instance
|
|
80
73
|
uri = URI("#{scheme}://#{address}:#{port}#{req.path}")
|
|
81
74
|
|
|
82
75
|
ActiveSupport::Notifications.instrument("request.net_http", {
|
|
@@ -28,20 +28,26 @@ module RailswatchGem
|
|
|
28
28
|
name = payload[:name]
|
|
29
29
|
return if name && IGNORED_NAMES.include?(name)
|
|
30
30
|
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
#
|
|
31
|
+
# 1. Capture the Application Trace
|
|
32
|
+
# Rails.backtrace_cleaner removes framework noise (gems, rails internals)
|
|
33
|
+
# leaving only the lines relevant to the user's application code.
|
|
34
|
+
app_trace = ::Rails.backtrace_cleaner.clean(caller)
|
|
34
35
|
|
|
36
|
+
# Basic PII safety: You might want to sanitize SQL here depending on your needs.
|
|
35
37
|
data = {
|
|
36
38
|
event_type: "query",
|
|
37
|
-
|
|
38
|
-
|
|
39
|
+
|
|
40
|
+
# Without this, the query logs might look 0ms off from the request logs.
|
|
41
|
+
timestamp: Time.at(event.time).utc.iso8601(6),
|
|
39
42
|
|
|
40
43
|
# Query Details
|
|
41
44
|
name: name || "SQL",
|
|
42
45
|
sql: payload[:sql],
|
|
43
46
|
cached: payload[:cached] || false,
|
|
44
47
|
|
|
48
|
+
# The New Field
|
|
49
|
+
backtrace: app_trace,
|
|
50
|
+
|
|
45
51
|
# Performance
|
|
46
52
|
duration_ms: event.duration.round(2)
|
|
47
53
|
}
|
|
@@ -49,6 +55,11 @@ module RailswatchGem
|
|
|
49
55
|
# Add connection_id if useful for debugging connection pool issues
|
|
50
56
|
data[:connection_id] = payload[:connection_id] if payload[:connection_id]
|
|
51
57
|
|
|
58
|
+
# Link to the parent request if we are inside one
|
|
59
|
+
if (req_id = Thread.current[:railswatch_request_id])
|
|
60
|
+
data[:request_id] = req_id
|
|
61
|
+
end
|
|
62
|
+
|
|
52
63
|
@client.record(data)
|
|
53
64
|
rescue => e
|
|
54
65
|
warn "RailswatchGem: Failed to process query event: #{e.message}"
|
|
@@ -12,7 +12,6 @@ module RailswatchGem
|
|
|
12
12
|
|
|
13
13
|
def start
|
|
14
14
|
# Subscribe to the standard Rails controller event.
|
|
15
|
-
# This event includes details like status, path, duration, and DB runtime.
|
|
16
15
|
ActiveSupport::Notifications.subscribe("process_action.action_controller") do |*args|
|
|
17
16
|
event = ActiveSupport::Notifications::Event.new(*args)
|
|
18
17
|
process_event(event)
|
|
@@ -40,8 +39,7 @@ module RailswatchGem
|
|
|
40
39
|
|
|
41
40
|
data = {
|
|
42
41
|
event_type: "request",
|
|
43
|
-
|
|
44
|
-
timestamp: Time.at(event.end).utc.iso8601,
|
|
42
|
+
timestamp: Time.at(event.time).utc.iso8601(6),
|
|
45
43
|
request_id: request_id,
|
|
46
44
|
|
|
47
45
|
# HTTP Context
|
|
@@ -109,17 +107,33 @@ module RailswatchGem
|
|
|
109
107
|
def extract_user(headers)
|
|
110
108
|
return nil unless headers.respond_to?(:env)
|
|
111
109
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
110
|
+
env = headers.env
|
|
111
|
+
|
|
112
|
+
# 1. Try Warden (Devise)
|
|
113
|
+
warden = env["warden"]
|
|
114
|
+
if warden
|
|
115
|
+
# Warden proxy might not have loaded the user yet
|
|
116
|
+
user = warden.user(run_callbacks: false) rescue nil
|
|
117
|
+
if user
|
|
118
|
+
return {
|
|
119
|
+
id: user.respond_to?(:id) ? user.id : nil,
|
|
120
|
+
email: user.respond_to?(:email) ? user.email : nil,
|
|
121
|
+
class: user.class.name
|
|
122
|
+
}
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# 2. Try CurrentAttributes (Modern Rails pattern)
|
|
127
|
+
if defined?(::Current) && ::Current.respond_to?(:user) && ::Current.user
|
|
128
|
+
user = ::Current.user
|
|
129
|
+
return {
|
|
116
130
|
id: user.respond_to?(:id) ? user.id : nil,
|
|
117
131
|
email: user.respond_to?(:email) ? user.email : nil,
|
|
118
132
|
class: user.class.name
|
|
119
133
|
}
|
|
120
|
-
else
|
|
121
|
-
nil
|
|
122
134
|
end
|
|
135
|
+
|
|
136
|
+
nil
|
|
123
137
|
rescue
|
|
124
138
|
nil
|
|
125
139
|
end
|
|
@@ -12,7 +12,6 @@ module RailswatchGem
|
|
|
12
12
|
|
|
13
13
|
def start
|
|
14
14
|
# Only instrument Rake if it is loaded.
|
|
15
|
-
# This prevents errors if the gem is used in a context where Rake isn't present.
|
|
16
15
|
if defined?(::Rake::Task)
|
|
17
16
|
patch_rake_tasks
|
|
18
17
|
|
|
@@ -36,20 +35,25 @@ module RailswatchGem
|
|
|
36
35
|
payload = event.payload
|
|
37
36
|
task_name = payload[:name]
|
|
38
37
|
|
|
39
|
-
#
|
|
38
|
+
# Ignore internal Railswatch tasks to prevent noise/loops
|
|
40
39
|
return if task_name.to_s.start_with?("railswatch:")
|
|
41
40
|
|
|
41
|
+
# Check for correlation (e.g. if task was triggered from a controller)
|
|
42
|
+
request_id = Thread.current[:railswatch_request_id]
|
|
43
|
+
|
|
42
44
|
data = {
|
|
43
45
|
event_type: "scheduled_task",
|
|
44
|
-
|
|
45
|
-
timestamp: Time.at(event.end).utc.iso8601,
|
|
46
|
+
timestamp: Time.at(event.time).utc.iso8601(6),
|
|
46
47
|
|
|
47
48
|
# Task Identity
|
|
48
49
|
name: task_name,
|
|
49
|
-
args: payload[:args].to_a,
|
|
50
|
+
args: payload[:args].to_a,
|
|
50
51
|
|
|
51
52
|
# Performance
|
|
52
|
-
duration_ms: event.duration.round(2)
|
|
53
|
+
duration_ms: event.duration.round(2),
|
|
54
|
+
|
|
55
|
+
# Correlation
|
|
56
|
+
request_id: request_id
|
|
53
57
|
}
|
|
54
58
|
|
|
55
59
|
# Error handling
|
|
@@ -71,7 +75,6 @@ module RailswatchGem
|
|
|
71
75
|
# ----------------------------------------------------------------
|
|
72
76
|
module RakeTaskPatch
|
|
73
77
|
def execute(args = nil)
|
|
74
|
-
# We explicitly capture 'name' here because it's available on the task instance
|
|
75
78
|
ActiveSupport::Notifications.instrument("task.rake", { name: name, args: args }) do
|
|
76
79
|
super
|
|
77
80
|
end
|