pg_reports 0.5.0 → 0.5.1
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/CHANGELOG.md +26 -0
- data/README.md +41 -0
- data/app/controllers/pg_reports/dashboard_controller.rb +168 -30
- data/app/views/layouts/pg_reports/application.html.erb +133 -124
- data/app/views/pg_reports/dashboard/_show_modals.html.erb +1 -1
- data/app/views/pg_reports/dashboard/_show_scripts.html.erb +70 -6
- data/app/views/pg_reports/dashboard/index.html.erb +91 -30
- data/lib/pg_reports/configuration.rb +8 -0
- data/lib/pg_reports/modules/system.rb +7 -1
- data/lib/pg_reports/query_monitor.rb +23 -10
- data/lib/pg_reports/version.rb +1 -1
- metadata +2 -2
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
const reportKey = '<%= @report_key %>';
|
|
6
6
|
const railsEnv = '<%= Rails.env %>';
|
|
7
7
|
const isDevelopment = railsEnv === 'development';
|
|
8
|
+
const allowRawQueryExecution = <%= PgReports.config.allow_raw_query_execution.to_s.downcase %>;
|
|
8
9
|
let syncingScroll = false;
|
|
9
10
|
let currentSort = { column: null, direction: 'asc' };
|
|
10
11
|
|
|
@@ -595,9 +596,10 @@
|
|
|
595
596
|
// Only show EXPLAIN ANALYZE for SELECT queries
|
|
596
597
|
const queryNormalized = row.query.trim().toLowerCase();
|
|
597
598
|
if (queryNormalized.startsWith('select')) {
|
|
598
|
-
// Use data attribute to
|
|
599
|
+
// Use data attribute to pass query hash for security
|
|
599
600
|
const queryBase64 = btoa(unescape(encodeURIComponent(row.query)));
|
|
600
|
-
|
|
601
|
+
const queryHash = row.query_hash || '';
|
|
602
|
+
html += `<button class="btn-explain" data-query-b64="${queryBase64}" data-query-hash="${queryHash}" onclick="event.stopPropagation(); runExplainAnalyzeFromButton(this)">📊 EXPLAIN ANALYZE</button>`;
|
|
601
603
|
}
|
|
602
604
|
}
|
|
603
605
|
|
|
@@ -1008,6 +1010,7 @@
|
|
|
1008
1010
|
// ==========================================
|
|
1009
1011
|
|
|
1010
1012
|
let currentExplainQuery = '';
|
|
1013
|
+
let currentExplainQueryHash = '';
|
|
1011
1014
|
let currentQueryParams = [];
|
|
1012
1015
|
|
|
1013
1016
|
// Parse $1, $2, etc. from query
|
|
@@ -1023,19 +1026,22 @@
|
|
|
1023
1026
|
// Decode base64 query from button and run analyzer
|
|
1024
1027
|
function runExplainAnalyzeFromButton(btn) {
|
|
1025
1028
|
const queryBase64 = btn.dataset.queryB64;
|
|
1029
|
+
const queryHash = btn.dataset.queryHash;
|
|
1030
|
+
|
|
1026
1031
|
if (!queryBase64) return;
|
|
1027
1032
|
|
|
1028
1033
|
try {
|
|
1029
1034
|
const query = decodeURIComponent(escape(atob(queryBase64)));
|
|
1030
|
-
runExplainAnalyze(query);
|
|
1035
|
+
runExplainAnalyze(query, queryHash);
|
|
1031
1036
|
} catch (e) {
|
|
1032
1037
|
showToast('Failed to decode query', 'error');
|
|
1033
1038
|
}
|
|
1034
1039
|
}
|
|
1035
1040
|
|
|
1036
1041
|
// Show query analyzer modal
|
|
1037
|
-
function runExplainAnalyze(query) {
|
|
1042
|
+
function runExplainAnalyze(query, queryHash = '') {
|
|
1038
1043
|
currentExplainQuery = query;
|
|
1044
|
+
currentExplainQueryHash = queryHash;
|
|
1039
1045
|
currentQueryParams = parseQueryParams(query);
|
|
1040
1046
|
|
|
1041
1047
|
const modal = document.getElementById('explain-modal');
|
|
@@ -1276,6 +1282,23 @@
|
|
|
1276
1282
|
async function executeExplainAnalyze() {
|
|
1277
1283
|
const loading = document.getElementById('explain-loading');
|
|
1278
1284
|
const content = document.getElementById('explain-content');
|
|
1285
|
+
|
|
1286
|
+
// Security check
|
|
1287
|
+
if (!allowRawQueryExecution) {
|
|
1288
|
+
const message = '⚠️ EXPLAIN ANALYZE отключен. Включите в конфигурации: config.allow_raw_query_execution = true';
|
|
1289
|
+
showToast(message, 'error');
|
|
1290
|
+
content.innerHTML = `<div class="error-message">
|
|
1291
|
+
<strong>⚠️ Query execution is disabled</strong><br><br>
|
|
1292
|
+
To enable this feature, add to your configuration:<br>
|
|
1293
|
+
<code style="display: block; margin-top: 0.5rem; padding: 0.5rem; background: rgba(0,0,0,0.2); border-radius: 4px;">
|
|
1294
|
+
PgReports.configure do |config|<br>
|
|
1295
|
+
config.allow_raw_query_execution = true<br>
|
|
1296
|
+
end
|
|
1297
|
+
</code>
|
|
1298
|
+
</div>`;
|
|
1299
|
+
return;
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1279
1302
|
const params = getParamValues();
|
|
1280
1303
|
|
|
1281
1304
|
loading.style.display = 'flex';
|
|
@@ -1288,7 +1311,7 @@
|
|
|
1288
1311
|
'Content-Type': 'application/json',
|
|
1289
1312
|
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]')?.content || ''
|
|
1290
1313
|
},
|
|
1291
|
-
body: JSON.stringify({
|
|
1314
|
+
body: JSON.stringify({ query_hash: currentExplainQueryHash, params: params })
|
|
1292
1315
|
});
|
|
1293
1316
|
|
|
1294
1317
|
const data = await response.json();
|
|
@@ -1309,6 +1332,23 @@
|
|
|
1309
1332
|
async function executeQuery() {
|
|
1310
1333
|
const loading = document.getElementById('explain-loading');
|
|
1311
1334
|
const content = document.getElementById('explain-content');
|
|
1335
|
+
|
|
1336
|
+
// Security check
|
|
1337
|
+
if (!allowRawQueryExecution) {
|
|
1338
|
+
const message = '⚠️ Выполнение запросов отключено. Включите в конфигурации: config.allow_raw_query_execution = true';
|
|
1339
|
+
showToast(message, 'error');
|
|
1340
|
+
content.innerHTML = `<div class="error-message">
|
|
1341
|
+
<strong>⚠️ Query execution is disabled</strong><br><br>
|
|
1342
|
+
To enable this feature, add to your configuration:<br>
|
|
1343
|
+
<code style="display: block; margin-top: 0.5rem; padding: 0.5rem; background: rgba(0,0,0,0.2); border-radius: 4px;">
|
|
1344
|
+
PgReports.configure do |config|<br>
|
|
1345
|
+
config.allow_raw_query_execution = true<br>
|
|
1346
|
+
end
|
|
1347
|
+
</code>
|
|
1348
|
+
</div>`;
|
|
1349
|
+
return;
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1312
1352
|
const params = getParamValues();
|
|
1313
1353
|
|
|
1314
1354
|
loading.style.display = 'flex';
|
|
@@ -1321,7 +1361,7 @@
|
|
|
1321
1361
|
'Content-Type': 'application/json',
|
|
1322
1362
|
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]')?.content || ''
|
|
1323
1363
|
},
|
|
1324
|
-
body: JSON.stringify({
|
|
1364
|
+
body: JSON.stringify({ query_hash: currentExplainQueryHash, params: params })
|
|
1325
1365
|
});
|
|
1326
1366
|
|
|
1327
1367
|
const data = await response.json();
|
|
@@ -1460,6 +1500,30 @@ end
|
|
|
1460
1500
|
async function createMigrationFile() {
|
|
1461
1501
|
if (!currentMigrationData) return;
|
|
1462
1502
|
|
|
1503
|
+
// Security check
|
|
1504
|
+
if (!allowRawQueryExecution) {
|
|
1505
|
+
const message = '⚠️ Создание миграций отключено. Включите в конфигурации: config.allow_raw_query_execution = true';
|
|
1506
|
+
showToast(message, 'error');
|
|
1507
|
+
|
|
1508
|
+
const modalBody = document.getElementById('migration-modal-body');
|
|
1509
|
+
if (modalBody) {
|
|
1510
|
+
const errorDiv = document.createElement('div');
|
|
1511
|
+
errorDiv.className = 'error-message';
|
|
1512
|
+
errorDiv.style.marginTop = '1rem';
|
|
1513
|
+
errorDiv.innerHTML = `
|
|
1514
|
+
<strong>⚠️ Migration creation is disabled</strong><br><br>
|
|
1515
|
+
To enable this feature, add to your configuration:<br>
|
|
1516
|
+
<code style="display: block; margin-top: 0.5rem; padding: 0.5rem; background: rgba(0,0,0,0.2); border-radius: 4px;">
|
|
1517
|
+
PgReports.configure do |config|<br>
|
|
1518
|
+
config.allow_raw_query_execution = true<br>
|
|
1519
|
+
end
|
|
1520
|
+
</code>
|
|
1521
|
+
`;
|
|
1522
|
+
modalBody.appendChild(errorDiv);
|
|
1523
|
+
}
|
|
1524
|
+
return;
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1463
1527
|
try {
|
|
1464
1528
|
const response = await fetch(`${pgReportsRoot}/create_migration`, {
|
|
1465
1529
|
method: 'POST',
|
|
@@ -222,6 +222,9 @@ pg_stat_statements.track = all</pre>
|
|
|
222
222
|
<button class="btn btn-small btn-danger" onclick="stopQueryMonitoring(this)" id="stop-monitor-btn" style="display: none;">
|
|
223
223
|
⏹ Stop Monitoring
|
|
224
224
|
</button>
|
|
225
|
+
<button class="btn btn-small btn-secondary" onclick="loadQueryHistory(this)" id="load-history-btn">
|
|
226
|
+
📜 Load History (50)
|
|
227
|
+
</button>
|
|
225
228
|
<div class="download-dropdown" id="monitor-download-dropdown" style="display: none;">
|
|
226
229
|
<button class="btn btn-small btn-secondary" onclick="toggleMonitorDownloadMenu(event)">
|
|
227
230
|
📥 Download
|
|
@@ -855,6 +858,9 @@ pg_stat_statements.track = all</pre>
|
|
|
855
858
|
let liveMonitoringEnabled = true;
|
|
856
859
|
let pollTimer = null;
|
|
857
860
|
let previousMetrics = null;
|
|
861
|
+
let metricsAvailable = true;
|
|
862
|
+
let consecutiveErrors = 0;
|
|
863
|
+
const MAX_CONSECUTIVE_ERRORS = 3;
|
|
858
864
|
const metricsHistory = {
|
|
859
865
|
connections: [],
|
|
860
866
|
tps: [],
|
|
@@ -867,8 +873,6 @@ pg_stat_statements.track = all</pre>
|
|
|
867
873
|
const panel = document.getElementById('live-monitoring');
|
|
868
874
|
if (!panel) return;
|
|
869
875
|
|
|
870
|
-
panel.style.display = 'block';
|
|
871
|
-
|
|
872
876
|
const savedState = localStorage.getItem('pgReportsLiveMonitoring');
|
|
873
877
|
if (savedState === 'disabled') {
|
|
874
878
|
liveMonitoringEnabled = false;
|
|
@@ -876,11 +880,47 @@ pg_stat_statements.track = all</pre>
|
|
|
876
880
|
}
|
|
877
881
|
|
|
878
882
|
if (liveMonitoringEnabled) {
|
|
879
|
-
|
|
880
|
-
|
|
883
|
+
// First try to fetch metrics to see if they're available
|
|
884
|
+
fetchLiveMetrics().then(success => {
|
|
885
|
+
if (success) {
|
|
886
|
+
panel.style.display = 'block';
|
|
887
|
+
startPolling();
|
|
888
|
+
} else {
|
|
889
|
+
showMetricsUnavailableMessage();
|
|
890
|
+
}
|
|
891
|
+
});
|
|
892
|
+
} else {
|
|
893
|
+
panel.style.display = 'block';
|
|
881
894
|
}
|
|
882
895
|
}
|
|
883
896
|
|
|
897
|
+
function showMetricsUnavailableMessage() {
|
|
898
|
+
const panel = document.getElementById('live-monitoring');
|
|
899
|
+
if (!panel) return;
|
|
900
|
+
|
|
901
|
+
panel.style.display = 'block';
|
|
902
|
+
panel.innerHTML = `
|
|
903
|
+
<div class="live-monitoring-header">
|
|
904
|
+
<div class="live-monitoring-title">
|
|
905
|
+
<span class="badge-dot error"></span>
|
|
906
|
+
<span>Live Monitoring Unavailable</span>
|
|
907
|
+
</div>
|
|
908
|
+
</div>
|
|
909
|
+
<div style="padding: 2rem; text-align: center; color: var(--text-muted);">
|
|
910
|
+
<p style="margin-bottom: 1rem;">Unable to fetch database statistics.</p>
|
|
911
|
+
<p style="font-size: 0.875rem;">This may be due to:</p>
|
|
912
|
+
<ul style="list-style: none; padding: 0; margin: 1rem 0; font-size: 0.875rem;">
|
|
913
|
+
<li>• Insufficient database permissions</li>
|
|
914
|
+
<li>• Database statistics views not accessible</li>
|
|
915
|
+
<li>• Connection issues</li>
|
|
916
|
+
</ul>
|
|
917
|
+
<button class="btn btn-small btn-primary" onclick="location.reload()" style="margin-top: 1rem;">
|
|
918
|
+
Retry
|
|
919
|
+
</button>
|
|
920
|
+
</div>
|
|
921
|
+
`;
|
|
922
|
+
}
|
|
923
|
+
|
|
884
924
|
function startPolling() {
|
|
885
925
|
if (pollTimer) clearInterval(pollTimer);
|
|
886
926
|
pollTimer = setInterval(fetchLiveMetrics, LIVE_CONFIG.pollInterval);
|
|
@@ -899,6 +939,9 @@ pg_stat_statements.track = all</pre>
|
|
|
899
939
|
updateToggleUI();
|
|
900
940
|
|
|
901
941
|
if (liveMonitoringEnabled) {
|
|
942
|
+
// Reset error counter when manually re-enabling
|
|
943
|
+
consecutiveErrors = 0;
|
|
944
|
+
metricsAvailable = true;
|
|
902
945
|
fetchLiveMetrics();
|
|
903
946
|
startPolling();
|
|
904
947
|
} else {
|
|
@@ -924,11 +967,38 @@ pg_stat_statements.track = all</pre>
|
|
|
924
967
|
const response = await fetch(`${pgReportsRoot}/live_metrics`);
|
|
925
968
|
const data = await response.json();
|
|
926
969
|
|
|
927
|
-
if (data.success) {
|
|
970
|
+
if (data.success && data.available !== false) {
|
|
971
|
+
consecutiveErrors = 0;
|
|
972
|
+
metricsAvailable = true;
|
|
928
973
|
updateMetricsDisplay(data.metrics);
|
|
974
|
+
return true;
|
|
975
|
+
} else {
|
|
976
|
+
consecutiveErrors++;
|
|
977
|
+
console.error('Metrics unavailable:', data.error);
|
|
978
|
+
|
|
979
|
+
// If we get too many consecutive errors, show error state
|
|
980
|
+
if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
|
|
981
|
+
metricsAvailable = false;
|
|
982
|
+
stopPolling();
|
|
983
|
+
showMetricsUnavailableMessage();
|
|
984
|
+
} else if (consecutiveErrors === 1) {
|
|
985
|
+
// Show toast on first error
|
|
986
|
+
showToast(data.error || 'Failed to fetch live metrics', 'error');
|
|
987
|
+
}
|
|
988
|
+
return false;
|
|
929
989
|
}
|
|
930
990
|
} catch (error) {
|
|
991
|
+
consecutiveErrors++;
|
|
931
992
|
console.error('Failed to fetch live metrics:', error);
|
|
993
|
+
|
|
994
|
+
if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
|
|
995
|
+
metricsAvailable = false;
|
|
996
|
+
stopPolling();
|
|
997
|
+
showMetricsUnavailableMessage();
|
|
998
|
+
} else if (consecutiveErrors === 1) {
|
|
999
|
+
showToast('Network error: ' + error.message, 'error');
|
|
1000
|
+
}
|
|
1001
|
+
return false;
|
|
932
1002
|
}
|
|
933
1003
|
}
|
|
934
1004
|
|
|
@@ -1095,9 +1165,8 @@ pg_stat_statements.track = all</pre>
|
|
|
1095
1165
|
updateQueryMonitorUI(true);
|
|
1096
1166
|
startQueryMonitorPolling();
|
|
1097
1167
|
} else {
|
|
1098
|
-
// Monitoring not active
|
|
1168
|
+
// Monitoring not active
|
|
1099
1169
|
queryMonitorEnabled = false;
|
|
1100
|
-
await loadQueryHistory();
|
|
1101
1170
|
updateQueryMonitorUI(false);
|
|
1102
1171
|
}
|
|
1103
1172
|
} catch (error) {
|
|
@@ -1107,33 +1176,28 @@ pg_stat_statements.track = all</pre>
|
|
|
1107
1176
|
}
|
|
1108
1177
|
}
|
|
1109
1178
|
|
|
1110
|
-
async function loadQueryHistory() {
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
`${pgReportsRoot}/query_monitor/history?limit=${QUERY_MONITOR_CONFIG.maxQueries}`
|
|
1114
|
-
);
|
|
1115
|
-
const data = await response.json();
|
|
1116
|
-
|
|
1117
|
-
if (data.success && data.queries && data.queries.length > 0) {
|
|
1118
|
-
renderQueryFeed(data.queries);
|
|
1119
|
-
}
|
|
1120
|
-
} catch (error) {
|
|
1121
|
-
console.error('Failed to load query history:', error);
|
|
1122
|
-
}
|
|
1123
|
-
}
|
|
1179
|
+
async function loadQueryHistory(button) {
|
|
1180
|
+
button.disabled = true;
|
|
1181
|
+
button.innerHTML = '<span class="spinner"></span> Loading...';
|
|
1124
1182
|
|
|
1125
|
-
async function loadSessionFromLog(sessionId) {
|
|
1126
1183
|
try {
|
|
1127
1184
|
const response = await fetch(
|
|
1128
|
-
`${pgReportsRoot}/query_monitor/history?
|
|
1185
|
+
`${pgReportsRoot}/query_monitor/history?limit=50`
|
|
1129
1186
|
);
|
|
1130
1187
|
const data = await response.json();
|
|
1131
1188
|
|
|
1132
1189
|
if (data.success && data.queries && data.queries.length > 0) {
|
|
1133
1190
|
renderQueryFeed(data.queries);
|
|
1191
|
+
showToast(`Loaded ${data.queries.length} queries from history`);
|
|
1192
|
+
} else {
|
|
1193
|
+
showToast('No query history found', 'error');
|
|
1134
1194
|
}
|
|
1135
1195
|
} catch (error) {
|
|
1136
|
-
console.error('Failed to load
|
|
1196
|
+
console.error('Failed to load query history:', error);
|
|
1197
|
+
showToast('Failed to load history: ' + error.message, 'error');
|
|
1198
|
+
} finally {
|
|
1199
|
+
button.disabled = false;
|
|
1200
|
+
button.innerHTML = '📜 Load History (50)';
|
|
1137
1201
|
}
|
|
1138
1202
|
}
|
|
1139
1203
|
|
|
@@ -1187,15 +1251,9 @@ pg_stat_statements.track = all</pre>
|
|
|
1187
1251
|
|
|
1188
1252
|
if (data.success) {
|
|
1189
1253
|
queryMonitorEnabled = false;
|
|
1190
|
-
const stoppedSessionId = data.session_id || currentSessionId;
|
|
1191
1254
|
currentSessionId = null;
|
|
1192
1255
|
stopQueryMonitorPolling();
|
|
1193
1256
|
|
|
1194
|
-
// Load complete session data from log file
|
|
1195
|
-
if (stoppedSessionId) {
|
|
1196
|
-
await loadSessionFromLog(stoppedSessionId);
|
|
1197
|
-
}
|
|
1198
|
-
|
|
1199
1257
|
updateQueryMonitorUI(false);
|
|
1200
1258
|
showToast(data.message);
|
|
1201
1259
|
} else {
|
|
@@ -1215,6 +1273,7 @@ pg_stat_statements.track = all</pre>
|
|
|
1215
1273
|
const indicator = document.getElementById('monitor-indicator');
|
|
1216
1274
|
const startBtn = document.getElementById('start-monitor-btn');
|
|
1217
1275
|
const stopBtn = document.getElementById('stop-monitor-btn');
|
|
1276
|
+
const loadHistoryBtn = document.getElementById('load-history-btn');
|
|
1218
1277
|
const sessionBadge = document.getElementById('session-badge');
|
|
1219
1278
|
const sessionIdEl = document.getElementById('session-id');
|
|
1220
1279
|
const downloadDropdown = document.getElementById('monitor-download-dropdown');
|
|
@@ -1228,6 +1287,7 @@ pg_stat_statements.track = all</pre>
|
|
|
1228
1287
|
stopBtn.style.display = 'inline-block';
|
|
1229
1288
|
stopBtn.disabled = false;
|
|
1230
1289
|
stopBtn.innerHTML = '⏹ Stop Monitoring';
|
|
1290
|
+
loadHistoryBtn.style.display = 'none'; // Hide when monitoring active
|
|
1231
1291
|
sessionBadge.style.display = 'inline-block';
|
|
1232
1292
|
sessionIdEl.textContent = currentSessionId ? currentSessionId.substring(0, 8) : '';
|
|
1233
1293
|
downloadDropdown.style.display = 'inline-block';
|
|
@@ -1237,6 +1297,7 @@ pg_stat_statements.track = all</pre>
|
|
|
1237
1297
|
startBtn.disabled = false;
|
|
1238
1298
|
startBtn.innerHTML = '▶ Start Monitoring';
|
|
1239
1299
|
stopBtn.style.display = 'none';
|
|
1300
|
+
loadHistoryBtn.style.display = 'inline-block'; // Show when monitoring stopped
|
|
1240
1301
|
sessionBadge.style.display = 'none';
|
|
1241
1302
|
|
|
1242
1303
|
// Keep results visible after stopping, only hide download if no queries
|
|
@@ -38,6 +38,9 @@ module PgReports
|
|
|
38
38
|
attr_accessor :query_monitor_max_queries # Maximum number of queries to keep in buffer
|
|
39
39
|
attr_accessor :query_monitor_backtrace_filter # Proc to filter backtrace lines
|
|
40
40
|
|
|
41
|
+
# Security settings
|
|
42
|
+
attr_accessor :allow_raw_query_execution # Allow execute_query and explain_analyze from dashboard
|
|
43
|
+
|
|
41
44
|
def initialize
|
|
42
45
|
# Telegram
|
|
43
46
|
@telegram_bot_token = ENV.fetch("PG_REPORTS_TELEGRAM_TOKEN", nil)
|
|
@@ -77,6 +80,11 @@ module PgReports
|
|
|
77
80
|
# Exclude gem paths, framework paths
|
|
78
81
|
!location.path.match?(%r{/(gems|ruby|railties)/})
|
|
79
82
|
}
|
|
83
|
+
|
|
84
|
+
# Security
|
|
85
|
+
@allow_raw_query_execution = ActiveModel::Type::Boolean.new.cast(
|
|
86
|
+
ENV.fetch("PG_REPORTS_ALLOW_RAW_QUERY_EXECUTION", false)
|
|
87
|
+
)
|
|
80
88
|
end
|
|
81
89
|
|
|
82
90
|
def connection
|
|
@@ -48,11 +48,17 @@ module PgReports
|
|
|
48
48
|
# Live metrics for dashboard monitoring
|
|
49
49
|
# @param long_query_threshold [Integer] Threshold in seconds for long queries
|
|
50
50
|
# @return [Hash] Metrics data
|
|
51
|
+
# @raise [StandardError] If no data is returned
|
|
51
52
|
def live_metrics(long_query_threshold: 60)
|
|
52
53
|
data = executor.execute_from_file(:system, :live_metrics,
|
|
53
54
|
long_query_threshold: long_query_threshold)
|
|
54
55
|
|
|
55
|
-
row = data.first
|
|
56
|
+
row = data.first
|
|
57
|
+
|
|
58
|
+
# If no data returned, something is wrong with the query or permissions
|
|
59
|
+
if row.nil? || row.empty?
|
|
60
|
+
raise StandardError, "No statistics data returned. Check database permissions and pg_stat views."
|
|
61
|
+
end
|
|
56
62
|
|
|
57
63
|
{
|
|
58
64
|
connections: {
|
|
@@ -123,7 +123,7 @@ module PgReports
|
|
|
123
123
|
|
|
124
124
|
queries
|
|
125
125
|
rescue => e
|
|
126
|
-
Rails.logger.warn("PgReports: Failed to load queries from log: #{e.message}")
|
|
126
|
+
Rails.logger.warn("PgReports: Failed to load queries from log: #{e.message}") if defined?(Rails)
|
|
127
127
|
[]
|
|
128
128
|
end
|
|
129
129
|
end
|
|
@@ -188,33 +188,46 @@ module PgReports
|
|
|
188
188
|
end
|
|
189
189
|
|
|
190
190
|
def query_from_pg_reports?
|
|
191
|
-
# Check if query originates from pg_reports gem code
|
|
191
|
+
# Check if query originates from pg_reports gem internal code
|
|
192
192
|
locations = caller_locations(0, 30)
|
|
193
193
|
return false unless locations
|
|
194
194
|
|
|
195
195
|
locations.any? do |location|
|
|
196
196
|
path = location.path
|
|
197
|
-
#
|
|
198
|
-
# But exclude test paths: /spec/
|
|
197
|
+
# Exclude test paths
|
|
199
198
|
next if path.include?("/spec/")
|
|
200
199
|
|
|
201
|
-
#
|
|
200
|
+
# Filter queries from pg_reports internal modules only:
|
|
201
|
+
# - Installed gem: /gems/pg_reports-X.Y.Z/lib/
|
|
202
|
+
# - Local gem: /pg_reports/lib/pg_reports/modules/
|
|
203
|
+
# - Dashboard controller: /pg_reports/app/controllers/pg_reports/
|
|
202
204
|
path.match?(%r{/gems/pg_reports[-\d.]+/lib/}) ||
|
|
203
|
-
path.match?(%r{/lib/pg_reports/})
|
|
205
|
+
path.match?(%r{/pg_reports/lib/pg_reports/modules/}) ||
|
|
206
|
+
path.match?(%r{/pg_reports/app/controllers/pg_reports/dashboard_controller\.rb})
|
|
204
207
|
end
|
|
205
208
|
end
|
|
206
209
|
|
|
207
210
|
def extract_source_location
|
|
208
|
-
filter_proc = PgReports.config.query_monitor_backtrace_filter
|
|
209
|
-
|
|
210
211
|
# Get caller locations, skip first few frames (this file, active_support)
|
|
211
|
-
|
|
212
|
+
# Increase limit to 50 to capture more of the stack
|
|
213
|
+
locations = caller_locations(5, 50)
|
|
212
214
|
|
|
213
215
|
return nil unless locations
|
|
214
216
|
|
|
215
217
|
# Find first application code location
|
|
218
|
+
# Look for paths that are NOT from gems/ruby/railties
|
|
216
219
|
app_location = locations.find do |location|
|
|
217
|
-
|
|
220
|
+
path = location.path
|
|
221
|
+
|
|
222
|
+
# Skip framework and gem paths
|
|
223
|
+
next if path.match?(%r{/(gems|ruby|railties)/})
|
|
224
|
+
|
|
225
|
+
# Skip pg_reports internal paths
|
|
226
|
+
next if path.match?(%r{/pg_reports/lib/pg_reports/})
|
|
227
|
+
next if path.match?(%r{/pg_reports/app/controllers/pg_reports/})
|
|
228
|
+
|
|
229
|
+
# This is likely application code
|
|
230
|
+
true
|
|
218
231
|
end
|
|
219
232
|
|
|
220
233
|
return nil unless app_location
|
data/lib/pg_reports/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: pg_reports
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.5.
|
|
4
|
+
version: 0.5.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Eldar Avatov
|
|
@@ -276,7 +276,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
276
276
|
- !ruby/object:Gem::Version
|
|
277
277
|
version: '0'
|
|
278
278
|
requirements: []
|
|
279
|
-
rubygems_version:
|
|
279
|
+
rubygems_version: 4.0.4
|
|
280
280
|
specification_version: 4
|
|
281
281
|
summary: PostgreSQL analysis and reporting tool with Telegram integration
|
|
282
282
|
test_files: []
|