builder_apm 0.4.2 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Gemfile.lock +1 -1
- data/app/controllers/builder_apm/diagnose_request_controller.rb +79 -0
- data/app/controllers/builder_apm/request_details_controller.rb +3 -2
- data/app/views/builder_apm/css/_dark.html.erb +161 -118
- data/app/views/builder_apm/css/_main.html.erb +342 -272
- data/app/views/builder_apm/js/_data_fetcher.html.erb +229 -236
- data/app/views/builder_apm/js/_request_details.html.erb +368 -164
- data/builder_apm.gemspec +1 -1
- data/config/routes.rb +3 -0
- data/lib/builder_apm/configuration.rb +6 -0
- data/lib/builder_apm/doctor/ai_doctor.rb +170 -0
- data/lib/builder_apm/doctor/backtrace_reducer.rb +104 -0
- data/lib/builder_apm/doctor/bravo_chat_ai.rb +85 -0
- data/lib/builder_apm/doctor/openai_chat_gpt.rb +84 -0
- data/lib/builder_apm/methods/instrumenter.rb +33 -6
- data/lib/builder_apm/middleware/timing.rb +6 -0
- data/lib/builder_apm/models/instrumenter.rb +15 -3
- data/lib/builder_apm/version.rb +1 -1
- data/lib/generators/builder_apm/templates/builder_apm_config.rb +7 -4
- metadata +8 -4
- data/README.md +0 -29
@@ -2,274 +2,267 @@
|
|
2
2
|
<%= render 'builder_apm/js/compress' %>
|
3
3
|
|
4
4
|
<script>
|
5
|
-
var allData = null;
|
6
|
-
var isAsc = false;
|
7
|
-
var current_sort_field = 'real_start_time';
|
8
|
-
var start_time = Date.now();
|
9
|
-
var data_gathered_counter = 0;
|
10
|
-
var time_cursor = <%= Time.now.to_f * 1000 %>;
|
11
|
-
var limit = 500;
|
12
|
-
|
13
|
-
function fetchDataAndUpdateStorage(onSuccess) {
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
}
|
23
|
-
function fetchAndProcessData(cursor, onSuccess) {
|
24
|
-
var type = typeof fetch_type !== 'undefined' ? fetch_type : 'timestamps';
|
25
|
-
|
26
|
-
// Make an AJAX request to fetch latest data
|
27
|
-
$.get('/builder_apm/request_data?cursor=' + cursor + '&limit=' + limit + '&type=' +type, function(newData) {
|
28
|
-
data_gathered_counter++;
|
29
|
-
newData = newData.map(request => calcDurations(request));
|
30
|
-
|
31
|
-
processData(allData, null, newData, onSuccess);
|
32
|
-
|
33
|
-
});
|
34
|
-
}
|
35
|
-
|
36
|
-
function loadLocalData(onSuccess) {
|
37
|
-
if(allData) {
|
38
|
-
onSuccess(allData);
|
39
|
-
} else {
|
40
|
-
var storedData = localStorage.getItem('builder_apm_requests');
|
41
|
-
if (storedData) {
|
42
|
-
var dataCompressed = decompress(storedData, function(result, error) {
|
43
|
-
processData(JSON.parse(result), error, [], onSuccess);
|
44
|
-
});
|
45
|
-
} else {
|
46
|
-
allData = [];
|
47
|
-
fetchDataAndUpdateStorage(onSuccess);
|
5
|
+
var allData = null;
|
6
|
+
var isAsc = false;
|
7
|
+
var current_sort_field = 'real_start_time';
|
8
|
+
var start_time = Date.now();
|
9
|
+
var data_gathered_counter = 0;
|
10
|
+
var time_cursor = <%= Time.now.to_f * 1000 %>;
|
11
|
+
var limit = 500;
|
12
|
+
|
13
|
+
function fetchDataAndUpdateStorage(onSuccess) {
|
14
|
+
start_time = Date.now();
|
15
|
+
var initialCursor = time_cursor || start_time;
|
16
|
+
if (allData == null) {
|
17
|
+
allData = [];
|
18
|
+
}
|
19
|
+
// Start fetching data
|
20
|
+
fetchAndProcessData(initialCursor, onSuccess);
|
21
|
+
|
48
22
|
}
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
time_cursor = lastRequest.end_time-0.001;
|
62
|
-
}
|
63
|
-
} catch(e) {
|
64
|
-
console.error("Error storing data to local storage. Might be out of storage space.", e);
|
65
|
-
alert('not enough local storage')
|
23
|
+
|
24
|
+
function fetchAndProcessData(cursor, onSuccess) {
|
25
|
+
var type = typeof fetch_type !== 'undefined' ? fetch_type : 'timestamps';
|
26
|
+
|
27
|
+
// Make an AJAX request to fetch latest data
|
28
|
+
$.get('/builder_apm/request_data?cursor=' + cursor + '&limit=' + limit + '&type=' + type, function (newData) {
|
29
|
+
data_gathered_counter++;
|
30
|
+
newData = newData.map(request => calcDurations(request));
|
31
|
+
|
32
|
+
processData(allData, null, newData, onSuccess);
|
33
|
+
|
34
|
+
});
|
66
35
|
}
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
36
|
+
|
37
|
+
function loadLocalData(onSuccess) {
|
38
|
+
if (allData) {
|
39
|
+
onSuccess(allData);
|
40
|
+
} else {
|
41
|
+
var storedData = localStorage.getItem('builder_apm_requests');
|
42
|
+
if (storedData) {
|
43
|
+
var dataCompressed = decompress(storedData, function (result, error) {
|
44
|
+
processData(JSON.parse(result), error, [], onSuccess);
|
45
|
+
});
|
46
|
+
} else {
|
47
|
+
allData = [];
|
48
|
+
fetchDataAndUpdateStorage(onSuccess);
|
49
|
+
}
|
50
|
+
}
|
51
|
+
}
|
52
|
+
|
53
|
+
function processData(storedData, error, newData, onSuccess) { // Append the new data to the stored data
|
54
|
+
allData = storedData.concat(newData);
|
55
|
+
try { // Update the cursor
|
56
|
+
if (newData.length > 0) {
|
57
|
+
var lastRequest = newData[newData.length - 1];
|
58
|
+
time_cursor = lastRequest.end_time - 0.001;
|
59
|
+
}
|
60
|
+
} catch (e) {
|
61
|
+
console.error("Error storing data to local storage. Might be out of storage space.", e);
|
62
|
+
alert('not enough local storage')
|
63
|
+
}
|
64
|
+
// Callback function after data fetched and stored
|
65
|
+
console.log(allData);
|
66
|
+
onSuccess(allData);
|
67
|
+
}
|
68
|
+
|
69
|
+
function loadRequest(request_id) {
|
70
|
+
var requestData = allData.find(function (item) {
|
71
|
+
return item['request_id'] == request_id;
|
89
72
|
});
|
90
|
-
|
91
|
-
|
92
|
-
setTimeout(function() {
|
93
|
-
autoFetchDataAndUpdateStorage(onSuccess);
|
94
|
-
}, 5000);
|
73
|
+
|
74
|
+
return requestData;
|
95
75
|
}
|
96
|
-
}
|
97
76
|
|
98
|
-
function
|
99
|
-
|
100
|
-
|
77
|
+
function autoFetchDataAndUpdateStorage(onSuccess) {
|
78
|
+
var autoUpdate = $('#autoUpdate').is(':checked');
|
79
|
+
if (autoUpdate) {
|
80
|
+
fetchDataAndUpdateStorage(function (updatedData) {
|
81
|
+
onSuccess(updatedData);
|
82
|
+
// Schedule the next run
|
83
|
+
setTimeout(function () {
|
84
|
+
autoFetchDataAndUpdateStorage(onSuccess);
|
85
|
+
}, 5000);
|
86
|
+
});
|
87
|
+
} else { // If auto update is not checked, schedule the next check
|
88
|
+
setTimeout(function () {
|
89
|
+
autoFetchDataAndUpdateStorage(onSuccess);
|
90
|
+
}, 5000);
|
91
|
+
}
|
101
92
|
}
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
index: index
|
107
|
-
}));
|
108
|
-
|
109
|
-
let queriesCount = {};
|
110
|
-
sqlQueries.forEach(queryData => {
|
111
|
-
let table = queryData.sql.match(/FROM ['`"]*([^ '`"]+)['`"]*/i)
|
112
|
-
if (table) {
|
113
|
-
let tableAndLine = `${table[1]}|${queryData.triggeringLine}`;
|
114
|
-
queriesCount[tableAndLine] = queriesCount[tableAndLine] || {count: 0, indices: []};
|
115
|
-
queriesCount[tableAndLine].count += 1;
|
116
|
-
queriesCount[tableAndLine].indices.push(queryData.index);
|
93
|
+
|
94
|
+
function processStack(stack, processSqlEvent) {
|
95
|
+
if (stack == null) {
|
96
|
+
stack = {}
|
117
97
|
}
|
118
|
-
|
98
|
+
let sqlEvents = stack.sql_events || [];
|
99
|
+
let sqlQueries = sqlEvents.map((event, index) => ({sql: event.sql, triggeringLine: event.triggering_line, index: index}));
|
100
|
+
|
101
|
+
let queriesCount = {};
|
102
|
+
sqlQueries.forEach(queryData => {
|
103
|
+
let table = queryData.sql.match(/FROM ['`"]*([^ '`"]+)['`"]*/i)
|
104
|
+
if (table) {
|
105
|
+
let tableAndLine = `${
|
106
|
+
table[1]
|
107
|
+
}|${
|
108
|
+
queryData.triggeringLine
|
109
|
+
}`;
|
110
|
+
queriesCount[tableAndLine] = queriesCount[tableAndLine] || {
|
111
|
+
count: 0,
|
112
|
+
indices: []
|
113
|
+
};
|
114
|
+
queriesCount[tableAndLine].count += 1;
|
115
|
+
queriesCount[tableAndLine].indices.push(queryData.index);
|
116
|
+
}
|
117
|
+
});
|
119
118
|
|
120
|
-
|
119
|
+
processSqlEvent(sqlEvents, queriesCount);
|
121
120
|
|
122
|
-
|
123
|
-
}
|
121
|
+
(stack.children || []).forEach(child => processStack(child, processSqlEvent));
|
122
|
+
}
|
124
123
|
|
125
|
-
function tagQueriesWithNPlusOne(requestInput) {
|
126
|
-
|
127
|
-
const requestArray = Array.isArray(requestInput) ? requestInput : [requestInput];
|
124
|
+
function tagQueriesWithNPlusOne(requestInput) { // If requestInput is not an array, convert it into an array
|
125
|
+
const requestArray = Array.isArray(requestInput) ? requestInput : [requestInput];
|
128
126
|
|
129
|
-
|
130
|
-
|
131
|
-
requestObject.has_n_plus_one = false;
|
127
|
+
requestArray.forEach(requestObject => { // Assume no N+1 issues are found initially
|
128
|
+
requestObject.has_n_plus_one = false;
|
132
129
|
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
});
|
130
|
+
processStack(requestObject.stack[0], function (sqlEvents, queriesCount) { // By default, add 'possibleNPlusOne: false' tag to all SQL events
|
131
|
+
sqlEvents.forEach(event => {
|
132
|
+
event.possibleNPlusOne = false;
|
133
|
+
});
|
138
134
|
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
requestObject.has_n_plus_one = true;
|
135
|
+
for (let tableAndLine in queriesCount) {
|
136
|
+
if (queriesCount[tableAndLine].count > 1) { // If any N+1 issue is found, set 'has_n_plus_one' to true
|
137
|
+
requestObject.has_n_plus_one = true;
|
143
138
|
|
144
|
-
|
145
|
-
|
146
|
-
|
139
|
+
queriesCount[tableAndLine].indices.forEach(index => {
|
140
|
+
sqlEvents[index].possibleNPlusOne = true; // Update the tag to true for N+1 issues
|
141
|
+
});
|
142
|
+
}
|
147
143
|
}
|
148
|
-
}
|
144
|
+
});
|
149
145
|
});
|
150
|
-
});
|
151
|
-
}
|
152
|
-
|
153
|
-
function addSortingClick() {
|
154
|
-
// Assuming your table headers have a class name "sortable"
|
155
|
-
$('.sortable').click(function() {
|
156
|
-
if (current_sort_field == $(this).data('field')) {
|
157
|
-
isAsc = !isAsc;
|
158
|
-
} else {
|
159
|
-
isAsc = false;
|
160
146
|
}
|
161
|
-
current_sort_field = $(this).data('field'); // Assuming data-field attribute contains the name of the field to sort
|
162
147
|
|
163
|
-
|
164
|
-
|
165
|
-
|
148
|
+
function addSortingClick() { // Assuming your table headers have a class name "sortable"
|
149
|
+
$('.sortable').click(function () {
|
150
|
+
if (current_sort_field == $(this).data('field')) {
|
151
|
+
isAsc = ! isAsc;
|
152
|
+
} else {
|
153
|
+
isAsc = false;
|
154
|
+
} current_sort_field = $(this).data('field'); // Assuming data-field attribute contains the name of the field to sort
|
166
155
|
|
167
|
-
|
168
|
-
|
169
|
-
if (a[field] < b[field]) {
|
170
|
-
return isAsc ? -1 : 1;
|
156
|
+
renderTable(allData);
|
157
|
+
});
|
171
158
|
}
|
172
|
-
|
173
|
-
|
159
|
+
|
160
|
+
function sortDataBy(field, data) {
|
161
|
+
data.sort(function (a, b) {
|
162
|
+
if (a[field] < b[field]) {
|
163
|
+
return isAsc ? -1 : 1;
|
164
|
+
}
|
165
|
+
if (a[field] > b[field]) {
|
166
|
+
return isAsc ? 1 : -1;
|
167
|
+
}
|
168
|
+
return 0; // equal
|
169
|
+
});
|
170
|
+
|
171
|
+
return data;
|
174
172
|
}
|
175
|
-
return 0; // equal
|
176
|
-
});
|
177
173
|
|
178
|
-
|
179
|
-
|
174
|
+
function getNPlusOneRequests(requestArray) {
|
175
|
+
let requestsWithIssues = [];
|
180
176
|
|
181
|
-
|
182
|
-
|
177
|
+
requestArray.forEach(requestObject => {
|
178
|
+
let hasNPlusOne = false;
|
183
179
|
|
184
|
-
|
185
|
-
|
180
|
+
processStack(requestObject.stack[0], function (sqlEvents) {
|
181
|
+
if (sqlEvents.some(event => event.possibleNPlusOne)) {
|
182
|
+
hasNPlusOne = true;
|
183
|
+
}
|
184
|
+
});
|
186
185
|
|
187
|
-
|
188
|
-
|
189
|
-
hasNPlusOne = true;
|
186
|
+
if (hasNPlusOne) {
|
187
|
+
requestsWithIssues.push(requestObject);
|
190
188
|
}
|
191
189
|
});
|
192
190
|
|
193
|
-
|
194
|
-
requestsWithIssues.push(requestObject);
|
195
|
-
}
|
196
|
-
});
|
197
|
-
|
198
|
-
return requestsWithIssues;
|
199
|
-
}
|
200
|
-
function aggregateRequests(requestArray) {
|
201
|
-
let aggregates = {};
|
202
|
-
requestArray.forEach(request => {
|
203
|
-
let key = `${request.controller_action}`;
|
204
|
-
if (!aggregates[key]) {
|
205
|
-
aggregates[key] = {
|
206
|
-
count: 0,
|
207
|
-
method: request.method,
|
208
|
-
path: request.path,
|
209
|
-
totalDuration: 0,
|
210
|
-
totalDbRuntime: 0,
|
211
|
-
totalViewRuntime: 0,
|
212
|
-
slowestDuration: 0,
|
213
|
-
slowestDbRuntime: 0,
|
214
|
-
slowestViewRuntime: 0
|
215
|
-
};
|
216
|
-
}
|
217
|
-
|
218
|
-
aggregates[key].count++;
|
219
|
-
aggregates[key].totalDuration += request.duration;
|
220
|
-
aggregates[key].totalDbRuntime += request.db_runtime || 0;
|
221
|
-
aggregates[key].totalViewRuntime += request.view_runtime || 0;
|
222
|
-
aggregates[key].slowestDuration = Math.max(aggregates[key].slowestDuration, request.duration);
|
223
|
-
aggregates[key].slowestDbRuntime = Math.max(aggregates[key].slowestDbRuntime, request.db_runtime || 0);
|
224
|
-
aggregates[key].slowestViewRuntime = Math.max(aggregates[key].slowestViewRuntime, request.view_runtime || 0);
|
225
|
-
});
|
226
|
-
|
227
|
-
let results = [];
|
228
|
-
for (let key in aggregates) {
|
229
|
-
let controller = key;
|
230
|
-
results.push({
|
231
|
-
controller,
|
232
|
-
method: aggregates[key].method,
|
233
|
-
path: aggregates[key].path,
|
234
|
-
count: aggregates[key].count,
|
235
|
-
averageDuration: aggregates[key].totalDuration / aggregates[key].count,
|
236
|
-
averageDbRuntime: aggregates[key].totalDbRuntime / aggregates[key].count,
|
237
|
-
averageViewRuntime: aggregates[key].totalViewRuntime / aggregates[key].count,
|
238
|
-
slowestDuration: aggregates[key].slowestDuration,
|
239
|
-
slowestDbRuntime: aggregates[key].slowestDbRuntime,
|
240
|
-
slowestViewRuntime: aggregates[key].slowestViewRuntime,
|
241
|
-
});
|
191
|
+
return requestsWithIssues;
|
242
192
|
}
|
243
|
-
|
244
|
-
}
|
193
|
+
function aggregateRequests(requestArray) {
|
194
|
+
let aggregates = {};
|
195
|
+
requestArray.forEach(request => {
|
196
|
+
let key = `${
|
197
|
+
request.controller_action
|
198
|
+
}`;
|
199
|
+
if (! aggregates[key]) {
|
200
|
+
aggregates[key] = {
|
201
|
+
count: 0,
|
202
|
+
method: request.method,
|
203
|
+
path: request.path,
|
204
|
+
totalDuration: 0,
|
205
|
+
totalDbRuntime: 0,
|
206
|
+
totalViewRuntime: 0,
|
207
|
+
slowestDuration: 0,
|
208
|
+
slowestDbRuntime: 0,
|
209
|
+
slowestViewRuntime: 0
|
210
|
+
};
|
211
|
+
}
|
245
212
|
|
213
|
+
aggregates[key].count ++;
|
214
|
+
aggregates[key].totalDuration += request.duration;
|
215
|
+
aggregates[key].totalDbRuntime += request.db_runtime || 0;
|
216
|
+
aggregates[key].totalViewRuntime += request.view_runtime || 0;
|
217
|
+
aggregates[key].slowestDuration = Math.max(aggregates[key].slowestDuration, request.duration);
|
218
|
+
aggregates[key].slowestDbRuntime = Math.max(aggregates[key].slowestDbRuntime, request.db_runtime || 0);
|
219
|
+
aggregates[key].slowestViewRuntime = Math.max(aggregates[key].slowestViewRuntime, request.view_runtime || 0);
|
220
|
+
});
|
246
221
|
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
222
|
+
let results = [];
|
223
|
+
for (let key in aggregates) {
|
224
|
+
let controller = key;
|
225
|
+
results.push({
|
226
|
+
controller,
|
227
|
+
method: aggregates[key].method,
|
228
|
+
path: aggregates[key].path,
|
229
|
+
count: aggregates[key].count,
|
230
|
+
averageDuration: aggregates[key].totalDuration / aggregates[key].count,
|
231
|
+
averageDbRuntime: aggregates[key].totalDbRuntime / aggregates[key].count,
|
232
|
+
averageViewRuntime: aggregates[key].totalViewRuntime / aggregates[key].count,
|
233
|
+
slowestDuration: aggregates[key].slowestDuration,
|
234
|
+
slowestDbRuntime: aggregates[key].slowestDbRuntime,
|
235
|
+
slowestViewRuntime: aggregates[key].slowestViewRuntime
|
236
|
+
});
|
260
237
|
}
|
261
|
-
|
262
|
-
let totalDbRuntime = stack.reduce((acc, curr) => acc + curr.calc_db_runtime, 0);
|
263
|
-
return {duration: totalDuration, dbRuntime: totalDbRuntime};
|
264
|
-
}
|
265
|
-
if(requestObj.stack == null) {
|
266
|
-
requestObj.stack = [];
|
238
|
+
return results;
|
267
239
|
}
|
268
|
-
let totals = iterateStack(requestObj.stack);
|
269
|
-
requestObj.calc_duration = totals.duration;
|
270
|
-
requestObj.calc_db_runtime = totals.dbRuntime;
|
271
|
-
return requestObj;
|
272
|
-
}
|
273
240
|
|
274
241
|
|
275
|
-
|
242
|
+
function calcDurations(requestObj) {
|
243
|
+
function iterateStack(stack) {
|
244
|
+
for (let item of stack) {
|
245
|
+
item.calc_duration = item.duration;
|
246
|
+
item.calc_db_runtime = 0;
|
247
|
+
for (let event of item.sql_events) {
|
248
|
+
item.calc_db_runtime += event.duration;
|
249
|
+
}
|
250
|
+
if (item.children.length > 0) {
|
251
|
+
let childrenTotals = iterateStack(item.children);
|
252
|
+
item.calc_duration += childrenTotals.duration;
|
253
|
+
item.calc_db_runtime += childrenTotals.dbRuntime;
|
254
|
+
}
|
255
|
+
}
|
256
|
+
let totalDuration = stack.reduce((acc, curr) => acc + curr.calc_duration, 0);
|
257
|
+
let totalDbRuntime = stack.reduce((acc, curr) => acc + curr.calc_db_runtime, 0);
|
258
|
+
return {duration: totalDuration, dbRuntime: totalDbRuntime};
|
259
|
+
}
|
260
|
+
if (requestObj.stack == null) {
|
261
|
+
requestObj.stack = [];
|
262
|
+
}
|
263
|
+
let totals = iterateStack(requestObj.stack);
|
264
|
+
requestObj.calc_duration = totals.duration;
|
265
|
+
requestObj.calc_db_runtime = totals.dbRuntime;
|
266
|
+
return requestObj;
|
267
|
+
}
|
268
|
+
</script>
|