builder_apm 0.2.5 → 0.3.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 (27) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +1 -1
  3. data/app/controllers/builder_apm/application_controller.rb +7 -0
  4. data/app/controllers/builder_apm/dashboard_controller.rb +20 -4
  5. data/app/controllers/builder_apm/error_requests_controller.rb +1 -1
  6. data/app/controllers/builder_apm/n_plus_one_controller.rb +1 -1
  7. data/app/controllers/builder_apm/recent_requests_controller.rb +1 -1
  8. data/app/controllers/builder_apm/request_analysis_controller.rb +1 -1
  9. data/app/controllers/builder_apm/request_data_controller.rb +36 -35
  10. data/app/controllers/builder_apm/request_details_controller.rb +4 -1
  11. data/app/controllers/builder_apm/slow_requests_controller.rb +1 -1
  12. data/app/controllers/builder_apm/wip_controller.rb +1 -1
  13. data/app/views/builder_apm/css/_main.html.erb +4 -1
  14. data/app/views/builder_apm/js/_dashboard.html.erb +4 -6
  15. data/app/views/builder_apm/js/_data_fetcher.html.erb +58 -37
  16. data/app/views/builder_apm/js/_error_requests.html.erb +21 -13
  17. data/app/views/builder_apm/js/_n_plus_one.html.erb +18 -10
  18. data/app/views/builder_apm/js/_recent_requests.html.erb +22 -18
  19. data/app/views/builder_apm/js/_request_analysis.html.erb +19 -10
  20. data/app/views/builder_apm/js/_request_details.html.erb +10 -22
  21. data/app/views/builder_apm/js/_slow_requests.html.erb +19 -10
  22. data/app/views/builder_apm/shared/_header.html.erb +0 -1
  23. data/lib/builder_apm/configuration.rb +2 -0
  24. data/lib/builder_apm/middleware/timing.rb +21 -4
  25. data/lib/builder_apm/models/instrumenter.rb +43 -8
  26. data/lib/builder_apm/version.rb +1 -1
  27. metadata +2 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0cbbeca0fe7a6139175c8b354026859775e2f5db9666bc3ac74d2d19a411e46b
4
- data.tar.gz: a571a27eb27d78926e237109863c07277e7cf90e8fe4b902f8545ac36a01c788
3
+ metadata.gz: 31a9a4393e090f4980960c61d6f5cb2cf307a352534b9f8f264c2238dcd281f4
4
+ data.tar.gz: a01935162ea2b1fa2701a080536aadc6259c3a9c102148ef1df4475ddd7a89de
5
5
  SHA512:
6
- metadata.gz: '049b33f6202dbd35201bcd9cf39fe26abc199fac70864ea0707956f03d2a532135793cdd8980cbcebf8faff2c8ed62fdaf41a97d5f31f898780214314a7e4edb'
7
- data.tar.gz: a04010fe3649dd05172dc2a4bfc4036f1bb1b886a1e2c91d4b0bd6f6563d4a1fec8174a07f0f4e75b1eb7ea688eb10c0ff4c87ec181899e9a657fbe530d40c82
6
+ metadata.gz: 61269a7ae1127eebfbdec316e015e64e6e0a76b23d81433c1a2569f4ccfabbebccc289d4f5588dfa46ac100d33dd5269fdc6e37208167598cdee7e6066ec35d5
7
+ data.tar.gz: 3736371f1c53590cf7e811c19fa7262e70470f50207d414087a08967bd3cdc7375f93a9256906df7ce44bb10aa14b887045da11144791169ca6a861cf19e02c3
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- builder_apm (0.2.2)
4
+ builder_apm (0.2.5)
5
5
  rails (>= 4.0, < 8)
6
6
  redis (~> 4.5)
7
7
 
@@ -0,0 +1,7 @@
1
+ module BuilderApm
2
+ class ApplicationController < ActionController::Base
3
+ def redis_client
4
+ @redis_client ||= BuilderApm::RedisClient.client
5
+ end
6
+ end
7
+ end
@@ -1,8 +1,24 @@
1
1
  module BuilderApm
2
- class DashboardController < ActionController::Base
3
- def index
2
+ class DashboardController < ApplicationController
3
+ def index
4
+ if params[:clear] == 'true'
5
+ keys = redis_client.zrange("builder_apm:timestamps", 0, -1)
6
+
7
+ keys = keys.map { |key| "builder_apm:Request:#{key}" }
8
+ # Delete base keys
9
+ redis_client.pipelined do |pipeline|
10
+ pipeline.del("builder_apm:timestamps")
11
+ pipeline.del("builder_apm:errors")
12
+ pipeline.del("builder_apm:n_plus_one")
13
+ pipeline.del("builder_apm:slow")
14
+ end
15
+ debugger
16
+ # Delete keys in chunks
17
+ keys.each_slice(1000) do |key_chunk|
18
+ redis_client.del(*key_chunk) unless key_chunk.empty?
19
+ end
4
20
  end
21
+ end
5
22
  end
6
- end
7
-
23
+ end
8
24
 
@@ -1,5 +1,5 @@
1
1
  module BuilderApm
2
- class ErrorRequestsController < ActionController::Base
2
+ class ErrorRequestsController < ApplicationController
3
3
  def index
4
4
  end
5
5
  end
@@ -1,5 +1,5 @@
1
1
  module BuilderApm
2
- class NPlusOneController < ActionController::Base
2
+ class NPlusOneController < ApplicationController
3
3
  def index
4
4
  end
5
5
  end
@@ -1,5 +1,5 @@
1
1
  module BuilderApm
2
- class RecentRequestsController < ActionController::Base
2
+ class RecentRequestsController < ApplicationController
3
3
  def index
4
4
  end
5
5
  end
@@ -1,5 +1,5 @@
1
1
  module BuilderApm
2
- class RequestAnalysisController < ActionController::Base
2
+ class RequestAnalysisController < ApplicationController
3
3
  def index
4
4
  end
5
5
  end
@@ -1,40 +1,41 @@
1
1
  module BuilderApm
2
- class RequestDataController < ActionController::Base
3
- def index
4
- cursor = (params[:cursor].presence&.to_f) || "-inf"
5
- limit = params[:limit].presence&.to_i || 50
6
-
7
- if cursor == "-inf"
8
- @requests = retrieve_all_data_from_redis(limit)
9
- else
10
- @requests = retrieve_data_from_redis_since(cursor, limit)
11
- end
12
-
13
- render json: @requests
14
- end
2
+ class RequestDataController < ApplicationController
3
+ def index
4
+ cursor = params[:cursor].presence&.to_f || Time.now.to_f
5
+ limit = params[:limit].presence&.to_i || 50
6
+ type = params[:type].presence || 'timestamps'
15
7
 
16
- private
8
+ purge_old_keys
9
+ @requests = retrieve_data_from_redis_since(cursor, limit, type)
17
10
 
18
- def retrieve_data_from_redis_since(cursor, limit)
19
- # Use ZRANGEBYSCORE to get a range of keys, then get the corresponding data
20
- keys = redis_client.zrangebyscore("builder_apm:timestamps", cursor, "+inf", limit: [0, limit])
21
- data = keys.map { |key| redis_client.get("builder_apm:Request:#{key}") }
22
- begin
23
- data.map { |d| JSON.parse(d) }
24
- rescue => e
25
- raise e
26
- end
27
- end
28
-
29
- def retrieve_all_data_from_redis(limit)
30
- # Similar to the above, but we get all records
31
- keys = redis_client.zrange("builder_apm:timestamps", -limit, -1)
32
- data = keys.map { |key| redis_client.get("builder_apm:Request:#{key}") }
11
+ render json: @requests
12
+ end
13
+
14
+ private
15
+
16
+ def purge_old_keys
17
+ time_limit = (Time.now - 1.hour).to_f * 1000
18
+ redis_client.zremrangebyscore('builder_apm:timestamps', '-inf', time_limit)
19
+ redis_client.zremrangebyscore('builder_apm:errors', '-inf', time_limit)
20
+ redis_client.zremrangebyscore('builder_apm:n_plus_one', '-inf', time_limit)
21
+ redis_client.zremrangebyscore('builder_apm:slow', '-inf', time_limit)
22
+ end
23
+
24
+ def retrieve_data_from_redis_since(cursor, limit, type)
25
+ # Use ZREVRANGEBYSCORE to get a range of keys, then get the corresponding data
26
+ # if(type == 'timestamps')
27
+ keys = redis_client.zrevrangebyscore("builder_apm:#{type}", cursor, "-inf", limit: [0, limit])
28
+ # else
29
+ # keys = redis_client.lrange("builder_apm:#{type}", 0, -1)
30
+ # end
31
+
32
+ keys = keys.map { |key| "builder_apm:Request:#{key}" }
33
+ data = keys.empty? ? [] : redis_client.mget(*keys)
34
+ begin
33
35
  data.map { |d| JSON.parse(d) }
36
+ rescue => e
37
+ raise e
34
38
  end
35
-
36
- def redis_client
37
- @redis_client ||= BuilderApm::RedisClient.client
38
- end
39
- end
40
- end
39
+ end
40
+ end
41
+ end
@@ -1,8 +1,11 @@
1
1
  module BuilderApm
2
- class RequestDetailsController < ActionController::Base
2
+ class RequestDetailsController < ApplicationController
3
3
  def index
4
4
  @request_id = params[:request_id]
5
+ @data = redis_client.get("builder_apm:Request:#{@request_id}") || {}
6
+
5
7
  end
8
+
6
9
  end
7
10
  end
8
11
 
@@ -1,5 +1,5 @@
1
1
  module BuilderApm
2
- class SlowRequestsController < ActionController::Base
2
+ class SlowRequestsController < ApplicationController
3
3
  def index
4
4
  end
5
5
  end
@@ -1,5 +1,5 @@
1
1
  module BuilderApm
2
- class WipController < ActionController::Base
2
+ class WipController < ApplicationController
3
3
  def index
4
4
  end
5
5
  end
@@ -64,7 +64,10 @@ body {
64
64
  font-size: 1em; /* Adjust to match your anchors */
65
65
  transition: background-color 0.3s ease, color 0.3s ease; /* Smooth transition */
66
66
  }
67
-
67
+ td.long_text{
68
+ max-width:400px;
69
+ overflow-wrap:break-word;
70
+ }
68
71
  th.sortable {
69
72
  cursor: pointer; /* Makes the button cursor appear as a hand */
70
73
  }
@@ -6,12 +6,10 @@ google.charts.setOnLoadCallback(drawCharts);
6
6
 
7
7
  function drawCharts() {
8
8
  prepareChartData(aggregationInterval);
9
- setInterval(function() {
10
- var autoUpdate = $("#autoUpdate").prop('checked');
11
- if (autoUpdate) {
12
- prepareChartData(aggregationInterval);
13
- }
14
- }, 5000);
9
+ autoFetchDataAndUpdateStorage(function(updatedData){
10
+ // Render the table with the updated data
11
+ prepareChartData(aggregationInterval)
12
+ });
15
13
  }
16
14
  function prepareChartData(aggregationInterval) {
17
15
  fetchDataAndUpdateStorage(function(updatedData) {
@@ -6,53 +6,50 @@ var allData = null;
6
6
  var isAsc = false;
7
7
  var current_sort_field = 'real_start_time';
8
8
  var start_time = Date.now();
9
+ var data_gathered_counter = 0;
10
+ var time_cursor = <%= Time.now.to_f * 1000 %>;
9
11
 
10
12
  function fetchDataAndUpdateStorage(onSuccess) {
11
13
  start_time = Date.now();
12
- var initialCursor = localStorage.getItem('builder_apm_cursor') || "-inf";
13
- var storedData = localStorage.getItem('builder_apm_requests');
14
- if (storedData) {
15
- var dataCompressed = decompress(storedData, function(result, error) {
16
- allData = JSON.parse(result) || [];
17
- fetchAndProcessData(initialCursor, onSuccess);
18
- });
19
- } else {
14
+ var initialCursor = time_cursor || start_time;
15
+ if(allData == null) {
20
16
  allData = [];
17
+ }
21
18
  // Start fetching data
22
19
  fetchAndProcessData(initialCursor, onSuccess);
23
- }
20
+
24
21
  }
25
22
  function fetchAndProcessData(cursor, onSuccess) {
26
- // Get the last request time
27
- var limit = 100;
23
+ var limit = 1000;
24
+ var type = typeof fetch_type !== 'undefined' ? fetch_type : 'timestamps';
28
25
 
29
26
  // Make an AJAX request to fetch latest data
30
- $.get('/builder_apm/request_data?cursor=' + cursor + '&limit=' + limit, function(newData) {
31
-
27
+ $.get('/builder_apm/request_data?cursor=' + cursor + '&limit=' + limit + '&type=' +type, function(newData) {
28
+ data_gathered_counter++;
32
29
  newData = newData.map(request => calcDurations(request));
33
30
 
34
31
  processData(allData, null, newData, onSuccess);
35
32
 
36
33
  // Find the latest request in the newly fetched data
37
- var latestRequest = newData.reduce((latest, current) => current.real_end_time > latest.real_end_time ? current : latest, newData[0]);
34
+ // var latestRequest = newData.reduce((latest, current) => current.real_end_time > latest.real_end_time ? current : latest, newData[0]);
38
35
 
39
36
  // If latest request's end_time is less than start_time, and there's more data (received data is equal to limit), fetch next batch
40
- if (latestRequest && latestRequest.real_end_time < start_time && newData.length === limit) {
41
- fetchAndProcessData(localStorage.getItem('builder_apm_cursor'), onSuccess);
42
- } else {
43
- try {
44
- // Store the updated data back to local storage
45
- compress(JSON.stringify(allData), function(result, error) {
46
- if(result != null) {
47
- localStorage.setItem('builder_apm_requests', result);
48
- }
49
- });
50
- } catch(e) {
51
- console.error("Error storing data to local storage. Might be out of storage space.", e);
52
- alert('not enough local storage')
53
- }
54
- onSuccess(allData);
55
- }
37
+ // if (data_gathered_counter < 5 && latestRequest && latestRequest.real_end_time < start_time && newData.length === limit) {
38
+ // fetchAndProcessData(var time_cursor = <%= Time.now.to_f * 1000 %>;, onSuccess);
39
+ // } else {
40
+ // try {
41
+ // // Store the updated data back to local storage
42
+ // compress(JSON.stringify(allData), function(result, error) {
43
+ // if(result != null) {
44
+ // localStorage.setItem('builder_apm_requests', result);
45
+ // }
46
+ // });
47
+ // } catch(e) {
48
+ // console.error("Error storing data to local storage. Might be out of storage space.", e);
49
+ // alert('not enough local storage')
50
+ // }
51
+ // onSuccess(allData);
52
+
56
53
  });
57
54
  }
58
55
 
@@ -81,8 +78,7 @@ function processData(storedData, error, newData, onSuccess) {
81
78
  // Update the cursor
82
79
  if (newData.length > 0) {
83
80
  var lastRequest = newData[newData.length - 1];
84
- cursor = lastRequest.real_end_time+0.001;
85
- localStorage.setItem('builder_apm_cursor', cursor.toString());
81
+ time_cursor = lastRequest.real_end_time-0.001;
86
82
  }
87
83
  } catch(e) {
88
84
  console.error("Error storing data to local storage. Might be out of storage space.", e);
@@ -101,6 +97,24 @@ function loadRequest(request_id) {
101
97
  return requestData;
102
98
  }
103
99
 
100
+ function autoFetchDataAndUpdateStorage(onSuccess) {
101
+ var autoUpdate = $('#autoUpdate').is(':checked');
102
+ if (autoUpdate) {
103
+ fetchDataAndUpdateStorage(function(updatedData) {
104
+ onSuccess(updatedData);
105
+ // Schedule the next run
106
+ setTimeout(function() {
107
+ autoFetchDataAndUpdateStorage(onSuccess);
108
+ }, 5000);
109
+ });
110
+ } else {
111
+ // If auto update is not checked, schedule the next check
112
+ setTimeout(function() {
113
+ autoFetchDataAndUpdateStorage(onSuccess);
114
+ }, 5000);
115
+ }
116
+ }
117
+
104
118
  function processStack(stack, processSqlEvent) {
105
119
  if(stack == null) {
106
120
  stack = {}
@@ -159,8 +173,12 @@ function tagQueriesWithNPlusOne(requestInput) {
159
173
  function addSortingClick() {
160
174
  // Assuming your table headers have a class name "sortable"
161
175
  $('.sortable').click(function() {
176
+ if (current_sort_field == $(this).data('field')) {
177
+ isAsc = !isAsc;
178
+ } else {
179
+ isAsc = false;
180
+ }
162
181
  current_sort_field = $(this).data('field'); // Assuming data-field attribute contains the name of the field to sort
163
- isAsc = !isAsc;
164
182
 
165
183
  renderTable(allData);
166
184
  });
@@ -202,10 +220,12 @@ function getNPlusOneRequests(requestArray) {
202
220
  function aggregateRequests(requestArray) {
203
221
  let aggregates = {};
204
222
  requestArray.forEach(request => {
205
- let key = `${request.controller}#${request.action}|${request.method}|${request.path}`;
223
+ let key = `${request.controller}#${request.action}`;
206
224
  if (!aggregates[key]) {
207
225
  aggregates[key] = {
208
226
  count: 0,
227
+ method: request.method,
228
+ path: request.path,
209
229
  totalDuration: 0,
210
230
  totalDbRuntime: 0,
211
231
  totalViewRuntime: 0,
@@ -214,6 +234,7 @@ function aggregateRequests(requestArray) {
214
234
  slowestViewRuntime: 0
215
235
  };
216
236
  }
237
+
217
238
  aggregates[key].count++;
218
239
  aggregates[key].totalDuration += request.duration;
219
240
  aggregates[key].totalDbRuntime += request.db_runtime || 0;
@@ -225,11 +246,11 @@ function aggregateRequests(requestArray) {
225
246
 
226
247
  let results = [];
227
248
  for (let key in aggregates) {
228
- let [controller, method, path] = key.split('|');
249
+ let controller = key;
229
250
  results.push({
230
251
  controller,
231
- method,
232
- path,
252
+ method: aggregates[key].method,
253
+ path: aggregates[key].path,
233
254
  count: aggregates[key].count,
234
255
  averageDuration: aggregates[key].totalDuration / aggregates[key].count,
235
256
  averageDbRuntime: aggregates[key].totalDbRuntime / aggregates[key].count,
@@ -1,24 +1,27 @@
1
1
  <script>
2
+ var fetch_type = 'errors';
2
3
  $(document).ready(function() {
3
-
4
- fetchDataAndUpdateStorage(function(updatedData) {
5
- renderTable(updatedData);
4
+ var storedData = localStorage.getItem('builder_apm_requests');
5
+ if (storedData) {
6
+ var dataCompressed = decompress(storedData, function(result, error) {
7
+ allData = JSON.parse(result) || [];
8
+ renderTable(allData);
6
9
  });
10
+ }
7
11
 
8
- setInterval(function() {
9
- var autoUpdate = $('#autoUpdate').is(':checked');
10
- if (autoUpdate) {
11
- fetchDataAndUpdateStorage(function(updatedData) {
12
- renderTable(updatedData);
13
- });
14
- }
15
- }, 5000);
12
+ fetchDataAndUpdateStorage(function(updatedData) {
13
+ renderTable(updatedData);
14
+ });
15
+
16
+ autoFetchDataAndUpdateStorage(function(updatedData){
17
+ // Render the table with the updated data
18
+ renderTable(updatedData);
19
+ });
16
20
 
17
21
  addSortingClick();
18
22
  });
19
23
 
20
24
  function renderTable(data, target = null) {
21
- data = data.filter(item => item.status === 500);
22
25
  data = sortDataBy(current_sort_field, data);
23
26
 
24
27
  // Get a reference to the table body
@@ -27,12 +30,17 @@ $(document).ready(function() {
27
30
  // Clear the table body
28
31
  tableBody.empty();
29
32
 
33
+ if(data.length == 0) {
34
+ var messageElement = $('<p>').text('No 500 Requests found');
35
+ tableBody.after(messageElement);
36
+ return;
37
+ }
30
38
  // Create new table rows for each data item
31
39
  data.forEach(function(item) {
32
40
  var row = $('<tr>');
33
41
 
34
42
  $('<td>').text(new Date(item['start_time']).toLocaleString()).appendTo(row);
35
- $('<td>').text(item['controller'] + '#' + item['action']).appendTo(row);
43
+ $('<td>').addClass('long_text').text(item['controller'] + '#' + item['action']).appendTo(row);
36
44
  $('<td>').text(item['method']).appendTo(row);
37
45
  $('<td>').text(item['status']).appendTo(row);
38
46
  $('<td>').append(item['exception_class']).appendTo(row);
@@ -1,23 +1,26 @@
1
1
  <script>
2
+ var fetch_type = 'n_plus_one';
2
3
  $(document).ready(function() {
4
+ var storedData = localStorage.getItem('builder_apm_requests');
5
+ if (storedData) {
6
+ var dataCompressed = decompress(storedData, function(result, error) {
7
+ allData = JSON.parse(result) || [];
8
+ renderTable(allData);
9
+ });
10
+ }
3
11
  fetchDataAndUpdateStorage(function(updatedData) {
4
12
  renderTable(updatedData);
5
13
  });
6
14
 
7
- setInterval(function() {
8
- var autoUpdate = $('#autoUpdate').is(':checked');
9
- if (autoUpdate) {
10
- fetchDataAndUpdateStorage(function(updatedData) {
11
- renderTable(updatedData);
12
- });
13
- }
14
- }, 5000);
15
+ autoFetchDataAndUpdateStorage(function(updatedData){
16
+ // Render the table with the updated data
17
+ renderTable(updatedData);
18
+ });
15
19
 
16
20
  addSortingClick();
17
21
  });
18
22
 
19
23
  function renderTable(data, target = null) {
20
- data = detectNPlusOne(data);
21
24
  data = sortDataBy(current_sort_field, data);
22
25
 
23
26
  // Get a reference to the table body
@@ -26,12 +29,17 @@ $(document).ready(function() {
26
29
  // Clear the table body
27
30
  tableBody.empty();
28
31
 
32
+ if(data.length == 0) {
33
+ var messageElement = $('<p>').text('No N+1 Requests found');
34
+ tableBody.after(messageElement);
35
+ return;
36
+ }
29
37
  // Create new table rows for each data item
30
38
  data.forEach(function(item) {
31
39
  var row = $('<tr>');
32
40
 
33
41
  $('<td>').text(new Date(item['start_time']).toLocaleString()).appendTo(row);
34
- $('<td>').text(item['controller'] + '#' + item['action']).appendTo(row);
42
+ $('<td>').addClass('long_text').text(item['controller'] + '#' + item['action']).appendTo(row);
35
43
  $('<td>').text(item['method']).appendTo(row);
36
44
  $('<td>').text(item['status']).appendTo(row);
37
45
  $('<td>').append(renderDuration(item['duration'])).appendTo(row);
@@ -1,26 +1,27 @@
1
1
  <script>
2
2
 
3
3
  $(document).ready(function() {
4
+ var storedData = localStorage.getItem('builder_apm_requests');
5
+ if (storedData) {
6
+ var dataCompressed = decompress(storedData, function(result, error) {
7
+ allData = JSON.parse(result) || [];
8
+ renderTable(allData);
9
+ });
10
+ }
4
11
  fetchDataAndUpdateStorage(function(updatedData) {
5
12
  // Render the table with the updated data
6
13
  renderTable(updatedData);
7
14
  });
8
15
 
9
16
 
10
- setInterval(function() {
11
- var autoUpdate = $('#autoUpdate').is(':checked');
12
- if (autoUpdate) {
13
- fetchDataAndUpdateStorage(function(updatedData) {
14
- // Render the table with the updated data
15
- renderTable(updatedData);
16
- });
17
- }
18
- }, 5000);
17
+ autoFetchDataAndUpdateStorage(function(updatedData){
18
+ // Render the table with the updated data
19
+ renderTable(updatedData);
20
+ });
19
21
 
20
22
  addSortingClick();
21
23
  });
22
24
 
23
-
24
25
  function renderTable(data, target = null) {
25
26
  data = sortDataBy(current_sort_field, data);
26
27
 
@@ -30,12 +31,18 @@ $(document).ready(function() {
30
31
  // Clear the table body
31
32
  tableBody.empty();
32
33
 
34
+
35
+ if(data.length == 0) {
36
+ var messageElement = $('<p>').text('No Requests found');
37
+ tableBody.after(messageElement);
38
+ return;
39
+ }
33
40
  // Create new table rows for each data item
34
41
  data.forEach(function(item) {
35
42
  var row = $('<tr>');
36
43
 
37
44
  $('<td>').text(new Date(item['start_time']).toLocaleString()).appendTo(row);
38
- $('<td>').text(item['controller'] + '#' + item['action']).appendTo(row);
45
+ $('<td>').addClass('long_text').text(item['controller'] + '#' + item['action']).appendTo(row);
39
46
  $('<td>').text(item['method']).appendTo(row);
40
47
  $('<td>').text(item['status']).appendTo(row);
41
48
  $('<td>').append(renderDuration(item['real_duration_time'])).appendTo(row);
@@ -46,13 +53,10 @@ $(document).ready(function() {
46
53
 
47
54
  if(item.stack && item.stack.length > 0) {
48
55
 
49
- var actionButton = $('<button>').text('Details');
50
- actionButton.on('click', function() {
51
- // Replace 'your-details-url' with the actual URL where the details are to be fetched.
52
- // It's assumed the ID is required as a URL parameter, modify as per your requirements.
53
- window.location.href = '<%= request_details_path %>?request_id=' + item['request_id'];
54
- });
55
- actionButton.appendTo(actionTd);
56
+ var actionLink = $('<a>').text('Details');
57
+ actionLink.attr('href', '<%= request_details_path %>?request_id=' + item['request_id']);
58
+ actionLink.attr('target', '_blank');
59
+ actionLink.appendTo(actionTd);
56
60
  }
57
61
  actionTd.appendTo(row);
58
62
 
@@ -1,17 +1,21 @@
1
1
  <script>
2
2
  $(document).ready(function() {
3
+ var storedData = localStorage.getItem('builder_apm_requests');
4
+ if (storedData) {
5
+ var dataCompressed = decompress(storedData, function(result, error) {
6
+ allData = JSON.parse(result) || [];
7
+ renderTable(allData);
8
+ });
9
+ }
10
+
3
11
  fetchDataAndUpdateStorage(function(updatedData) {
4
12
  renderTable(updatedData);
5
13
  });
6
14
 
7
- setInterval(function() {
8
- var autoUpdate = $('#autoUpdate').is(':checked');
9
- if (autoUpdate) {
10
- fetchDataAndUpdateStorage(function(updatedData) {
11
- renderTable(updatedData);
12
- });
13
- }
14
- }, 5000);
15
+ autoFetchDataAndUpdateStorage(function(updatedData){
16
+ // Render the table with the updated data
17
+ renderTable(updatedData);
18
+ });
15
19
 
16
20
  addSortingClick();
17
21
  });
@@ -26,13 +30,18 @@ $(document).ready(function() {
26
30
  // Clear the table body
27
31
  tableBody.empty();
28
32
 
33
+ if(data.length == 0) {
34
+ var messageElement = $('<p>').text('No Requests found');
35
+ tableBody.after(messageElement);
36
+ return;
37
+ }
29
38
  // Create new table rows for each data item
30
39
  data.forEach(function(item) {
31
40
  var row = $('<tr>');
32
41
 
33
- $('<td>').text(item['controller']).appendTo(row);
42
+ $('<td>').addClass('long_text').text(item['controller']).appendTo(row);
34
43
  $('<td>').text(item['method']).appendTo(row);
35
- $('<td>').text(item['path']).appendTo(row);
44
+ $('<td>').addClass('long_text').text(item['path']).appendTo(row);
36
45
  $('<td>').text(item['count']).appendTo(row);
37
46
  $('<td>').append(renderDuration(item['averageDuration'])).appendTo(row);
38
47
  $('<td>').append(renderDuration(item['averageDbRuntime'])).appendTo(row);
@@ -1,31 +1,19 @@
1
1
  <script>
2
2
  var request_id = '<%= @request_id %>';
3
+ var request_data = <%= @data.to_json.html_safe %>;
3
4
  var current_index = 0;
4
5
 
5
6
  $(document).ready(function() {
6
7
  prepPage();
7
- loadLocalData(function(){
8
- var request = loadRequest(request_id);
9
- tagQueriesWithNPlusOne(request);
10
- const preparedData = flattenData(request.stack);
11
-
12
- $("#details_div").empty()
13
- if(request.exception_message) {
14
- $("#details_div").append(errorDetails(request));
15
- }
16
- $("#details_div").append(renderDetails(preparedData));
17
-
18
- // if(request.has_n_plus_one){
19
- // // Find all div elements with 'possible_n_plus_one' class
20
- // $('div.possible_n_plus_one').each(function() {
21
- // // Traverse up the parent hierarchy
22
- // $(this).parents().each(function() {
23
- // // If the parent has the 'has_children' class, trigger a click event
24
- // $(this).show();
25
- // });
26
- // });
27
- // }
28
- });
8
+ var request = JSON.parse(request_data);
9
+ tagQueriesWithNPlusOne(request);
10
+ const preparedData = flattenData(request.stack);
11
+
12
+ $("#details_div").empty()
13
+ if(request.exception_message) {
14
+ $("#details_div").append(errorDetails(request));
15
+ }
16
+ $("#details_div").append(renderDetails(preparedData));
29
17
  });
30
18
 
31
19
  function prepPage(){
@@ -1,23 +1,27 @@
1
1
  <script>
2
+ var fetch_type = 'slow';
2
3
  $(document).ready(function() {
4
+ var storedData = localStorage.getItem('builder_apm_requests');
5
+ if (storedData) {
6
+ var dataCompressed = decompress(storedData, function(result, error) {
7
+ allData = JSON.parse(result) || [];
8
+ renderTable(allData);
9
+ });
10
+ }
11
+
3
12
  fetchDataAndUpdateStorage(function(updatedData) {
4
13
  renderTable(updatedData);
5
14
  });
6
15
 
7
- setInterval(function() {
8
- var autoUpdate = $('#autoUpdate').is(':checked');
9
- if (autoUpdate) {
10
- fetchDataAndUpdateStorage(function(updatedData) {
11
- renderTable(updatedData);
12
- });
13
- }
14
- }, 5000);
16
+ autoFetchDataAndUpdateStorage(function(updatedData){
17
+ // Render the table with the updated data
18
+ renderTable(updatedData);
19
+ });
15
20
 
16
21
  addSortingClick();
17
22
  });
18
23
 
19
24
  function renderTable(data, target = null) {
20
- data = data.filter(item => item.duration > 1500);
21
25
  data = sortDataBy(current_sort_field, data);
22
26
 
23
27
  // Get a reference to the table body
@@ -26,12 +30,17 @@ $(document).ready(function() {
26
30
  // Clear the table body
27
31
  tableBody.empty();
28
32
 
33
+ if(data.length == 0) {
34
+ var messageElement = $('<p>').text('No Slow Requests found');
35
+ tableBody.after(messageElement);
36
+ return;
37
+ }
29
38
  // Create new table rows for each data item
30
39
  data.forEach(function(item) {
31
40
  var row = $('<tr>');
32
41
 
33
42
  $('<td>').text(new Date(item['start_time']).toLocaleString()).appendTo(row);
34
- $('<td>').text(item['controller'] + '#' + item['action']).appendTo(row);
43
+ $('<td>').addClass('long_text').text(item['controller'] + '#' + item['action']).appendTo(row);
35
44
  $('<td>').text(item['method']).appendTo(row);
36
45
  $('<td>').text(item['status']).appendTo(row);
37
46
  $('<td>').append(renderDuration(item['duration'])).appendTo(row);
@@ -46,7 +46,6 @@ $(document).ready(function() {
46
46
  <li><%= link_to 'Slow Requests', slow_requests_path, class: ("active" if current_page?(slow_requests_path)) %></li>
47
47
  <li><%= link_to 'N+1', n_plus_one_path, class: ("active" if current_page?(n_plus_one_path)) %></li>
48
48
  <li id="dark-mode-toggle"><button id="darkModeToggle" class="nav-button">Toggle Dark Mode</button></li>
49
- <li id="dark-mode-toggle"><button id="clearData" class="nav-button">Clear Data</button></li>
50
49
  </ul>
51
50
  </nav>
52
51
  <div id="options">
@@ -4,12 +4,14 @@ module BuilderApm
4
4
  attr_accessor :enable_controller_profiler
5
5
  attr_accessor :enable_active_record_profiler
6
6
  attr_accessor :enable_methods_profiler
7
+ attr_accessor :enable_n_plus_one_profiler
7
8
 
8
9
  def initialize
9
10
  @redis_url = 'redis://localhost:6379/0'
10
11
  @enable_controller_profiler = true
11
12
  @enable_active_record_profiler = true
12
13
  @enable_methods_profiler = true
14
+ @enable_n_plus_one_profiler = true
13
15
  end
14
16
  end
15
17
  end
@@ -9,6 +9,8 @@ module BuilderApm
9
9
  def call(env)
10
10
  request_id = env["action_dispatch.request_id"]
11
11
  Thread.current["request_id"] = request_id
12
+ Thread.current[:n_plus_one_duration] = 0
13
+ Thread.current[:has_n_plus_one] = false
12
14
  start_time = Time.now.to_f * 1000
13
15
 
14
16
  @status, @headers, @response = @app.call(env)
@@ -18,6 +20,10 @@ module BuilderApm
18
20
  end_time = Time.now.to_f * 1000
19
21
  handle_timing(start_time, end_time, request_id)
20
22
 
23
+ Thread.current['request_data'] = nil
24
+ Thread.current[:has_n_plus_one] = nil
25
+ Thread.current[:n_plus_one_duration] = nil
26
+
21
27
  [@status, @headers, @response]
22
28
  end
23
29
 
@@ -28,14 +34,25 @@ module BuilderApm
28
34
  data = Thread.current['request_data']
29
35
 
30
36
  if data
37
+ data[:has_n_plus_one] = Thread.current[:has_n_plus_one]
38
+ data[:n_plus_one_duration] = Thread.current[:n_plus_one_duration]
31
39
  data[:real_start_time] = start_time
32
40
  data[:real_end_time] = end_time
33
41
  data[:real_duration_time] = end_time - start_time
34
- Thread.current['request_data'] = nil
35
-
42
+
36
43
  begin
37
- @redis_client.zadd("builder_apm:timestamps", end_time, request_id)
38
- @redis_client.set("builder_apm:Request:#{data[:request_id]}", data.to_json)
44
+ @redis_client.pipelined do |pipeline|
45
+ pipeline.rpush("builder_apm:Analysis:#{data[:controller]}##{data[:action]}:duration", data[:real_duration_time]||0)
46
+ pipeline.rpush("builder_apm:Analysis:#{data[:controller]}##{data[:action]}:db_runtime", data[:db_runtime]||0)
47
+ pipeline.rpush("builder_apm:Analysis:#{data[:controller]}##{data[:action]}:view_runtime", data[:view_runtime]||0)
48
+
49
+ pipeline.zadd("builder_apm:timestamps", end_time, request_id)
50
+ pipeline.zadd("builder_apm:errors", end_time, request_id) if data[:status] == 500
51
+ pipeline.zadd("builder_apm:n_plus_one", end_time, request_id) if data[:has_n_plus_one]
52
+ pipeline.zadd("builder_apm:slow", end_time, request_id) if data[:real_duration_time] > 1500
53
+ ttl = (1.hour + 1.minute).to_i
54
+ pipeline.set("builder_apm:Request:#{data[:request_id]}", data.to_json, ex: ttl)
55
+ end
39
56
  rescue => e
40
57
  Rails.logger.error "Redis Missing"
41
58
  end
@@ -54,14 +54,25 @@ module BuilderApm
54
54
  end
55
55
 
56
56
  def store_sql_query_data(sql_query_data)
57
- Thread.current[:sql_event_id] = sql_query_data[:sql_id]
58
-
59
- if Thread.current[:stack]&.any?
60
- Thread.current[:stack].last[:sql_events].push(sql_query_data)
61
- else
62
- (Thread.current[:stack] ||= []).push({sql_events: [sql_query_data], children: []})
63
- end
57
+ Thread.current[:sql_event_id] = sql_query_data[:sql_id]
58
+
59
+ # Create the stack if it doesn't exist yet
60
+ stack = (Thread.current[:stack] ||= [])
61
+
62
+ if stack&.any?
63
+ stack.last[:sql_events].push(sql_query_data)
64
+ else
65
+ stack.push({sql_events: [sql_query_data], children: []})
64
66
  end
67
+ # Do the N+1 check if it wasn't done yet
68
+ if BuilderApm.configuration.enable_n_plus_one_profiler && Thread.current[:has_n_plus_one] == false
69
+ start_time = Time.now.to_f * 1000
70
+ perform_n_plus_one_check(stack)
71
+ duration = (Time.now.to_f * 1000) - start_time
72
+ Thread.current[:n_plus_one_duration] ||= 0
73
+ Thread.current[:n_plus_one_duration] += duration
74
+ end
75
+ end
65
76
 
66
77
  def update_last_sql_query_data_with_instantiation_info(event)
67
78
  stack = Thread.current[:stack]
@@ -81,7 +92,31 @@ module BuilderApm
81
92
  ensure
82
93
  Thread.current[:sql_event_id] = nil
83
94
  end
84
- end
95
+ end
96
+
97
+ # Add this method to perform the N+1 check
98
+ def perform_n_plus_one_check(stack)
99
+ sql_queries = stack.map { |frame| frame[:sql_events] }.flatten
100
+
101
+ # Group queries count by table and triggering_line
102
+ queries_count = Hash.new { |h, k| h[k] = {count: 0, indices: []} }
103
+ sql_queries.each_with_index do |query, index|
104
+ match = query[:sql].match(/FROM ['`"]*([^ '`"]+)['`"]*/i)
105
+ if match
106
+ table_and_line = "#{match[1]}|#{query[:triggering_line]}"
107
+ queries_count[table_and_line][:count] += 1
108
+ queries_count[table_and_line][:indices].push(index)
109
+ end
110
+ end
111
+
112
+ # If any N+1 issue is found, set 'has_n_plus_one' to true and return
113
+ queries_count.each do |_, value|
114
+ if value[:count] > 1
115
+ Thread.current[:has_n_plus_one] = true
116
+ return
117
+ end
118
+ end
119
+ end
85
120
  end
86
121
  end
87
122
  end
@@ -1,3 +1,3 @@
1
1
  module BuilderApm
2
- VERSION = "0.2.5"
2
+ VERSION = "0.3.0"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: builder_apm
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.5
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Paul Ketelle
@@ -60,6 +60,7 @@ files:
60
60
  - LICENSE.txt
61
61
  - README.md
62
62
  - Rakefile
63
+ - app/controllers/builder_apm/application_controller.rb
63
64
  - app/controllers/builder_apm/dashboard_controller.rb
64
65
  - app/controllers/builder_apm/error_requests_controller.rb
65
66
  - app/controllers/builder_apm/n_plus_one_controller.rb