solid_apm 0.8.2 → 0.10.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.
- checksums.yaml +4 -4
- data/README.md +153 -0
- data/app/jobs/solid_apm/cleanup_job.rb +13 -0
- data/app/models/solid_apm/span_subscriber/action_dispatch.rb +1 -1
- data/app/models/solid_apm/span_subscriber/base.rb +1 -0
- data/app/models/solid_apm/transaction.rb +9 -0
- data/lib/solid_apm/cleanup_service.rb +51 -0
- data/lib/solid_apm/engine.rb +26 -0
- data/lib/solid_apm/mcp/impactful_transactions_resource.rb +146 -0
- data/lib/solid_apm/mcp/spans_for_transaction_tool.rb +30 -0
- data/lib/solid_apm/middleware.rb +35 -6
- data/lib/solid_apm/sampler.rb +12 -0
- data/lib/solid_apm/version.rb +1 -1
- data/lib/solid_apm.rb +14 -2
- data/lib/tasks/solid_apm_tasks.rake +18 -4
- metadata +7 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e3d5be7324da26b5f4720bc0ba9e61717be304286b9ecad84e09adf503095699
|
4
|
+
data.tar.gz: 8969d758c906bb7de44b4024e1f7353bc63f1f4dda0f2ee5631df1e2fa436e2e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1ed2237e30cabd350515555a5e6a66006777835d4e07adcc2dc2758f0fe8e474e931e3da204f44309671923ba1eb1fbcbc32203eaf459a6cebffc2eb3030f3fa
|
7
|
+
data.tar.gz: fb86bb4e14bf08207255ca5e0121b6e50a5ba43e3a4d394d777362df614d47d25e0d54508a66660c01c7ecccf3c53cc00b395981bee6a32378a5903ca64dd268
|
data/README.md
CHANGED
@@ -5,6 +5,7 @@ Rails engine to manage APM data without using a third party service.
|
|
5
5
|
|
6
6
|
<img src="./docs/img.png" width="600px">
|
7
7
|
<img src="./docs/img_1.png" width="600px">
|
8
|
+
<img src="./docs/img_2.png" width="600px">
|
8
9
|
|
9
10
|
## Installation
|
10
11
|
|
@@ -51,6 +52,158 @@ class ApplicationController
|
|
51
52
|
end
|
52
53
|
```
|
53
54
|
|
55
|
+
## Configuration
|
56
|
+
|
57
|
+
SolidAPM can be configured using the following options in your `config/initializers/solid_apm.rb` file:
|
58
|
+
|
59
|
+
### Database Connection
|
60
|
+
|
61
|
+
Configure the database connection for SolidAPM:
|
62
|
+
|
63
|
+
```ruby
|
64
|
+
SolidApm.connects_to = { database: { writing: :solid_apm } }
|
65
|
+
```
|
66
|
+
|
67
|
+
### ActiveRecord Logger Silencing
|
68
|
+
|
69
|
+
Control whether ActiveRecord logger is silenced during SolidAPM operations (default: `true`):
|
70
|
+
|
71
|
+
```ruby
|
72
|
+
# Disable ActiveRecord logger silencing to see SQL queries in logs
|
73
|
+
SolidApm.silence_active_record_logger = false
|
74
|
+
```
|
75
|
+
|
76
|
+
### Transaction Sampling
|
77
|
+
|
78
|
+
Control the sampling rate for transactions using a "1 out of N" approach (default: `1`):
|
79
|
+
|
80
|
+
```ruby
|
81
|
+
# Sample every transaction (default behavior)
|
82
|
+
SolidApm.transaction_sampling = 1
|
83
|
+
|
84
|
+
# Sample 1 out of every 2 transactions (50% sampling)
|
85
|
+
SolidApm.transaction_sampling = 2
|
86
|
+
|
87
|
+
# Sample 1 out of every 5 transactions (20% sampling)
|
88
|
+
SolidApm.transaction_sampling = 5
|
89
|
+
|
90
|
+
# Sample 1 out of every 10 transactions (10% sampling)
|
91
|
+
SolidApm.transaction_sampling = 10
|
92
|
+
```
|
93
|
+
|
94
|
+
The sampling is done per-thread using a round-robin counter, ensuring even distribution across requests.
|
95
|
+
This is useful for high-traffic applications where you want to reduce the volume of
|
96
|
+
APM data while still maintaining representative performance insights.
|
97
|
+
|
98
|
+
### Transaction Name Filtering
|
99
|
+
|
100
|
+
Filter specific transactions by name using exact string matches or regular expressions:
|
101
|
+
|
102
|
+
```ruby
|
103
|
+
# Filter specific transactions by exact name
|
104
|
+
SolidApm.transaction_filters += ['HomeController#index', /^Rails::HealthController/]
|
105
|
+
```
|
106
|
+
|
107
|
+
## Data Cleanup
|
108
|
+
|
109
|
+
SolidAPM provides a rake task to clean up old transaction data to manage database size over time.
|
110
|
+
|
111
|
+
### Manual Cleanup
|
112
|
+
|
113
|
+
Clean up transactions older than 1 month (default):
|
114
|
+
|
115
|
+
```shell
|
116
|
+
bin/rails solid_apm:cleanup
|
117
|
+
```
|
118
|
+
|
119
|
+
Clean up transactions with custom time periods:
|
120
|
+
|
121
|
+
```shell
|
122
|
+
# Delete transactions older than 1 week
|
123
|
+
bin/rails solid_apm:cleanup[1.week.ago]
|
124
|
+
```
|
125
|
+
|
126
|
+
### Automated Cleanup with ActiveJob
|
127
|
+
|
128
|
+
For production applications, it's recommended to set up automated cleanup.
|
129
|
+
|
130
|
+
Example with SolidQueue. Configure recurring cleanup in your `config/recurring.yml`:
|
131
|
+
|
132
|
+
```yaml
|
133
|
+
solid_apm_cleanup_weekly:
|
134
|
+
class: SolidApm::CleanupJob
|
135
|
+
cron: "0 3 * * *" # Every day at 3 AM
|
136
|
+
args: ["1.week.ago"]
|
137
|
+
```
|
138
|
+
|
139
|
+
## How it works
|
140
|
+
|
141
|
+
SolidAPM stores information in the form of transactions, representing incoming HTTP requests which
|
142
|
+
listen to a variety of spans (events) from `ActiveSupport::Instrument`. Each span
|
143
|
+
saves backtrace information to easily find the source of issues.
|
144
|
+
|
145
|
+
### Request transaction
|
146
|
+
|
147
|
+
It is based on [ActionDispatch](https://guides.rubyonrails.org/active_support_instrumentation.html#action-dispatch)
|
148
|
+
events to start and end a transaction.
|
149
|
+
|
150
|
+
A Rack middleware uses [`rack.after_reply`](https://github.blog/engineering/architecture-optimization/performance-at-github-deferring-stats-with-rack-after_reply/)
|
151
|
+
to bulk insert transactions and spans after delivering the response, so tracking your application
|
152
|
+
doesn't add delay to the client.
|
153
|
+
|
154
|
+
### Spans saved
|
155
|
+
|
156
|
+
* Request
|
157
|
+
* Rendering
|
158
|
+
* SQL requests and transactions
|
159
|
+
* Rails cache
|
160
|
+
* Net/HTTP
|
161
|
+
|
162
|
+
## MCP Server
|
163
|
+
|
164
|
+
SolidAPM offers an optional MCP server to allow an AI agent to interact with SolidAPM
|
165
|
+
and help identify issues in your application, such as
|
166
|
+
N+1 queries, slow queries and more. The AI agent can analyze and suggest fixes for these issues.
|
167
|
+
|
168
|
+
### MCP Server Configuration
|
169
|
+
|
170
|
+
The MCP server is only mounted if the [fast-mcp](https://github.com/yjacquin/fast-mcp) gem is installed by your application.
|
171
|
+
|
172
|
+
1. Add to your Gemfile:
|
173
|
+
|
174
|
+
```ruby
|
175
|
+
# Work in progress, plus patch for MCP 2025-06-18 Protocol Revision
|
176
|
+
# with StreamableHTTP support
|
177
|
+
# https://github.com/yjacquin/fast-mcp/issues/109
|
178
|
+
gem 'fast-mcp', branch: 'transport', github: 'Bhacaz/fast-mcp'
|
179
|
+
```
|
180
|
+
|
181
|
+
2. Configure the MCP server in your `config/initializers/solid_apm.rb`:
|
182
|
+
|
183
|
+
```ruby
|
184
|
+
SolidApm.mcp_server_config = {
|
185
|
+
name: 'my-app-solid-apm',
|
186
|
+
path: '/solid_apm/mcp',
|
187
|
+
auth_token: Rails.application.credentials.solid_apm[:mcp_auth_token]
|
188
|
+
}
|
189
|
+
```
|
190
|
+
|
191
|
+
3. Test the MCP server by running:
|
192
|
+
|
193
|
+
```shell
|
194
|
+
curl -X POST http://localhost:3000/solid_apm/mcp \
|
195
|
+
-H "Content-Type: application/json" \
|
196
|
+
-H "Accept: application/json" \
|
197
|
+
-H "Authorization: Bearer <AUTH_TOKEN>" \
|
198
|
+
-d '{"jsonrpc":"2.0","method":"tools/list","id":1}
|
199
|
+
```
|
200
|
+
|
201
|
+
### MCP usage
|
202
|
+
|
203
|
+
1. Add the MCP resource `impactful-transactions` to the context of your prompt.
|
204
|
+
2. Prompt example: "Analyze the impactful transactions of my application and suggest improvements, base on the spans details."
|
205
|
+
3. Allow the AI agent to use the MCP tool `spans-for-transaction` to retrieve the longest spans for a specific transaction.
|
206
|
+
|
54
207
|
## TODOs
|
55
208
|
|
56
209
|
### Features
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SolidApm
|
4
|
+
class CleanupJob < ApplicationJob
|
5
|
+
def perform(older_than = '1.month.ago')
|
6
|
+
result = CleanupService.new(older_than: older_than).call
|
7
|
+
|
8
|
+
Rails.logger.info "SolidApm::CleanupJob completed: deleted #{result[:deleted_count]} transactions older than #{result[:cutoff_time]} (#{result[:older_than]})"
|
9
|
+
|
10
|
+
result
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -13,7 +13,7 @@ module SolidApm
|
|
13
13
|
transaction.duration = ((finish.to_f - start.to_f) * 1000).round(6)
|
14
14
|
transaction.metadata = {
|
15
15
|
params: payload[:request].params.except(:controller, :action),
|
16
|
-
context: SpanSubscriber::Base.context
|
16
|
+
context: SpanSubscriber::Base.context || {}
|
17
17
|
}
|
18
18
|
SpanSubscriber::Base.context = {}
|
19
19
|
end
|
@@ -4,5 +4,14 @@ module SolidApm
|
|
4
4
|
has_many :spans, -> { order(:timestamp, :sequence) }, foreign_key: 'transaction_id', dependent: :delete_all
|
5
5
|
|
6
6
|
attribute :uuid, :string, default: -> { SecureRandom.uuid }
|
7
|
+
|
8
|
+
def self.metadata_filter
|
9
|
+
@metadata_filter ||= ActiveSupport::ParameterFilter
|
10
|
+
.new(Rails.application.config.filter_parameters)
|
11
|
+
end
|
12
|
+
|
13
|
+
def metadata=(value = {})
|
14
|
+
super(self.class.metadata_filter.filter(value))
|
15
|
+
end
|
7
16
|
end
|
8
17
|
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SolidApm
|
4
|
+
class CleanupService
|
5
|
+
# Regex to match safe time expressions like "1.week.ago", "2.months.ago", etc.
|
6
|
+
DURATION_PATTERN = /\A(\d+)\.(second|minute|hour|day|week|month|year)s?\.ago\z/.freeze
|
7
|
+
def initialize(older_than: '1.month.ago')
|
8
|
+
@older_than = older_than
|
9
|
+
end
|
10
|
+
|
11
|
+
def call
|
12
|
+
cutoff_time = parse_time_expression(@older_than)
|
13
|
+
deleted_count = Transaction.where(timestamp: ...cutoff_time).destroy_all.size
|
14
|
+
|
15
|
+
{
|
16
|
+
cutoff_time: cutoff_time,
|
17
|
+
deleted_count: deleted_count,
|
18
|
+
older_than: @older_than
|
19
|
+
}
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def parse_time_expression(expression)
|
25
|
+
match = expression.match(DURATION_PATTERN)
|
26
|
+
raise ArgumentError, 'Invalid time expression format' unless match
|
27
|
+
|
28
|
+
number = match[1].to_i
|
29
|
+
unit = match[2]
|
30
|
+
|
31
|
+
case unit
|
32
|
+
when 'second'
|
33
|
+
number.seconds.ago
|
34
|
+
when 'minute'
|
35
|
+
number.minutes.ago
|
36
|
+
when 'hour'
|
37
|
+
number.hours.ago
|
38
|
+
when 'day'
|
39
|
+
number.days.ago
|
40
|
+
when 'week'
|
41
|
+
number.weeks.ago
|
42
|
+
when 'month'
|
43
|
+
number.months.ago
|
44
|
+
when 'year'
|
45
|
+
number.years.ago
|
46
|
+
else
|
47
|
+
raise ArgumentError, "Unsupported time unit: #{unit}"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
data/lib/solid_apm/engine.rb
CHANGED
@@ -10,6 +10,32 @@ module SolidApm
|
|
10
10
|
app.config.assets.precompile += %w( application.css application.js )
|
11
11
|
end
|
12
12
|
|
13
|
+
begin
|
14
|
+
# Mount the MCP server only if the main app added the fast_mcp in is Gemfile.
|
15
|
+
require 'fast_mcp'
|
16
|
+
initializer "solid_apm.mount_mcp_server" do |app|
|
17
|
+
mcp_server_config = SolidApm.mcp_server_config.reverse_merge(
|
18
|
+
name: 'solid-apm-mcp',
|
19
|
+
version: '1.0.0',
|
20
|
+
path: '/solid_apm/mcp'
|
21
|
+
)
|
22
|
+
|
23
|
+
FastMcp.mount_in_rails(
|
24
|
+
app,
|
25
|
+
**mcp_server_config
|
26
|
+
) do |server|
|
27
|
+
app.config.after_initialize do
|
28
|
+
require_relative 'mcp/spans_for_transaction_tool'
|
29
|
+
require_relative 'mcp/impactful_transactions_resource'
|
30
|
+
server.register_resources(SolidApm::Mcp::ImpactfulTransactionsResource)
|
31
|
+
server.register_tools(SolidApm::Mcp::SpansForTransactionTool)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
rescue LoadError
|
36
|
+
# Ignored
|
37
|
+
end
|
38
|
+
|
13
39
|
config.after_initialize do
|
14
40
|
SpanSubscriber::Base.subscribe!
|
15
41
|
end
|
@@ -0,0 +1,146 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SolidApm
|
4
|
+
module Mcp
|
5
|
+
class ImpactfulTransactionsResource < FastMcp::Resource
|
6
|
+
uri "solid-apm://impactful-transactions"
|
7
|
+
resource_name "impactful_transactions"
|
8
|
+
mime_type "application/json"
|
9
|
+
description "Returns the most impactful transactions with comprehensive performance metrics"
|
10
|
+
|
11
|
+
def content
|
12
|
+
transactions_with_impact = calculate_impactful_transactions
|
13
|
+
|
14
|
+
result = {
|
15
|
+
metadata: {
|
16
|
+
total_transactions_analyzed: SolidApm::Transaction.where(timestamp: 24.hours.ago..).count,
|
17
|
+
analysis_period: "last 24 hours",
|
18
|
+
ordered_by: "More impactful first: based on P95 latency, transaction frequency, span complexity."
|
19
|
+
},
|
20
|
+
transactions: transactions_with_impact.map do |transaction_data|
|
21
|
+
{
|
22
|
+
id: transaction_data[:transaction].id,
|
23
|
+
uuid: transaction_data[:transaction].uuid,
|
24
|
+
name: transaction_data[:transaction].name,
|
25
|
+
type: transaction_data[:transaction].type,
|
26
|
+
metrics: {
|
27
|
+
p95_latency_ms: transaction_data[:p95_latency],
|
28
|
+
avg_duration_ms: transaction_data[:avg_duration],
|
29
|
+
max_duration_ms: transaction_data[:max_duration],
|
30
|
+
transactions_per_minute: transaction_data[:tpm],
|
31
|
+
max_transactions_per_minute: transaction_data[:max_tpm],
|
32
|
+
avg_spans_per_transaction: transaction_data[:avg_spans],
|
33
|
+
max_spans_per_transaction: transaction_data[:max_spans],
|
34
|
+
total_occurrences: transaction_data[:total_count],
|
35
|
+
},
|
36
|
+
sample_transaction: {
|
37
|
+
uuid: transaction_data[:sample_transaction]&.uuid,
|
38
|
+
duration_ms: transaction_data[:sample_transaction]&.duration,
|
39
|
+
span_count: transaction_data[:sample_span_count],
|
40
|
+
timestamp: transaction_data[:sample_transaction]&.timestamp
|
41
|
+
}
|
42
|
+
}
|
43
|
+
end
|
44
|
+
}
|
45
|
+
|
46
|
+
JSON.generate(result)
|
47
|
+
rescue StandardError => e
|
48
|
+
JSON.generate({ error: e.message, backtrace: e.backtrace.first(5) })
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def calculate_impactful_transactions
|
54
|
+
# Get transactions from last 24 hours for more relevant data
|
55
|
+
cutoff_time = 24.hours.ago
|
56
|
+
|
57
|
+
# Group transactions by name and type to aggregate metrics
|
58
|
+
transaction_groups = SolidApm::Transaction.includes(:spans).where(timestamp: cutoff_time..).group_by { |t| [t.name, t.type] }
|
59
|
+
|
60
|
+
impact_data = transaction_groups.map do |group_key, transactions| name, type = group_key
|
61
|
+
durations = transactions.map(&:duration).compact
|
62
|
+
span_counts = transactions.map { |t| t.spans.size }
|
63
|
+
|
64
|
+
next if durations.empty?
|
65
|
+
|
66
|
+
# Calculate P95 latency
|
67
|
+
p95_latency = calculate_percentile(durations, 95)
|
68
|
+
avg_duration = durations.sum / durations.size.to_f
|
69
|
+
max_duration = durations.max
|
70
|
+
|
71
|
+
# Calculate transaction frequency metrics
|
72
|
+
total_count = transactions.size
|
73
|
+
time_span_hours = [(Time.current - transactions.map(&:timestamp).min) / 1.hour, 1].max
|
74
|
+
tpm = (total_count / (time_span_hours * 60)).round(2)
|
75
|
+
|
76
|
+
# Calculate max TPM by looking at busiest minute
|
77
|
+
max_tpm = calculate_max_tpm(transactions)
|
78
|
+
|
79
|
+
# Span complexity metrics
|
80
|
+
avg_spans = span_counts.sum / span_counts.size.to_f
|
81
|
+
max_spans = span_counts.max || 0
|
82
|
+
|
83
|
+
# Get a representative sample transaction
|
84
|
+
sample_transaction = transactions.max_by(&:duration)
|
85
|
+
sample_span_count = sample_transaction&.spans&.size || 0
|
86
|
+
|
87
|
+
{
|
88
|
+
transaction: transactions.first, # Representative transaction for metadata
|
89
|
+
p95_latency: p95_latency.round(2),
|
90
|
+
avg_duration: avg_duration.round(2),
|
91
|
+
max_duration: max_duration.round(2),
|
92
|
+
tpm: tpm,
|
93
|
+
max_tpm: max_tpm,
|
94
|
+
avg_spans: avg_spans.round(1),
|
95
|
+
max_spans: max_spans,
|
96
|
+
total_count: total_count,
|
97
|
+
sample_transaction: sample_transaction,
|
98
|
+
sample_span_count: sample_span_count
|
99
|
+
}
|
100
|
+
end.compact
|
101
|
+
|
102
|
+
# Sort by impact score and return top 10
|
103
|
+
impact_data.sort_by do |data|
|
104
|
+
-calculate_impact_score(p95_latency: data[:p95_latency],
|
105
|
+
tpm: data[:tpm],
|
106
|
+
avg_spans: data[:avg_spans],
|
107
|
+
total_count: data[:total_count])
|
108
|
+
end.first(10)
|
109
|
+
end
|
110
|
+
|
111
|
+
def calculate_percentile(array, percentile)
|
112
|
+
return 0 if array.empty?
|
113
|
+
|
114
|
+
sorted = array.sort
|
115
|
+
index = (percentile / 100.0) * (sorted.length - 1)
|
116
|
+
|
117
|
+
if index == index.to_i
|
118
|
+
sorted[index.to_i]
|
119
|
+
else lower = sorted[index.floor]
|
120
|
+
upper = sorted[index.ceil]
|
121
|
+
lower + (upper - lower) * (index - index.floor)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def calculate_max_tpm(transactions)
|
126
|
+
return 0 if transactions.empty?
|
127
|
+
|
128
|
+
# Group by minute and find the busiest minute
|
129
|
+
minute_counts = transactions.group_by { |t| t.timestamp.beginning_of_minute }.values.map(&:size)
|
130
|
+
minute_counts.max || 0
|
131
|
+
end
|
132
|
+
|
133
|
+
def calculate_impact_score(p95_latency:, tpm:, avg_spans:, total_count:)
|
134
|
+
# Weighted impact score calculation
|
135
|
+
# Higher scores indicate more impactful transactions
|
136
|
+
|
137
|
+
latency_score = Math.log([p95_latency, 1].max) * 20 # Log scale for latency
|
138
|
+
frequency_score = Math.log([tpm, 1].max) * 30 # Log scale for frequency
|
139
|
+
complexity_score = Math.log([avg_spans, 1].max) * 15 # Span complexity
|
140
|
+
volume_score = Math.log([total_count, 1].max) * 10 # Total volume
|
141
|
+
|
142
|
+
(latency_score + frequency_score + complexity_score + volume_score).round(2)
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SolidApm
|
4
|
+
module Mcp
|
5
|
+
class SpansForTransactionTool < FastMcp::Tool
|
6
|
+
tool_name "spans-for-transaction"
|
7
|
+
description "Returns spans for a specific transaction uuid in the APM system, with backtrace information and metadata"
|
8
|
+
|
9
|
+
arguments do
|
10
|
+
required(:transaction_uuid)
|
11
|
+
.filled(:string)
|
12
|
+
.description("The UUID of the transaction to retrieve spans for")
|
13
|
+
end
|
14
|
+
|
15
|
+
def call(transaction_uuid:)
|
16
|
+
transaction = SolidApm::Transaction.find_by!(uuid: transaction_uuid)
|
17
|
+
JSON.generate({
|
18
|
+
transaction: transaction,
|
19
|
+
spans: transaction.spans
|
20
|
+
}.as_json
|
21
|
+
)
|
22
|
+
rescue StandardError => e
|
23
|
+
JSON.generate({
|
24
|
+
error: e.message,
|
25
|
+
backtrace: e.backtrace.first(5)
|
26
|
+
}.as_json)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/lib/solid_apm/middleware.rb
CHANGED
@@ -23,22 +23,51 @@ module SolidApm
|
|
23
23
|
def self.call
|
24
24
|
transaction = SpanSubscriber::Base.transaction
|
25
25
|
SpanSubscriber::Base.transaction = nil
|
26
|
-
|
26
|
+
|
27
|
+
if transaction.nil? ||
|
28
|
+
transaction_filtered?(transaction.name) ||
|
29
|
+
!Sampler.should_sample?
|
30
|
+
|
27
31
|
SpanSubscriber::Base.spans = nil
|
28
32
|
return
|
29
33
|
end
|
30
34
|
|
31
|
-
|
32
|
-
transaction
|
35
|
+
with_silence_logger do
|
36
|
+
ApplicationRecord.transaction do
|
37
|
+
transaction.save!
|
33
38
|
|
34
|
-
|
35
|
-
|
39
|
+
SpanSubscriber::Base.spans.each do |span|
|
40
|
+
span[:transaction_id] = transaction.id
|
41
|
+
end
|
42
|
+
SolidApm::Span.insert_all SpanSubscriber::Base.spans
|
36
43
|
end
|
37
|
-
SolidApm::Span.insert_all SpanSubscriber::Base.spans
|
38
44
|
end
|
39
45
|
SpanSubscriber::Base.spans = nil
|
40
46
|
end
|
41
47
|
|
48
|
+
def self.transaction_filtered?(transaction_name)
|
49
|
+
SolidApm.transaction_filters.any? do |filter|
|
50
|
+
case filter
|
51
|
+
when String
|
52
|
+
transaction_name == filter
|
53
|
+
when Regexp
|
54
|
+
filter.match?(transaction_name)
|
55
|
+
else
|
56
|
+
false
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.with_silence_logger
|
62
|
+
if SolidApm.silence_active_record_logger && ActiveRecord::Base.logger
|
63
|
+
ActiveRecord::Base.logger.silence { yield }
|
64
|
+
else
|
65
|
+
yield
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# Initialize a new transaction and reset spans
|
70
|
+
|
42
71
|
def self.init_transaction
|
43
72
|
now = Time.zone.now
|
44
73
|
SpanSubscriber::Base.transaction = Transaction.new(
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module SolidApm
|
2
|
+
class Sampler
|
3
|
+
def self.should_sample?
|
4
|
+
return true if SolidApm.transaction_sampling <= 1
|
5
|
+
|
6
|
+
thread_counter = Thread.current[:solid_apm_counter] ||= 0
|
7
|
+
Thread.current[:solid_apm_counter] = (thread_counter + 1) % SolidApm.transaction_sampling
|
8
|
+
|
9
|
+
Thread.current[:solid_apm_counter] == 0
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
data/lib/solid_apm/version.rb
CHANGED
data/lib/solid_apm.rb
CHANGED
@@ -3,11 +3,23 @@ require 'groupdate'
|
|
3
3
|
require 'active_median'
|
4
4
|
require 'apexcharts'
|
5
5
|
|
6
|
-
require
|
7
|
-
require
|
6
|
+
require 'solid_apm/version'
|
7
|
+
require 'solid_apm/engine'
|
8
|
+
require 'solid_apm/sampler'
|
9
|
+
require 'solid_apm/cleanup_service'
|
8
10
|
|
9
11
|
module SolidApm
|
10
12
|
mattr_accessor :connects_to
|
13
|
+
mattr_accessor :mcp_server_config, default: {}
|
14
|
+
mattr_accessor :silence_active_record_logger, default: true
|
15
|
+
mattr_accessor :transaction_sampling, default: 1
|
16
|
+
mattr_accessor(
|
17
|
+
:transaction_filters, default: [
|
18
|
+
/^SolidApm::/,
|
19
|
+
/^ActionDispatch::Request::PASS_NOT_FOUND/,
|
20
|
+
'Rails::HealthController#show'
|
21
|
+
]
|
22
|
+
)
|
11
23
|
|
12
24
|
def self.set_context(context)
|
13
25
|
SpanSubscriber::Base.context = context
|
@@ -1,4 +1,18 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
1
|
+
namespace :solid_apm do
|
2
|
+
desc 'Delete old transactions (default: older than 1 month). Usage: rake solid_apm:cleanup[1.week.ago]'
|
3
|
+
task :cleanup, [:older_than] => :environment do |_task, args|
|
4
|
+
older_than = args[:older_than] || '1.month.ago'
|
5
|
+
|
6
|
+
begin
|
7
|
+
result = SolidApm::CleanupService.new(older_than: older_than).call
|
8
|
+
|
9
|
+
puts "Deleting transactions older than #{result[:cutoff_time]}..."
|
10
|
+
puts "Deleted #{result[:deleted_count]} transactions"
|
11
|
+
rescue StandardError => e
|
12
|
+
puts "Error: #{e.message}"
|
13
|
+
puts "Please provide a valid time expression like '1.week.ago', '2.months.ago', etc."
|
14
|
+
puts 'Supported formats: [number].[unit].ago where unit is: second(s), minute(s), hour(s), day(s), week(s), month(s), year(s)'
|
15
|
+
exit 1
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: solid_apm
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.10.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jean-Francis Bastien
|
@@ -125,6 +125,7 @@ files:
|
|
125
125
|
- app/controllers/solid_apm/transactions_controller.rb
|
126
126
|
- app/helpers/solid_apm/application_helper.rb
|
127
127
|
- app/jobs/solid_apm/application_job.rb
|
128
|
+
- app/jobs/solid_apm/cleanup_job.rb
|
128
129
|
- app/models/solid_apm/application_record.rb
|
129
130
|
- app/models/solid_apm/span.rb
|
130
131
|
- app/models/solid_apm/span_subscriber/action_dispatch.rb
|
@@ -145,8 +146,12 @@ files:
|
|
145
146
|
- db/migrate/20240608015633_create_solid_apm_transactions.rb
|
146
147
|
- db/migrate/20240608021940_create_solid_apm_spans.rb
|
147
148
|
- lib/solid_apm.rb
|
149
|
+
- lib/solid_apm/cleanup_service.rb
|
148
150
|
- lib/solid_apm/engine.rb
|
151
|
+
- lib/solid_apm/mcp/impactful_transactions_resource.rb
|
152
|
+
- lib/solid_apm/mcp/spans_for_transaction_tool.rb
|
149
153
|
- lib/solid_apm/middleware.rb
|
154
|
+
- lib/solid_apm/sampler.rb
|
150
155
|
- lib/solid_apm/version.rb
|
151
156
|
- lib/tasks/solid_apm_tasks.rake
|
152
157
|
homepage: https://github.com/Bhacaz/solid_apm
|
@@ -170,7 +175,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
170
175
|
- !ruby/object:Gem::Version
|
171
176
|
version: '0'
|
172
177
|
requirements: []
|
173
|
-
rubygems_version: 3.6.
|
178
|
+
rubygems_version: 3.6.9
|
174
179
|
specification_version: 4
|
175
180
|
summary: SolidApm is a DB base engine for Application Performance Monitoring.
|
176
181
|
test_files: []
|