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.
- checksums.yaml +4 -4
- data/Gemfile.lock +1 -1
- data/app/controllers/builder_apm/application_controller.rb +7 -0
- data/app/controllers/builder_apm/dashboard_controller.rb +20 -4
- data/app/controllers/builder_apm/error_requests_controller.rb +1 -1
- data/app/controllers/builder_apm/n_plus_one_controller.rb +1 -1
- data/app/controllers/builder_apm/recent_requests_controller.rb +1 -1
- data/app/controllers/builder_apm/request_analysis_controller.rb +1 -1
- data/app/controllers/builder_apm/request_data_controller.rb +36 -35
- data/app/controllers/builder_apm/request_details_controller.rb +4 -1
- data/app/controllers/builder_apm/slow_requests_controller.rb +1 -1
- data/app/controllers/builder_apm/wip_controller.rb +1 -1
- data/app/views/builder_apm/css/_main.html.erb +4 -1
- data/app/views/builder_apm/js/_dashboard.html.erb +4 -6
- data/app/views/builder_apm/js/_data_fetcher.html.erb +58 -37
- data/app/views/builder_apm/js/_error_requests.html.erb +21 -13
- data/app/views/builder_apm/js/_n_plus_one.html.erb +18 -10
- data/app/views/builder_apm/js/_recent_requests.html.erb +22 -18
- data/app/views/builder_apm/js/_request_analysis.html.erb +19 -10
- data/app/views/builder_apm/js/_request_details.html.erb +10 -22
- data/app/views/builder_apm/js/_slow_requests.html.erb +19 -10
- data/app/views/builder_apm/shared/_header.html.erb +0 -1
- data/lib/builder_apm/configuration.rb +2 -0
- data/lib/builder_apm/middleware/timing.rb +21 -4
- data/lib/builder_apm/models/instrumenter.rb +43 -8
- data/lib/builder_apm/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 31a9a4393e090f4980960c61d6f5cb2cf307a352534b9f8f264c2238dcd281f4
|
4
|
+
data.tar.gz: a01935162ea2b1fa2701a080536aadc6259c3a9c102148ef1df4475ddd7a89de
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 61269a7ae1127eebfbdec316e015e64e6e0a76b23d81433c1a2569f4ccfabbebccc289d4f5588dfa46ac100d33dd5269fdc6e37208167598cdee7e6066ec35d5
|
7
|
+
data.tar.gz: 3736371f1c53590cf7e811c19fa7262e70470f50207d414087a08967bd3cdc7375f93a9256906df7ce44bb10aa14b887045da11144791169ca6a861cf19e02c3
|
data/Gemfile.lock
CHANGED
@@ -1,8 +1,24 @@
|
|
1
1
|
module BuilderApm
|
2
|
-
class DashboardController <
|
3
|
-
|
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,40 +1,41 @@
|
|
1
1
|
module BuilderApm
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
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
|
-
|
8
|
+
purge_old_keys
|
9
|
+
@requests = retrieve_data_from_redis_since(cursor, limit, type)
|
17
10
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
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
|
-
|
37
|
-
|
38
|
-
end
|
39
|
-
end
|
40
|
-
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -1,8 +1,11 @@
|
|
1
1
|
module BuilderApm
|
2
|
-
class RequestDetailsController <
|
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
|
|
@@ -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
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
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 =
|
13
|
-
|
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
|
-
|
27
|
-
var
|
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
|
-
|
42
|
-
} else {
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
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
|
-
|
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}
|
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
|
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
|
-
|
5
|
-
|
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
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
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
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
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
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
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
|
50
|
-
|
51
|
-
|
52
|
-
|
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
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
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
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
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
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
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
|
-
|
35
|
-
|
42
|
+
|
36
43
|
begin
|
37
|
-
@redis_client.
|
38
|
-
|
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
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
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
|
data/lib/builder_apm/version.rb
CHANGED
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.
|
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
|