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