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.
- 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 -36
- 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/error_requests/index.html.erb +1 -0
- data/app/views/builder_apm/js/_dashboard.html.erb +4 -6
- data/app/views/builder_apm/js/_data_fetcher.html.erb +74 -33
- data/app/views/builder_apm/js/_error_requests.html.erb +22 -13
- data/app/views/builder_apm/js/_n_plus_one.html.erb +19 -10
- data/app/views/builder_apm/js/_recent_requests.html.erb +23 -18
- data/app/views/builder_apm/js/_request_analysis.html.erb +20 -10
- data/app/views/builder_apm/js/_request_details.html.erb +10 -22
- data/app/views/builder_apm/js/_slow_requests.html.erb +20 -10
- data/app/views/builder_apm/n_plus_one/index.html.erb +1 -0
- data/app/views/builder_apm/recent_requests/index.html.erb +1 -0
- data/app/views/builder_apm/request_analysis/index.html.erb +1 -0
- data/app/views/builder_apm/shared/_header.html.erb +0 -1
- data/app/views/builder_apm/slow_requests/index.html.erb +1 -0
- data/lib/builder_apm/configuration.rb +2 -0
- data/lib/builder_apm/controllers/instrumenter.rb +3 -5
- data/lib/builder_apm/middleware/timing.rb +25 -4
- data/lib/builder_apm/models/instrumenter.rb +43 -8
- data/lib/builder_apm/version.rb +1 -1
- metadata +3 -2
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,41 +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
|
-
|
|
33
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
end
|
|
40
|
-
end
|
|
41
|
-
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
|
}
|
|
@@ -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
|
-
|
|
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) {
|
|
@@ -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
|
-
|
|
11
|
-
var
|
|
12
|
-
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
22
|
-
//
|
|
23
|
-
var
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
} else {
|
|
29
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
249
|
+
let controller = key;
|
|
210
250
|
results.push({
|
|
211
251
|
controller,
|
|
212
|
-
|
|
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
|
-
|
|
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,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
|
-
|
|
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,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
|
-
|
|
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,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
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
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,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['
|
|
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
|
-
|
|
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,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.
|
|
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
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
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,14 +1,14 @@
|
|
|
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
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2023-07-
|
|
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
|