pghero 2.6.0 → 2.7.4
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 +23 -0
- data/LICENSE.txt +1 -1
- data/README.md +2 -8
- data/app/assets/javascripts/pghero/Chart.bundle.js +4432 -2965
- data/app/assets/javascripts/pghero/application.js +7 -0
- data/app/assets/javascripts/pghero/jquery.js +756 -482
- data/app/assets/javascripts/pghero/nouislider.js +287 -94
- data/app/assets/stylesheets/pghero/application.css +4 -0
- data/app/assets/stylesheets/pghero/nouislider.css +22 -11
- data/app/controllers/pg_hero/home_controller.rb +2 -6
- data/lib/pghero.rb +16 -4
- data/lib/pghero/database.rb +5 -2
- data/lib/pghero/methods/basic.rb +9 -1
- data/lib/pghero/methods/query_stats.rb +77 -20
- data/lib/pghero/methods/sequences.rb +2 -1
- data/lib/pghero/methods/space.rb +4 -0
- data/lib/pghero/methods/suggested_indexes.rb +18 -2
- data/lib/pghero/methods/system.rb +1 -2
- data/lib/pghero/methods/users.rb +2 -2
- data/lib/pghero/version.rb +1 -1
- 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 +13 -91
@@ -1,4 +1,4 @@
|
|
1
|
-
/*! nouislider - 14.
|
1
|
+
/*! nouislider - 14.6.1 - 8/17/2020 */
|
2
2
|
/* Functional styling;
|
3
3
|
* These styles are required for noUiSlider to function.
|
4
4
|
* You don't need to change these rules to apply your design.
|
@@ -18,7 +18,6 @@
|
|
18
18
|
}
|
19
19
|
.noUi-target {
|
20
20
|
position: relative;
|
21
|
-
direction: ltr;
|
22
21
|
}
|
23
22
|
.noUi-base,
|
24
23
|
.noUi-connects {
|
@@ -39,7 +38,7 @@
|
|
39
38
|
position: absolute;
|
40
39
|
z-index: 1;
|
41
40
|
top: 0;
|
42
|
-
|
41
|
+
right: 0;
|
43
42
|
-ms-transform-origin: 0 0;
|
44
43
|
-webkit-transform-origin: 0 0;
|
45
44
|
-webkit-transform-style: preserve-3d;
|
@@ -56,9 +55,9 @@
|
|
56
55
|
}
|
57
56
|
/* Offset direction
|
58
57
|
*/
|
59
|
-
|
60
|
-
left:
|
61
|
-
right:
|
58
|
+
.noUi-txt-dir-rtl.noUi-horizontal .noUi-origin {
|
59
|
+
left: 0;
|
60
|
+
right: auto;
|
62
61
|
}
|
63
62
|
/* Give origins 0 height/width so they don't interfere with clicking the
|
64
63
|
* connect elements.
|
@@ -94,7 +93,7 @@ html:not([dir="rtl"]) .noUi-horizontal .noUi-origin {
|
|
94
93
|
.noUi-horizontal .noUi-handle {
|
95
94
|
width: 34px;
|
96
95
|
height: 28px;
|
97
|
-
|
96
|
+
right: -17px;
|
98
97
|
top: -6px;
|
99
98
|
}
|
100
99
|
.noUi-vertical {
|
@@ -103,12 +102,12 @@ html:not([dir="rtl"]) .noUi-horizontal .noUi-origin {
|
|
103
102
|
.noUi-vertical .noUi-handle {
|
104
103
|
width: 28px;
|
105
104
|
height: 34px;
|
106
|
-
|
105
|
+
right: -6px;
|
107
106
|
top: -17px;
|
108
107
|
}
|
109
|
-
|
110
|
-
|
111
|
-
|
108
|
+
.noUi-txt-dir-rtl.noUi-horizontal .noUi-handle {
|
109
|
+
left: -17px;
|
110
|
+
right: auto;
|
112
111
|
}
|
113
112
|
/* Styling;
|
114
113
|
* Giving the connect element a border radius causes issues with using transform: scale
|
@@ -297,3 +296,15 @@ html:not([dir="rtl"]) .noUi-horizontal .noUi-handle {
|
|
297
296
|
top: 50%;
|
298
297
|
right: 120%;
|
299
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
|
+
}
|
@@ -2,7 +2,7 @@ module PgHero
|
|
2
2
|
class HomeController < ActionController::Base
|
3
3
|
layout "pg_hero/application"
|
4
4
|
|
5
|
-
protect_from_forgery
|
5
|
+
protect_from_forgery with: :exception
|
6
6
|
|
7
7
|
http_basic_authenticate_with name: PgHero.username, password: PgHero.password if PgHero.password
|
8
8
|
|
@@ -373,11 +373,7 @@ module PgHero
|
|
373
373
|
protected
|
374
374
|
|
375
375
|
def redirect_backward(options = {})
|
376
|
-
|
377
|
-
redirect_back options.merge(fallback_location: root_path)
|
378
|
-
else
|
379
|
-
redirect_to :back, options
|
380
|
-
end
|
376
|
+
redirect_back fallback_location: root_path, **options
|
381
377
|
end
|
382
378
|
|
383
379
|
def set_database
|
data/lib/pghero.rb
CHANGED
@@ -113,13 +113,13 @@ module PgHero
|
|
113
113
|
|
114
114
|
if !ENV["PGHERO_DATABASE_URL"] && spec_supported?
|
115
115
|
ActiveRecord::Base.configurations.configs_for(env_name: env, include_replicas: true).each do |db|
|
116
|
-
databases[db.
|
116
|
+
databases[db.send(spec_name_key)] = {"spec" => db.send(spec_name_key)}
|
117
117
|
end
|
118
118
|
end
|
119
119
|
|
120
120
|
if databases.empty?
|
121
121
|
databases["primary"] = {
|
122
|
-
"url" => ENV["PGHERO_DATABASE_URL"] || ActiveRecord::Base
|
122
|
+
"url" => ENV["PGHERO_DATABASE_URL"] || connection_config(ActiveRecord::Base)
|
123
123
|
}
|
124
124
|
end
|
125
125
|
|
@@ -196,13 +196,13 @@ module PgHero
|
|
196
196
|
# stats for old databases are not cleaned up since we can't use an index
|
197
197
|
def clean_query_stats
|
198
198
|
each_database do |database|
|
199
|
-
|
199
|
+
database.clean_query_stats
|
200
200
|
end
|
201
201
|
end
|
202
202
|
|
203
203
|
def clean_space_stats
|
204
204
|
each_database do |database|
|
205
|
-
|
205
|
+
database.clean_space_stats
|
206
206
|
end
|
207
207
|
end
|
208
208
|
|
@@ -211,6 +211,18 @@ module PgHero
|
|
211
211
|
ActiveRecord::VERSION::MAJOR >= 6
|
212
212
|
end
|
213
213
|
|
214
|
+
# private
|
215
|
+
def connection_config(model)
|
216
|
+
ActiveRecord::VERSION::STRING.to_f >= 6.1 ? model.connection_db_config.configuration_hash : model.connection_config
|
217
|
+
end
|
218
|
+
|
219
|
+
# private
|
220
|
+
# Rails 6.1 deprecate `spec_name` and use `name` for configurations
|
221
|
+
# https://github.com/rails/rails/pull/38536
|
222
|
+
def spec_name_key
|
223
|
+
ActiveRecord::VERSION::STRING.to_f >= 6.1 ? :name : :spec_name
|
224
|
+
end
|
225
|
+
|
214
226
|
private
|
215
227
|
|
216
228
|
def each_database
|
data/lib/pghero/database.rb
CHANGED
@@ -149,11 +149,14 @@ module PgHero
|
|
149
149
|
# resolve spec
|
150
150
|
if !url && config["spec"]
|
151
151
|
raise Error, "Spec requires Rails 6+" unless PgHero.spec_supported?
|
152
|
-
|
152
|
+
config_options = {env_name: PgHero.env, PgHero.spec_name_key => config["spec"], include_replicas: true}
|
153
|
+
resolved = ActiveRecord::Base.configurations.configs_for(**config_options)
|
153
154
|
raise Error, "Spec not found: #{config["spec"]}" unless resolved
|
154
|
-
url = resolved.config
|
155
|
+
url = ActiveRecord::VERSION::STRING.to_f >= 6.1 ? resolved.configuration_hash : resolved.config
|
155
156
|
end
|
156
157
|
|
158
|
+
url = url.dup
|
159
|
+
|
157
160
|
Class.new(PgHero::Connection) do
|
158
161
|
def self.name
|
159
162
|
"PgHero::Connection::Database#{object_id}"
|
data/lib/pghero/methods/basic.rb
CHANGED
@@ -18,6 +18,10 @@ module PgHero
|
|
18
18
|
select_one("SELECT current_database()")
|
19
19
|
end
|
20
20
|
|
21
|
+
def current_user
|
22
|
+
select_one("SELECT current_user")
|
23
|
+
end
|
24
|
+
|
21
25
|
def server_version
|
22
26
|
@server_version ||= select_one("SHOW server_version")
|
23
27
|
end
|
@@ -38,7 +42,11 @@ module PgHero
|
|
38
42
|
retries = 0
|
39
43
|
begin
|
40
44
|
result = conn.select_all(add_source(squish(sql)))
|
41
|
-
|
45
|
+
if ActiveRecord::VERSION::STRING.to_f >= 6.1
|
46
|
+
result = result.map(&:symbolize_keys)
|
47
|
+
else
|
48
|
+
result = result.map { |row| Hash[row.map { |col, val| [col.to_sym, result.column_types[col].send(:cast_value, val)] }] }
|
49
|
+
end
|
42
50
|
if filter_data
|
43
51
|
query_columns.each do |column|
|
44
52
|
result.each do |row|
|
@@ -56,8 +56,46 @@ module PgHero
|
|
56
56
|
true
|
57
57
|
end
|
58
58
|
|
59
|
-
|
60
|
-
|
59
|
+
# TODO scope by database in PgHero 3.0
|
60
|
+
# (add database: database_name to options)
|
61
|
+
def reset_query_stats(**options)
|
62
|
+
reset_instance_query_stats(**options)
|
63
|
+
end
|
64
|
+
|
65
|
+
# resets query stats for the entire instance
|
66
|
+
# it's possible to reset stats for a specific
|
67
|
+
# database, user or query hash in Postgres 12+
|
68
|
+
def reset_instance_query_stats(database: nil, user: nil, query_hash: nil, raise_errors: false)
|
69
|
+
if database || user || query_hash
|
70
|
+
raise PgHero::Error, "Requires PostgreSQL 12+" if server_version_num < 120000
|
71
|
+
|
72
|
+
if database
|
73
|
+
database_id = execute("SELECT oid FROM pg_database WHERE datname = #{quote(database)}").first.try(:[], "oid")
|
74
|
+
raise PgHero::Error, "Database not found: #{database}" unless database_id
|
75
|
+
else
|
76
|
+
database_id = 0
|
77
|
+
end
|
78
|
+
|
79
|
+
if user
|
80
|
+
user_id = execute("SELECT usesysid FROM pg_user WHERE usename = #{quote(user)}").first.try(:[], "usesysid")
|
81
|
+
raise PgHero::Error, "User not found: #{user}" unless user_id
|
82
|
+
else
|
83
|
+
user_id = 0
|
84
|
+
end
|
85
|
+
|
86
|
+
if query_hash
|
87
|
+
query_id = query_hash.to_i
|
88
|
+
# may not be needed
|
89
|
+
# but not intuitive that all query hashes are reset with 0
|
90
|
+
raise PgHero::Error, "Invalid query hash: #{query_hash}" if query_id == 0
|
91
|
+
else
|
92
|
+
query_id = 0
|
93
|
+
end
|
94
|
+
|
95
|
+
execute("SELECT pg_stat_statements_reset(#{quote(user_id.to_i)}, #{quote(database_id.to_i)}, #{quote(query_id.to_i)})")
|
96
|
+
else
|
97
|
+
execute("SELECT pg_stat_statements_reset()")
|
98
|
+
end
|
61
99
|
true
|
62
100
|
rescue ActiveRecord::StatementInvalid => e
|
63
101
|
raise e if raise_errors
|
@@ -104,31 +142,32 @@ module PgHero
|
|
104
142
|
query_stats[database_id] = query_stats(limit: 1000000, database: database_name)
|
105
143
|
end
|
106
144
|
|
107
|
-
|
145
|
+
query_stats = query_stats.select { |_, v| v.any? }
|
108
146
|
|
109
|
-
|
147
|
+
# nothing to do
|
148
|
+
return if query_stats.empty?
|
149
|
+
|
150
|
+
# use mapping, not query stats here
|
151
|
+
# TODO add option for this, and make default in PgHero 3.0
|
152
|
+
if false # mapping.size == 1 && server_version_num >= 120000
|
110
153
|
query_stats.each do |db_id, db_query_stats|
|
111
|
-
if
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
now,
|
120
|
-
supports_query_hash ? qs[:query_hash] : nil,
|
121
|
-
qs[:user]
|
122
|
-
]
|
123
|
-
end
|
124
|
-
|
125
|
-
columns = %w[database query total_time calls captured_at query_hash user]
|
126
|
-
insert_stats("pghero_query_stats", columns, values)
|
154
|
+
if reset_query_stats(database: mapping[db_id], raise_errors: raise_errors)
|
155
|
+
insert_query_stats(db_id, db_query_stats, now)
|
156
|
+
end
|
157
|
+
end
|
158
|
+
else
|
159
|
+
if reset_query_stats(raise_errors: raise_errors)
|
160
|
+
query_stats.each do |db_id, db_query_stats|
|
161
|
+
insert_query_stats(db_id, db_query_stats, now)
|
127
162
|
end
|
128
163
|
end
|
129
164
|
end
|
130
165
|
end
|
131
166
|
|
167
|
+
def clean_query_stats
|
168
|
+
PgHero::QueryStats.where(database: id).where("captured_at < ?", 14.days.ago).delete_all
|
169
|
+
end
|
170
|
+
|
132
171
|
def slow_queries(query_stats: nil, **options)
|
133
172
|
query_stats ||= self.query_stats(options)
|
134
173
|
query_stats.select { |q| q[:calls].to_i >= slow_query_calls.to_i && q[:average_time].to_f >= slow_query_ms.to_f }
|
@@ -287,6 +326,24 @@ module PgHero
|
|
287
326
|
def normalize_query(query)
|
288
327
|
squish(query.to_s.gsub(/\?(, ?\?)+/, "?").gsub(/\/\*.+?\*\//, ""))
|
289
328
|
end
|
329
|
+
|
330
|
+
def insert_query_stats(db_id, db_query_stats, now)
|
331
|
+
values =
|
332
|
+
db_query_stats.map do |qs|
|
333
|
+
[
|
334
|
+
db_id,
|
335
|
+
qs[:query],
|
336
|
+
qs[:total_minutes] * 60 * 1000,
|
337
|
+
qs[:calls],
|
338
|
+
now,
|
339
|
+
supports_query_hash? ? qs[:query_hash] : nil,
|
340
|
+
qs[:user]
|
341
|
+
]
|
342
|
+
end
|
343
|
+
|
344
|
+
columns = %w[database query total_time calls captured_at query_hash user]
|
345
|
+
insert_stats("pghero_query_stats", columns, values)
|
346
|
+
end
|
290
347
|
end
|
291
348
|
end
|
292
349
|
end
|
data/lib/pghero/methods/space.rb
CHANGED
@@ -129,6 +129,10 @@ module PgHero
|
|
129
129
|
insert_stats("pghero_space_stats", columns, values) if values.any?
|
130
130
|
end
|
131
131
|
|
132
|
+
def clean_space_stats
|
133
|
+
PgHero::SpaceStats.where(database: id).where("captured_at < ?", 90.days.ago).delete_all
|
134
|
+
end
|
135
|
+
|
132
136
|
def space_stats_enabled?
|
133
137
|
table_exists?("pghero_space_stats")
|
134
138
|
end
|
@@ -30,7 +30,23 @@ module PgHero
|
|
30
30
|
if best_index[:found]
|
31
31
|
index = best_index[:index]
|
32
32
|
best_index[:table_indexes] = indexes_by_table[index[:table]].to_a
|
33
|
-
|
33
|
+
|
34
|
+
# indexes of same type
|
35
|
+
indexes = existing_columns[index[:using] || "btree"][index[:table]]
|
36
|
+
|
37
|
+
if best_index[:structure][:sort].empty?
|
38
|
+
# gist indexes without an opclass
|
39
|
+
# (opclass is part of column name, so columns won't match if opclass present)
|
40
|
+
indexes += existing_columns["gist"][index[:table]]
|
41
|
+
|
42
|
+
# hash indexes work for equality
|
43
|
+
indexes += existing_columns["hash"][index[:table]] if best_index[:structure][:where].all? { |v| v[:op] == "=" }
|
44
|
+
|
45
|
+
# brin indexes work for all
|
46
|
+
indexes += existing_columns["brin"][index[:table]]
|
47
|
+
end
|
48
|
+
|
49
|
+
covering_index = indexes.find { |e| index_covers?(e.map { |v| v.sub(/ inet_ops\z/, "") }, index[:columns]) }
|
34
50
|
if covering_index
|
35
51
|
best_index[:covering_index] = covering_index
|
36
52
|
best_index[:explanation] = "Covered by index on (#{covering_index.join(", ")})"
|
@@ -86,7 +102,7 @@ module PgHero
|
|
86
102
|
# get stats about columns for relevant tables
|
87
103
|
tables = parts.values.map { |t| t[:table] }.uniq
|
88
104
|
# TODO get schema from query structure, then try search path
|
89
|
-
schema =
|
105
|
+
schema = PgHero.connection_config(connection_model)[:schema] || "public"
|
90
106
|
if tables.any?
|
91
107
|
row_stats = Hash[table_stats(table: tables, schema: schema).map { |i| [i[:table], i[:estimated_rows]] }]
|
92
108
|
col_stats = column_stats(table: tables, schema: schema).group_by { |i| i[:table] }
|
@@ -144,7 +144,7 @@ module PgHero
|
|
144
144
|
|
145
145
|
# validate input since we need to interpolate below
|
146
146
|
raise Error, "Invalid metric name" unless metric_name =~ /\A[a-z\/_]+\z/i
|
147
|
-
raise Error, "Invalid database id" unless gcp_database_id =~ /\A[a-
|
147
|
+
raise Error, "Invalid database id" unless gcp_database_id =~ /\A[a-z0-9\-:]+\z/i
|
148
148
|
|
149
149
|
# we handle three situations:
|
150
150
|
# 1. google-cloud-monitoring-v3
|
@@ -276,7 +276,6 @@ module PgHero
|
|
276
276
|
|
277
277
|
def add_missing_data(data, start_time, end_time, period)
|
278
278
|
time = start_time
|
279
|
-
end_time = end_time
|
280
279
|
while time < end_time
|
281
280
|
data[time] ||= nil
|
282
281
|
time += period
|
data/lib/pghero/methods/users.rb
CHANGED
@@ -5,7 +5,7 @@ module PgHero
|
|
5
5
|
# TODO quote in 3.0, but still not officially supported
|
6
6
|
def create_user(user, password: nil, schema: "public", database: nil, readonly: false, tables: nil)
|
7
7
|
password ||= random_password
|
8
|
-
database ||=
|
8
|
+
database ||= PgHero.connection_config(connection_model)[:database]
|
9
9
|
|
10
10
|
commands =
|
11
11
|
[
|
@@ -44,7 +44,7 @@ module PgHero
|
|
44
44
|
# documented as unsafe to pass user input
|
45
45
|
# TODO quote in 3.0, but still not officially supported
|
46
46
|
def drop_user(user, schema: "public", database: nil)
|
47
|
-
database ||=
|
47
|
+
database ||= PgHero.connection_config(connection_model)[:database]
|
48
48
|
|
49
49
|
# thanks shiftb
|
50
50
|
commands =
|
data/lib/pghero/version.rb
CHANGED
@@ -0,0 +1,9 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2018 Chart.js Contributors
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
6
|
+
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
8
|
+
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013-2019 Andrew Kane
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|