pghero_fork 2.7.3

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 (75) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +391 -0
  3. data/CONTRIBUTING.md +42 -0
  4. data/LICENSE.txt +22 -0
  5. data/README.md +3 -0
  6. data/app/assets/images/pghero/favicon.png +0 -0
  7. data/app/assets/javascripts/pghero/Chart.bundle.js +20755 -0
  8. data/app/assets/javascripts/pghero/application.js +158 -0
  9. data/app/assets/javascripts/pghero/chartkick.js +2436 -0
  10. data/app/assets/javascripts/pghero/highlight.pack.js +2 -0
  11. data/app/assets/javascripts/pghero/jquery.js +10872 -0
  12. data/app/assets/javascripts/pghero/nouislider.js +2672 -0
  13. data/app/assets/stylesheets/pghero/application.css +514 -0
  14. data/app/assets/stylesheets/pghero/arduino-light.css +86 -0
  15. data/app/assets/stylesheets/pghero/nouislider.css +310 -0
  16. data/app/controllers/pg_hero/home_controller.rb +449 -0
  17. data/app/helpers/pg_hero/home_helper.rb +30 -0
  18. data/app/views/layouts/pg_hero/application.html.erb +68 -0
  19. data/app/views/pg_hero/home/_connections_table.html.erb +16 -0
  20. data/app/views/pg_hero/home/_live_queries_table.html.erb +51 -0
  21. data/app/views/pg_hero/home/_queries_table.html.erb +72 -0
  22. data/app/views/pg_hero/home/_query_stats_slider.html.erb +16 -0
  23. data/app/views/pg_hero/home/_suggested_index.html.erb +18 -0
  24. data/app/views/pg_hero/home/connections.html.erb +32 -0
  25. data/app/views/pg_hero/home/explain.html.erb +27 -0
  26. data/app/views/pg_hero/home/index.html.erb +518 -0
  27. data/app/views/pg_hero/home/index_bloat.html.erb +72 -0
  28. data/app/views/pg_hero/home/live_queries.html.erb +11 -0
  29. data/app/views/pg_hero/home/maintenance.html.erb +55 -0
  30. data/app/views/pg_hero/home/queries.html.erb +33 -0
  31. data/app/views/pg_hero/home/relation_space.html.erb +14 -0
  32. data/app/views/pg_hero/home/show_query.html.erb +106 -0
  33. data/app/views/pg_hero/home/space.html.erb +83 -0
  34. data/app/views/pg_hero/home/system.html.erb +34 -0
  35. data/app/views/pg_hero/home/tune.html.erb +53 -0
  36. data/config/routes.rb +32 -0
  37. data/lib/generators/pghero/config_generator.rb +13 -0
  38. data/lib/generators/pghero/query_stats_generator.rb +18 -0
  39. data/lib/generators/pghero/space_stats_generator.rb +18 -0
  40. data/lib/generators/pghero/templates/config.yml.tt +46 -0
  41. data/lib/generators/pghero/templates/query_stats.rb.tt +15 -0
  42. data/lib/generators/pghero/templates/space_stats.rb.tt +13 -0
  43. data/lib/pghero.rb +246 -0
  44. data/lib/pghero/connection.rb +5 -0
  45. data/lib/pghero/database.rb +175 -0
  46. data/lib/pghero/engine.rb +16 -0
  47. data/lib/pghero/methods/basic.rb +160 -0
  48. data/lib/pghero/methods/connections.rb +77 -0
  49. data/lib/pghero/methods/constraints.rb +30 -0
  50. data/lib/pghero/methods/explain.rb +29 -0
  51. data/lib/pghero/methods/indexes.rb +332 -0
  52. data/lib/pghero/methods/kill.rb +28 -0
  53. data/lib/pghero/methods/maintenance.rb +93 -0
  54. data/lib/pghero/methods/queries.rb +75 -0
  55. data/lib/pghero/methods/query_stats.rb +349 -0
  56. data/lib/pghero/methods/replication.rb +74 -0
  57. data/lib/pghero/methods/sequences.rb +124 -0
  58. data/lib/pghero/methods/settings.rb +37 -0
  59. data/lib/pghero/methods/space.rb +141 -0
  60. data/lib/pghero/methods/suggested_indexes.rb +329 -0
  61. data/lib/pghero/methods/system.rb +287 -0
  62. data/lib/pghero/methods/tables.rb +68 -0
  63. data/lib/pghero/methods/users.rb +87 -0
  64. data/lib/pghero/query_stats.rb +5 -0
  65. data/lib/pghero/space_stats.rb +5 -0
  66. data/lib/pghero/stats.rb +6 -0
  67. data/lib/pghero/version.rb +3 -0
  68. data/lib/tasks/pghero.rake +27 -0
  69. data/licenses/LICENSE-chart.js.txt +9 -0
  70. data/licenses/LICENSE-chartkick.js.txt +22 -0
  71. data/licenses/LICENSE-highlight.js.txt +29 -0
  72. data/licenses/LICENSE-jquery.txt +20 -0
  73. data/licenses/LICENSE-moment.txt +22 -0
  74. data/licenses/LICENSE-nouislider.txt +21 -0
  75. metadata +130 -0
@@ -0,0 +1,86 @@
1
+ /*
2
+
3
+ Arduino® Light Theme - Stefania Mellai <s.mellai@arduino.cc>
4
+
5
+ */
6
+
7
+ .hljs {
8
+ display: block;
9
+ overflow-x: auto;
10
+ }
11
+
12
+ .hljs,
13
+ .hljs-subst {
14
+ color: #434f54;
15
+ }
16
+
17
+ .hljs-keyword,
18
+ .hljs-attribute,
19
+ .hljs-selector-tag,
20
+ .hljs-doctag,
21
+ .hljs-name {
22
+ color: #00979D;
23
+ }
24
+
25
+ .hljs-built_in,
26
+ .hljs-literal,
27
+ .hljs-bullet,
28
+ .hljs-code,
29
+ .hljs-addition {
30
+ color: #D35400;
31
+ }
32
+
33
+ .hljs-regexp,
34
+ .hljs-symbol,
35
+ .hljs-variable,
36
+ .hljs-template-variable,
37
+ .hljs-link,
38
+ .hljs-selector-attr,
39
+ .hljs-selector-pseudo {
40
+ color: #00979D;
41
+ }
42
+
43
+ .hljs-type,
44
+ .hljs-string,
45
+ .hljs-selector-id,
46
+ .hljs-selector-class,
47
+ .hljs-quote,
48
+ .hljs-template-tag,
49
+ .hljs-deletion {
50
+ color: #005C5F;
51
+ }
52
+
53
+ .hljs-title,
54
+ .hljs-section {
55
+ color: #880000;
56
+ font-weight: bold;
57
+ }
58
+
59
+ .hljs-comment {
60
+ color: #777;
61
+ }
62
+
63
+ .hljs-meta-keyword {
64
+ color: #728E00;
65
+ }
66
+
67
+ .hljs-meta {
68
+ color: #728E00;
69
+ color: #434f54;
70
+ }
71
+
72
+ .hljs-emphasis {
73
+ font-style: italic;
74
+ }
75
+
76
+ .hljs-strong {
77
+ font-weight: bold;
78
+ }
79
+
80
+ .hljs-function {
81
+ color: #728E00;
82
+ }
83
+
84
+ .hljs-number {
85
+ color: #8A7B52;
86
+ }
@@ -0,0 +1,310 @@
1
+ /*! nouislider - 14.6.1 - 8/17/2020 */
2
+ /* Functional styling;
3
+ * These styles are required for noUiSlider to function.
4
+ * You don't need to change these rules to apply your design.
5
+ */
6
+ .noUi-target,
7
+ .noUi-target * {
8
+ -webkit-touch-callout: none;
9
+ -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
10
+ -webkit-user-select: none;
11
+ -ms-touch-action: none;
12
+ touch-action: none;
13
+ -ms-user-select: none;
14
+ -moz-user-select: none;
15
+ user-select: none;
16
+ -moz-box-sizing: border-box;
17
+ box-sizing: border-box;
18
+ }
19
+ .noUi-target {
20
+ position: relative;
21
+ }
22
+ .noUi-base,
23
+ .noUi-connects {
24
+ width: 100%;
25
+ height: 100%;
26
+ position: relative;
27
+ z-index: 1;
28
+ }
29
+ /* Wrapper for all connect elements.
30
+ */
31
+ .noUi-connects {
32
+ overflow: hidden;
33
+ z-index: 0;
34
+ }
35
+ .noUi-connect,
36
+ .noUi-origin {
37
+ will-change: transform;
38
+ position: absolute;
39
+ z-index: 1;
40
+ top: 0;
41
+ right: 0;
42
+ -ms-transform-origin: 0 0;
43
+ -webkit-transform-origin: 0 0;
44
+ -webkit-transform-style: preserve-3d;
45
+ transform-origin: 0 0;
46
+ transform-style: flat;
47
+ }
48
+ .noUi-connect {
49
+ height: 100%;
50
+ width: 100%;
51
+ }
52
+ .noUi-origin {
53
+ height: 10%;
54
+ width: 10%;
55
+ }
56
+ /* Offset direction
57
+ */
58
+ .noUi-txt-dir-rtl.noUi-horizontal .noUi-origin {
59
+ left: 0;
60
+ right: auto;
61
+ }
62
+ /* Give origins 0 height/width so they don't interfere with clicking the
63
+ * connect elements.
64
+ */
65
+ .noUi-vertical .noUi-origin {
66
+ width: 0;
67
+ }
68
+ .noUi-horizontal .noUi-origin {
69
+ height: 0;
70
+ }
71
+ .noUi-handle {
72
+ -webkit-backface-visibility: hidden;
73
+ backface-visibility: hidden;
74
+ position: absolute;
75
+ }
76
+ .noUi-touch-area {
77
+ height: 100%;
78
+ width: 100%;
79
+ }
80
+ .noUi-state-tap .noUi-connect,
81
+ .noUi-state-tap .noUi-origin {
82
+ -webkit-transition: transform 0.3s;
83
+ transition: transform 0.3s;
84
+ }
85
+ .noUi-state-drag * {
86
+ cursor: inherit !important;
87
+ }
88
+ /* Slider size and handle placement;
89
+ */
90
+ .noUi-horizontal {
91
+ height: 18px;
92
+ }
93
+ .noUi-horizontal .noUi-handle {
94
+ width: 34px;
95
+ height: 28px;
96
+ right: -17px;
97
+ top: -6px;
98
+ }
99
+ .noUi-vertical {
100
+ width: 18px;
101
+ }
102
+ .noUi-vertical .noUi-handle {
103
+ width: 28px;
104
+ height: 34px;
105
+ right: -6px;
106
+ top: -17px;
107
+ }
108
+ .noUi-txt-dir-rtl.noUi-horizontal .noUi-handle {
109
+ left: -17px;
110
+ right: auto;
111
+ }
112
+ /* Styling;
113
+ * Giving the connect element a border radius causes issues with using transform: scale
114
+ */
115
+ .noUi-target {
116
+ background: #FAFAFA;
117
+ border-radius: 4px;
118
+ border: 1px solid #D3D3D3;
119
+ box-shadow: inset 0 1px 1px #F0F0F0, 0 3px 6px -5px #BBB;
120
+ }
121
+ .noUi-connects {
122
+ border-radius: 3px;
123
+ }
124
+ .noUi-connect {
125
+ background: #3FB8AF;
126
+ }
127
+ /* Handles and cursors;
128
+ */
129
+ .noUi-draggable {
130
+ cursor: ew-resize;
131
+ }
132
+ .noUi-vertical .noUi-draggable {
133
+ cursor: ns-resize;
134
+ }
135
+ .noUi-handle {
136
+ border: 1px solid #D9D9D9;
137
+ border-radius: 3px;
138
+ background: #FFF;
139
+ cursor: default;
140
+ box-shadow: inset 0 0 1px #FFF, inset 0 1px 7px #EBEBEB, 0 3px 6px -3px #BBB;
141
+ }
142
+ .noUi-active {
143
+ box-shadow: inset 0 0 1px #FFF, inset 0 1px 7px #DDD, 0 3px 6px -3px #BBB;
144
+ }
145
+ /* Handle stripes;
146
+ */
147
+ .noUi-handle:before,
148
+ .noUi-handle:after {
149
+ content: "";
150
+ display: block;
151
+ position: absolute;
152
+ height: 14px;
153
+ width: 1px;
154
+ background: #E8E7E6;
155
+ left: 14px;
156
+ top: 6px;
157
+ }
158
+ .noUi-handle:after {
159
+ left: 17px;
160
+ }
161
+ .noUi-vertical .noUi-handle:before,
162
+ .noUi-vertical .noUi-handle:after {
163
+ width: 14px;
164
+ height: 1px;
165
+ left: 6px;
166
+ top: 14px;
167
+ }
168
+ .noUi-vertical .noUi-handle:after {
169
+ top: 17px;
170
+ }
171
+ /* Disabled state;
172
+ */
173
+ [disabled] .noUi-connect {
174
+ background: #B8B8B8;
175
+ }
176
+ [disabled].noUi-target,
177
+ [disabled].noUi-handle,
178
+ [disabled] .noUi-handle {
179
+ cursor: not-allowed;
180
+ }
181
+ /* Base;
182
+ *
183
+ */
184
+ .noUi-pips,
185
+ .noUi-pips * {
186
+ -moz-box-sizing: border-box;
187
+ box-sizing: border-box;
188
+ }
189
+ .noUi-pips {
190
+ position: absolute;
191
+ color: #999;
192
+ }
193
+ /* Values;
194
+ *
195
+ */
196
+ .noUi-value {
197
+ position: absolute;
198
+ white-space: nowrap;
199
+ text-align: center;
200
+ }
201
+ .noUi-value-sub {
202
+ color: #ccc;
203
+ font-size: 10px;
204
+ }
205
+ /* Markings;
206
+ *
207
+ */
208
+ .noUi-marker {
209
+ position: absolute;
210
+ background: #CCC;
211
+ }
212
+ .noUi-marker-sub {
213
+ background: #AAA;
214
+ }
215
+ .noUi-marker-large {
216
+ background: #AAA;
217
+ }
218
+ /* Horizontal layout;
219
+ *
220
+ */
221
+ .noUi-pips-horizontal {
222
+ padding: 10px 0;
223
+ height: 80px;
224
+ top: 100%;
225
+ left: 0;
226
+ width: 100%;
227
+ }
228
+ .noUi-value-horizontal {
229
+ -webkit-transform: translate(-50%, 50%);
230
+ transform: translate(-50%, 50%);
231
+ }
232
+ .noUi-rtl .noUi-value-horizontal {
233
+ -webkit-transform: translate(50%, 50%);
234
+ transform: translate(50%, 50%);
235
+ }
236
+ .noUi-marker-horizontal.noUi-marker {
237
+ margin-left: -1px;
238
+ width: 2px;
239
+ height: 5px;
240
+ }
241
+ .noUi-marker-horizontal.noUi-marker-sub {
242
+ height: 10px;
243
+ }
244
+ .noUi-marker-horizontal.noUi-marker-large {
245
+ height: 15px;
246
+ }
247
+ /* Vertical layout;
248
+ *
249
+ */
250
+ .noUi-pips-vertical {
251
+ padding: 0 10px;
252
+ height: 100%;
253
+ top: 0;
254
+ left: 100%;
255
+ }
256
+ .noUi-value-vertical {
257
+ -webkit-transform: translate(0, -50%);
258
+ transform: translate(0, -50%);
259
+ padding-left: 25px;
260
+ }
261
+ .noUi-rtl .noUi-value-vertical {
262
+ -webkit-transform: translate(0, 50%);
263
+ transform: translate(0, 50%);
264
+ }
265
+ .noUi-marker-vertical.noUi-marker {
266
+ width: 5px;
267
+ height: 2px;
268
+ margin-top: -1px;
269
+ }
270
+ .noUi-marker-vertical.noUi-marker-sub {
271
+ width: 10px;
272
+ }
273
+ .noUi-marker-vertical.noUi-marker-large {
274
+ width: 15px;
275
+ }
276
+ .noUi-tooltip {
277
+ display: block;
278
+ position: absolute;
279
+ border: 1px solid #D9D9D9;
280
+ border-radius: 3px;
281
+ background: #fff;
282
+ color: #000;
283
+ padding: 5px;
284
+ text-align: center;
285
+ white-space: nowrap;
286
+ }
287
+ .noUi-horizontal .noUi-tooltip {
288
+ -webkit-transform: translate(-50%, 0);
289
+ transform: translate(-50%, 0);
290
+ left: 50%;
291
+ bottom: 120%;
292
+ }
293
+ .noUi-vertical .noUi-tooltip {
294
+ -webkit-transform: translate(0, -50%);
295
+ transform: translate(0, -50%);
296
+ top: 50%;
297
+ right: 120%;
298
+ }
299
+ .noUi-horizontal .noUi-origin > .noUi-tooltip {
300
+ -webkit-transform: translate(50%, 0);
301
+ transform: translate(50%, 0);
302
+ left: auto;
303
+ bottom: 10px;
304
+ }
305
+ .noUi-vertical .noUi-origin > .noUi-tooltip {
306
+ -webkit-transform: translate(0, -18px);
307
+ transform: translate(0, -18px);
308
+ top: auto;
309
+ right: 28px;
310
+ }
@@ -0,0 +1,449 @@
1
+ module PgHero
2
+ class HomeController < ActionController::Base
3
+ layout "pg_hero/application"
4
+
5
+ protect_from_forgery with: :exception
6
+
7
+ http_basic_authenticate_with name: PgHero.username, password: PgHero.password if PgHero.password
8
+
9
+ before_action :set_database
10
+ before_action :set_query_stats_enabled
11
+ before_action :set_show_details, only: [:index, :queries, :show_query]
12
+ before_action :ensure_query_stats, only: [:queries]
13
+
14
+ def index
15
+ @title = "Overview"
16
+ @extended = params[:extended]
17
+
18
+ if @replica
19
+ @replication_lag = @database.replication_lag
20
+ @good_replication_lag = @replication_lag ? @replication_lag < 5 : true
21
+ else
22
+ @inactive_replication_slots = @database.replication_slots.select { |r| !r[:active] }
23
+ end
24
+
25
+ @autovacuum_queries, @long_running_queries = @database.long_running_queries.partition { |q| q[:query].starts_with?("autovacuum:") }
26
+
27
+ connection_states = @database.connection_states
28
+ @total_connections = connection_states.values.sum
29
+ @idle_connections = connection_states["idle in transaction"].to_i
30
+
31
+ @good_total_connections = @total_connections < @database.total_connections_threshold
32
+ @good_idle_connections = @idle_connections < 100
33
+
34
+ @transaction_id_danger = @database.transaction_id_danger(threshold: 1500000000)
35
+
36
+ @readable_sequences, @unreadable_sequences = @database.sequences.partition { |s| s[:readable] }
37
+
38
+ @sequence_danger = @database.sequence_danger(threshold: (params[:sequence_threshold] || 0.9).to_f, sequences: @readable_sequences)
39
+
40
+ @indexes = @database.indexes
41
+ @invalid_indexes = @database.invalid_indexes(indexes: @indexes)
42
+ @invalid_constraints = @database.invalid_constraints
43
+ @duplicate_indexes = @database.duplicate_indexes(indexes: @indexes)
44
+
45
+ if @query_stats_enabled
46
+ @query_stats = @database.query_stats(historical: true, start_at: 3.hours.ago)
47
+ @slow_queries = @database.slow_queries(query_stats: @query_stats)
48
+ set_suggested_indexes((params[:min_average_time] || 20).to_f, (params[:min_calls] || 50).to_i)
49
+ else
50
+ @query_stats_available = @database.query_stats_available?
51
+ @query_stats_extension_enabled = @database.query_stats_extension_enabled? if @query_stats_available
52
+ @suggested_indexes = []
53
+ end
54
+
55
+ if @extended
56
+ @index_hit_rate = @database.index_hit_rate || 0
57
+ @table_hit_rate = @database.table_hit_rate || 0
58
+ @good_cache_rate = @table_hit_rate >= @database.cache_hit_rate_threshold / 100.0 && @index_hit_rate >= @database.cache_hit_rate_threshold / 100.0
59
+ @unused_indexes = @database.unused_indexes(max_scans: 0)
60
+ end
61
+
62
+ @show_migrations = PgHero.show_migrations
63
+ end
64
+
65
+ def space
66
+ @title = "Space"
67
+ @days = (params[:days] || 7).to_i
68
+ @database_size = @database.database_size
69
+ @relation_sizes = params[:tables] ? @database.table_sizes : @database.relation_sizes
70
+ @space_stats_enabled = @database.space_stats_enabled? && !params[:tables]
71
+ if @space_stats_enabled
72
+ space_growth = @database.space_growth(days: @days, relation_sizes: @relation_sizes)
73
+ @growth_bytes_by_relation = Hash[ space_growth.map { |r| [[r[:schema], r[:relation]], r[:growth_bytes]] } ]
74
+ case params[:sort]
75
+ when "growth"
76
+ @relation_sizes.sort_by! { |r| s = @growth_bytes_by_relation[[r[:schema], r[:relation]]]; [s ? 0 : 1, -s.to_i, r[:schema], r[:relation]] }
77
+ when "name"
78
+ @relation_sizes.sort_by! { |r| r[:relation] || r[:table] }
79
+ end
80
+ end
81
+
82
+ across = params[:across].to_s.split(",")
83
+ @unused_indexes = @database.unused_indexes(max_scans: 0, across: across)
84
+ @unused_index_names = Set.new(@unused_indexes.map { |r| r[:index] })
85
+ @show_migrations = PgHero.show_migrations
86
+ @system_stats_enabled = @database.system_stats_enabled?
87
+ @index_bloat = [] # @database.index_bloat
88
+ end
89
+
90
+ def relation_space
91
+ @schema = params[:schema] || "public"
92
+ @relation = params[:relation]
93
+ @title = @relation
94
+ relation_space_stats = @database.relation_space_stats(@relation, schema: @schema)
95
+ @chart_data = [{name: "Value", data: relation_space_stats.map { |r| [r[:captured_at].change(sec: 0), r[:size_bytes].to_i] }, library: chart_library_options}]
96
+ end
97
+
98
+ def index_bloat
99
+ @title = "Index Bloat"
100
+ @index_bloat = @database.index_bloat
101
+ @show_sql = params[:sql]
102
+ end
103
+
104
+ def live_queries
105
+ @title = "Live Queries"
106
+ @running_queries = @database.running_queries(all: true)
107
+ @vacuum_progress = @database.vacuum_progress.index_by { |q| q[:pid] }
108
+
109
+ if params[:state]
110
+ @running_queries.select! { |q| q[:state] == params[:state] }
111
+ end
112
+ end
113
+
114
+ def queries
115
+ @title = "Queries"
116
+ @sort = %w(average_time calls).include?(params[:sort]) ? params[:sort] : nil
117
+ @min_average_time = params[:min_average_time] ? params[:min_average_time].to_i : nil
118
+ @min_calls = params[:min_calls] ? params[:min_calls].to_i : nil
119
+
120
+ if @historical_query_stats_enabled
121
+ begin
122
+ @start_at = params[:start_at] ? Time.zone.parse(params[:start_at]) : 24.hours.ago
123
+ @end_at = Time.zone.parse(params[:end_at]) if params[:end_at]
124
+ rescue
125
+ @error = true
126
+ end
127
+ end
128
+
129
+ @query_stats =
130
+ if @historical_query_stats_enabled && !request.xhr?
131
+ []
132
+ else
133
+ @database.query_stats(
134
+ historical: true,
135
+ start_at: @start_at,
136
+ end_at: @end_at,
137
+ sort: @sort,
138
+ min_average_time: @min_average_time,
139
+ min_calls: @min_calls
140
+ )
141
+ end
142
+
143
+ @indexes = @database.indexes
144
+ set_suggested_indexes
145
+
146
+ # fix back button issue with caching
147
+ response.headers["Cache-Control"] = "must-revalidate, no-store, no-cache, private"
148
+ if request.xhr?
149
+ render layout: false, partial: "queries_table", locals: {queries: @query_stats, xhr: true}
150
+ end
151
+ end
152
+
153
+ def show_query
154
+ @query_hash = params[:query_hash].to_i
155
+ @user = params[:user].to_s
156
+ @title = @query_hash
157
+
158
+ stats = @database.query_stats(historical: true, query_hash: @query_hash, start_at: 24.hours.ago).find { |qs| qs[:user] == @user }
159
+ if stats
160
+ @query = stats[:query]
161
+ @explainable_query = stats[:explainable_query]
162
+
163
+ if @show_details
164
+ query_hash_stats = @database.query_hash_stats(@query_hash, user: @user)
165
+
166
+ @chart_data = [{name: "Value", data: query_hash_stats.map { |r| [r[:captured_at].change(sec: 0), (r[:total_minutes] * 60 * 1000).round] }, library: chart_library_options}]
167
+ @chart2_data = [{name: "Value", data: query_hash_stats.map { |r| [r[:captured_at].change(sec: 0), r[:average_time].round(1)] }, library: chart_library_options}]
168
+ @chart3_data = [{name: "Value", data: query_hash_stats.map { |r| [r[:captured_at].change(sec: 0), r[:calls]] }, library: chart_library_options}]
169
+
170
+ @origins = Hash[query_hash_stats.group_by { |r| r[:origin].to_s }.map { |k, v| [k, v.size] }]
171
+ @total_count = query_hash_stats.size
172
+ end
173
+
174
+ @tables = PgQuery.parse(@query).tables rescue []
175
+ @tables.sort!
176
+
177
+ if @tables.any?
178
+ @row_counts = Hash[@database.table_stats(table: @tables).map { |i| [i[:table], i[:estimated_rows]] }]
179
+ @indexes_by_table = @database.indexes.group_by { |i| i[:table] }
180
+ end
181
+ else
182
+ render_text "Unknown query"
183
+ end
184
+ end
185
+
186
+ def system
187
+ @title = "System"
188
+ @periods = {
189
+ "1 hour" => {duration: 1.hour, period: 60.seconds},
190
+ "1 day" => {duration: 1.day, period: 10.minutes},
191
+ "1 week" => {duration: 1.week, period: 30.minutes},
192
+ "2 weeks" => {duration: 2.weeks, period: 1.hours}
193
+ }
194
+ if @database.system_stats_provider == :azure
195
+ # doesn't support 10, just 5 and 15
196
+ @periods["1 day"][:period] = 15.minutes
197
+ end
198
+
199
+ @duration = (params[:duration] || 1.hour).to_i
200
+ @period = (params[:period] || 60.seconds).to_i
201
+
202
+ if @duration / @period > 1440
203
+ render_text "Too many data points"
204
+ elsif @period % 60 != 0
205
+ render_text "Period must be a multiple of 60"
206
+ end
207
+ end
208
+
209
+ def cpu_usage
210
+ render json: [{name: "CPU", data: @database.cpu_usage(**system_params).map { |k, v| [k, v ? v.round : v] }, library: chart_library_options}]
211
+ end
212
+
213
+ def connection_stats
214
+ render json: [{name: "Connections", data: @database.connection_stats(**system_params), library: chart_library_options}]
215
+ end
216
+
217
+ def replication_lag_stats
218
+ render json: [{name: "Lag", data: @database.replication_lag_stats(**system_params), library: chart_library_options}]
219
+ end
220
+
221
+ def load_stats
222
+ stats =
223
+ case @database.system_stats_provider
224
+ when :azure
225
+ [
226
+ {name: "IO Consumption", data: @database.azure_stats("io_consumption_percent", **system_params), library: chart_library_options}
227
+ ]
228
+ when :gcp
229
+ [
230
+ {name: "Read Ops", data: @database.read_iops_stats(**system_params).map { |k, v| [k, v ? v.round : v] }, library: chart_library_options},
231
+ {name: "Write Ops", data: @database.write_iops_stats(**system_params).map { |k, v| [k, v ? v.round : v] }, library: chart_library_options}
232
+ ]
233
+ else
234
+ [
235
+ {name: "Read IOPS", data: @database.read_iops_stats(**system_params).map { |k, v| [k, v ? v.round : v] }, library: chart_library_options},
236
+ {name: "Write IOPS", data: @database.write_iops_stats(**system_params).map { |k, v| [k, v ? v.round : v] }, library: chart_library_options}
237
+ ]
238
+ end
239
+ render json: stats
240
+ end
241
+
242
+ def free_space_stats
243
+ render json: [
244
+ {name: "Free Space", data: @database.free_space_stats(duration: 14.days, period: 1.hour), library: chart_library_options},
245
+ ]
246
+ end
247
+
248
+ def explain
249
+ @title = "Explain"
250
+ @query = params[:query]
251
+ # TODO use get + token instead of post so users can share links
252
+ # need to prevent CSRF and DoS
253
+ if request.post? && @query
254
+ begin
255
+ prefix =
256
+ case params[:commit]
257
+ when "Analyze"
258
+ "ANALYZE "
259
+ when "Visualize"
260
+ "(ANALYZE, COSTS, VERBOSE, BUFFERS, FORMAT JSON) "
261
+ else
262
+ ""
263
+ end
264
+ @explanation = @database.explain("#{prefix}#{@query}")
265
+ @suggested_index = @database.suggested_indexes(queries: [@query]).first if @database.suggested_indexes_enabled?
266
+ @visualize = params[:commit] == "Visualize"
267
+ rescue ActiveRecord::StatementInvalid => e
268
+ @error = e.message
269
+
270
+ if @error.include?("bind message supplies 0 parameters")
271
+ @error = "Can't explain queries with bind parameters"
272
+ end
273
+ end
274
+ end
275
+ end
276
+
277
+ def tune
278
+ @title = "Tune"
279
+ @settings = @database.settings
280
+ @autovacuum_settings = @database.autovacuum_settings if params[:autovacuum]
281
+ end
282
+
283
+ def connections
284
+ @title = "Connections"
285
+ connections = @database.connections
286
+
287
+ @total_connections = connections.count
288
+ @connection_sources = group_connections(connections, [:database, :user, :source, :ip])
289
+ @connections_by_database = group_connections_by_key(connections, :database)
290
+ @connections_by_user = group_connections_by_key(connections, :user)
291
+
292
+ if params[:security] && @database.server_version_num >= 90500
293
+ connections.each do |connection|
294
+ connection[:ssl_status] =
295
+ if connection[:ssl]
296
+ # no way to tell if client used verify-full
297
+ # so connection may not be actually secure
298
+ "SSL"
299
+ else
300
+ # variety of reasons for no SSL
301
+ if !connection[:database].present?
302
+ "Internal Process"
303
+ elsif !connection[:ip]
304
+ if connection[:state]
305
+ "Socket"
306
+ else
307
+ # tcp or socket, don't have permission to tell
308
+ "No SSL"
309
+ end
310
+ else
311
+ # tcp
312
+ # could separate out localhost since this should be safe
313
+ "No SSL"
314
+ end
315
+ end
316
+ end
317
+
318
+ @connections_by_ssl_status = group_connections_by_key(connections, :ssl_status)
319
+ end
320
+ end
321
+
322
+ def maintenance
323
+ @title = "Maintenance"
324
+ @maintenance_info = @database.maintenance_info
325
+ @time_zone = PgHero.time_zone
326
+ @show_dead_rows = params[:dead_rows]
327
+ end
328
+
329
+ def kill
330
+ if @database.kill(params[:pid])
331
+ redirect_backward notice: "Query killed"
332
+ else
333
+ redirect_backward notice: "Query no longer running"
334
+ end
335
+ end
336
+
337
+ def kill_long_running_queries
338
+ @database.kill_long_running_queries
339
+ redirect_backward notice: "Queries killed"
340
+ end
341
+
342
+ def kill_all
343
+ @database.kill_all
344
+ redirect_backward notice: "Connections killed"
345
+ end
346
+
347
+ def enable_query_stats
348
+ @database.enable_query_stats
349
+ redirect_backward notice: "Query stats enabled"
350
+ rescue ActiveRecord::StatementInvalid
351
+ redirect_backward alert: "The database user does not have permission to enable query stats"
352
+ end
353
+
354
+ def reset_query_stats
355
+ @database.reset_query_stats
356
+ redirect_backward notice: "Query stats reset"
357
+ rescue ActiveRecord::StatementInvalid
358
+ redirect_backward alert: "The database user does not have permission to reset query stats"
359
+ end
360
+
361
+ protected
362
+
363
+ def redirect_backward(options = {})
364
+ if Rails.version >= "5.1"
365
+ redirect_back options.merge(fallback_location: root_path)
366
+ else
367
+ redirect_to :back, options
368
+ end
369
+ end
370
+
371
+ def set_database
372
+ @databases = PgHero.databases.values
373
+ if params[:database]
374
+ # don't do direct lookup, since you don't want to call to_sym on user input
375
+ @database = @databases.find { |d| d.id == params[:database] }
376
+ elsif @databases.size > 1
377
+ redirect_to url_for(controller: controller_name, action: action_name, database: @databases.first.id)
378
+ else
379
+ @database = @databases.first
380
+ end
381
+ end
382
+
383
+ def default_url_options
384
+ {database: params[:database]}
385
+ end
386
+
387
+ def set_query_stats_enabled
388
+ @query_stats_enabled = @database.query_stats_enabled?
389
+ @system_stats_enabled = @database.system_stats_enabled?
390
+ @replica = @database.replica?
391
+ end
392
+
393
+ def set_suggested_indexes(min_average_time = 0, min_calls = 0)
394
+ @suggested_indexes_by_query =
395
+ if @database.suggested_indexes_enabled?
396
+ @database.suggested_indexes_by_query(query_stats: @query_stats.select { |qs| qs[:average_time] >= min_average_time && qs[:calls] >= min_calls })
397
+ else
398
+ {}
399
+ end
400
+
401
+ @suggested_indexes = @database.suggested_indexes(suggested_indexes_by_query: @suggested_indexes_by_query, indexes: @indexes)
402
+ @query_stats_by_query = @query_stats.index_by { |q| q[:query] }
403
+ @debug = params[:debug].present?
404
+ end
405
+
406
+ def system_params
407
+ {
408
+ duration: params[:duration],
409
+ period: params[:period],
410
+ series: true
411
+ }.delete_if { |_, v| v.nil? }
412
+ end
413
+
414
+ def chart_library_options
415
+ {pointRadius: 0, pointHoverRadius: 0, pointHitRadius: 5, borderWidth: 4}
416
+ end
417
+
418
+ def set_show_details
419
+ @historical_query_stats_enabled = @query_stats_enabled && @database.historical_query_stats_enabled?
420
+ @show_details = @historical_query_stats_enabled && @database.supports_query_hash?
421
+ end
422
+
423
+ def group_connections(connections, keys)
424
+ connections
425
+ .group_by { |conn| conn.slice(*keys) }
426
+ .map { |k, v| k.merge(total_connections: v.count) }
427
+ .sort_by { |v| [-v[:total_connections]] + keys.map { |k| v[k].to_s } }
428
+ end
429
+
430
+ def group_connections_by_key(connections, key)
431
+ group_connections(connections, [key]).map { |v| [v[key], v[:total_connections]] }.to_h
432
+ end
433
+
434
+ # def check_api
435
+ # render_text "No support for Rails API. See https://github.com/pghero/pghero for a standalone app." if Rails.application.config.try(:api_only)
436
+ # end
437
+
438
+ # TODO return error status code
439
+ def render_text(message)
440
+ render plain: message
441
+ end
442
+
443
+ def ensure_query_stats
444
+ unless @query_stats_enabled
445
+ redirect_to root_path, alert: "Query stats not enabled"
446
+ end
447
+ end
448
+ end
449
+ end