pghero 2.2.1 → 2.7.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of pghero might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/CHANGELOG.md +100 -53
- data/README.md +20 -8
- data/app/assets/javascripts/pghero/Chart.bundle.js +16260 -15580
- data/app/assets/javascripts/pghero/application.js +8 -7
- data/app/assets/javascripts/pghero/chartkick.js +1973 -1325
- data/app/assets/javascripts/pghero/highlight.pack.js +2 -2
- data/app/assets/javascripts/pghero/jquery.js +3605 -4015
- data/app/assets/javascripts/pghero/nouislider.js +2479 -0
- data/app/assets/stylesheets/pghero/application.css +1 -1
- data/app/assets/stylesheets/pghero/nouislider.css +299 -0
- data/app/controllers/pg_hero/home_controller.rb +97 -42
- data/app/helpers/pg_hero/home_helper.rb +11 -0
- data/app/views/pg_hero/home/_live_queries_table.html.erb +14 -3
- data/app/views/pg_hero/home/connections.html.erb +9 -0
- data/app/views/pg_hero/home/index.html.erb +49 -10
- data/app/views/pg_hero/home/live_queries.html.erb +1 -1
- data/app/views/pg_hero/home/maintenance.html.erb +16 -2
- data/app/views/pg_hero/home/relation_space.html.erb +2 -2
- data/app/views/pg_hero/home/show_query.html.erb +4 -5
- data/app/views/pg_hero/home/space.html.erb +3 -3
- data/app/views/pg_hero/home/system.html.erb +4 -4
- data/app/views/pg_hero/home/tune.html.erb +2 -1
- data/lib/generators/pghero/config_generator.rb +1 -1
- data/lib/generators/pghero/query_stats_generator.rb +3 -20
- data/lib/generators/pghero/space_stats_generator.rb +3 -20
- data/lib/generators/pghero/templates/config.yml.tt +21 -1
- data/lib/pghero.rb +82 -17
- data/lib/pghero/database.rb +104 -19
- data/lib/pghero/methods/basic.rb +34 -25
- data/lib/pghero/methods/connections.rb +35 -0
- data/lib/pghero/methods/constraints.rb +30 -0
- data/lib/pghero/methods/explain.rb +1 -1
- data/lib/pghero/methods/indexes.rb +1 -1
- data/lib/pghero/methods/maintenance.rb +3 -1
- data/lib/pghero/methods/queries.rb +7 -3
- data/lib/pghero/methods/query_stats.rb +93 -25
- data/lib/pghero/methods/sequences.rb +1 -1
- data/lib/pghero/methods/space.rb +4 -0
- data/lib/pghero/methods/suggested_indexes.rb +1 -1
- data/lib/pghero/methods/system.rb +219 -23
- data/lib/pghero/methods/users.rb +4 -0
- data/lib/pghero/query_stats.rb +1 -3
- data/lib/pghero/space_stats.rb +5 -0
- data/lib/pghero/stats.rb +6 -0
- data/lib/pghero/version.rb +1 -1
- data/lib/tasks/pghero.rake +10 -4
- metadata +15 -12
- data/app/assets/javascripts/pghero/jquery.nouislider.min.js +0 -31
- data/app/assets/stylesheets/pghero/jquery.nouislider.css +0 -165
@@ -0,0 +1,299 @@
|
|
1
|
+
/*! nouislider - 14.0.3 - 10/10/2019 */
|
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
|
+
direction: ltr;
|
22
|
+
}
|
23
|
+
.noUi-base,
|
24
|
+
.noUi-connects {
|
25
|
+
width: 100%;
|
26
|
+
height: 100%;
|
27
|
+
position: relative;
|
28
|
+
z-index: 1;
|
29
|
+
}
|
30
|
+
/* Wrapper for all connect elements.
|
31
|
+
*/
|
32
|
+
.noUi-connects {
|
33
|
+
overflow: hidden;
|
34
|
+
z-index: 0;
|
35
|
+
}
|
36
|
+
.noUi-connect,
|
37
|
+
.noUi-origin {
|
38
|
+
will-change: transform;
|
39
|
+
position: absolute;
|
40
|
+
z-index: 1;
|
41
|
+
top: 0;
|
42
|
+
left: 0;
|
43
|
+
-ms-transform-origin: 0 0;
|
44
|
+
-webkit-transform-origin: 0 0;
|
45
|
+
-webkit-transform-style: preserve-3d;
|
46
|
+
transform-origin: 0 0;
|
47
|
+
transform-style: flat;
|
48
|
+
}
|
49
|
+
.noUi-connect {
|
50
|
+
height: 100%;
|
51
|
+
width: 100%;
|
52
|
+
}
|
53
|
+
.noUi-origin {
|
54
|
+
height: 10%;
|
55
|
+
width: 10%;
|
56
|
+
}
|
57
|
+
/* Offset direction
|
58
|
+
*/
|
59
|
+
html:not([dir="rtl"]) .noUi-horizontal .noUi-origin {
|
60
|
+
left: auto;
|
61
|
+
right: 0;
|
62
|
+
}
|
63
|
+
/* Give origins 0 height/width so they don't interfere with clicking the
|
64
|
+
* connect elements.
|
65
|
+
*/
|
66
|
+
.noUi-vertical .noUi-origin {
|
67
|
+
width: 0;
|
68
|
+
}
|
69
|
+
.noUi-horizontal .noUi-origin {
|
70
|
+
height: 0;
|
71
|
+
}
|
72
|
+
.noUi-handle {
|
73
|
+
-webkit-backface-visibility: hidden;
|
74
|
+
backface-visibility: hidden;
|
75
|
+
position: absolute;
|
76
|
+
}
|
77
|
+
.noUi-touch-area {
|
78
|
+
height: 100%;
|
79
|
+
width: 100%;
|
80
|
+
}
|
81
|
+
.noUi-state-tap .noUi-connect,
|
82
|
+
.noUi-state-tap .noUi-origin {
|
83
|
+
-webkit-transition: transform 0.3s;
|
84
|
+
transition: transform 0.3s;
|
85
|
+
}
|
86
|
+
.noUi-state-drag * {
|
87
|
+
cursor: inherit !important;
|
88
|
+
}
|
89
|
+
/* Slider size and handle placement;
|
90
|
+
*/
|
91
|
+
.noUi-horizontal {
|
92
|
+
height: 18px;
|
93
|
+
}
|
94
|
+
.noUi-horizontal .noUi-handle {
|
95
|
+
width: 34px;
|
96
|
+
height: 28px;
|
97
|
+
left: -17px;
|
98
|
+
top: -6px;
|
99
|
+
}
|
100
|
+
.noUi-vertical {
|
101
|
+
width: 18px;
|
102
|
+
}
|
103
|
+
.noUi-vertical .noUi-handle {
|
104
|
+
width: 28px;
|
105
|
+
height: 34px;
|
106
|
+
left: -6px;
|
107
|
+
top: -17px;
|
108
|
+
}
|
109
|
+
html:not([dir="rtl"]) .noUi-horizontal .noUi-handle {
|
110
|
+
right: -17px;
|
111
|
+
left: auto;
|
112
|
+
}
|
113
|
+
/* Styling;
|
114
|
+
* Giving the connect element a border radius causes issues with using transform: scale
|
115
|
+
*/
|
116
|
+
.noUi-target {
|
117
|
+
background: #FAFAFA;
|
118
|
+
border-radius: 4px;
|
119
|
+
border: 1px solid #D3D3D3;
|
120
|
+
box-shadow: inset 0 1px 1px #F0F0F0, 0 3px 6px -5px #BBB;
|
121
|
+
}
|
122
|
+
.noUi-connects {
|
123
|
+
border-radius: 3px;
|
124
|
+
}
|
125
|
+
.noUi-connect {
|
126
|
+
background: #3FB8AF;
|
127
|
+
}
|
128
|
+
/* Handles and cursors;
|
129
|
+
*/
|
130
|
+
.noUi-draggable {
|
131
|
+
cursor: ew-resize;
|
132
|
+
}
|
133
|
+
.noUi-vertical .noUi-draggable {
|
134
|
+
cursor: ns-resize;
|
135
|
+
}
|
136
|
+
.noUi-handle {
|
137
|
+
border: 1px solid #D9D9D9;
|
138
|
+
border-radius: 3px;
|
139
|
+
background: #FFF;
|
140
|
+
cursor: default;
|
141
|
+
box-shadow: inset 0 0 1px #FFF, inset 0 1px 7px #EBEBEB, 0 3px 6px -3px #BBB;
|
142
|
+
}
|
143
|
+
.noUi-active {
|
144
|
+
box-shadow: inset 0 0 1px #FFF, inset 0 1px 7px #DDD, 0 3px 6px -3px #BBB;
|
145
|
+
}
|
146
|
+
/* Handle stripes;
|
147
|
+
*/
|
148
|
+
.noUi-handle:before,
|
149
|
+
.noUi-handle:after {
|
150
|
+
content: "";
|
151
|
+
display: block;
|
152
|
+
position: absolute;
|
153
|
+
height: 14px;
|
154
|
+
width: 1px;
|
155
|
+
background: #E8E7E6;
|
156
|
+
left: 14px;
|
157
|
+
top: 6px;
|
158
|
+
}
|
159
|
+
.noUi-handle:after {
|
160
|
+
left: 17px;
|
161
|
+
}
|
162
|
+
.noUi-vertical .noUi-handle:before,
|
163
|
+
.noUi-vertical .noUi-handle:after {
|
164
|
+
width: 14px;
|
165
|
+
height: 1px;
|
166
|
+
left: 6px;
|
167
|
+
top: 14px;
|
168
|
+
}
|
169
|
+
.noUi-vertical .noUi-handle:after {
|
170
|
+
top: 17px;
|
171
|
+
}
|
172
|
+
/* Disabled state;
|
173
|
+
*/
|
174
|
+
[disabled] .noUi-connect {
|
175
|
+
background: #B8B8B8;
|
176
|
+
}
|
177
|
+
[disabled].noUi-target,
|
178
|
+
[disabled].noUi-handle,
|
179
|
+
[disabled] .noUi-handle {
|
180
|
+
cursor: not-allowed;
|
181
|
+
}
|
182
|
+
/* Base;
|
183
|
+
*
|
184
|
+
*/
|
185
|
+
.noUi-pips,
|
186
|
+
.noUi-pips * {
|
187
|
+
-moz-box-sizing: border-box;
|
188
|
+
box-sizing: border-box;
|
189
|
+
}
|
190
|
+
.noUi-pips {
|
191
|
+
position: absolute;
|
192
|
+
color: #999;
|
193
|
+
}
|
194
|
+
/* Values;
|
195
|
+
*
|
196
|
+
*/
|
197
|
+
.noUi-value {
|
198
|
+
position: absolute;
|
199
|
+
white-space: nowrap;
|
200
|
+
text-align: center;
|
201
|
+
}
|
202
|
+
.noUi-value-sub {
|
203
|
+
color: #ccc;
|
204
|
+
font-size: 10px;
|
205
|
+
}
|
206
|
+
/* Markings;
|
207
|
+
*
|
208
|
+
*/
|
209
|
+
.noUi-marker {
|
210
|
+
position: absolute;
|
211
|
+
background: #CCC;
|
212
|
+
}
|
213
|
+
.noUi-marker-sub {
|
214
|
+
background: #AAA;
|
215
|
+
}
|
216
|
+
.noUi-marker-large {
|
217
|
+
background: #AAA;
|
218
|
+
}
|
219
|
+
/* Horizontal layout;
|
220
|
+
*
|
221
|
+
*/
|
222
|
+
.noUi-pips-horizontal {
|
223
|
+
padding: 10px 0;
|
224
|
+
height: 80px;
|
225
|
+
top: 100%;
|
226
|
+
left: 0;
|
227
|
+
width: 100%;
|
228
|
+
}
|
229
|
+
.noUi-value-horizontal {
|
230
|
+
-webkit-transform: translate(-50%, 50%);
|
231
|
+
transform: translate(-50%, 50%);
|
232
|
+
}
|
233
|
+
.noUi-rtl .noUi-value-horizontal {
|
234
|
+
-webkit-transform: translate(50%, 50%);
|
235
|
+
transform: translate(50%, 50%);
|
236
|
+
}
|
237
|
+
.noUi-marker-horizontal.noUi-marker {
|
238
|
+
margin-left: -1px;
|
239
|
+
width: 2px;
|
240
|
+
height: 5px;
|
241
|
+
}
|
242
|
+
.noUi-marker-horizontal.noUi-marker-sub {
|
243
|
+
height: 10px;
|
244
|
+
}
|
245
|
+
.noUi-marker-horizontal.noUi-marker-large {
|
246
|
+
height: 15px;
|
247
|
+
}
|
248
|
+
/* Vertical layout;
|
249
|
+
*
|
250
|
+
*/
|
251
|
+
.noUi-pips-vertical {
|
252
|
+
padding: 0 10px;
|
253
|
+
height: 100%;
|
254
|
+
top: 0;
|
255
|
+
left: 100%;
|
256
|
+
}
|
257
|
+
.noUi-value-vertical {
|
258
|
+
-webkit-transform: translate(0, -50%);
|
259
|
+
transform: translate(0, -50%);
|
260
|
+
padding-left: 25px;
|
261
|
+
}
|
262
|
+
.noUi-rtl .noUi-value-vertical {
|
263
|
+
-webkit-transform: translate(0, 50%);
|
264
|
+
transform: translate(0, 50%);
|
265
|
+
}
|
266
|
+
.noUi-marker-vertical.noUi-marker {
|
267
|
+
width: 5px;
|
268
|
+
height: 2px;
|
269
|
+
margin-top: -1px;
|
270
|
+
}
|
271
|
+
.noUi-marker-vertical.noUi-marker-sub {
|
272
|
+
width: 10px;
|
273
|
+
}
|
274
|
+
.noUi-marker-vertical.noUi-marker-large {
|
275
|
+
width: 15px;
|
276
|
+
}
|
277
|
+
.noUi-tooltip {
|
278
|
+
display: block;
|
279
|
+
position: absolute;
|
280
|
+
border: 1px solid #D9D9D9;
|
281
|
+
border-radius: 3px;
|
282
|
+
background: #fff;
|
283
|
+
color: #000;
|
284
|
+
padding: 5px;
|
285
|
+
text-align: center;
|
286
|
+
white-space: nowrap;
|
287
|
+
}
|
288
|
+
.noUi-horizontal .noUi-tooltip {
|
289
|
+
-webkit-transform: translate(-50%, 0);
|
290
|
+
transform: translate(-50%, 0);
|
291
|
+
left: 50%;
|
292
|
+
bottom: 120%;
|
293
|
+
}
|
294
|
+
.noUi-vertical .noUi-tooltip {
|
295
|
+
-webkit-transform: translate(0, -50%);
|
296
|
+
transform: translate(0, -50%);
|
297
|
+
top: 50%;
|
298
|
+
right: 120%;
|
299
|
+
}
|
@@ -2,22 +2,25 @@ module PgHero
|
|
2
2
|
class HomeController < ActionController::Base
|
3
3
|
layout "pg_hero/application"
|
4
4
|
|
5
|
-
protect_from_forgery
|
6
|
-
|
7
|
-
http_basic_authenticate_with name:
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
#
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
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 :check_api
|
10
|
+
before_action :set_database
|
11
|
+
before_action :set_query_stats_enabled
|
12
|
+
before_action :set_show_details, only: [:index, :queries, :show_query]
|
13
|
+
before_action :ensure_query_stats, only: [:queries]
|
14
|
+
|
15
|
+
if PgHero.config["override_csp"]
|
16
|
+
# note: this does not take into account asset hosts
|
17
|
+
# which can be a string with %d or a proc
|
18
|
+
# https://api.rubyonrails.org/classes/ActionView/Helpers/AssetUrlHelper.html
|
19
|
+
# users should set CSP manually if needed
|
20
|
+
# see https://github.com/ankane/pghero/issues/297
|
21
|
+
after_action do
|
22
|
+
response.headers["Content-Security-Policy"] = "default-src 'self' 'unsafe-inline'"
|
23
|
+
end
|
21
24
|
end
|
22
25
|
|
23
26
|
def index
|
@@ -48,6 +51,7 @@ module PgHero
|
|
48
51
|
|
49
52
|
@indexes = @database.indexes
|
50
53
|
@invalid_indexes = @database.invalid_indexes(indexes: @indexes)
|
54
|
+
@invalid_constraints = @database.invalid_constraints
|
51
55
|
@duplicate_indexes = @database.duplicate_indexes(indexes: @indexes)
|
52
56
|
|
53
57
|
if @query_stats_enabled
|
@@ -100,7 +104,7 @@ module PgHero
|
|
100
104
|
@relation = params[:relation]
|
101
105
|
@title = @relation
|
102
106
|
relation_space_stats = @database.relation_space_stats(@relation, schema: @schema)
|
103
|
-
@chart_data = [{name: "Value", data: relation_space_stats.map { |r| [r[:captured_at],
|
107
|
+
@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}]
|
104
108
|
end
|
105
109
|
|
106
110
|
def index_bloat
|
@@ -199,6 +203,11 @@ module PgHero
|
|
199
203
|
"1 week" => {duration: 1.week, period: 30.minutes},
|
200
204
|
"2 weeks" => {duration: 2.weeks, period: 1.hours}
|
201
205
|
}
|
206
|
+
if @database.system_stats_provider == :azure
|
207
|
+
# doesn't support 10, just 5 and 15
|
208
|
+
@periods["1 day"][:period] = 15.minutes
|
209
|
+
end
|
210
|
+
|
202
211
|
@duration = (params[:duration] || 1.hour).to_i
|
203
212
|
@period = (params[:period] || 60.seconds).to_i
|
204
213
|
|
@@ -210,27 +219,41 @@ module PgHero
|
|
210
219
|
end
|
211
220
|
|
212
221
|
def cpu_usage
|
213
|
-
render json: [{name: "CPU", data: @database.cpu_usage(system_params).map { |k, v| [k, v.round] }, library: chart_library_options}]
|
222
|
+
render json: [{name: "CPU", data: @database.cpu_usage(**system_params).map { |k, v| [k, v ? v.round : v] }, library: chart_library_options}]
|
214
223
|
end
|
215
224
|
|
216
225
|
def connection_stats
|
217
|
-
render json: [{name: "Connections", data: @database.connection_stats(system_params), library: chart_library_options}]
|
226
|
+
render json: [{name: "Connections", data: @database.connection_stats(**system_params), library: chart_library_options}]
|
218
227
|
end
|
219
228
|
|
220
229
|
def replication_lag_stats
|
221
|
-
render json: [{name: "Lag", data: @database.replication_lag_stats(system_params), library: chart_library_options}]
|
230
|
+
render json: [{name: "Lag", data: @database.replication_lag_stats(**system_params), library: chart_library_options}]
|
222
231
|
end
|
223
232
|
|
224
233
|
def load_stats
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
234
|
+
stats =
|
235
|
+
case @database.system_stats_provider
|
236
|
+
when :azure
|
237
|
+
[
|
238
|
+
{name: "IO Consumption", data: @database.azure_stats("io_consumption_percent", **system_params), library: chart_library_options}
|
239
|
+
]
|
240
|
+
when :gcp
|
241
|
+
[
|
242
|
+
{name: "Read Ops", data: @database.read_iops_stats(**system_params).map { |k, v| [k, v ? v.round : v] }, library: chart_library_options},
|
243
|
+
{name: "Write Ops", data: @database.write_iops_stats(**system_params).map { |k, v| [k, v ? v.round : v] }, library: chart_library_options}
|
244
|
+
]
|
245
|
+
else
|
246
|
+
[
|
247
|
+
{name: "Read IOPS", data: @database.read_iops_stats(**system_params).map { |k, v| [k, v ? v.round : v] }, library: chart_library_options},
|
248
|
+
{name: "Write IOPS", data: @database.write_iops_stats(**system_params).map { |k, v| [k, v ? v.round : v] }, library: chart_library_options}
|
249
|
+
]
|
250
|
+
end
|
251
|
+
render json: stats
|
229
252
|
end
|
230
253
|
|
231
254
|
def free_space_stats
|
232
255
|
render json: [
|
233
|
-
{name: "Free Space", data: @database.free_space_stats(duration: 14.days, period: 1.hour)
|
256
|
+
{name: "Free Space", data: @database.free_space_stats(duration: 14.days, period: 1.hour), library: chart_library_options},
|
234
257
|
]
|
235
258
|
end
|
236
259
|
|
@@ -271,17 +294,48 @@ module PgHero
|
|
271
294
|
|
272
295
|
def connections
|
273
296
|
@title = "Connections"
|
274
|
-
|
275
|
-
|
297
|
+
connections = @database.connections
|
298
|
+
|
299
|
+
@total_connections = connections.count
|
300
|
+
@connection_sources = group_connections(connections, [:database, :user, :source, :ip])
|
301
|
+
@connections_by_database = group_connections_by_key(connections, :database)
|
302
|
+
@connections_by_user = group_connections_by_key(connections, :user)
|
303
|
+
|
304
|
+
if params[:security] && @database.server_version_num >= 90500
|
305
|
+
connections.each do |connection|
|
306
|
+
connection[:ssl_status] =
|
307
|
+
if connection[:ssl]
|
308
|
+
# no way to tell if client used verify-full
|
309
|
+
# so connection may not be actually secure
|
310
|
+
"SSL"
|
311
|
+
else
|
312
|
+
# variety of reasons for no SSL
|
313
|
+
if !connection[:database].present?
|
314
|
+
"Internal Process"
|
315
|
+
elsif !connection[:ip]
|
316
|
+
if connection[:state]
|
317
|
+
"Socket"
|
318
|
+
else
|
319
|
+
# tcp or socket, don't have permission to tell
|
320
|
+
"No SSL"
|
321
|
+
end
|
322
|
+
else
|
323
|
+
# tcp
|
324
|
+
# could separate out localhost since this should be safe
|
325
|
+
"No SSL"
|
326
|
+
end
|
327
|
+
end
|
328
|
+
end
|
276
329
|
|
277
|
-
|
278
|
-
|
330
|
+
@connections_by_ssl_status = group_connections_by_key(connections, :ssl_status)
|
331
|
+
end
|
279
332
|
end
|
280
333
|
|
281
334
|
def maintenance
|
282
335
|
@title = "Maintenance"
|
283
336
|
@maintenance_info = @database.maintenance_info
|
284
337
|
@time_zone = PgHero.time_zone
|
338
|
+
@show_dead_rows = params[:dead_rows]
|
285
339
|
end
|
286
340
|
|
287
341
|
def kill
|
@@ -364,12 +418,13 @@ module PgHero
|
|
364
418
|
def system_params
|
365
419
|
{
|
366
420
|
duration: params[:duration],
|
367
|
-
period: params[:period]
|
421
|
+
period: params[:period],
|
422
|
+
series: true
|
368
423
|
}.delete_if { |_, v| v.nil? }
|
369
424
|
end
|
370
425
|
|
371
426
|
def chart_library_options
|
372
|
-
{pointRadius: 0, pointHitRadius: 5, borderWidth: 4}
|
427
|
+
{pointRadius: 0, pointHoverRadius: 0, pointHitRadius: 5, borderWidth: 4}
|
373
428
|
end
|
374
429
|
|
375
430
|
def set_show_details
|
@@ -377,24 +432,24 @@ module PgHero
|
|
377
432
|
@show_details = @historical_query_stats_enabled && @database.supports_query_hash?
|
378
433
|
end
|
379
434
|
|
380
|
-
def group_connections(
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
435
|
+
def group_connections(connections, keys)
|
436
|
+
connections
|
437
|
+
.group_by { |conn| conn.slice(*keys) }
|
438
|
+
.map { |k, v| k.merge(total_connections: v.count) }
|
439
|
+
.sort_by { |v| [-v[:total_connections]] + keys.map { |k| v[k].to_s } }
|
440
|
+
end
|
441
|
+
|
442
|
+
def group_connections_by_key(connections, key)
|
443
|
+
group_connections(connections, [key]).map { |v| [v[key], v[:total_connections]] }.to_h
|
386
444
|
end
|
387
445
|
|
388
446
|
def check_api
|
389
447
|
render_text "No support for Rails API. See https://github.com/pghero/pghero for a standalone app." if Rails.application.config.try(:api_only)
|
390
448
|
end
|
391
449
|
|
450
|
+
# TODO return error status code
|
392
451
|
def render_text(message)
|
393
|
-
|
394
|
-
render plain: message
|
395
|
-
else
|
396
|
-
render text: message
|
397
|
-
end
|
452
|
+
render plain: message
|
398
453
|
end
|
399
454
|
|
400
455
|
def ensure_query_stats
|