pghero_fork 2.7.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +391 -0
- data/CONTRIBUTING.md +42 -0
- data/LICENSE.txt +22 -0
- data/README.md +3 -0
- data/app/assets/images/pghero/favicon.png +0 -0
- data/app/assets/javascripts/pghero/Chart.bundle.js +20755 -0
- data/app/assets/javascripts/pghero/application.js +158 -0
- data/app/assets/javascripts/pghero/chartkick.js +2436 -0
- data/app/assets/javascripts/pghero/highlight.pack.js +2 -0
- data/app/assets/javascripts/pghero/jquery.js +10872 -0
- data/app/assets/javascripts/pghero/nouislider.js +2672 -0
- data/app/assets/stylesheets/pghero/application.css +514 -0
- data/app/assets/stylesheets/pghero/arduino-light.css +86 -0
- data/app/assets/stylesheets/pghero/nouislider.css +310 -0
- data/app/controllers/pg_hero/home_controller.rb +449 -0
- data/app/helpers/pg_hero/home_helper.rb +30 -0
- data/app/views/layouts/pg_hero/application.html.erb +68 -0
- data/app/views/pg_hero/home/_connections_table.html.erb +16 -0
- data/app/views/pg_hero/home/_live_queries_table.html.erb +51 -0
- data/app/views/pg_hero/home/_queries_table.html.erb +72 -0
- data/app/views/pg_hero/home/_query_stats_slider.html.erb +16 -0
- data/app/views/pg_hero/home/_suggested_index.html.erb +18 -0
- data/app/views/pg_hero/home/connections.html.erb +32 -0
- data/app/views/pg_hero/home/explain.html.erb +27 -0
- data/app/views/pg_hero/home/index.html.erb +518 -0
- data/app/views/pg_hero/home/index_bloat.html.erb +72 -0
- data/app/views/pg_hero/home/live_queries.html.erb +11 -0
- data/app/views/pg_hero/home/maintenance.html.erb +55 -0
- data/app/views/pg_hero/home/queries.html.erb +33 -0
- data/app/views/pg_hero/home/relation_space.html.erb +14 -0
- data/app/views/pg_hero/home/show_query.html.erb +106 -0
- data/app/views/pg_hero/home/space.html.erb +83 -0
- data/app/views/pg_hero/home/system.html.erb +34 -0
- data/app/views/pg_hero/home/tune.html.erb +53 -0
- data/config/routes.rb +32 -0
- data/lib/generators/pghero/config_generator.rb +13 -0
- data/lib/generators/pghero/query_stats_generator.rb +18 -0
- data/lib/generators/pghero/space_stats_generator.rb +18 -0
- data/lib/generators/pghero/templates/config.yml.tt +46 -0
- data/lib/generators/pghero/templates/query_stats.rb.tt +15 -0
- data/lib/generators/pghero/templates/space_stats.rb.tt +13 -0
- data/lib/pghero.rb +246 -0
- data/lib/pghero/connection.rb +5 -0
- data/lib/pghero/database.rb +175 -0
- data/lib/pghero/engine.rb +16 -0
- data/lib/pghero/methods/basic.rb +160 -0
- data/lib/pghero/methods/connections.rb +77 -0
- data/lib/pghero/methods/constraints.rb +30 -0
- data/lib/pghero/methods/explain.rb +29 -0
- data/lib/pghero/methods/indexes.rb +332 -0
- data/lib/pghero/methods/kill.rb +28 -0
- data/lib/pghero/methods/maintenance.rb +93 -0
- data/lib/pghero/methods/queries.rb +75 -0
- data/lib/pghero/methods/query_stats.rb +349 -0
- data/lib/pghero/methods/replication.rb +74 -0
- data/lib/pghero/methods/sequences.rb +124 -0
- data/lib/pghero/methods/settings.rb +37 -0
- data/lib/pghero/methods/space.rb +141 -0
- data/lib/pghero/methods/suggested_indexes.rb +329 -0
- data/lib/pghero/methods/system.rb +287 -0
- data/lib/pghero/methods/tables.rb +68 -0
- data/lib/pghero/methods/users.rb +87 -0
- data/lib/pghero/query_stats.rb +5 -0
- data/lib/pghero/space_stats.rb +5 -0
- data/lib/pghero/stats.rb +6 -0
- data/lib/pghero/version.rb +3 -0
- data/lib/tasks/pghero.rake +27 -0
- data/licenses/LICENSE-chart.js.txt +9 -0
- data/licenses/LICENSE-chartkick.js.txt +22 -0
- data/licenses/LICENSE-highlight.js.txt +29 -0
- data/licenses/LICENSE-jquery.txt +20 -0
- data/licenses/LICENSE-moment.txt +22 -0
- data/licenses/LICENSE-nouislider.txt +21 -0
- 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
|