builder_apm 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +1 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +6 -0
  5. data/CODE_OF_CONDUCT.md +74 -0
  6. data/Gemfile +17 -0
  7. data/Gemfile.lock +196 -0
  8. data/LICENSE.txt +21 -0
  9. data/README.md +50 -0
  10. data/Rakefile +28 -0
  11. data/app/controllers/builder_apm/dashboard_controller.rb +8 -0
  12. data/app/controllers/builder_apm/error_requests_controller.rb +8 -0
  13. data/app/controllers/builder_apm/n_plus_one_controller.rb +8 -0
  14. data/app/controllers/builder_apm/recent_requests_controller.rb +8 -0
  15. data/app/controllers/builder_apm/request_analysis_controller.rb +8 -0
  16. data/app/controllers/builder_apm/request_data_controller.rb +41 -0
  17. data/app/controllers/builder_apm/request_details_controller.rb +9 -0
  18. data/app/controllers/builder_apm/slow_requests_controller.rb +8 -0
  19. data/app/controllers/builder_apm/wip_controller.rb +8 -0
  20. data/app/views/builder_apm/css/_dark.html.erb +119 -0
  21. data/app/views/builder_apm/css/_main.html.erb +268 -0
  22. data/app/views/builder_apm/dashboard/index.html.erb +10 -0
  23. data/app/views/builder_apm/error_requests/index.html.erb +23 -0
  24. data/app/views/builder_apm/js/_compress.html.erb +93 -0
  25. data/app/views/builder_apm/js/_dashboard.html.erb +199 -0
  26. data/app/views/builder_apm/js/_data_fetcher.html.erb +254 -0
  27. data/app/views/builder_apm/js/_error_requests.html.erb +65 -0
  28. data/app/views/builder_apm/js/_lzma.html.erb +2670 -0
  29. data/app/views/builder_apm/js/_n_plus_one.html.erb +79 -0
  30. data/app/views/builder_apm/js/_recent_requests.html.erb +82 -0
  31. data/app/views/builder_apm/js/_request_analysis.html.erb +77 -0
  32. data/app/views/builder_apm/js/_request_details.html.erb +204 -0
  33. data/app/views/builder_apm/js/_slow_requests.html.erb +74 -0
  34. data/app/views/builder_apm/n_plus_one/index.html.erb +21 -0
  35. data/app/views/builder_apm/recent_requests/index.html.erb +21 -0
  36. data/app/views/builder_apm/request_analysis/index.html.erb +24 -0
  37. data/app/views/builder_apm/request_details/index.html.erb +7 -0
  38. data/app/views/builder_apm/shared/_footer.html.erb +3 -0
  39. data/app/views/builder_apm/shared/_header.html.erb +55 -0
  40. data/app/views/builder_apm/slow_requests/index.html.erb +21 -0
  41. data/app/views/builder_apm/wip/index.html.erb +5 -0
  42. data/bin/console +14 -0
  43. data/bin/setup +8 -0
  44. data/builder_apm.gemspec +23 -0
  45. data/config/routes.rb +12 -0
  46. data/lib/builder_apm/configuration.rb +15 -0
  47. data/lib/builder_apm/controllers/instrumenter.rb +88 -0
  48. data/lib/builder_apm/engine.rb +17 -0
  49. data/lib/builder_apm/methods/instrumenter.rb +79 -0
  50. data/lib/builder_apm/middleware/timing.rb +56 -0
  51. data/lib/builder_apm/models/instrumenter.rb +82 -0
  52. data/lib/builder_apm/railtie.rb +9 -0
  53. data/lib/builder_apm/redis_client.rb +11 -0
  54. data/lib/builder_apm/version.rb +3 -0
  55. data/lib/builder_apm.rb +22 -0
  56. data/lib/generators/builder_apm/install_generator.rb +21 -0
  57. data/lib/generators/builder_apm/templates/builder_apm_config.rb +6 -0
  58. data/lib/generators/builder_apm/templates/create_builder_apm_requests.rb +21 -0
  59. data/lib/generators/builder_apm/templates/create_builder_apm_sql_queries.rb +17 -0
  60. 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>