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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d9e86e9f297b739658918cd083749603dc9b051f9155a3468f59dde33b8ce908
4
- data.tar.gz: 55d5fb852f0c5cfc244e3344a4e97a1de2adffa2e4c2f19fb0c01617ec639d17
3
+ metadata.gz: 83b0d150b9b3726489be0f60b057b7f88220e159fbcb2fdff0c908f50666891b
4
+ data.tar.gz: 53415be3a6facc48a9baa9ccec55d5d98334ad64fb74c354b4007e519c358f71
5
5
  SHA512:
6
- metadata.gz: afc6919e18594a95b783024a82ae597a8f199ce51427e846ca937224257642443a0ebdcfb6974cd55764d8179b4d4dd298846614b0d9b24817b90307caac0eea
7
- data.tar.gz: 74ab8984fba2107643fd85106e655917836b670f869ff5caae4a272b021176e71b77d35efe9291c17dd29a101c1c34d0cbc867179bacd87cdb3472da3aaae609
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: false },
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 },
@@ -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
- }.compact
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
- # Optionally log SQL (configurable, defaults to off)
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
- subscribe("render_template.action_view") do |event|
19
- handle_render_template(event)
20
- end
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
- subscribe("render_partial.action_view") do |event|
23
- handle_render_partial(event)
24
- end
75
+ ActiveSupport::Notifications.monotonic_subscribe(
76
+ "render_partial.action_view",
77
+ ViewSubscriber.new("render_partial")
78
+ )
25
79
 
26
- subscribe("render_collection.action_view") do |event|
27
- handle_render_collection(event)
28
- end
80
+ ActiveSupport::Notifications.monotonic_subscribe(
81
+ "render_collection.action_view",
82
+ ViewSubscriber.new("render_collection")
83
+ )
29
84
 
30
- subscribe("render_layout.action_view") do |event|
31
- handle_render_layout(event)
32
- end
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
- def handle_render_layout(event)
83
- record_view_span("render_layout", event)
84
- end
85
-
86
- def record_view_span(type, event)
87
- return unless MiniAPM.enabled?
88
- return unless Context.current_trace
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MiniAPM
4
- VERSION = "1.1.0"
4
+ VERSION = "1.3.1"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: miniapm
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chris Hasinski