builder_apm 0.2.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 +7 -0
- data/.gitignore +1 -0
- data/.rspec +2 -0
- data/.travis.yml +6 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +17 -0
- data/Gemfile.lock +196 -0
- data/LICENSE.txt +21 -0
- data/README.md +50 -0
- data/Rakefile +28 -0
- data/app/controllers/builder_apm/dashboard_controller.rb +8 -0
- data/app/controllers/builder_apm/error_requests_controller.rb +8 -0
- data/app/controllers/builder_apm/n_plus_one_controller.rb +8 -0
- data/app/controllers/builder_apm/recent_requests_controller.rb +8 -0
- data/app/controllers/builder_apm/request_analysis_controller.rb +8 -0
- data/app/controllers/builder_apm/request_data_controller.rb +41 -0
- data/app/controllers/builder_apm/request_details_controller.rb +9 -0
- data/app/controllers/builder_apm/slow_requests_controller.rb +8 -0
- data/app/controllers/builder_apm/wip_controller.rb +8 -0
- data/app/views/builder_apm/css/_dark.html.erb +119 -0
- data/app/views/builder_apm/css/_main.html.erb +268 -0
- data/app/views/builder_apm/dashboard/index.html.erb +10 -0
- data/app/views/builder_apm/error_requests/index.html.erb +23 -0
- data/app/views/builder_apm/js/_compress.html.erb +93 -0
- data/app/views/builder_apm/js/_dashboard.html.erb +199 -0
- data/app/views/builder_apm/js/_data_fetcher.html.erb +254 -0
- data/app/views/builder_apm/js/_error_requests.html.erb +65 -0
- data/app/views/builder_apm/js/_lzma.html.erb +2670 -0
- data/app/views/builder_apm/js/_n_plus_one.html.erb +79 -0
- data/app/views/builder_apm/js/_recent_requests.html.erb +82 -0
- data/app/views/builder_apm/js/_request_analysis.html.erb +77 -0
- data/app/views/builder_apm/js/_request_details.html.erb +204 -0
- data/app/views/builder_apm/js/_slow_requests.html.erb +74 -0
- data/app/views/builder_apm/n_plus_one/index.html.erb +21 -0
- data/app/views/builder_apm/recent_requests/index.html.erb +21 -0
- data/app/views/builder_apm/request_analysis/index.html.erb +24 -0
- data/app/views/builder_apm/request_details/index.html.erb +7 -0
- data/app/views/builder_apm/shared/_footer.html.erb +3 -0
- data/app/views/builder_apm/shared/_header.html.erb +55 -0
- data/app/views/builder_apm/slow_requests/index.html.erb +21 -0
- data/app/views/builder_apm/wip/index.html.erb +5 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/builder_apm.gemspec +23 -0
- data/config/routes.rb +12 -0
- data/lib/builder_apm/configuration.rb +15 -0
- data/lib/builder_apm/controllers/instrumenter.rb +88 -0
- data/lib/builder_apm/engine.rb +17 -0
- data/lib/builder_apm/methods/instrumenter.rb +79 -0
- data/lib/builder_apm/middleware/timing.rb +56 -0
- data/lib/builder_apm/models/instrumenter.rb +82 -0
- data/lib/builder_apm/railtie.rb +9 -0
- data/lib/builder_apm/redis_client.rb +11 -0
- data/lib/builder_apm/version.rb +3 -0
- data/lib/builder_apm.rb +22 -0
- data/lib/generators/builder_apm/install_generator.rb +21 -0
- data/lib/generators/builder_apm/templates/builder_apm_config.rb +6 -0
- data/lib/generators/builder_apm/templates/create_builder_apm_requests.rb +21 -0
- data/lib/generators/builder_apm/templates/create_builder_apm_sql_queries.rb +17 -0
- metadata +135 -0
@@ -0,0 +1,254 @@
|
|
1
|
+
<%= render 'builder_apm/js/lzma' %>
|
2
|
+
<%= render 'builder_apm/js/compress' %>
|
3
|
+
|
4
|
+
<script>
|
5
|
+
var allData = null;
|
6
|
+
var isAsc = true;
|
7
|
+
var current_sort_field = 'start_time';
|
8
|
+
|
9
|
+
function fetchDataAndUpdateStorage(onSuccess) {
|
10
|
+
// Get the last request time
|
11
|
+
var cursor = localStorage.getItem('builder_apm_cursor') || "-inf";
|
12
|
+
var limit = 50;
|
13
|
+
|
14
|
+
// Make an AJAX request to fetch latest data
|
15
|
+
$.get('/builder_apm/request_data?cursor=' + cursor + '&limit=' + limit, function(newData) {
|
16
|
+
|
17
|
+
newData = newData.map(request => calcDurations(request));
|
18
|
+
|
19
|
+
if(allData != null){
|
20
|
+
processData(allData, null, newData, onSuccess);
|
21
|
+
} else {
|
22
|
+
// Retrieve the stored data from local storage
|
23
|
+
var storedData = localStorage.getItem('builder_apm_requests');
|
24
|
+
if (storedData) {
|
25
|
+
var dataCompressed = decompress(storedData, function(result, error) {
|
26
|
+
processData(JSON.parse(result), error, newData, onSuccess);
|
27
|
+
});
|
28
|
+
} else {
|
29
|
+
processData([], null, newData, onSuccess);
|
30
|
+
}
|
31
|
+
}
|
32
|
+
});
|
33
|
+
}
|
34
|
+
|
35
|
+
function loadLocalData(onSuccess) {
|
36
|
+
if(allData) {
|
37
|
+
onSuccess(allData);
|
38
|
+
} else {
|
39
|
+
var storedData = localStorage.getItem('builder_apm_requests');
|
40
|
+
if (storedData) {
|
41
|
+
var dataCompressed = decompress(storedData, function(result, error) {
|
42
|
+
processData(JSON.parse(result), error, [], onSuccess);
|
43
|
+
});
|
44
|
+
} else {
|
45
|
+
fetchDataAndUpdateStorage(onSuccess);
|
46
|
+
}
|
47
|
+
}
|
48
|
+
}
|
49
|
+
|
50
|
+
function processData(storedData, error, newData, onSuccess) {
|
51
|
+
|
52
|
+
// Append the new data to the stored data
|
53
|
+
allData = storedData.concat(newData);
|
54
|
+
try {
|
55
|
+
// Store the updated data back to local storage
|
56
|
+
compress(JSON.stringify(allData), function(result, error) {
|
57
|
+
if(result != null) {
|
58
|
+
localStorage.setItem('builder_apm_requests', result);
|
59
|
+
}
|
60
|
+
});
|
61
|
+
|
62
|
+
// Update the cursor
|
63
|
+
if (newData.length > 0) {
|
64
|
+
var lastRequest = newData[newData.length - 1];
|
65
|
+
cursor = lastRequest.end_time+0.001;
|
66
|
+
localStorage.setItem('builder_apm_cursor', cursor.toString());
|
67
|
+
}
|
68
|
+
} catch(e) {
|
69
|
+
console.error("Error storing data to local storage. Might be out of storage space.", e);
|
70
|
+
alert('not enough local storage')
|
71
|
+
}
|
72
|
+
// Callback function after data fetched and stored
|
73
|
+
console.log(allData);
|
74
|
+
onSuccess(allData);
|
75
|
+
}
|
76
|
+
|
77
|
+
function loadRequest(request_id) {
|
78
|
+
var requestData = allData.find(function(item) {
|
79
|
+
return item['request_id'] == request_id;
|
80
|
+
});
|
81
|
+
|
82
|
+
return requestData;
|
83
|
+
}
|
84
|
+
|
85
|
+
function processStack(stack, processSqlEvent) {
|
86
|
+
if(stack == null) {
|
87
|
+
stack = {}
|
88
|
+
}
|
89
|
+
let sqlEvents = stack.sql_events || [];
|
90
|
+
let sqlQueries = sqlEvents.map((event, index) => ({
|
91
|
+
sql: event.sql,
|
92
|
+
triggeringLine: event.triggering_line,
|
93
|
+
index: index
|
94
|
+
}));
|
95
|
+
|
96
|
+
let queriesCount = {};
|
97
|
+
sqlQueries.forEach(queryData => {
|
98
|
+
let table = queryData.sql.match(/FROM ['`"]*([^ '`"]+)['`"]*/i)
|
99
|
+
if (table) {
|
100
|
+
let tableAndLine = `${table[1]}|${queryData.triggeringLine}`;
|
101
|
+
queriesCount[tableAndLine] = queriesCount[tableAndLine] || {count: 0, indices: []};
|
102
|
+
queriesCount[tableAndLine].count += 1;
|
103
|
+
queriesCount[tableAndLine].indices.push(queryData.index);
|
104
|
+
}
|
105
|
+
});
|
106
|
+
|
107
|
+
processSqlEvent(sqlEvents, queriesCount);
|
108
|
+
|
109
|
+
(stack.children || []).forEach(child => processStack(child, processSqlEvent));
|
110
|
+
}
|
111
|
+
|
112
|
+
function tagQueriesWithNPlusOne(requestInput) {
|
113
|
+
// If requestInput is not an array, convert it into an array
|
114
|
+
const requestArray = Array.isArray(requestInput) ? requestInput : [requestInput];
|
115
|
+
|
116
|
+
requestArray.forEach(requestObject => {
|
117
|
+
// Assume no N+1 issues are found initially
|
118
|
+
requestObject.has_n_plus_one = false;
|
119
|
+
|
120
|
+
processStack(requestObject.stack[0], function(sqlEvents, queriesCount) {
|
121
|
+
// By default, add 'possibleNPlusOne: false' tag to all SQL events
|
122
|
+
sqlEvents.forEach(event => {
|
123
|
+
event.possibleNPlusOne = false;
|
124
|
+
});
|
125
|
+
|
126
|
+
for (let tableAndLine in queriesCount) {
|
127
|
+
if (queriesCount[tableAndLine].count > 1) {
|
128
|
+
// If any N+1 issue is found, set 'has_n_plus_one' to true
|
129
|
+
requestObject.has_n_plus_one = true;
|
130
|
+
|
131
|
+
queriesCount[tableAndLine].indices.forEach(index => {
|
132
|
+
sqlEvents[index].possibleNPlusOne = true; // Update the tag to true for N+1 issues
|
133
|
+
});
|
134
|
+
}
|
135
|
+
}
|
136
|
+
});
|
137
|
+
});
|
138
|
+
}
|
139
|
+
|
140
|
+
function addSortingClick() {
|
141
|
+
// Assuming your table headers have a class name "sortable"
|
142
|
+
$('.sortable').click(function() {
|
143
|
+
current_sort_field = $(this).data('field'); // Assuming data-field attribute contains the name of the field to sort
|
144
|
+
isAsc = !isAsc;
|
145
|
+
|
146
|
+
renderTable(allData);
|
147
|
+
});
|
148
|
+
}
|
149
|
+
|
150
|
+
function sortDataBy(field, data) {
|
151
|
+
data.sort(function(a, b) {
|
152
|
+
if (a[field] < b[field]) {
|
153
|
+
return isAsc ? -1 : 1;
|
154
|
+
}
|
155
|
+
if (a[field] > b[field]) {
|
156
|
+
return isAsc ? 1 : -1;
|
157
|
+
}
|
158
|
+
return 0; // equal
|
159
|
+
});
|
160
|
+
|
161
|
+
return data;
|
162
|
+
}
|
163
|
+
|
164
|
+
function getNPlusOneRequests(requestArray) {
|
165
|
+
let requestsWithIssues = [];
|
166
|
+
|
167
|
+
requestArray.forEach(requestObject => {
|
168
|
+
let hasNPlusOne = false;
|
169
|
+
|
170
|
+
processStack(requestObject.stack[0], function(sqlEvents) {
|
171
|
+
if (sqlEvents.some(event => event.possibleNPlusOne)) {
|
172
|
+
hasNPlusOne = true;
|
173
|
+
}
|
174
|
+
});
|
175
|
+
|
176
|
+
if (hasNPlusOne) {
|
177
|
+
requestsWithIssues.push(requestObject);
|
178
|
+
}
|
179
|
+
});
|
180
|
+
|
181
|
+
return requestsWithIssues;
|
182
|
+
}
|
183
|
+
function aggregateRequests(requestArray) {
|
184
|
+
let aggregates = {};
|
185
|
+
requestArray.forEach(request => {
|
186
|
+
let key = `${request.controller}#${request.method}|${request.path}`;
|
187
|
+
if (!aggregates[key]) {
|
188
|
+
aggregates[key] = {
|
189
|
+
count: 0,
|
190
|
+
totalDuration: 0,
|
191
|
+
totalDbRuntime: 0,
|
192
|
+
totalViewRuntime: 0,
|
193
|
+
slowestDuration: 0,
|
194
|
+
slowestDbRuntime: 0,
|
195
|
+
slowestViewRuntime: 0
|
196
|
+
};
|
197
|
+
}
|
198
|
+
aggregates[key].count++;
|
199
|
+
aggregates[key].totalDuration += request.duration;
|
200
|
+
aggregates[key].totalDbRuntime += request.db_runtime || 0;
|
201
|
+
aggregates[key].totalViewRuntime += request.view_runtime || 0;
|
202
|
+
aggregates[key].slowestDuration = Math.max(aggregates[key].slowestDuration, request.duration);
|
203
|
+
aggregates[key].slowestDbRuntime = Math.max(aggregates[key].slowestDbRuntime, request.db_runtime || 0);
|
204
|
+
aggregates[key].slowestViewRuntime = Math.max(aggregates[key].slowestViewRuntime, request.view_runtime || 0);
|
205
|
+
});
|
206
|
+
|
207
|
+
let results = [];
|
208
|
+
for (let key in aggregates) {
|
209
|
+
let [controller, path] = key.split('|');
|
210
|
+
results.push({
|
211
|
+
controller,
|
212
|
+
path,
|
213
|
+
count: aggregates[key].count,
|
214
|
+
averageDuration: aggregates[key].totalDuration / aggregates[key].count,
|
215
|
+
averageDbRuntime: aggregates[key].totalDbRuntime / aggregates[key].count,
|
216
|
+
averageViewRuntime: aggregates[key].totalViewRuntime / aggregates[key].count,
|
217
|
+
slowestDuration: aggregates[key].slowestDuration,
|
218
|
+
slowestDbRuntime: aggregates[key].slowestDbRuntime,
|
219
|
+
slowestViewRuntime: aggregates[key].slowestViewRuntime,
|
220
|
+
});
|
221
|
+
}
|
222
|
+
return results;
|
223
|
+
}
|
224
|
+
|
225
|
+
|
226
|
+
function calcDurations(requestObj) {
|
227
|
+
function iterateStack(stack) {
|
228
|
+
for (let item of stack) {
|
229
|
+
item.calc_duration = item.duration;
|
230
|
+
item.calc_db_runtime = 0;
|
231
|
+
for (let event of item.sql_events) {
|
232
|
+
item.calc_db_runtime += event.duration;
|
233
|
+
}
|
234
|
+
if (item.children.length > 0) {
|
235
|
+
let childrenTotals = iterateStack(item.children);
|
236
|
+
item.calc_duration += childrenTotals.duration;
|
237
|
+
item.calc_db_runtime += childrenTotals.dbRuntime;
|
238
|
+
}
|
239
|
+
}
|
240
|
+
let totalDuration = stack.reduce((acc, curr) => acc + curr.calc_duration, 0);
|
241
|
+
let totalDbRuntime = stack.reduce((acc, curr) => acc + curr.calc_db_runtime, 0);
|
242
|
+
return {duration: totalDuration, dbRuntime: totalDbRuntime};
|
243
|
+
}
|
244
|
+
if(requestObj.stack == null) {
|
245
|
+
requestObj.stack = [];
|
246
|
+
}
|
247
|
+
let totals = iterateStack(requestObj.stack);
|
248
|
+
requestObj.calc_duration = totals.duration;
|
249
|
+
requestObj.calc_db_runtime = totals.dbRuntime;
|
250
|
+
return requestObj;
|
251
|
+
}
|
252
|
+
|
253
|
+
|
254
|
+
</script>
|
@@ -0,0 +1,65 @@
|
|
1
|
+
<script>
|
2
|
+
$(document).ready(function() {
|
3
|
+
|
4
|
+
fetchDataAndUpdateStorage(function(updatedData) {
|
5
|
+
renderTable(updatedData);
|
6
|
+
});
|
7
|
+
|
8
|
+
setInterval(function() {
|
9
|
+
var autoUpdate = $('#autoUpdate').is(':checked');
|
10
|
+
if (autoUpdate) {
|
11
|
+
fetchDataAndUpdateStorage(function(updatedData) {
|
12
|
+
renderTable(updatedData);
|
13
|
+
});
|
14
|
+
}
|
15
|
+
}, 5000);
|
16
|
+
|
17
|
+
addSortingClick();
|
18
|
+
});
|
19
|
+
|
20
|
+
function renderTable(data, target = null) {
|
21
|
+
data = data.filter(item => item.status === 500);
|
22
|
+
data = sortDataBy(current_sort_field, data);
|
23
|
+
|
24
|
+
// Get a reference to the table body
|
25
|
+
var tableBody = target || $('table tbody');
|
26
|
+
|
27
|
+
// Clear the table body
|
28
|
+
tableBody.empty();
|
29
|
+
|
30
|
+
// Create new table rows for each data item
|
31
|
+
data.forEach(function(item) {
|
32
|
+
var row = $('<tr>');
|
33
|
+
|
34
|
+
$('<td>').text(new Date(item['start_time']).toLocaleString()).appendTo(row);
|
35
|
+
$('<td>').text(item['controller'] + '#' + item['action']).appendTo(row);
|
36
|
+
$('<td>').text(item['status']).appendTo(row);
|
37
|
+
$('<td>').append(item['exception_class']).appendTo(row);
|
38
|
+
$('<td>').append(item['exception_message']).appendTo(row);
|
39
|
+
|
40
|
+
trace_cell = $('<td>');
|
41
|
+
if( item['exception_backtrace']) {
|
42
|
+
for(let i = 0; i < item['exception_backtrace'].length && i < 3; i++) {
|
43
|
+
var lineElement = $('<p>').text(item['exception_backtrace'][i]);
|
44
|
+
trace_cell.append(lineElement);
|
45
|
+
}
|
46
|
+
}
|
47
|
+
trace_cell.appendTo(row);
|
48
|
+
|
49
|
+
// Action column
|
50
|
+
var actionTd = $('<td>');
|
51
|
+
var actionButton = $('<button>').text('Details');
|
52
|
+
actionButton.on('click', function() {
|
53
|
+
// Replace 'your-details-url' with the actual URL where the details are to be fetched.
|
54
|
+
// It's assumed the ID is required as a URL parameter, modify as per your requirements.
|
55
|
+
window.location.href = '<%= request_details_path %>?request_id=' + item['request_id'];
|
56
|
+
});
|
57
|
+
actionButton.appendTo(actionTd);
|
58
|
+
actionTd.appendTo(row);
|
59
|
+
|
60
|
+
// Append the row to the table body
|
61
|
+
tableBody.append(row);
|
62
|
+
});
|
63
|
+
}
|
64
|
+
|
65
|
+
</script>
|