solid_apm 0.9.0 → 0.11.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 +107 -0
- data/app/assets/javascripts/solid_apm/application.js +19 -0
- data/app/assets/javascripts/solid_apm/time_range_form.js +219 -0
- data/app/assets/stylesheets/solid_apm/application.css +44 -0
- data/app/controllers/solid_apm/transactions_controller.rb +74 -12
- data/app/helpers/solid_apm/application_helper.rb +76 -19
- data/app/jobs/solid_apm/cleanup_job.rb +13 -0
- data/app/models/solid_apm/span_subscriber/base.rb +1 -0
- data/app/views/solid_apm/application/_time_range_form.html.erb +98 -28
- data/app/views/solid_apm/transactions/_charts.html.erb +89 -5
- data/lib/solid_apm/cleanup_service.rb +51 -0
- data/lib/solid_apm/middleware.rb +19 -2
- data/lib/solid_apm/sampler.rb +12 -0
- data/lib/solid_apm/version.rb +1 -1
- data/lib/solid_apm.rb +13 -2
- data/lib/tasks/solid_apm_tasks.rake +18 -4
- metadata +6 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 96ffe2124d7df431b9e6d3bc95f0ed77d047bf387d1dce1008f5bc654519b7b7
|
4
|
+
data.tar.gz: c4ae53a38929c8eb07e30be80e372c07e0e846165295222857008fc5cf442950
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d1278425ff0312598f4d75676c5e6ba6eb1e97d6033dfbc45f85e107323aa334a695c75f4118e7d312b28b5640426d6a4725cb3fa8ed37d5d261f9336c349c94
|
7
|
+
data.tar.gz: e0397780ca57f21a8495d02473f67fd0ab3ad9fa60975d6062a13269a07d6a2a1ad87a32c7201f1891f726cbccc7b8282e14e42bd17fe300bf05eac5a8d9cefe
|
data/README.md
CHANGED
@@ -52,6 +52,113 @@ class ApplicationController
|
|
52
52
|
end
|
53
53
|
```
|
54
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
|
+
|
55
162
|
## MCP Server
|
56
163
|
|
57
164
|
SolidAPM offers an optional MCP server to allow an AI agent to interact with SolidAPM
|
@@ -4,6 +4,25 @@ import {
|
|
4
4
|
} from "https://unpkg.com/@hotwired/stimulus/dist/stimulus.js";
|
5
5
|
window.Stimulus = Application.start();
|
6
6
|
|
7
|
+
// Global function for chart selection
|
8
|
+
window.handleChartSelection = function(minTimestamp, maxTimestamp) {
|
9
|
+
const currentUrl = new URL(window.location.href);
|
10
|
+
const params = new URLSearchParams(currentUrl.search);
|
11
|
+
|
12
|
+
// Convert from milliseconds to seconds (ApexCharts provides timestamps in milliseconds)
|
13
|
+
params.set('from_timestamp', Math.floor(minTimestamp / 1000));
|
14
|
+
params.set('to_timestamp', Math.floor(maxTimestamp / 1000));
|
15
|
+
|
16
|
+
// Remove relative time params
|
17
|
+
params.delete('from_value');
|
18
|
+
params.delete('from_unit');
|
19
|
+
params.delete('to_value');
|
20
|
+
params.delete('to_unit');
|
21
|
+
|
22
|
+
// Navigate to the new URL
|
23
|
+
window.location.href = `${currentUrl.pathname}?${params.toString()}`;
|
24
|
+
};
|
25
|
+
|
7
26
|
//= require_tree .
|
8
27
|
|
9
28
|
// require "./controllers/spans-chart_controller"
|
@@ -0,0 +1,219 @@
|
|
1
|
+
class TimeRangeForm {
|
2
|
+
constructor() {
|
3
|
+
this.form = document.getElementById('time-range-form');
|
4
|
+
this.relativeTab = document.getElementById('relative-tab');
|
5
|
+
this.absoluteTab = document.getElementById('absolute-tab');
|
6
|
+
this.relativePanel = document.getElementById('relative-panel');
|
7
|
+
this.absolutePanel = document.getElementById('absolute-panel');
|
8
|
+
this.customFromControl = document.getElementById('custom-from-control');
|
9
|
+
this.customToControl = document.getElementById('custom-to-control');
|
10
|
+
|
11
|
+
// Timezone handling
|
12
|
+
this.browserTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
13
|
+
this.timezoneOffset = new Date().getTimezoneOffset();
|
14
|
+
|
15
|
+
this.init();
|
16
|
+
}
|
17
|
+
|
18
|
+
init() {
|
19
|
+
this.setupEventListeners();
|
20
|
+
this.initializeFormState();
|
21
|
+
this.addTimezoneToForm();
|
22
|
+
this.adjustAbsoluteTimes();
|
23
|
+
}
|
24
|
+
|
25
|
+
setupEventListeners() {
|
26
|
+
if (this.form) {
|
27
|
+
this.form.addEventListener('submit', (e) => this.handleFormSubmit(e));
|
28
|
+
}
|
29
|
+
}
|
30
|
+
|
31
|
+
switchToRelative(event) {
|
32
|
+
event.preventDefault();
|
33
|
+
|
34
|
+
this.relativeTab.classList.add('is-primary');
|
35
|
+
this.absoluteTab.classList.remove('is-primary');
|
36
|
+
this.relativePanel.classList.remove('is-hidden');
|
37
|
+
this.absolutePanel.classList.add('is-hidden');
|
38
|
+
|
39
|
+
this.removeFields(['from_timestamp', 'to_timestamp']);
|
40
|
+
this.cleanupUrlParams(['from_timestamp', 'to_timestamp', 'from_datetime', 'to_datetime']);
|
41
|
+
}
|
42
|
+
|
43
|
+
switchToAbsolute(event) {
|
44
|
+
event.preventDefault();
|
45
|
+
|
46
|
+
this.absoluteTab.classList.add('is-primary');
|
47
|
+
this.relativeTab.classList.remove('is-primary');
|
48
|
+
this.absolutePanel.classList.remove('is-hidden');
|
49
|
+
this.relativePanel.classList.add('is-hidden');
|
50
|
+
|
51
|
+
this.cleanupUrlParams(['quick_range', 'from_value', 'from_unit', 'to_value', 'to_unit', 'quick_range_apply']);
|
52
|
+
}
|
53
|
+
|
54
|
+
handleQuickRangeChange(select) {
|
55
|
+
const isCustom = select.value === 'custom';
|
56
|
+
|
57
|
+
this.toggleVisibility(this.customFromControl, isCustom);
|
58
|
+
this.toggleVisibility(this.customToControl, isCustom);
|
59
|
+
|
60
|
+
if (!isCustom) {
|
61
|
+
this.applyQuickRange();
|
62
|
+
}
|
63
|
+
}
|
64
|
+
|
65
|
+
applyQuickRange() {
|
66
|
+
const quickRangeSelect = this.form.querySelector('[name="quick_range"]');
|
67
|
+
if (!quickRangeSelect || quickRangeSelect.value === 'custom') return;
|
68
|
+
|
69
|
+
this.removeFields(['from_datetime', 'to_datetime', 'from_timestamp', 'to_timestamp']);
|
70
|
+
this.addHiddenField('quick_range_apply', quickRangeSelect.value);
|
71
|
+
this.form.submit();
|
72
|
+
}
|
73
|
+
|
74
|
+
handleFormSubmit(event) {
|
75
|
+
const isAbsoluteMode = !this.absolutePanel.classList.contains('is-hidden');
|
76
|
+
|
77
|
+
if (isAbsoluteMode) {
|
78
|
+
this.handleAbsoluteModeSubmit();
|
79
|
+
} else {
|
80
|
+
this.handleRelativeModeSubmit();
|
81
|
+
}
|
82
|
+
}
|
83
|
+
|
84
|
+
handleAbsoluteModeSubmit() {
|
85
|
+
const fromDatetime = this.form.querySelector('[name="from_datetime"]');
|
86
|
+
const toDatetime = this.form.querySelector('[name="to_datetime"]');
|
87
|
+
|
88
|
+
if (fromDatetime?.value && toDatetime?.value) {
|
89
|
+
const fromTimestamp = Math.floor(new Date(fromDatetime.value).getTime() / 1000);
|
90
|
+
const toTimestamp = Math.floor(new Date(toDatetime.value).getTime() / 1000);
|
91
|
+
|
92
|
+
fromDatetime.disabled = true;
|
93
|
+
toDatetime.disabled = true;
|
94
|
+
|
95
|
+
this.addHiddenField('from_timestamp', fromTimestamp);
|
96
|
+
this.addHiddenField('to_timestamp', toTimestamp);
|
97
|
+
this.addHiddenField('browser_timezone', this.browserTimezone);
|
98
|
+
this.removeFields(['quick_range', 'from_value', 'from_unit', 'to_value', 'to_unit', 'quick_range_apply']);
|
99
|
+
}
|
100
|
+
}
|
101
|
+
|
102
|
+
handleRelativeModeSubmit() {
|
103
|
+
const quickRangeSelect = this.form.querySelector('[name="quick_range"]');
|
104
|
+
const quickRangeValue = quickRangeSelect?.value;
|
105
|
+
|
106
|
+
if (quickRangeValue && quickRangeValue !== 'custom') {
|
107
|
+
this.removeFields(['from_value', 'from_unit', 'to_value', 'to_unit', 'quick_range_apply']);
|
108
|
+
} else if (quickRangeValue === 'custom') {
|
109
|
+
this.removeFields(['quick_range_apply']);
|
110
|
+
}
|
111
|
+
|
112
|
+
this.removeFields(['from_datetime', 'to_datetime', 'from_timestamp', 'to_timestamp']);
|
113
|
+
}
|
114
|
+
|
115
|
+
initializeFormState() {
|
116
|
+
const urlParams = new URLSearchParams(window.location.search);
|
117
|
+
const hasCustomParams = urlParams.has('from_value') && urlParams.has('from_unit');
|
118
|
+
const hasQuickRange = urlParams.has('quick_range') && urlParams.get('quick_range') !== 'custom';
|
119
|
+
const quickRangeSelect = this.form.querySelector('[name="quick_range"]');
|
120
|
+
|
121
|
+
if (hasQuickRange) {
|
122
|
+
this.toggleVisibility(this.customFromControl, false);
|
123
|
+
this.toggleVisibility(this.customToControl, false);
|
124
|
+
} else if (hasCustomParams || urlParams.get('quick_range') === 'custom') {
|
125
|
+
if (quickRangeSelect) quickRangeSelect.value = 'custom';
|
126
|
+
this.toggleVisibility(this.customFromControl, true);
|
127
|
+
this.toggleVisibility(this.customToControl, true);
|
128
|
+
} else {
|
129
|
+
// Default state - show quick range only
|
130
|
+
this.toggleVisibility(this.customFromControl, false);
|
131
|
+
this.toggleVisibility(this.customToControl, false);
|
132
|
+
}
|
133
|
+
}
|
134
|
+
|
135
|
+
// Utility methods
|
136
|
+
removeFields(fieldNames) {
|
137
|
+
fieldNames.forEach(name => {
|
138
|
+
this.form.querySelectorAll(`[name="${name}"]`).forEach(field => field.remove());
|
139
|
+
});
|
140
|
+
}
|
141
|
+
|
142
|
+
addHiddenField(name, value) {
|
143
|
+
const input = document.createElement('input');
|
144
|
+
input.type = 'hidden';
|
145
|
+
input.name = name;
|
146
|
+
input.value = value;
|
147
|
+
this.form.appendChild(input);
|
148
|
+
}
|
149
|
+
|
150
|
+
toggleVisibility(element, show) {
|
151
|
+
if (!element) return;
|
152
|
+
element.classList.toggle('is-hidden', !show);
|
153
|
+
}
|
154
|
+
|
155
|
+
cleanupUrlParams(params) {
|
156
|
+
const url = new URL(window.location);
|
157
|
+
params.forEach(param => url.searchParams.delete(param));
|
158
|
+
window.history.replaceState({}, '', url);
|
159
|
+
}
|
160
|
+
|
161
|
+
// Timezone-related methods
|
162
|
+
addTimezoneToForm() {
|
163
|
+
// Add timezone information to form for server processing
|
164
|
+
this.addHiddenField('browser_timezone', this.browserTimezone);
|
165
|
+
}
|
166
|
+
|
167
|
+
adjustAbsoluteTimes() {
|
168
|
+
// Convert timestamps from URL to browser timezone for datetime-local inputs
|
169
|
+
const urlParams = new URLSearchParams(window.location.search);
|
170
|
+
const fromTimestamp = urlParams.get('from_timestamp');
|
171
|
+
const toTimestamp = urlParams.get('to_timestamp');
|
172
|
+
|
173
|
+
if (fromTimestamp && toTimestamp) {
|
174
|
+
const fromDatetime = this.form.querySelector('[name="from_datetime"]');
|
175
|
+
const toDatetime = this.form.querySelector('[name="to_datetime"]');
|
176
|
+
|
177
|
+
if (fromDatetime && toDatetime) {
|
178
|
+
// Convert UTC timestamps to local datetime strings
|
179
|
+
const fromDate = new Date(parseInt(fromTimestamp) * 1000);
|
180
|
+
const toDate = new Date(parseInt(toTimestamp) * 1000);
|
181
|
+
|
182
|
+
fromDatetime.value = this.formatDatetimeLocal(fromDate);
|
183
|
+
toDatetime.value = this.formatDatetimeLocal(toDate);
|
184
|
+
}
|
185
|
+
}
|
186
|
+
}
|
187
|
+
|
188
|
+
formatDatetimeLocal(date) {
|
189
|
+
// Format date for datetime-local input (YYYY-MM-DDTHH:MM)
|
190
|
+
const year = date.getFullYear();
|
191
|
+
const month = String(date.getMonth() + 1).padStart(2, '0');
|
192
|
+
const day = String(date.getDate()).padStart(2, '0');
|
193
|
+
const hours = String(date.getHours()).padStart(2, '0');
|
194
|
+
const minutes = String(date.getMinutes()).padStart(2, '0');
|
195
|
+
|
196
|
+
return `${year}-${month}-${day}T${hours}:${minutes}`;
|
197
|
+
}
|
198
|
+
|
199
|
+
}
|
200
|
+
|
201
|
+
// Global functions for onclick handlers (maintaining backward compatibility)
|
202
|
+
let timeRangeFormInstance;
|
203
|
+
|
204
|
+
function switchToRelative(event) {
|
205
|
+
timeRangeFormInstance?.switchToRelative(event);
|
206
|
+
}
|
207
|
+
|
208
|
+
function switchToAbsolute(event) {
|
209
|
+
timeRangeFormInstance?.switchToAbsolute(event);
|
210
|
+
}
|
211
|
+
|
212
|
+
function handleQuickRangeChange(select) {
|
213
|
+
timeRangeFormInstance?.handleQuickRangeChange(select);
|
214
|
+
}
|
215
|
+
|
216
|
+
// Initialize when DOM is ready
|
217
|
+
document.addEventListener('DOMContentLoaded', function() {
|
218
|
+
timeRangeFormInstance = new TimeRangeForm();
|
219
|
+
});
|
@@ -13,3 +13,47 @@
|
|
13
13
|
*= require_tree .
|
14
14
|
*= require_self
|
15
15
|
*/
|
16
|
+
|
17
|
+
/* Time Range Component Enhancements */
|
18
|
+
#time-range-form .box {
|
19
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
20
|
+
}
|
21
|
+
|
22
|
+
#time-range-form .tabs ul {
|
23
|
+
border-bottom: 1px solid #dbdbdb;
|
24
|
+
}
|
25
|
+
|
26
|
+
#time-range-form .field.has-addons .control:not(:last-child) .input,
|
27
|
+
#time-range-form .field.has-addons .control:not(:last-child) .select select {
|
28
|
+
border-right: none;
|
29
|
+
}
|
30
|
+
|
31
|
+
#time-range-form .field.has-addons .control:not(:first-child) .input,
|
32
|
+
#time-range-form .field.has-addons .control:not(:first-child) .select select {
|
33
|
+
border-left: none;
|
34
|
+
}
|
35
|
+
|
36
|
+
#time-range-form .label.is-small {
|
37
|
+
font-size: 0.75rem;
|
38
|
+
margin-bottom: 0.25rem;
|
39
|
+
}
|
40
|
+
|
41
|
+
/* Chart Container Enhancements */
|
42
|
+
#charts-container .level {
|
43
|
+
margin-bottom: 0.75rem;
|
44
|
+
}
|
45
|
+
|
46
|
+
#charts-container .tags.has-addons .tag {
|
47
|
+
font-size: 0.75rem;
|
48
|
+
}
|
49
|
+
|
50
|
+
/* Smooth transitions */
|
51
|
+
#relative-panel,
|
52
|
+
#absolute-panel,
|
53
|
+
#custom-relative-range {
|
54
|
+
transition: opacity 0.2s ease-in-out;
|
55
|
+
}
|
56
|
+
|
57
|
+
.is-hidden {
|
58
|
+
display: none !important;
|
59
|
+
}
|
@@ -13,13 +13,12 @@ module SolidApm
|
|
13
13
|
end
|
14
14
|
|
15
15
|
@transactions_scope = Transaction.where(timestamp: from_to_range)
|
16
|
-
if params[:name].present?
|
17
|
-
@transactions_scope = @transactions_scope.where(name: params[:name])
|
18
|
-
end
|
16
|
+
@transactions_scope = @transactions_scope.where(name: params[:name]) if params[:name].present?
|
19
17
|
transaction_names = @transactions_scope.distinct.pluck(:name)
|
20
18
|
latency_95p = @transactions_scope.group(:name).percentile(:duration, 0.95)
|
21
19
|
latency_median = @transactions_scope.group(:name).median(:duration)
|
22
|
-
tmp_dict = @transactions_scope.group(:name).group_by_minute(:timestamp,
|
20
|
+
tmp_dict = @transactions_scope.group(:name).group_by_minute(:timestamp,
|
21
|
+
series: false).count.each_with_object({}) do |(k, v), h|
|
23
22
|
current_value = h[k.first] ||= 0
|
24
23
|
h[k.first] = v if v > current_value
|
25
24
|
end
|
@@ -38,6 +37,7 @@ module SolidApm
|
|
38
37
|
end
|
39
38
|
|
40
39
|
return if @aggregated_transactions.empty?
|
40
|
+
|
41
41
|
# Find the maximum and minimum impact values
|
42
42
|
max_impact = @aggregated_transactions.values.max_by(&:impact).impact
|
43
43
|
min_impact = @aggregated_transactions.values.min_by(&:impact).impact
|
@@ -50,9 +50,13 @@ module SolidApm
|
|
50
50
|
end
|
51
51
|
@aggregated_transactions = @aggregated_transactions.sort_by { |_, v| -v.impact }.to_h
|
52
52
|
|
53
|
-
|
53
|
+
grouping_method, grouping_options = smart_time_grouping(from_to_range)
|
54
|
+
scope = @transactions_scope.public_send(grouping_method, :timestamp, range: from_to_range, **grouping_options)
|
54
55
|
@throughput_data = scope.count
|
55
56
|
@latency_data = scope.median(:duration).transform_values(&:to_i)
|
57
|
+
|
58
|
+
# Pass browser timezone info to view for chart rendering
|
59
|
+
@browser_timezone = params[:browser_timezone]
|
56
60
|
end
|
57
61
|
|
58
62
|
def spans
|
@@ -63,15 +67,71 @@ module SolidApm
|
|
63
67
|
private
|
64
68
|
|
65
69
|
def from_to_range
|
66
|
-
params[:
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
params[:
|
71
|
-
|
70
|
+
if params[:from_timestamp].present? && params[:to_timestamp].present?
|
71
|
+
# Timestamps from browser are already in UTC/local time
|
72
|
+
from = Time.zone.at(params[:from_timestamp].to_i)
|
73
|
+
to = Time.zone.at(params[:to_timestamp].to_i)
|
74
|
+
elsif params[:quick_range_apply].present? || (params[:quick_range].present? && params[:quick_range] != 'custom')
|
75
|
+
to = Time.current
|
76
|
+
quick_range_value = params[:quick_range_apply] || params[:quick_range]
|
77
|
+
from = case quick_range_value
|
78
|
+
when '5m'
|
79
|
+
5.minutes.ago
|
80
|
+
when '15m'
|
81
|
+
15.minutes.ago
|
82
|
+
when '30m'
|
83
|
+
30.minutes.ago
|
84
|
+
when '1h'
|
85
|
+
1.hour.ago
|
86
|
+
when '3h'
|
87
|
+
3.hours.ago
|
88
|
+
when '6h'
|
89
|
+
6.hours.ago
|
90
|
+
when '12h'
|
91
|
+
12.hours.ago
|
92
|
+
when '24h'
|
93
|
+
24.hours.ago
|
94
|
+
when '3d'
|
95
|
+
3.days.ago
|
96
|
+
when '7d'
|
97
|
+
7.days.ago
|
98
|
+
else
|
99
|
+
1.hour.ago
|
100
|
+
end
|
101
|
+
elsif params[:from_value].present? && params[:from_unit].present?
|
102
|
+
from = params[:from_value].to_i.public_send(params[:from_unit].to_sym).ago
|
103
|
+
to_value = params[:to_value].present? ? params[:to_value].to_i : 0
|
104
|
+
to_unit = params[:to_unit].present? ? params[:to_unit] : 'seconds'
|
105
|
+
to = to_value.public_send(to_unit.to_sym).ago
|
106
|
+
else
|
107
|
+
# Default fallback
|
108
|
+
from = 1.hour.ago
|
109
|
+
to = Time.current
|
110
|
+
end
|
72
111
|
(from..to)
|
73
112
|
end
|
74
113
|
|
114
|
+
def smart_time_grouping(range)
|
115
|
+
duration_seconds = (range.end - range.begin).to_i
|
116
|
+
|
117
|
+
case duration_seconds
|
118
|
+
when 0..3600 # 1 hour or less - group by minute
|
119
|
+
[:group_by_minute, {}]
|
120
|
+
when 3601..86_400 # 1-24 hours - group by 15 minutes
|
121
|
+
[:group_by_minute, { n: 15 }]
|
122
|
+
when 86_401..604_800 # 1-7 days - group by hour
|
123
|
+
[:group_by_hour, {}]
|
124
|
+
when 604_801..2_592_000 # 1 week to 1 month - group by 6 hours
|
125
|
+
[:group_by_hour, { n: 6 }]
|
126
|
+
when 2_592_001..7_776_000 # 1-3 months - group by day
|
127
|
+
[:group_by_day, {}]
|
128
|
+
when 7_776_001..31_536_000 # 3 months to 1 year - group by week
|
129
|
+
[:group_by_week, {}]
|
130
|
+
else # more than 1 year - group by month
|
131
|
+
[:group_by_month, {}]
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
75
135
|
def n_intervals_seconds(range, intervals_count: 30)
|
76
136
|
start_time = range.begin
|
77
137
|
end_time = range.end
|
@@ -85,7 +145,9 @@ module SolidApm
|
|
85
145
|
time_range_in_seconds = (end_time - start_time).to_i
|
86
146
|
time_interval_duration_in_seconds = (time_range_in_seconds / intervals_count.to_f).round
|
87
147
|
|
88
|
-
items.chunk
|
148
|
+
items.chunk do |item|
|
149
|
+
Time.zone.at(item.created_at.to_i / time_interval_duration_in_seconds * time_interval_duration_in_seconds, 0)
|
150
|
+
end.to_h
|
89
151
|
end
|
90
152
|
end
|
91
153
|
end
|
@@ -19,33 +19,90 @@ module SolidApm
|
|
19
19
|
end
|
20
20
|
end
|
21
21
|
|
22
|
-
def area_chart_options
|
22
|
+
def area_chart_options(browser_timezone: nil)
|
23
|
+
timezone_js = if browser_timezone.present?
|
24
|
+
"if (!val) return ''; return new Date(val).toLocaleString('en-US', { timeZone: '#{browser_timezone}', hour12: false })"
|
25
|
+
else
|
26
|
+
"if (!val) return ''; return new Date(val).toLocaleString('en-US', { hour12: false })"
|
27
|
+
end
|
28
|
+
|
29
|
+
# Create date formatter for x-axis labels that respects timezone
|
30
|
+
xaxis_formatter = if browser_timezone.present?
|
31
|
+
"if (!val) return ''; return new Date(val).toLocaleString('en-US', { timeZone: '#{browser_timezone}', hour: '2-digit', minute: '2-digit', day: '2-digit', month: 'short', hour12: false })"
|
32
|
+
else
|
33
|
+
"if (!val) return ''; return new Date(val).toLocaleString('en-US', { hour: '2-digit', minute: '2-digit', day: '2-digit', month: 'short', hour12: false })"
|
34
|
+
end
|
35
|
+
|
23
36
|
{
|
24
37
|
module: true,
|
25
38
|
chart: {
|
26
|
-
type: 'area',
|
27
|
-
|
28
|
-
|
29
|
-
|
39
|
+
type: 'area',
|
40
|
+
height: '200',
|
41
|
+
background: '0',
|
42
|
+
foreColor: '#ffffff55',
|
43
|
+
zoom: {
|
44
|
+
enabled: true,
|
45
|
+
autoScaleYaxis: true
|
46
|
+
},
|
47
|
+
selection: {
|
48
|
+
enabled: true,
|
49
|
+
type: 'x'
|
50
|
+
},
|
51
|
+
toolbar: {
|
52
|
+
show: false
|
53
|
+
},
|
54
|
+
events: {
|
55
|
+
zoomed: {
|
56
|
+
function: {
|
57
|
+
args: 'chartContext, { xaxis, yaxis }',
|
58
|
+
body: 'handleChartSelection(xaxis.min, xaxis.max)'
|
59
|
+
}
|
60
|
+
},
|
61
|
+
selection: {
|
62
|
+
function: {
|
63
|
+
args: 'chartContext, { xaxis, yaxis }',
|
64
|
+
body: 'handleChartSelection(xaxis.min, xaxis.max)'
|
65
|
+
}
|
66
|
+
}
|
30
67
|
}
|
31
68
|
},
|
32
69
|
xaxis: {
|
33
|
-
|
34
|
-
|
70
|
+
type: 'datetime',
|
71
|
+
tooltip: {
|
72
|
+
enabled: false
|
73
|
+
},
|
74
|
+
labels: {
|
75
|
+
formatter: {
|
76
|
+
function: {
|
77
|
+
args: 'val',
|
78
|
+
body: xaxis_formatter
|
79
|
+
}
|
80
|
+
}
|
81
|
+
}
|
82
|
+
},
|
83
|
+
stroke: {
|
84
|
+
curve: 'smooth'
|
85
|
+
},
|
86
|
+
theme: {
|
87
|
+
mode: 'dark'
|
88
|
+
},
|
89
|
+
grid: {
|
90
|
+
show: true,
|
91
|
+
borderColor: '#ffffff55'
|
92
|
+
},
|
93
|
+
dataLabels: {
|
35
94
|
enabled: false
|
36
|
-
|
95
|
+
},
|
96
|
+
tooltip: {
|
97
|
+
x: {
|
98
|
+
formatter: {
|
99
|
+
function: {
|
100
|
+
args: 'val',
|
101
|
+
body: timezone_js
|
102
|
+
}
|
103
|
+
}
|
104
|
+
}
|
37
105
|
}
|
38
|
-
},
|
39
|
-
stroke: {
|
40
|
-
curve: 'smooth'
|
41
|
-
}, theme: {
|
42
|
-
mode: 'dark',
|
43
|
-
}, grid: {
|
44
|
-
show: true, borderColor: '#ffffff55',
|
45
|
-
}, dataLabels: {
|
46
|
-
enabled: false
|
47
|
-
},
|
48
|
-
tooltip: {x: {formatter: {function: {args: "val", body: "return new Date(val).toLocaleString()"}} }}
|
49
106
|
}
|
50
107
|
end
|
51
108
|
end
|
@@ -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
|
@@ -1,34 +1,104 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
1
|
+
<%
|
2
|
+
is_absolute_mode = params[:from_timestamp].present?
|
3
|
+
is_custom_range = (params[:from_value].present? && params[:from_unit].present?) || params[:quick_range] == 'custom'
|
4
|
+
|
5
|
+
time_ranges = {
|
6
|
+
"Last 5 minutes" => "5m", "Last 15 minutes" => "15m", "Last 30 minutes" => "30m",
|
7
|
+
"Last 1 hour" => "1h", "Last 3 hours" => "3h", "Last 6 hours" => "6h",
|
8
|
+
"Last 12 hours" => "12h", "Last 24 hours" => "24h", "Last 3 days" => "3d",
|
9
|
+
"Last 7 days" => "7d", "Custom" => "custom"
|
10
|
+
}
|
11
|
+
|
12
|
+
time_units = %w[minutes hours days weeks months]
|
13
|
+
to_time_units = %w[seconds minutes hours days weeks]
|
14
|
+
%>
|
15
|
+
|
16
|
+
<%= form_with path: transactions_path, method: :get, local: true, id: 'time-range-form' do |f| %>
|
17
|
+
<div class="field is-grouped is-justify-content-center" style="margin-bottom: 1rem;">
|
18
|
+
<div class="control">
|
19
|
+
<div class="buttons has-addons">
|
20
|
+
<% [['Relative', !is_absolute_mode], ['Absolute', is_absolute_mode]].each do |label, is_active| %>
|
21
|
+
<button type="button"
|
22
|
+
class="button is-small <%= 'is-primary' if is_active %>"
|
23
|
+
id="<%= label.downcase %>-tab"
|
24
|
+
onclick="switchTo<%= label %>(event)">
|
25
|
+
<%= label %>
|
26
|
+
</button>
|
27
|
+
<% end %>
|
28
|
+
</div>
|
13
29
|
</div>
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
30
|
+
|
31
|
+
</div>
|
32
|
+
|
33
|
+
<div id="relative-panel" class="<%= 'is-hidden' if is_absolute_mode %>">
|
34
|
+
<div class="field is-grouped is-grouped-centered is-align-items-end" style="flex-wrap: nowrap;">
|
35
|
+
<div class="control">
|
36
|
+
<div class="select">
|
37
|
+
<%= f.select :quick_range, time_ranges, {
|
38
|
+
selected: params[:quick_range] || (is_custom_range ? 'custom' : '1h'),
|
39
|
+
include_blank: false
|
40
|
+
}, {
|
41
|
+
class: 'select-input',
|
42
|
+
onchange: 'handleQuickRangeChange(this)'
|
43
|
+
} %>
|
44
|
+
</div>
|
45
|
+
</div>
|
46
|
+
|
47
|
+
<% [
|
48
|
+
{ field: :from_value, default: 60, min: 1, units: time_units, default_unit: 'minutes', label: 'From', id: 'custom-from-control' },
|
49
|
+
{ field: :to_value, default: 0, min: 0, units: to_time_units, default_unit: 'seconds', label: 'To', id: 'custom-to-control' }
|
50
|
+
].each do |config| %>
|
51
|
+
<div class="control <%= 'is-hidden' unless is_custom_range %>" id="<%= config[:id] %>">
|
52
|
+
<label class="label is-small"><%= config[:label] %></label>
|
53
|
+
<div class="field has-addons">
|
54
|
+
<div class="control">
|
55
|
+
<%= f.number_field config[:field],
|
56
|
+
value: params[config[:field]] || config[:default],
|
57
|
+
min: config[:min],
|
58
|
+
class: 'input has-text-centered',
|
59
|
+
style: 'width: 3.5em' %>
|
60
|
+
</div>
|
61
|
+
<div class="control">
|
62
|
+
<div class="select">
|
63
|
+
<%= f.select "#{config[:field].to_s.split('_')[0]}_unit", config[:units],
|
64
|
+
{ selected: params["#{config[:field].to_s.split('_')[0]}_unit"] || config[:default_unit] } %>
|
65
|
+
</div>
|
66
|
+
</div>
|
67
|
+
<div class="control">
|
68
|
+
<span class="button is-static">ago</span>
|
69
|
+
</div>
|
70
|
+
</div>
|
71
|
+
</div>
|
72
|
+
<% end %>
|
73
|
+
|
74
|
+
<div class="control">
|
75
|
+
<%= f.submit 'Apply', class: 'button is-primary' %>
|
76
|
+
</div>
|
26
77
|
</div>
|
78
|
+
</div>
|
27
79
|
|
28
|
-
|
29
|
-
<%= f.hidden_field :name, value: params[:name] %>
|
30
|
-
<% end %>
|
80
|
+
<%= f.hidden_field :name, value: params[:name] if params[:name] %>
|
31
81
|
|
32
|
-
|
82
|
+
<div id="absolute-panel" class="<%= 'is-hidden' unless is_absolute_mode %>">
|
83
|
+
<div class="field is-grouped is-grouped-centered is-align-items-end">
|
84
|
+
<% [['from', 1.hour.ago], ['to', Time.current]].each do |prefix, default_time| %>
|
85
|
+
<div class="control">
|
86
|
+
<label class="label is-small">
|
87
|
+
<%= prefix.capitalize %>
|
88
|
+
</label>
|
89
|
+
<%= f.datetime_local_field "#{prefix}_datetime",
|
90
|
+
value: (params["#{prefix}_timestamp"] ?
|
91
|
+
Time.zone.at(params["#{prefix}_timestamp"].to_i).strftime('%Y-%m-%dT%H:%M') :
|
92
|
+
default_time.strftime('%Y-%m-%dT%H:%M')),
|
93
|
+
class: 'input',
|
94
|
+
id: "#{prefix}_datetime" %>
|
95
|
+
</div>
|
96
|
+
<% end %>
|
97
|
+
<div class="control">
|
98
|
+
<%= f.submit 'Apply', class: 'button is-primary' %>
|
99
|
+
</div>
|
100
|
+
</div>
|
33
101
|
</div>
|
34
102
|
<% end %>
|
103
|
+
|
104
|
+
<%= javascript_include_tag 'solid_apm/time_range_form' %>
|
@@ -1,10 +1,94 @@
|
|
1
|
-
<div class="columns pt-2" style="height: 16em">
|
1
|
+
<div class="columns pt-2" style="height: 16em" id="charts-container">
|
2
2
|
<div class="column">
|
3
|
-
<
|
4
|
-
|
3
|
+
<div class="level">
|
4
|
+
<div class="level-left">
|
5
|
+
<div class="level-item">
|
6
|
+
<h2 class="title is-5">Throughput</h2>
|
7
|
+
</div>
|
8
|
+
</div>
|
9
|
+
</div>
|
10
|
+
<div id="throughput-chart">
|
11
|
+
<%= area_chart({ name: 'req/min', data: @throughput_data }, area_chart_options(browser_timezone: @browser_timezone).merge(colors: ['#43BCCD'], refresh: 30)) %>
|
12
|
+
</div>
|
5
13
|
</div>
|
6
14
|
<div class="column">
|
7
|
-
<
|
8
|
-
|
15
|
+
<div class="level">
|
16
|
+
<div class="level-left">
|
17
|
+
<div class="level-item">
|
18
|
+
<h2 class="title is-5">Latency</h2>
|
19
|
+
</div>
|
20
|
+
</div>
|
21
|
+
</div>
|
22
|
+
<div id="latency-chart">
|
23
|
+
<%= area_chart({ name: 'ms', data: @latency_data }, area_chart_options(browser_timezone: @browser_timezone).merge(colors: ['#13d8aa'], refresh: 30)) %>
|
24
|
+
</div>
|
9
25
|
</div>
|
10
26
|
</div>
|
27
|
+
|
28
|
+
<script>
|
29
|
+
// Chart selection functionality for time range
|
30
|
+
function handleChartSelection(minTimestamp, maxTimestamp) {
|
31
|
+
const form = document.getElementById('time-range-form');
|
32
|
+
if (!form) return;
|
33
|
+
|
34
|
+
// Convert chart selection to timestamps
|
35
|
+
const fromTimestamp = Math.floor(minTimestamp / 1000);
|
36
|
+
const toTimestamp = Math.floor(maxTimestamp / 1000);
|
37
|
+
|
38
|
+
// Get browser timezone
|
39
|
+
const browserTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
40
|
+
|
41
|
+
// Create new URL with absolute time range
|
42
|
+
const url = new URL(window.location.href);
|
43
|
+
url.searchParams.set('from_timestamp', fromTimestamp);
|
44
|
+
url.searchParams.set('to_timestamp', toTimestamp);
|
45
|
+
url.searchParams.set('browser_timezone', browserTimezone);
|
46
|
+
|
47
|
+
// Remove relative time parameters
|
48
|
+
['quick_range', 'quick_range_apply', 'from_value', 'from_unit', 'to_value', 'to_unit'].forEach(param => {
|
49
|
+
url.searchParams.delete(param);
|
50
|
+
});
|
51
|
+
|
52
|
+
// Navigate to new URL
|
53
|
+
window.location.href = url.toString();
|
54
|
+
}
|
55
|
+
|
56
|
+
// Chart refresh functionality
|
57
|
+
function refreshCharts() {
|
58
|
+
const form = document.getElementById('time-range-form');
|
59
|
+
if (form) {
|
60
|
+
const formData = new FormData(form);
|
61
|
+
const params = new URLSearchParams(formData);
|
62
|
+
|
63
|
+
fetch('<%= transactions_path %>?' + params.toString(), {
|
64
|
+
headers: {
|
65
|
+
'Accept': 'text/html',
|
66
|
+
'X-Requested-With': 'XMLHttpRequest'
|
67
|
+
}
|
68
|
+
})
|
69
|
+
.then(response => response.text())
|
70
|
+
.then(html => {
|
71
|
+
const parser = new DOMParser();
|
72
|
+
const doc = parser.parseFromString(html, 'text/html');
|
73
|
+
const newChartsContainer = doc.getElementById('charts-container');
|
74
|
+
if (newChartsContainer) {
|
75
|
+
document.getElementById('charts-container').innerHTML = newChartsContainer.innerHTML;
|
76
|
+
}
|
77
|
+
})
|
78
|
+
.catch(error => console.error('Error refreshing charts:', error));
|
79
|
+
}
|
80
|
+
}
|
81
|
+
|
82
|
+
// Auto-refresh charts based on time range
|
83
|
+
document.addEventListener('DOMContentLoaded', function() {
|
84
|
+
const currentUrl = new URL(window.location.href);
|
85
|
+
const hasRecentTimeRange = currentUrl.searchParams.get('quick_range_apply') === '5m' ||
|
86
|
+
currentUrl.searchParams.get('quick_range_apply') === '15m' ||
|
87
|
+
currentUrl.searchParams.get('quick_range_apply') === '30m';
|
88
|
+
|
89
|
+
if (hasRecentTimeRange) {
|
90
|
+
// Auto-refresh every 30 seconds for recent time ranges
|
91
|
+
setInterval(refreshCharts, 30000);
|
92
|
+
}
|
93
|
+
});
|
94
|
+
</script>
|
@@ -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/middleware.rb
CHANGED
@@ -23,7 +23,11 @@ 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
|
@@ -41,8 +45,21 @@ module SolidApm
|
|
41
45
|
SpanSubscriber::Base.spans = nil
|
42
46
|
end
|
43
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
|
+
|
44
61
|
def self.with_silence_logger
|
45
|
-
if ActiveRecord::Base.logger
|
62
|
+
if SolidApm.silence_active_record_logger && ActiveRecord::Base.logger
|
46
63
|
ActiveRecord::Base.logger.silence { yield }
|
47
64
|
else
|
48
65
|
yield
|
@@ -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,12 +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
|
11
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
|
+
)
|
12
23
|
|
13
24
|
def self.set_context(context)
|
14
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.11.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jean-Francis Bastien
|
@@ -119,12 +119,14 @@ files:
|
|
119
119
|
- Rakefile
|
120
120
|
- app/assets/config/solid_apm_manifest.js
|
121
121
|
- app/assets/javascripts/solid_apm/application.js
|
122
|
+
- app/assets/javascripts/solid_apm/time_range_form.js
|
122
123
|
- app/assets/stylesheets/solid_apm/application.css
|
123
124
|
- app/controllers/solid_apm/application_controller.rb
|
124
125
|
- app/controllers/solid_apm/spans_controller.rb
|
125
126
|
- app/controllers/solid_apm/transactions_controller.rb
|
126
127
|
- app/helpers/solid_apm/application_helper.rb
|
127
128
|
- app/jobs/solid_apm/application_job.rb
|
129
|
+
- app/jobs/solid_apm/cleanup_job.rb
|
128
130
|
- app/models/solid_apm/application_record.rb
|
129
131
|
- app/models/solid_apm/span.rb
|
130
132
|
- app/models/solid_apm/span_subscriber/action_dispatch.rb
|
@@ -145,10 +147,12 @@ files:
|
|
145
147
|
- db/migrate/20240608015633_create_solid_apm_transactions.rb
|
146
148
|
- db/migrate/20240608021940_create_solid_apm_spans.rb
|
147
149
|
- lib/solid_apm.rb
|
150
|
+
- lib/solid_apm/cleanup_service.rb
|
148
151
|
- lib/solid_apm/engine.rb
|
149
152
|
- lib/solid_apm/mcp/impactful_transactions_resource.rb
|
150
153
|
- lib/solid_apm/mcp/spans_for_transaction_tool.rb
|
151
154
|
- lib/solid_apm/middleware.rb
|
155
|
+
- lib/solid_apm/sampler.rb
|
152
156
|
- lib/solid_apm/version.rb
|
153
157
|
- lib/tasks/solid_apm_tasks.rake
|
154
158
|
homepage: https://github.com/Bhacaz/solid_apm
|
@@ -172,7 +176,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
172
176
|
- !ruby/object:Gem::Version
|
173
177
|
version: '0'
|
174
178
|
requirements: []
|
175
|
-
rubygems_version: 3.6.
|
179
|
+
rubygems_version: 3.6.9
|
176
180
|
specification_version: 4
|
177
181
|
summary: SolidApm is a DB base engine for Application Performance Monitoring.
|
178
182
|
test_files: []
|