builder_apm 0.2.4 → 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 (33) 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 -36
  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/error_requests/index.html.erb +1 -0
  15. data/app/views/builder_apm/js/_dashboard.html.erb +4 -6
  16. data/app/views/builder_apm/js/_data_fetcher.html.erb +74 -33
  17. data/app/views/builder_apm/js/_error_requests.html.erb +22 -13
  18. data/app/views/builder_apm/js/_n_plus_one.html.erb +19 -10
  19. data/app/views/builder_apm/js/_recent_requests.html.erb +23 -18
  20. data/app/views/builder_apm/js/_request_analysis.html.erb +20 -10
  21. data/app/views/builder_apm/js/_request_details.html.erb +10 -22
  22. data/app/views/builder_apm/js/_slow_requests.html.erb +20 -10
  23. data/app/views/builder_apm/n_plus_one/index.html.erb +1 -0
  24. data/app/views/builder_apm/recent_requests/index.html.erb +1 -0
  25. data/app/views/builder_apm/request_analysis/index.html.erb +1 -0
  26. data/app/views/builder_apm/shared/_header.html.erb +0 -1
  27. data/app/views/builder_apm/slow_requests/index.html.erb +1 -0
  28. data/lib/builder_apm/configuration.rb +2 -0
  29. data/lib/builder_apm/controllers/instrumenter.rb +3 -5
  30. data/lib/builder_apm/middleware/timing.rb +25 -4
  31. data/lib/builder_apm/models/instrumenter.rb +43 -8
  32. data/lib/builder_apm/version.rb +1 -1
  33. metadata +3 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9e345a9d0a4268df1fd70c2dd2311eba4ce9fe9172907a74ec03b6922b6623fd
4
- data.tar.gz: e657d3f16327b2c8aa0a79f90a5a80f57350205fccb516ac6d58d7d1e3e0bcc9
3
+ metadata.gz: 31a9a4393e090f4980960c61d6f5cb2cf307a352534b9f8f264c2238dcd281f4
4
+ data.tar.gz: a01935162ea2b1fa2701a080536aadc6259c3a9c102148ef1df4475ddd7a89de
5
5
  SHA512:
6
- metadata.gz: 3b644056f083a38a59fdb5f94c26f5108e72474cc5d7c01e76ef3f7d56802b0b87c96602f723e260f9b29894de749852b3c198f80dcf0c3a28960143e28cd029
7
- data.tar.gz: c09bfba63427594bc9a7b51cc70122902d6407b8e65415994a934c48654a756c65a3ab4e5119aa811e491f1639107fbc2b2b27c9bb36a566d0b12a2926153da5
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,41 +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
- debugger
26
- raise e
27
- end
28
- end
29
-
30
- def retrieve_all_data_from_redis(limit)
31
- # Similar to the above, but we get all records
32
- keys = redis_client.zrange("builder_apm:timestamps", -limit, -1)
33
- 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
34
35
  data.map { |d| JSON.parse(d) }
36
+ rescue => e
37
+ raise e
35
38
  end
36
-
37
- def redis_client
38
- @redis_client ||= BuilderApm::RedisClient.client
39
- end
40
- end
41
- 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
  }
@@ -5,6 +5,7 @@
5
5
  <tr>
6
6
  <th class="sortable" data-field="start_time">Time</th>
7
7
  <th class="sortable" data-field="controller">Controller#Action</th>
8
+ <th class="sortable" data-field="method">Method</th>
8
9
  <th class="sortable" data-field="status">Status</th>
9
10
  <th class="sortable" data-field="exception_class">Error Class</th>
10
11
  <th class="sortable" data-field="exception_message">Error Message</th>
@@ -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) {
@@ -5,32 +5,53 @@
5
5
  var allData = null;
6
6
  var isAsc = false;
7
7
  var current_sort_field = 'real_start_time';
8
+ var start_time = Date.now();
9
+ var data_gathered_counter = 0;
10
+ var time_cursor = <%= Time.now.to_f * 1000 %>;
8
11
 
9
12
  function fetchDataAndUpdateStorage(onSuccess) {
10
- // Get the last request time
11
- var cursor = localStorage.getItem('builder_apm_cursor') || "-inf";
12
- var limit = 50;
13
+ start_time = Date.now();
14
+ var initialCursor = time_cursor || start_time;
15
+ if(allData == null) {
16
+ allData = [];
17
+ }
18
+ // Start fetching data
19
+ fetchAndProcessData(initialCursor, onSuccess);
13
20
 
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));
21
+ }
22
+ function fetchAndProcessData(cursor, onSuccess) {
23
+ var limit = 1000;
24
+ var type = typeof fetch_type !== 'undefined' ? fetch_type : 'timestamps';
25
+
26
+ // Make an AJAX request to fetch latest data
27
+ $.get('/builder_apm/request_data?cursor=' + cursor + '&limit=' + limit + '&type=' +type, function(newData) {
28
+ data_gathered_counter++;
29
+ newData = newData.map(request => calcDurations(request));
18
30
 
19
- if(allData != null){
20
31
  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
- }
32
+
33
+ // Find the latest request in the newly fetched data
34
+ // var latestRequest = newData.reduce((latest, current) => current.real_end_time > latest.real_end_time ? current : latest, newData[0]);
35
+
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
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
+
53
+ });
54
+ }
34
55
 
35
56
  function loadLocalData(onSuccess) {
36
57
  if(allData) {
@@ -42,6 +63,7 @@ function loadLocalData(onSuccess) {
42
63
  processData(JSON.parse(result), error, [], onSuccess);
43
64
  });
44
65
  } else {
66
+ allData = [];
45
67
  fetchDataAndUpdateStorage(onSuccess);
46
68
  }
47
69
  }
@@ -52,18 +74,11 @@ function processData(storedData, error, newData, onSuccess) {
52
74
  // Append the new data to the stored data
53
75
  allData = storedData.concat(newData);
54
76
  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
77
 
62
78
  // Update the cursor
63
79
  if (newData.length > 0) {
64
80
  var lastRequest = newData[newData.length - 1];
65
- cursor = lastRequest.real_end_time+0.001;
66
- localStorage.setItem('builder_apm_cursor', cursor.toString());
81
+ time_cursor = lastRequest.real_end_time-0.001;
67
82
  }
68
83
  } catch(e) {
69
84
  console.error("Error storing data to local storage. Might be out of storage space.", e);
@@ -82,6 +97,24 @@ function loadRequest(request_id) {
82
97
  return requestData;
83
98
  }
84
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
+
85
118
  function processStack(stack, processSqlEvent) {
86
119
  if(stack == null) {
87
120
  stack = {}
@@ -140,8 +173,12 @@ function tagQueriesWithNPlusOne(requestInput) {
140
173
  function addSortingClick() {
141
174
  // Assuming your table headers have a class name "sortable"
142
175
  $('.sortable').click(function() {
176
+ if (current_sort_field == $(this).data('field')) {
177
+ isAsc = !isAsc;
178
+ } else {
179
+ isAsc = false;
180
+ }
143
181
  current_sort_field = $(this).data('field'); // Assuming data-field attribute contains the name of the field to sort
144
- isAsc = !isAsc;
145
182
 
146
183
  renderTable(allData);
147
184
  });
@@ -183,10 +220,12 @@ function getNPlusOneRequests(requestArray) {
183
220
  function aggregateRequests(requestArray) {
184
221
  let aggregates = {};
185
222
  requestArray.forEach(request => {
186
- let key = `${request.controller}#${request.method}|${request.path}`;
223
+ let key = `${request.controller}#${request.action}`;
187
224
  if (!aggregates[key]) {
188
225
  aggregates[key] = {
189
226
  count: 0,
227
+ method: request.method,
228
+ path: request.path,
190
229
  totalDuration: 0,
191
230
  totalDbRuntime: 0,
192
231
  totalViewRuntime: 0,
@@ -195,6 +234,7 @@ function aggregateRequests(requestArray) {
195
234
  slowestViewRuntime: 0
196
235
  };
197
236
  }
237
+
198
238
  aggregates[key].count++;
199
239
  aggregates[key].totalDuration += request.duration;
200
240
  aggregates[key].totalDbRuntime += request.db_runtime || 0;
@@ -206,10 +246,11 @@ function aggregateRequests(requestArray) {
206
246
 
207
247
  let results = [];
208
248
  for (let key in aggregates) {
209
- let [controller, path] = key.split('|');
249
+ let controller = key;
210
250
  results.push({
211
251
  controller,
212
- path,
252
+ method: aggregates[key].method,
253
+ path: aggregates[key].path,
213
254
  count: aggregates[key].count,
214
255
  averageDuration: aggregates[key].totalDuration / aggregates[key].count,
215
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,18 @@ $(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);
44
+ $('<td>').text(item['method']).appendTo(row);
36
45
  $('<td>').text(item['status']).appendTo(row);
37
46
  $('<td>').append(item['exception_class']).appendTo(row);
38
47
  $('<td>').append(item['exception_message']).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,18 @@ $(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);
43
+ $('<td>').text(item['method']).appendTo(row);
35
44
  $('<td>').text(item['status']).appendTo(row);
36
45
  $('<td>').append(renderDuration(item['duration'])).appendTo(row);
37
46
  $('<td>').append(renderDuration(item['db_runtime'])).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,19 @@ $(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);
46
+ $('<td>').text(item['method']).appendTo(row);
39
47
  $('<td>').text(item['status']).appendTo(row);
40
48
  $('<td>').append(renderDuration(item['real_duration_time'])).appendTo(row);
41
49
  $('<td>').append(renderDuration(item['calc_db_runtime'])).appendTo(row);
@@ -45,13 +53,10 @@ $(document).ready(function() {
45
53
 
46
54
  if(item.stack && item.stack.length > 0) {
47
55
 
48
- var actionButton = $('<button>').text('Details');
49
- actionButton.on('click', function() {
50
- // Replace 'your-details-url' with the actual URL where the details are to be fetched.
51
- // It's assumed the ID is required as a URL parameter, modify as per your requirements.
52
- window.location.href = '<%= request_details_path %>?request_id=' + item['request_id'];
53
- });
54
- 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);
55
60
  }
56
61
  actionTd.appendTo(row);
57
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,12 +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);
34
- $('<td>').text(item['path']).appendTo(row);
42
+ $('<td>').addClass('long_text').text(item['controller']).appendTo(row);
43
+ $('<td>').text(item['method']).appendTo(row);
44
+ $('<td>').addClass('long_text').text(item['path']).appendTo(row);
35
45
  $('<td>').text(item['count']).appendTo(row);
36
46
  $('<td>').append(renderDuration(item['averageDuration'])).appendTo(row);
37
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,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 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);
44
+ $('<td>').text(item['method']).appendTo(row);
35
45
  $('<td>').text(item['status']).appendTo(row);
36
46
  $('<td>').append(renderDuration(item['duration'])).appendTo(row);
37
47
  $('<td>').append(renderDuration(item['db_runtime'])).appendTo(row);
@@ -5,6 +5,7 @@
5
5
  <tr>
6
6
  <th class="sortable" data-field="start_time">Time</th>
7
7
  <th class="sortable" data-field="controller">Controller#Action</th>
8
+ <th class="sortable" data-field="method">Method</th>
8
9
  <th class="sortable" data-field="status">Status</th>
9
10
  <th class="sortable" data-field="real_duration_time">Duration (ms)</th>
10
11
  <th class="sortable" data-field="calc_db_runtime">DB Runtime (ms)</th>
@@ -5,6 +5,7 @@
5
5
  <tr>
6
6
  <th class="sortable" data-field="start_time">Time</th>
7
7
  <th class="sortable" data-field="controller">Controller#Action</th>
8
+ <th class="sortable" data-field="method">Method</th>
8
9
  <th class="sortable" data-field="status">Status</th>
9
10
  <th class="sortable" data-field="real_duration_time">Duration (ms)</th>
10
11
  <th class="sortable" data-field="calc_db_runtime">DB Runtime (ms)</th>
@@ -4,6 +4,7 @@
4
4
  <thead>
5
5
  <tr>
6
6
  <th class="sortable" data-field="controller">Controller#Action</th>
7
+ <th class="sortable" data-field="method">Method</th>
7
8
  <th class="sortable" data-field="path">Url</th>
8
9
  <th class="sortable" data-field="count">Requests</th>
9
10
  <th class="sortable" data-field="averageDuration">Avg Duration (ms)</th>
@@ -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">
@@ -5,6 +5,7 @@
5
5
  <tr>
6
6
  <th class="sortable" data-field="start_time">Time</th>
7
7
  <th class="sortable" data-field="controller">Controller#Action</th>
8
+ <th class="sortable" data-field="method">Method</th>
8
9
  <th class="sortable" data-field="status">Status</th>
9
10
  <th class="sortable" data-field="real_duration_time">Duration (ms)</th>
10
11
  <th class="sortable" data-field="calc_db_runtime">DB Runtime (ms)</th>
@@ -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
@@ -55,14 +55,12 @@ module BuilderApm
55
55
  view_runtime: event.payload[:view_runtime],
56
56
  stack: Thread.current[:stack]
57
57
  }
58
-
59
- if event.payload[:exception_object]
58
+ if event.payload[:exception_object]
60
59
  exception = event.payload[:exception_object]
61
- status = ActionDispatch::ExceptionWrapper.new(Rails.backtrace_cleaner, exception).status_code
62
- data[:status] = status
60
+ data[:status] = ActionDispatch::ExceptionWrapper.new(Rails.application.routes, exception).status_code
63
61
  data[:exception_class] = exception.class
64
62
  data[:exception_message] = exception.message
65
- data[:exception_backtrace] = exception.backtrace #.select { |line| line.start_with?(Rails.root.to_s) }
63
+ data[:exception_backtrace] = exception.backtrace
66
64
  end
67
65
 
68
66
  data
@@ -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,13 +34,28 @@ 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
-
36
- @redis_client.zadd("builder_apm:timestamps", end_time, request_id)
37
- @redis_client.set("builder_apm:Request:#{data[:request_id]}", data.to_json)
42
+
43
+ begin
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
56
+ rescue => e
57
+ Rails.logger.error "Redis Missing"
58
+ end
38
59
  end
39
60
  end
40
61
  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.4"
2
+ VERSION = "0.3.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: builder_apm
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.4
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Paul Ketelle
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-07-24 00:00:00.000000000 Z
11
+ date: 2023-07-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -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