miniapm 1.1.0 → 1.3.1
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/CHANGELOG.md +6 -0
- data/lib/miniapm/configuration.rb +11 -1
- data/lib/miniapm/error_event.rb +64 -3
- data/lib/miniapm/instrumentations/activerecord.rb +11 -1
- data/lib/miniapm/instrumentations/rails/controller.rb +78 -66
- data/lib/miniapm/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: 83b0d150b9b3726489be0f60b057b7f88220e159fbcb2fdff0c908f50666891b
|
|
4
|
+
data.tar.gz: 53415be3a6facc48a9baa9ccec55d5d98334ad64fb74c354b4007e519c358f71
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 24b383ee8664419cd4393cfd07532bc2ab7aee2d9d7bc922ab30db878a50623b6d844688bedbf73c605005b51b0cc2bff255cd5fcf7f741953068068bdbefe42
|
|
7
|
+
data.tar.gz: 23cdb70815eb61eae638f17b860fa3c8309443d89ed58f1f332006b53026393b6a42ce8b2c7a6e95a1650e6f2be5e59990ccccb483e1634c058f63a548e5729e
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.3.1] - 2026-01-04
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Filter SolidCable/SolidQueue polling queries by default to reduce noise
|
|
14
|
+
- New `ignored_tables` option for ActiveRecord instrumentation (supports strings and regex)
|
|
15
|
+
|
|
10
16
|
## [1.0.0] - 2026-01-03
|
|
11
17
|
|
|
12
18
|
### Added
|
|
@@ -136,9 +136,19 @@ module MiniAPM
|
|
|
136
136
|
end
|
|
137
137
|
|
|
138
138
|
class InstrumentationConfig
|
|
139
|
+
# Tables used for internal polling by Rails infrastructure gems
|
|
140
|
+
# These create noise in traces without providing value
|
|
141
|
+
INTERNAL_POLLING_TABLES = %w[
|
|
142
|
+
solid_cable_messages
|
|
143
|
+
solid_queue_processes
|
|
144
|
+
solid_queue_ready_executions
|
|
145
|
+
solid_queue_scheduled_executions
|
|
146
|
+
solid_queue_semaphores
|
|
147
|
+
].freeze
|
|
148
|
+
|
|
139
149
|
DEFAULTS = {
|
|
140
150
|
rails: { enabled: true },
|
|
141
|
-
activerecord: { enabled: true, log_sql:
|
|
151
|
+
activerecord: { enabled: true, log_sql: true, ignored_tables: INTERNAL_POLLING_TABLES },
|
|
142
152
|
activejob: { enabled: true },
|
|
143
153
|
sidekiq: { enabled: true },
|
|
144
154
|
cache: { enabled: true },
|
data/lib/miniapm/error_event.rb
CHANGED
|
@@ -7,7 +7,10 @@ module MiniAPM
|
|
|
7
7
|
class ErrorEvent
|
|
8
8
|
attr_reader :exception_class, :message, :backtrace, :fingerprint
|
|
9
9
|
attr_reader :request_id, :user_id, :params, :timestamp
|
|
10
|
-
attr_reader :context
|
|
10
|
+
attr_reader :context, :source_context
|
|
11
|
+
|
|
12
|
+
# Number of lines to include before and after the error line
|
|
13
|
+
CONTEXT_LINES = 5
|
|
11
14
|
|
|
12
15
|
def initialize(
|
|
13
16
|
exception_class:,
|
|
@@ -29,6 +32,7 @@ module MiniAPM
|
|
|
29
32
|
@params = filter_params(params)
|
|
30
33
|
@timestamp = timestamp || Time.now.utc
|
|
31
34
|
@context = context
|
|
35
|
+
@source_context = extract_source_context
|
|
32
36
|
end
|
|
33
37
|
|
|
34
38
|
def self.from_exception(exception, context = {})
|
|
@@ -44,7 +48,7 @@ module MiniAPM
|
|
|
44
48
|
end
|
|
45
49
|
|
|
46
50
|
def to_h
|
|
47
|
-
{
|
|
51
|
+
hash = {
|
|
48
52
|
exception_class: @exception_class,
|
|
49
53
|
message: @message,
|
|
50
54
|
backtrace: @backtrace,
|
|
@@ -53,7 +57,9 @@ module MiniAPM
|
|
|
53
57
|
user_id: @user_id,
|
|
54
58
|
params: @params,
|
|
55
59
|
timestamp: @timestamp.iso8601
|
|
56
|
-
}
|
|
60
|
+
}
|
|
61
|
+
hash[:source_context] = @source_context if @source_context
|
|
62
|
+
hash.compact
|
|
57
63
|
end
|
|
58
64
|
|
|
59
65
|
private
|
|
@@ -126,5 +132,60 @@ module MiniAPM
|
|
|
126
132
|
string = string.to_s
|
|
127
133
|
string.length > max_length ? string[0, max_length] + "..." : string
|
|
128
134
|
end
|
|
135
|
+
|
|
136
|
+
def extract_source_context
|
|
137
|
+
# Find first application backtrace line (not gem/stdlib)
|
|
138
|
+
app_line = @backtrace.find do |line|
|
|
139
|
+
!line.include?("/gems/") &&
|
|
140
|
+
!line.include?("/ruby/") &&
|
|
141
|
+
!line.include?("/vendor/") &&
|
|
142
|
+
!line.include?("/bundle/") &&
|
|
143
|
+
!line.start_with?("<")
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
return nil unless app_line
|
|
147
|
+
|
|
148
|
+
# Parse file:line from backtrace line
|
|
149
|
+
# Format: "/path/to/file.rb:123:in `method_name'" or "/path/to/file.rb:123"
|
|
150
|
+
match = app_line.match(/\A(.+?):(\d+)/)
|
|
151
|
+
return nil unless match
|
|
152
|
+
|
|
153
|
+
file_path = match[1]
|
|
154
|
+
lineno = match[2].to_i
|
|
155
|
+
|
|
156
|
+
# Handle relative paths (e.g., "app/controllers/...") by prepending Rails.root
|
|
157
|
+
unless file_path.start_with?("/")
|
|
158
|
+
if defined?(Rails) && Rails.respond_to?(:root) && Rails.root
|
|
159
|
+
file_path = File.join(Rails.root.to_s, file_path)
|
|
160
|
+
else
|
|
161
|
+
return nil
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
return nil unless File.exist?(file_path) && File.readable?(file_path)
|
|
166
|
+
|
|
167
|
+
begin
|
|
168
|
+
lines = File.readlines(file_path)
|
|
169
|
+
return nil if lineno < 1 || lineno > lines.length
|
|
170
|
+
|
|
171
|
+
# Calculate context range (0-indexed)
|
|
172
|
+
start_line = [lineno - CONTEXT_LINES - 1, 0].max
|
|
173
|
+
end_line = [lineno + CONTEXT_LINES - 1, lines.length - 1].min
|
|
174
|
+
|
|
175
|
+
pre_context = lines[start_line...lineno - 1].map(&:chomp)
|
|
176
|
+
context_line = lines[lineno - 1]&.chomp || ""
|
|
177
|
+
post_context = lines[lineno..end_line].map(&:chomp)
|
|
178
|
+
|
|
179
|
+
{
|
|
180
|
+
file: file_path,
|
|
181
|
+
lineno: lineno,
|
|
182
|
+
pre_context: pre_context,
|
|
183
|
+
context_line: context_line,
|
|
184
|
+
post_context: post_context
|
|
185
|
+
}
|
|
186
|
+
rescue StandardError
|
|
187
|
+
nil
|
|
188
|
+
end
|
|
189
|
+
end
|
|
129
190
|
end
|
|
130
191
|
end
|
|
@@ -33,6 +33,9 @@ module MiniAPM
|
|
|
33
33
|
operation = extract_operation(sql)
|
|
34
34
|
table = extract_table(sql)
|
|
35
35
|
|
|
36
|
+
# Skip queries to ignored tables (e.g., SolidCable/SolidQueue polling)
|
|
37
|
+
return if table && ignored_table?(table)
|
|
38
|
+
|
|
36
39
|
name = [operation, table].compact.join(" ")
|
|
37
40
|
name = operation if name.empty?
|
|
38
41
|
|
|
@@ -43,7 +46,7 @@ module MiniAPM
|
|
|
43
46
|
|
|
44
47
|
attributes["db.sql.table"] = table if table
|
|
45
48
|
|
|
46
|
-
#
|
|
49
|
+
# Log SQL (configurable, defaults to on)
|
|
47
50
|
if MiniAPM.configuration.instrumentations.options(:activerecord)[:log_sql]
|
|
48
51
|
attributes["db.statement"] = truncate_sql(sql)
|
|
49
52
|
end
|
|
@@ -114,6 +117,13 @@ module MiniAPM
|
|
|
114
117
|
def truncate_sql(sql, max_length: 2000)
|
|
115
118
|
sql.length > max_length ? sql[0...max_length] + "..." : sql
|
|
116
119
|
end
|
|
120
|
+
|
|
121
|
+
def ignored_table?(table)
|
|
122
|
+
ignored = MiniAPM.configuration.instrumentations.options(:activerecord)[:ignored_tables]
|
|
123
|
+
return false unless ignored
|
|
124
|
+
|
|
125
|
+
ignored.any? { |pattern| pattern.is_a?(Regexp) ? table.match?(pattern) : table == pattern }
|
|
126
|
+
end
|
|
117
127
|
end
|
|
118
128
|
end
|
|
119
129
|
end
|
|
@@ -4,32 +4,88 @@ module MiniAPM
|
|
|
4
4
|
module Instrumentations
|
|
5
5
|
module Rails
|
|
6
6
|
class Controller < Base
|
|
7
|
+
# Subscriber class for view rendering that tracks span context properly
|
|
8
|
+
class ViewSubscriber
|
|
9
|
+
def initialize(type)
|
|
10
|
+
@type = type
|
|
11
|
+
@spans = {}
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def start(name, id, payload)
|
|
15
|
+
return unless MiniAPM.enabled?
|
|
16
|
+
return unless Context.current_trace
|
|
17
|
+
|
|
18
|
+
template = payload[:identifier] || payload[:virtual_path] || "unknown"
|
|
19
|
+
|
|
20
|
+
# Clean up template path
|
|
21
|
+
if defined?(::Rails.root) && ::Rails.root
|
|
22
|
+
template = template.sub(::Rails.root.to_s + "/", "")
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
template_name = File.basename(template)
|
|
26
|
+
|
|
27
|
+
attributes = {
|
|
28
|
+
"rails.template" => template,
|
|
29
|
+
"rails.template.type" => @type
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
attributes["rails.layout"] = payload[:layout] if payload[:layout]
|
|
33
|
+
attributes["rails.collection.count"] = payload[:count] if payload[:count]
|
|
34
|
+
|
|
35
|
+
span = Span.new(
|
|
36
|
+
name: "#{@type} #{template_name}",
|
|
37
|
+
category: :view,
|
|
38
|
+
trace_id: Context.current_trace_id,
|
|
39
|
+
parent_span_id: Context.current_span&.span_id,
|
|
40
|
+
attributes: attributes
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
# Store span by event ID and push to context
|
|
44
|
+
@spans[id] = span
|
|
45
|
+
Context.push_span(span)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def finish(name, id, payload)
|
|
49
|
+
span = @spans.delete(id)
|
|
50
|
+
return unless span
|
|
51
|
+
|
|
52
|
+
# Pop from context and finish
|
|
53
|
+
Context.pop_span
|
|
54
|
+
span.finish
|
|
55
|
+
MiniAPM.record_span(span)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
7
59
|
class << self
|
|
8
60
|
def install!
|
|
9
61
|
return if installed?
|
|
10
62
|
mark_installed!
|
|
11
63
|
|
|
12
|
-
# Subscribe to controller processing
|
|
64
|
+
# Subscribe to controller processing (still uses standard subscribe)
|
|
13
65
|
subscribe("process_action.action_controller") do |event|
|
|
14
66
|
handle_process_action(event)
|
|
15
67
|
end
|
|
16
68
|
|
|
17
|
-
# Subscribe to view rendering
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
69
|
+
# Subscribe to view rendering with monotonic subscribers for proper nesting
|
|
70
|
+
ActiveSupport::Notifications.monotonic_subscribe(
|
|
71
|
+
"render_template.action_view",
|
|
72
|
+
ViewSubscriber.new("render_template")
|
|
73
|
+
)
|
|
21
74
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
75
|
+
ActiveSupport::Notifications.monotonic_subscribe(
|
|
76
|
+
"render_partial.action_view",
|
|
77
|
+
ViewSubscriber.new("render_partial")
|
|
78
|
+
)
|
|
25
79
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
80
|
+
ActiveSupport::Notifications.monotonic_subscribe(
|
|
81
|
+
"render_collection.action_view",
|
|
82
|
+
ViewSubscriber.new("render_collection")
|
|
83
|
+
)
|
|
29
84
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
85
|
+
ActiveSupport::Notifications.monotonic_subscribe(
|
|
86
|
+
"render_layout.action_view",
|
|
87
|
+
ViewSubscriber.new("render_layout")
|
|
88
|
+
)
|
|
33
89
|
end
|
|
34
90
|
|
|
35
91
|
private
|
|
@@ -64,60 +120,16 @@ module MiniAPM
|
|
|
64
120
|
# Record exception if present
|
|
65
121
|
if payload[:exception_object]
|
|
66
122
|
span.record_exception(payload[:exception_object])
|
|
67
|
-
end
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
def handle_render_template(event)
|
|
71
|
-
record_view_span("render_template", event)
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
def handle_render_partial(event)
|
|
75
|
-
record_view_span("render_partial", event)
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
def handle_render_collection(event)
|
|
79
|
-
record_view_span("render_collection", event)
|
|
80
|
-
end
|
|
81
123
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
payload = event.payload
|
|
91
|
-
template = payload[:identifier] || payload[:virtual_path] || "unknown"
|
|
92
|
-
|
|
93
|
-
# Clean up template path
|
|
94
|
-
if defined?(::Rails.root) && ::Rails.root
|
|
95
|
-
template = template.sub(::Rails.root.to_s + "/", "")
|
|
124
|
+
# Also send to dedicated errors endpoint (with source context)
|
|
125
|
+
MiniAPM.record_error(
|
|
126
|
+
payload[:exception_object],
|
|
127
|
+
context: {
|
|
128
|
+
request_id: payload[:request]&.request_id,
|
|
129
|
+
params: payload[:params]
|
|
130
|
+
}
|
|
131
|
+
)
|
|
96
132
|
end
|
|
97
|
-
|
|
98
|
-
template_name = File.basename(template)
|
|
99
|
-
|
|
100
|
-
attributes = {
|
|
101
|
-
"rails.template" => template,
|
|
102
|
-
"rails.template.type" => type
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
if payload[:layout]
|
|
106
|
-
attributes["rails.layout"] = payload[:layout]
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
if payload[:count]
|
|
110
|
-
attributes["rails.collection.count"] = payload[:count]
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
span = create_span_from_event(
|
|
114
|
-
event,
|
|
115
|
-
name: "#{type} #{template_name}",
|
|
116
|
-
category: :view,
|
|
117
|
-
attributes: attributes
|
|
118
|
-
)
|
|
119
|
-
|
|
120
|
-
record_span(span)
|
|
121
133
|
end
|
|
122
134
|
end
|
|
123
135
|
end
|
data/lib/miniapm/version.rb
CHANGED