solid_apm 0.8.2 → 0.9.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 +46 -0
- data/app/models/solid_apm/span_subscriber/action_dispatch.rb +1 -1
- data/app/models/solid_apm/transaction.rb +9 -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 +18 -6
- data/lib/solid_apm/version.rb +1 -1
- data/lib/solid_apm.rb +1 -0
- metadata +3 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: dc8464230e852711199328548a375cc742873a408f04b34fd793e305d854420a
|
4
|
+
data.tar.gz: 27a42427c5fb310b459bcf673c9fedd4dc1a0d0852a15fb4fa367bc6fed475a8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c249809b73d29facaccf681ba95bf60fce23debccc69850f8bb309d826d5b13e7ec093d2acb5346518e0e1c8b5a479743735fa7071a69b63e5a3672f3c5edb00
|
7
|
+
data.tar.gz: '093e4a4e39e436b26307a67559517ac2e1cc3e3375aa7afeb980db674301e0571b74e74ad68ca8895667557465461e790b1158d0ab8c48f16f53ad301cb37eb8'
|
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,51 @@ class ApplicationController
|
|
51
52
|
end
|
52
53
|
```
|
53
54
|
|
55
|
+
## MCP Server
|
56
|
+
|
57
|
+
SolidAPM offers an optional MCP server to allow an AI agent to interact with SolidAPM
|
58
|
+
and help identify issues in your application, such as
|
59
|
+
N+1 queries, slow queries and more. The AI agent can analyze and suggest fixes for these issues.
|
60
|
+
|
61
|
+
### MCP Server Configuration
|
62
|
+
|
63
|
+
The MCP server is only mounted if the [fast-mcp](https://github.com/yjacquin/fast-mcp) gem is installed by your application.
|
64
|
+
|
65
|
+
1. Add to your Gemfile:
|
66
|
+
|
67
|
+
```ruby
|
68
|
+
# Work in progress, plus patch for MCP 2025-06-18 Protocol Revision
|
69
|
+
# with StreamableHTTP support
|
70
|
+
# https://github.com/yjacquin/fast-mcp/issues/109
|
71
|
+
gem 'fast-mcp', branch: 'transport', github: 'Bhacaz/fast-mcp'
|
72
|
+
```
|
73
|
+
|
74
|
+
2. Configure the MCP server in your `config/initializers/solid_apm.rb`:
|
75
|
+
|
76
|
+
```ruby
|
77
|
+
SolidApm.mcp_server_config = {
|
78
|
+
name: 'my-app-solid-apm',
|
79
|
+
path: '/solid_apm/mcp',
|
80
|
+
auth_token: Rails.application.credentials.solid_apm[:mcp_auth_token]
|
81
|
+
}
|
82
|
+
```
|
83
|
+
|
84
|
+
3. Test the MCP server by running:
|
85
|
+
|
86
|
+
```shell
|
87
|
+
curl -X POST http://localhost:3000/solid_apm/mcp \
|
88
|
+
-H "Content-Type: application/json" \
|
89
|
+
-H "Accept: application/json" \
|
90
|
+
-H "Authorization: Bearer <AUTH_TOKEN>" \
|
91
|
+
-d '{"jsonrpc":"2.0","method":"tools/list","id":1}
|
92
|
+
```
|
93
|
+
|
94
|
+
### MCP usage
|
95
|
+
|
96
|
+
1. Add the MCP resource `impactful-transactions` to the context of your prompt.
|
97
|
+
2. Prompt example: "Analyze the impactful transactions of my application and suggest improvements, base on the spans details."
|
98
|
+
3. Allow the AI agent to use the MCP tool `spans-for-transaction` to retrieve the longest spans for a specific transaction.
|
99
|
+
|
54
100
|
## TODOs
|
55
101
|
|
56
102
|
### Features
|
@@ -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
|
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,34 @@ module SolidApm
|
|
23
23
|
def self.call
|
24
24
|
transaction = SpanSubscriber::Base.transaction
|
25
25
|
SpanSubscriber::Base.transaction = nil
|
26
|
-
if transaction.nil? || transaction.name.start_with?('SolidApm::')
|
26
|
+
if transaction.nil? || transaction.name.start_with?('SolidApm::') || transaction.name.start_with?('ActionDispatch::Request::PASS_NOT_FOUND')
|
27
27
|
SpanSubscriber::Base.spans = nil
|
28
28
|
return
|
29
29
|
end
|
30
30
|
|
31
|
-
|
32
|
-
transaction
|
31
|
+
with_silence_logger do
|
32
|
+
ApplicationRecord.transaction do
|
33
|
+
transaction.save!
|
33
34
|
|
34
|
-
|
35
|
-
|
35
|
+
SpanSubscriber::Base.spans.each do |span|
|
36
|
+
span[:transaction_id] = transaction.id
|
37
|
+
end
|
38
|
+
SolidApm::Span.insert_all SpanSubscriber::Base.spans
|
36
39
|
end
|
37
|
-
SolidApm::Span.insert_all SpanSubscriber::Base.spans
|
38
40
|
end
|
39
41
|
SpanSubscriber::Base.spans = nil
|
40
42
|
end
|
41
43
|
|
44
|
+
def self.with_silence_logger
|
45
|
+
if ActiveRecord::Base.logger
|
46
|
+
ActiveRecord::Base.logger.silence { yield }
|
47
|
+
else
|
48
|
+
yield
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Initialize a new transaction and reset spans
|
53
|
+
|
42
54
|
def self.init_transaction
|
43
55
|
now = Time.zone.now
|
44
56
|
SpanSubscriber::Base.transaction = Transaction.new(
|
data/lib/solid_apm/version.rb
CHANGED
data/lib/solid_apm.rb
CHANGED
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.9.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jean-Francis Bastien
|
@@ -146,6 +146,8 @@ files:
|
|
146
146
|
- db/migrate/20240608021940_create_solid_apm_spans.rb
|
147
147
|
- lib/solid_apm.rb
|
148
148
|
- lib/solid_apm/engine.rb
|
149
|
+
- lib/solid_apm/mcp/impactful_transactions_resource.rb
|
150
|
+
- lib/solid_apm/mcp/spans_for_transaction_tool.rb
|
149
151
|
- lib/solid_apm/middleware.rb
|
150
152
|
- lib/solid_apm/version.rb
|
151
153
|
- lib/tasks/solid_apm_tasks.rake
|