pg_reports 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +104 -0
  3. data/README.md +129 -4
  4. data/app/controllers/pg_reports/dashboard_controller.rb +188 -25
  5. data/app/views/layouts/pg_reports/application.html.erb +282 -0
  6. data/app/views/pg_reports/dashboard/_show_scripts.html.erb +184 -23
  7. data/app/views/pg_reports/dashboard/_show_styles.html.erb +373 -0
  8. data/app/views/pg_reports/dashboard/index.html.erb +419 -0
  9. data/config/locales/en.yml +45 -0
  10. data/config/locales/ru.yml +45 -0
  11. data/config/routes.rb +8 -0
  12. data/lib/pg_reports/configuration.rb +13 -0
  13. data/lib/pg_reports/dashboard/reports_registry.rb +24 -1
  14. data/lib/pg_reports/definitions/connections/connection_churn.yml +49 -0
  15. data/lib/pg_reports/definitions/connections/pool_saturation.yml +42 -0
  16. data/lib/pg_reports/definitions/connections/pool_usage.yml +43 -0
  17. data/lib/pg_reports/definitions/connections/pool_wait_times.yml +44 -0
  18. data/lib/pg_reports/definitions/queries/missing_index_queries.yml +3 -3
  19. data/lib/pg_reports/explain_analyzer.rb +338 -0
  20. data/lib/pg_reports/modules/schema_analysis.rb +4 -6
  21. data/lib/pg_reports/modules/system.rb +19 -2
  22. data/lib/pg_reports/query_monitor.rb +280 -0
  23. data/lib/pg_reports/sql/connections/connection_churn.sql +37 -0
  24. data/lib/pg_reports/sql/connections/pool_saturation.sql +90 -0
  25. data/lib/pg_reports/sql/connections/pool_usage.sql +31 -0
  26. data/lib/pg_reports/sql/connections/pool_wait_times.sql +19 -0
  27. data/lib/pg_reports/sql/queries/all_queries.sql +17 -15
  28. data/lib/pg_reports/sql/queries/expensive_queries.sql +9 -4
  29. data/lib/pg_reports/sql/queries/heavy_queries.sql +14 -12
  30. data/lib/pg_reports/sql/queries/low_cache_hit_queries.sql +16 -14
  31. data/lib/pg_reports/sql/queries/missing_index_queries.sql +18 -16
  32. data/lib/pg_reports/sql/queries/slow_queries.sql +14 -12
  33. data/lib/pg_reports/sql/system/databases_list.sql +8 -0
  34. data/lib/pg_reports/version.rb +1 -1
  35. data/lib/pg_reports.rb +2 -0
  36. metadata +56 -3
@@ -540,6 +540,288 @@
540
540
  justify-content: center;
541
541
  }
542
542
  }
543
+
544
+ /* ============================================
545
+ Query Monitoring Panel
546
+ ============================================ */
547
+ .query-monitoring-panel {
548
+ margin-bottom: 2rem;
549
+ background: var(--bg-card);
550
+ border: 1px solid var(--border-color);
551
+ border-radius: 16px;
552
+ padding: 1.25rem 1.5rem;
553
+ }
554
+
555
+ .query-monitoring-header {
556
+ display: flex;
557
+ align-items: center;
558
+ justify-content: space-between;
559
+ margin-bottom: 1.25rem;
560
+ padding-bottom: 1rem;
561
+ border-bottom: 1px solid var(--border-color);
562
+ }
563
+
564
+ .query-monitoring-title {
565
+ display: flex;
566
+ align-items: center;
567
+ gap: 0.75rem;
568
+ font-weight: 600;
569
+ font-size: 1rem;
570
+ }
571
+
572
+ .monitoring-indicator {
573
+ width: 8px;
574
+ height: 8px;
575
+ border-radius: 50%;
576
+ background: var(--text-muted);
577
+ }
578
+
579
+ .monitoring-indicator.active {
580
+ background: var(--accent-rose);
581
+ animation: pulse 2s infinite;
582
+ }
583
+
584
+ @keyframes pulse {
585
+ 0%, 100% {
586
+ opacity: 1;
587
+ }
588
+ 50% {
589
+ opacity: 0.5;
590
+ }
591
+ }
592
+
593
+ .session-badge {
594
+ margin-left: 0.5rem;
595
+ padding: 0.375rem 0.75rem;
596
+ background: var(--bg-tertiary);
597
+ border: 1px solid var(--border-color);
598
+ border-radius: 8px;
599
+ font-size: 0.7rem;
600
+ font-weight: 500;
601
+ color: var(--text-secondary);
602
+ }
603
+
604
+ .session-badge strong {
605
+ color: var(--accent-purple);
606
+ font-family: <%= PgReports.config.load_external_fonts ? "'JetBrains Mono', " : "" %>monospace;
607
+ }
608
+
609
+ .query-monitoring-controls {
610
+ display: flex;
611
+ align-items: center;
612
+ gap: 1rem;
613
+ }
614
+
615
+ .query-monitoring-count {
616
+ font-size: 0.8rem;
617
+ color: var(--text-secondary);
618
+ }
619
+
620
+ .query-monitoring-count strong {
621
+ color: var(--accent-blue);
622
+ font-weight: 600;
623
+ }
624
+
625
+ /* Query Feed */
626
+ .query-feed {
627
+ max-height: 600px;
628
+ overflow-y: auto;
629
+ background: var(--bg-tertiary);
630
+ border: 1px solid var(--border-color);
631
+ border-radius: 12px;
632
+ padding: 1rem;
633
+ }
634
+
635
+ .query-feed::-webkit-scrollbar {
636
+ width: 8px;
637
+ }
638
+
639
+ .query-feed::-webkit-scrollbar-track {
640
+ background: var(--bg-secondary);
641
+ border-radius: 4px;
642
+ }
643
+
644
+ .query-feed::-webkit-scrollbar-thumb {
645
+ background: var(--border-color);
646
+ border-radius: 4px;
647
+ }
648
+
649
+ .query-feed::-webkit-scrollbar-thumb:hover {
650
+ background: var(--text-muted);
651
+ }
652
+
653
+ .query-feed-empty {
654
+ text-align: center;
655
+ padding: 3rem;
656
+ color: var(--text-muted);
657
+ font-size: 0.9rem;
658
+ }
659
+
660
+ .query-item {
661
+ background: var(--bg-card);
662
+ border: 1px solid var(--border-color);
663
+ border-radius: 8px;
664
+ padding: 0.875rem;
665
+ margin-bottom: 0.75rem;
666
+ font-size: 0.85rem;
667
+ transition: all 0.15s;
668
+ }
669
+
670
+ .query-item:hover {
671
+ border-color: var(--accent-purple);
672
+ }
673
+
674
+ .query-item:last-child {
675
+ margin-bottom: 0;
676
+ }
677
+
678
+ .query-header {
679
+ display: flex;
680
+ justify-content: space-between;
681
+ align-items: center;
682
+ margin-bottom: 0.5rem;
683
+ }
684
+
685
+ .query-meta {
686
+ display: flex;
687
+ gap: 1rem;
688
+ font-size: 0.75rem;
689
+ color: var(--text-muted);
690
+ }
691
+
692
+ .query-timestamp {
693
+ color: var(--text-secondary);
694
+ }
695
+
696
+ .query-duration {
697
+ color: var(--accent-amber);
698
+ font-weight: 600;
699
+ }
700
+
701
+ .query-duration.fast {
702
+ color: var(--accent-green);
703
+ }
704
+
705
+ .query-duration.slow {
706
+ color: var(--accent-rose);
707
+ }
708
+
709
+ .query-name {
710
+ color: var(--text-secondary);
711
+ font-style: italic;
712
+ }
713
+
714
+ .query-expand-btn {
715
+ background: transparent;
716
+ border: none;
717
+ color: var(--text-muted);
718
+ cursor: pointer;
719
+ padding: 0.25rem 0.5rem;
720
+ font-size: 0.7rem;
721
+ transition: all 0.15s;
722
+ border-radius: 4px;
723
+ }
724
+
725
+ .query-expand-btn:hover {
726
+ background: var(--bg-tertiary);
727
+ color: var(--accent-purple);
728
+ }
729
+
730
+ .query-sql-wrapper {
731
+ margin-bottom: 0.5rem;
732
+ }
733
+
734
+ .query-sql {
735
+ padding: 0.75rem;
736
+ background: var(--bg-primary);
737
+ border: 1px solid var(--border-color);
738
+ border-radius: 6px;
739
+ font-family: <%= PgReports.config.load_external_fonts ? "'JetBrains Mono', " : "" %>monospace;
740
+ font-size: 0.75rem;
741
+ color: var(--accent-blue);
742
+ word-wrap: break-word;
743
+ overflow-x: auto;
744
+ }
745
+
746
+ .query-sql-collapsed {
747
+ white-space: nowrap;
748
+ overflow: hidden;
749
+ text-overflow: ellipsis;
750
+ }
751
+
752
+ .query-sql-expanded {
753
+ white-space: pre-wrap;
754
+ }
755
+
756
+ .query-source {
757
+ font-size: 0.7rem;
758
+ color: var(--text-muted);
759
+ font-family: <%= PgReports.config.load_external_fonts ? "'JetBrains Mono', " : "" %>monospace;
760
+ }
761
+
762
+ .query-source a {
763
+ color: var(--accent-purple);
764
+ text-decoration: none;
765
+ }
766
+
767
+ .query-source a:hover {
768
+ text-decoration: underline;
769
+ }
770
+
771
+ /* Download Dropdown */
772
+ .download-dropdown {
773
+ position: relative;
774
+ display: inline-block;
775
+ }
776
+
777
+ .download-menu {
778
+ position: absolute;
779
+ top: calc(100% + 0.5rem);
780
+ right: 0;
781
+ min-width: 180px;
782
+ background: var(--bg-card);
783
+ border: 1px solid var(--border-color);
784
+ border-radius: 10px;
785
+ box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
786
+ z-index: 100;
787
+ overflow: hidden;
788
+ }
789
+
790
+ .download-menu a {
791
+ display: block;
792
+ padding: 0.75rem 1rem;
793
+ color: var(--text-secondary);
794
+ text-decoration: none;
795
+ font-size: 0.875rem;
796
+ transition: all 0.15s ease;
797
+ border-bottom: 1px solid var(--border-color);
798
+ }
799
+
800
+ .download-menu a:last-child {
801
+ border-bottom: none;
802
+ }
803
+
804
+ .download-menu a:hover {
805
+ background: var(--bg-tertiary);
806
+ color: var(--text-primary);
807
+ }
808
+
809
+ .btn-small {
810
+ height: 32px;
811
+ padding: 0 1rem;
812
+ font-size: 0.8rem;
813
+ }
814
+
815
+ .btn-danger {
816
+ background: var(--accent-rose);
817
+ color: white;
818
+ border-color: var(--accent-rose);
819
+ }
820
+
821
+ .btn-danger:hover {
822
+ opacity: 0.9;
823
+ transform: translateY(-1px);
824
+ }
543
825
  </style>
544
826
  </head>
545
827
  <body>
@@ -999,7 +999,9 @@
999
999
  }
1000
1000
 
1001
1001
  // Render saved records on page load
1002
- document.addEventListener('DOMContentLoaded', renderSavedRecords);
1002
+ document.addEventListener('DOMContentLoaded', function() {
1003
+ renderSavedRecords();
1004
+ });
1003
1005
 
1004
1006
  // ==========================================
1005
1007
  // EXPLAIN ANALYZE FUNCTIONALITY
@@ -1090,6 +1092,186 @@
1090
1092
  return params;
1091
1093
  }
1092
1094
 
1095
+ // Render EXPLAIN ANALYZE with colored nodes and problem detection
1096
+ function renderExplainAnalysis(data) {
1097
+ let html = '';
1098
+
1099
+ // Show summary if there are problems
1100
+ if (data.summary && data.summary.total_problems > 0) {
1101
+ html += '<div class="explain-summary explain-summary-' + data.summary.status + '">';
1102
+ html += '<div class="explain-summary-header">';
1103
+ html += '<span class="explain-summary-icon">' + data.summary.status_icon + '</span>';
1104
+ html += '<span class="explain-summary-title">' + escapeHtml(data.summary.status_text) + '</span>';
1105
+ html += '</div>';
1106
+ html += '<div class="explain-summary-stats">';
1107
+ if (data.summary.critical_problems > 0) {
1108
+ html += '<span class="explain-summary-stat critical">🔴 ' + data.summary.critical_problems + ' critical</span>';
1109
+ }
1110
+ if (data.summary.warnings > 0) {
1111
+ html += '<span class="explain-summary-stat warning">⚠️ ' + data.summary.warnings + ' warnings</span>';
1112
+ }
1113
+ html += '</div>';
1114
+ html += '</div>';
1115
+ }
1116
+
1117
+ // Show stats
1118
+ if (data.stats) {
1119
+ html += '<div class="explain-stats">';
1120
+ if (data.stats.planning_time !== undefined) {
1121
+ const planningClass = data.stats.planning_time > 100 ? 'stat-warning' : '';
1122
+ html += '<div class="explain-stat ' + planningClass + '">';
1123
+ html += '<span class="explain-stat-label">Planning Time</span>';
1124
+ html += '<span class="explain-stat-value">' + data.stats.planning_time.toFixed(2) + ' ms</span>';
1125
+ html += '</div>';
1126
+ }
1127
+ if (data.stats.execution_time !== undefined) {
1128
+ const execClass = data.stats.execution_time > 1000 ? 'stat-critical' : (data.stats.execution_time > 100 ? 'stat-warning' : '');
1129
+ html += '<div class="explain-stat ' + execClass + '">';
1130
+ html += '<span class="explain-stat-label">Execution Time</span>';
1131
+ html += '<span class="explain-stat-value">' + data.stats.execution_time.toFixed(2) + ' ms</span>';
1132
+ html += '</div>';
1133
+ }
1134
+ if (data.stats.total_cost !== undefined) {
1135
+ html += '<div class="explain-stat">';
1136
+ html += '<span class="explain-stat-label">Total Cost</span>';
1137
+ html += '<span class="explain-stat-value">' + data.stats.total_cost.toFixed(2) + '</span>';
1138
+ html += '</div>';
1139
+ }
1140
+ if (data.stats.rows_estimated !== undefined) {
1141
+ html += '<div class="explain-stat">';
1142
+ html += '<span class="explain-stat-label">Estimated Rows</span>';
1143
+ html += '<span class="explain-stat-value">' + data.stats.rows_estimated + '</span>';
1144
+ html += '</div>';
1145
+ }
1146
+ html += '</div>';
1147
+ }
1148
+
1149
+ // Show problems list
1150
+ if (data.problems && data.problems.length > 0) {
1151
+ html += '<div class="explain-problems">';
1152
+ html += '<div class="explain-problems-header">⚠️ Detected Issues</div>';
1153
+ html += '<div class="explain-problems-list">';
1154
+
1155
+ data.problems.forEach(problem => {
1156
+ const severityClass = 'problem-' + problem.severity;
1157
+ const severityIcon = problem.severity === 'critical' ? '🔴' : (problem.severity === 'warning' ? '⚠️' : 'ℹ️');
1158
+
1159
+ html += '<div class="explain-problem ' + severityClass + '">';
1160
+ html += '<div class="explain-problem-header">';
1161
+ html += '<span class="explain-problem-icon">' + severityIcon + '</span>';
1162
+ html += '<span class="explain-problem-message">' + escapeHtml(problem.message) + '</span>';
1163
+ if (problem.line_number) {
1164
+ html += '<span class="explain-problem-line">Line ' + problem.line_number + '</span>';
1165
+ }
1166
+ html += '</div>';
1167
+ if (problem.details) {
1168
+ html += '<div class="explain-problem-details">' + escapeHtml(problem.details) + '</div>';
1169
+ }
1170
+ if (problem.recommendation) {
1171
+ html += '<div class="explain-problem-recommendation">💡 ' + escapeHtml(problem.recommendation) + '</div>';
1172
+ }
1173
+ html += '</div>';
1174
+ });
1175
+
1176
+ html += '</div>';
1177
+ html += '</div>';
1178
+ }
1179
+
1180
+ // Show annotated EXPLAIN output
1181
+ if (data.annotated_lines && data.annotated_lines.length > 0) {
1182
+ html += '<div class="explain-output">';
1183
+ html += '<div class="explain-output-header">';
1184
+ html += '📊 Execution Plan';
1185
+ html += '<button class="btn-copy-small" onclick="copyExplainOutput()" title="Copy to clipboard">📋 Copy</button>';
1186
+ html += '</div>';
1187
+ html += '<div class="explain-lines">';
1188
+
1189
+ data.annotated_lines.forEach(line => {
1190
+ let lineClass = 'explain-line';
1191
+ let nodeColorClass = '';
1192
+ let tooltip = '';
1193
+
1194
+ // Add color class based on node type
1195
+ if (line.node_info) {
1196
+ nodeColorClass = 'node-' + line.node_info.color;
1197
+ tooltip = line.node_info.description;
1198
+ }
1199
+
1200
+ // Highlight timing lines
1201
+ if (line.is_timing) {
1202
+ lineClass += ' explain-line-timing';
1203
+ }
1204
+
1205
+ // Check if this line has a problem
1206
+ const lineProblem = data.problems?.find(p => p.line_number === line.line_number);
1207
+ if (lineProblem) {
1208
+ lineClass += ' explain-line-problem';
1209
+ lineClass += ' problem-' + lineProblem.severity;
1210
+ }
1211
+
1212
+ html += '<div class="' + lineClass + '" data-line="' + line.line_number + '">';
1213
+
1214
+ // Line number
1215
+ html += '<span class="explain-line-number">' + line.line_number + '</span>';
1216
+
1217
+ // Content with indentation
1218
+ const indentPx = line.indent_level * 20;
1219
+ html += '<span class="explain-line-content ' + nodeColorClass + '" style="padding-left: ' + indentPx + 'px"';
1220
+ if (tooltip) {
1221
+ html += ' title="' + escapeHtml(tooltip) + '"';
1222
+ }
1223
+ html += '>';
1224
+
1225
+ // Highlight node type if present
1226
+ let lineText = escapeHtml(line.text.trim());
1227
+ if (line.node_type) {
1228
+ lineText = lineText.replace(
1229
+ escapeHtml(line.node_type),
1230
+ '<span class="explain-node-type">' + escapeHtml(line.node_type) + '</span>'
1231
+ );
1232
+ }
1233
+
1234
+ // Highlight metrics
1235
+ lineText = lineText
1236
+ .replace(/\bcost=([\d.]+)\.\.([\d.]+)/g, '<span class="explain-metric">cost=<span class="metric-value">$1..$2</span></span>')
1237
+ .replace(/\brows=(\d+)/g, '<span class="explain-metric">rows=<span class="metric-value">$1</span></span>')
1238
+ .replace(/\bactual time=([\d.]+)\.\.([\d.]+)/g, '<span class="explain-metric">actual time=<span class="metric-value">$1..$2</span></span>')
1239
+ .replace(/\bloops=(\d+)/g, '<span class="explain-metric">loops=<span class="metric-value">$1</span></span>');
1240
+
1241
+ html += lineText;
1242
+ html += '</span>';
1243
+
1244
+ // Problem indicator
1245
+ if (lineProblem) {
1246
+ const problemIcon = lineProblem.severity === 'critical' ? '🔴' : '⚠️';
1247
+ html += '<span class="explain-line-problem-indicator" title="' + escapeHtml(lineProblem.message) + '">' + problemIcon + '</span>';
1248
+ }
1249
+
1250
+ html += '</div>';
1251
+ });
1252
+
1253
+ html += '</div>';
1254
+ html += '</div>';
1255
+ } else {
1256
+ // Fallback to raw output
1257
+ html += '<div class="explain-result">' + escapeHtml(data.explain) + '</div>';
1258
+ }
1259
+
1260
+ return html;
1261
+ }
1262
+
1263
+ // Copy EXPLAIN output to clipboard
1264
+ function copyExplainOutput() {
1265
+ const lines = document.querySelectorAll('.explain-line-content');
1266
+ const text = Array.from(lines).map(line => line.textContent).join('\n');
1267
+
1268
+ navigator.clipboard.writeText(text).then(() => {
1269
+ showToast('EXPLAIN output copied to clipboard');
1270
+ }).catch(() => {
1271
+ showToast('Failed to copy', 'error');
1272
+ });
1273
+ }
1274
+
1093
1275
  // Execute EXPLAIN ANALYZE with parameters
1094
1276
  async function executeExplainAnalyze() {
1095
1277
  const loading = document.getElementById('explain-loading');
@@ -1113,28 +1295,7 @@
1113
1295
  loading.style.display = 'none';
1114
1296
 
1115
1297
  if (data.success) {
1116
- let html = '';
1117
-
1118
- // Show stats if available
1119
- if (data.stats) {
1120
- html += '<div class="explain-stats">';
1121
- if (data.stats.planning_time) {
1122
- html += `<div class="explain-stat"><span class="explain-stat-label">Planning Time</span><span class="explain-stat-value">${data.stats.planning_time} ms</span></div>`;
1123
- }
1124
- if (data.stats.execution_time) {
1125
- html += `<div class="explain-stat"><span class="explain-stat-label">Execution Time</span><span class="explain-stat-value">${data.stats.execution_time} ms</span></div>`;
1126
- }
1127
- if (data.stats.total_cost) {
1128
- html += `<div class="explain-stat"><span class="explain-stat-label">Total Cost</span><span class="explain-stat-value">${data.stats.total_cost}</span></div>`;
1129
- }
1130
- if (data.stats.rows) {
1131
- html += `<div class="explain-stat"><span class="explain-stat-label">Rows</span><span class="explain-stat-value">${data.stats.rows}</span></div>`;
1132
- }
1133
- html += '</div>';
1134
- }
1135
-
1136
- html += `<div class="explain-result">${escapeHtml(data.explain)}</div>`;
1137
- content.innerHTML = html;
1298
+ content.innerHTML = renderExplainAnalysis(data);
1138
1299
  } else {
1139
1300
  content.innerHTML = `<div class="error-message">${escapeHtml(data.error || 'Failed to run EXPLAIN ANALYZE')}</div>`;
1140
1301
  }