pghero_fork 2.7.3

Sign up to get free protection for your applications and to get access to all the features.
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