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.
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>