builder_apm 0.2.5 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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