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.

Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +100 -53
  3. data/README.md +20 -8
  4. data/app/assets/javascripts/pghero/Chart.bundle.js +16260 -15580
  5. data/app/assets/javascripts/pghero/application.js +8 -7
  6. data/app/assets/javascripts/pghero/chartkick.js +1973 -1325
  7. data/app/assets/javascripts/pghero/highlight.pack.js +2 -2
  8. data/app/assets/javascripts/pghero/jquery.js +3605 -4015
  9. data/app/assets/javascripts/pghero/nouislider.js +2479 -0
  10. data/app/assets/stylesheets/pghero/application.css +1 -1
  11. data/app/assets/stylesheets/pghero/nouislider.css +299 -0
  12. data/app/controllers/pg_hero/home_controller.rb +97 -42
  13. data/app/helpers/pg_hero/home_helper.rb +11 -0
  14. data/app/views/pg_hero/home/_live_queries_table.html.erb +14 -3
  15. data/app/views/pg_hero/home/connections.html.erb +9 -0
  16. data/app/views/pg_hero/home/index.html.erb +49 -10
  17. data/app/views/pg_hero/home/live_queries.html.erb +1 -1
  18. data/app/views/pg_hero/home/maintenance.html.erb +16 -2
  19. data/app/views/pg_hero/home/relation_space.html.erb +2 -2
  20. data/app/views/pg_hero/home/show_query.html.erb +4 -5
  21. data/app/views/pg_hero/home/space.html.erb +3 -3
  22. data/app/views/pg_hero/home/system.html.erb +4 -4
  23. data/app/views/pg_hero/home/tune.html.erb +2 -1
  24. data/lib/generators/pghero/config_generator.rb +1 -1
  25. data/lib/generators/pghero/query_stats_generator.rb +3 -20
  26. data/lib/generators/pghero/space_stats_generator.rb +3 -20
  27. data/lib/generators/pghero/templates/config.yml.tt +21 -1
  28. data/lib/pghero.rb +82 -17
  29. data/lib/pghero/database.rb +104 -19
  30. data/lib/pghero/methods/basic.rb +34 -25
  31. data/lib/pghero/methods/connections.rb +35 -0
  32. data/lib/pghero/methods/constraints.rb +30 -0
  33. data/lib/pghero/methods/explain.rb +1 -1
  34. data/lib/pghero/methods/indexes.rb +1 -1
  35. data/lib/pghero/methods/maintenance.rb +3 -1
  36. data/lib/pghero/methods/queries.rb +7 -3
  37. data/lib/pghero/methods/query_stats.rb +93 -25
  38. data/lib/pghero/methods/sequences.rb +1 -1
  39. data/lib/pghero/methods/space.rb +4 -0
  40. data/lib/pghero/methods/suggested_indexes.rb +1 -1
  41. data/lib/pghero/methods/system.rb +219 -23
  42. data/lib/pghero/methods/users.rb +4 -0
  43. data/lib/pghero/query_stats.rb +1 -3
  44. data/lib/pghero/space_stats.rb +5 -0
  45. data/lib/pghero/stats.rb +6 -0
  46. data/lib/pghero/version.rb +1 -1
  47. data/lib/tasks/pghero.rake +10 -4
  48. metadata +15 -12
  49. data/app/assets/javascripts/pghero/jquery.nouislider.min.js +0 -31
  50. data/app/assets/stylesheets/pghero/jquery.nouislider.css +0 -165
@@ -1,5 +1,5 @@
1
1
  /*
2
- *= require ./jquery.nouislider
2
+ *= require ./nouislider
3
3
  *= require ./arduino-light
4
4
  *= require_self
5
5
  */
@@ -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: ENV["PGHERO_USERNAME"], password: ENV["PGHERO_PASSWORD"] if ENV["PGHERO_PASSWORD"]
8
-
9
- if respond_to?(:before_action)
10
- before_action :check_api
11
- before_action :set_database
12
- before_action :set_query_stats_enabled
13
- before_action :set_show_details, only: [:index, :queries, :show_query]
14
- before_action :ensure_query_stats, only: [:queries]
15
- else
16
- # no need to check API in earlier versions
17
- before_filter :set_database
18
- before_filter :set_query_stats_enabled
19
- before_filter :set_show_details, only: [:index, :queries, :show_query]
20
- before_filter :ensure_query_stats, only: [:queries]
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], (r[:size_bytes].to_f / 1.megabyte).round(1)] }, library: chart_library_options}]
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
- render json: [
226
- {name: "Read IOPS", data: @database.read_iops_stats(system_params).map { |k, v| [k, v.round] }, library: chart_library_options},
227
- {name: "Write IOPS", data: @database.write_iops_stats(system_params).map { |k, v| [k, v.round] }, library: chart_library_options}
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).map { |k, v| [k, (v / 1.gigabyte).round] }, library: chart_library_options},
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
- @connection_sources = @database.connection_sources
275
- @total_connections = @connection_sources.sum { |cs| cs[:total_connections] }
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
- @connections_by_database = group_connections(@connection_sources, :database)
278
- @connections_by_user = group_connections(@connection_sources, :user)
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(connection_sources, key)
381
- top_connections = Hash.new(0)
382
- connection_sources.each do |source|
383
- top_connections[source[key]] += source[:total_connections]
384
- end
385
- top_connections.sort_by { |k, v| [-v, k] }
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
- if Rails::VERSION::MAJOR >= 5
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