pghero 2.4.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 +30 -0
- data/README.md +19 -8
- data/app/assets/javascripts/pghero/application.js +1 -1
- data/app/controllers/pg_hero/home_controller.rb +79 -19
- data/app/helpers/pg_hero/home_helper.rb +11 -0
- data/app/views/pg_hero/home/_live_queries_table.html.erb +3 -1
- data/app/views/pg_hero/home/connections.html.erb +9 -0
- data/app/views/pg_hero/home/index.html.erb +2 -2
- data/app/views/pg_hero/home/maintenance.html.erb +16 -2
- data/app/views/pg_hero/home/space.html.erb +1 -1
- data/app/views/pg_hero/home/tune.html.erb +2 -1
- data/lib/generators/pghero/templates/config.yml.tt +11 -4
- data/lib/pghero.rb +29 -13
- data/lib/pghero/database.rb +81 -21
- data/lib/pghero/methods/basic.rb +32 -7
- data/lib/pghero/methods/connections.rb +35 -0
- data/lib/pghero/methods/explain.rb +1 -1
- data/lib/pghero/methods/maintenance.rb +3 -1
- data/lib/pghero/methods/queries.rb +6 -2
- data/lib/pghero/methods/query_stats.rb +93 -25
- data/lib/pghero/methods/space.rb +4 -0
- data/lib/pghero/methods/suggested_indexes.rb +1 -1
- data/lib/pghero/methods/system.rb +227 -15
- data/lib/pghero/methods/users.rb +4 -0
- data/lib/pghero/version.rb +1 -1
- metadata +3 -3
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
|
@@ -48,7 +48,7 @@ module PgHero
|
|
48
48
|
def suggested_indexes(suggested_indexes_by_query: nil, **options)
|
49
49
|
indexes = []
|
50
50
|
|
51
|
-
(suggested_indexes_by_query || self.suggested_indexes_by_query(options)).select { |_s, i| i[:found] && !i[:covering_index] }.group_by { |_s, i| i[:index] }.each do |index, group|
|
51
|
+
(suggested_indexes_by_query || self.suggested_indexes_by_query(**options)).select { |_s, i| i[:found] && !i[:covering_index] }.group_by { |_s, i| i[:index] }.each do |index, group|
|
52
52
|
details = {}
|
53
53
|
group.map(&:second).each do |g|
|
54
54
|
details = details.except(:index).deep_merge(g)
|
@@ -1,31 +1,46 @@
|
|
1
1
|
module PgHero
|
2
2
|
module Methods
|
3
3
|
module System
|
4
|
+
def system_stats_enabled?
|
5
|
+
!system_stats_provider.nil?
|
6
|
+
end
|
7
|
+
|
8
|
+
# TODO remove defined checks in 3.0
|
9
|
+
def system_stats_provider
|
10
|
+
if aws_db_instance_identifier && (defined?(Aws) || defined?(AWS))
|
11
|
+
:aws
|
12
|
+
elsif gcp_database_id
|
13
|
+
:gcp
|
14
|
+
elsif azure_resource_id
|
15
|
+
:azure
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
4
19
|
def cpu_usage(**options)
|
5
|
-
|
20
|
+
system_stats(:cpu, **options)
|
6
21
|
end
|
7
22
|
|
8
23
|
def connection_stats(**options)
|
9
|
-
|
24
|
+
system_stats(:connections, **options)
|
10
25
|
end
|
11
26
|
|
12
27
|
def replication_lag_stats(**options)
|
13
|
-
|
28
|
+
system_stats(:replication_lag, **options)
|
14
29
|
end
|
15
30
|
|
16
31
|
def read_iops_stats(**options)
|
17
|
-
|
32
|
+
system_stats(:read_iops, **options)
|
18
33
|
end
|
19
34
|
|
20
35
|
def write_iops_stats(**options)
|
21
|
-
|
36
|
+
system_stats(:write_iops, **options)
|
22
37
|
end
|
23
38
|
|
24
39
|
def free_space_stats(**options)
|
25
|
-
|
40
|
+
system_stats(:free_space, **options)
|
26
41
|
end
|
27
42
|
|
28
|
-
def rds_stats(metric_name, duration: nil, period: nil, offset: nil)
|
43
|
+
def rds_stats(metric_name, duration: nil, period: nil, offset: nil, series: false)
|
29
44
|
if system_stats_enabled?
|
30
45
|
aws_options = {region: region}
|
31
46
|
if access_key_id
|
@@ -43,16 +58,14 @@ module PgHero
|
|
43
58
|
duration = (duration || 1.hour).to_i
|
44
59
|
period = (period || 1.minute).to_i
|
45
60
|
offset = (offset || 0).to_i
|
46
|
-
|
47
|
-
|
48
|
-
# ceil period
|
49
|
-
end_time = Time.at((end_time.to_f / period).ceil * period)
|
61
|
+
end_time = Time.at(((Time.now - offset).to_f / period).ceil * period)
|
62
|
+
start_time = end_time - duration
|
50
63
|
|
51
64
|
resp = client.get_metric_statistics(
|
52
65
|
namespace: "AWS/RDS",
|
53
66
|
metric_name: metric_name,
|
54
|
-
dimensions: [{name: "DBInstanceIdentifier", value:
|
55
|
-
start_time:
|
67
|
+
dimensions: [{name: "DBInstanceIdentifier", value: aws_db_instance_identifier}],
|
68
|
+
start_time: start_time.iso8601,
|
56
69
|
end_time: end_time.iso8601,
|
57
70
|
period: period,
|
58
71
|
statistics: ["Average"]
|
@@ -61,14 +74,213 @@ module PgHero
|
|
61
74
|
resp[:datapoints].sort_by { |d| d[:timestamp] }.each do |d|
|
62
75
|
data[d[:timestamp]] = d[:average]
|
63
76
|
end
|
77
|
+
|
78
|
+
add_missing_data(data, start_time, end_time, period) if series
|
79
|
+
|
64
80
|
data
|
65
81
|
else
|
66
82
|
raise NotEnabled, "System stats not enabled"
|
67
83
|
end
|
68
84
|
end
|
69
85
|
|
70
|
-
def
|
71
|
-
|
86
|
+
def azure_stats(metric_name, duration: nil, period: nil, offset: nil, series: false)
|
87
|
+
# TODO DRY with RDS stats
|
88
|
+
duration = (duration || 1.hour).to_i
|
89
|
+
period = (period || 1.minute).to_i
|
90
|
+
offset = (offset || 0).to_i
|
91
|
+
end_time = Time.at(((Time.now - offset).to_f / period).ceil * period)
|
92
|
+
start_time = end_time - duration
|
93
|
+
|
94
|
+
interval =
|
95
|
+
case period
|
96
|
+
when 60
|
97
|
+
"PT1M"
|
98
|
+
when 300
|
99
|
+
"PT5M"
|
100
|
+
when 900
|
101
|
+
"PT15M"
|
102
|
+
when 1800
|
103
|
+
"PT30M"
|
104
|
+
when 3600
|
105
|
+
"PT1H"
|
106
|
+
else
|
107
|
+
raise Error, "Unsupported period"
|
108
|
+
end
|
109
|
+
|
110
|
+
client = Azure::Monitor::Profiles::Latest::Mgmt::Client.new
|
111
|
+
timespan = "#{start_time.iso8601}/#{end_time.iso8601}"
|
112
|
+
results = client.metrics.list(
|
113
|
+
azure_resource_id,
|
114
|
+
metricnames: metric_name,
|
115
|
+
aggregation: "Average",
|
116
|
+
timespan: timespan,
|
117
|
+
interval: interval
|
118
|
+
)
|
119
|
+
|
120
|
+
data = {}
|
121
|
+
result = results.value.first
|
122
|
+
if result
|
123
|
+
result.timeseries.first.data.each do |point|
|
124
|
+
data[point.time_stamp.to_time] = point.average
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
add_missing_data(data, start_time, end_time, period) if series
|
129
|
+
|
130
|
+
data
|
131
|
+
end
|
132
|
+
|
133
|
+
private
|
134
|
+
|
135
|
+
def gcp_stats(metric_name, duration: nil, period: nil, offset: nil, series: false)
|
136
|
+
require "google/cloud/monitoring/v3"
|
137
|
+
|
138
|
+
# TODO DRY with RDS stats
|
139
|
+
duration = (duration || 1.hour).to_i
|
140
|
+
period = (period || 1.minute).to_i
|
141
|
+
offset = (offset || 0).to_i
|
142
|
+
end_time = Time.at(((Time.now - offset).to_f / period).ceil * period)
|
143
|
+
start_time = end_time - duration
|
144
|
+
|
145
|
+
# validate input since we need to interpolate below
|
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-z\-:]+\z/i
|
148
|
+
|
149
|
+
# we handle three situations:
|
150
|
+
# 1. google-cloud-monitoring-v3
|
151
|
+
# 2. google-cloud-monitoring >= 1
|
152
|
+
# 3. google-cloud-monitoring < 1
|
153
|
+
|
154
|
+
# for situations 1 and 2
|
155
|
+
# Google::Cloud::Monitoring.metric_service is documented
|
156
|
+
# but doesn't work for situation 1
|
157
|
+
if defined?(Google::Cloud::Monitoring::V3::MetricService::Client)
|
158
|
+
client = Google::Cloud::Monitoring::V3::MetricService::Client.new
|
159
|
+
|
160
|
+
interval = Google::Cloud::Monitoring::V3::TimeInterval.new
|
161
|
+
interval.end_time = Google::Protobuf::Timestamp.new(seconds: end_time.to_i)
|
162
|
+
# subtract period to make sure we get first data point
|
163
|
+
interval.start_time = Google::Protobuf::Timestamp.new(seconds: (start_time - period).to_i)
|
164
|
+
|
165
|
+
aggregation = Google::Cloud::Monitoring::V3::Aggregation.new
|
166
|
+
# may be better to use ALIGN_NEXT_OLDER for space stats to show most recent data point
|
167
|
+
# stick with average for now to match AWS
|
168
|
+
aggregation.per_series_aligner = Google::Cloud::Monitoring::V3::Aggregation::Aligner::ALIGN_MEAN
|
169
|
+
aggregation.alignment_period = period
|
170
|
+
|
171
|
+
results = client.list_time_series({
|
172
|
+
name: "projects/#{gcp_database_id.split(":").first}",
|
173
|
+
filter: "metric.type = \"cloudsql.googleapis.com/database/#{metric_name}\" AND resource.label.database_id = \"#{gcp_database_id}\"",
|
174
|
+
interval: interval,
|
175
|
+
view: Google::Cloud::Monitoring::V3::ListTimeSeriesRequest::TimeSeriesView::FULL,
|
176
|
+
aggregation: aggregation
|
177
|
+
})
|
178
|
+
else
|
179
|
+
require "google/cloud/monitoring"
|
180
|
+
|
181
|
+
client = Google::Cloud::Monitoring::Metric.new
|
182
|
+
|
183
|
+
interval = Google::Monitoring::V3::TimeInterval.new
|
184
|
+
interval.end_time = Google::Protobuf::Timestamp.new(seconds: end_time.to_i)
|
185
|
+
# subtract period to make sure we get first data point
|
186
|
+
interval.start_time = Google::Protobuf::Timestamp.new(seconds: (start_time - period).to_i)
|
187
|
+
|
188
|
+
aggregation = Google::Monitoring::V3::Aggregation.new
|
189
|
+
# may be better to use ALIGN_NEXT_OLDER for space stats to show most recent data point
|
190
|
+
# stick with average for now to match AWS
|
191
|
+
aggregation.per_series_aligner = Google::Monitoring::V3::Aggregation::Aligner::ALIGN_MEAN
|
192
|
+
aggregation.alignment_period = period
|
193
|
+
|
194
|
+
results = client.list_time_series(
|
195
|
+
"projects/#{gcp_database_id.split(":").first}",
|
196
|
+
"metric.type = \"cloudsql.googleapis.com/database/#{metric_name}\" AND resource.label.database_id = \"#{gcp_database_id}\"",
|
197
|
+
interval,
|
198
|
+
Google::Monitoring::V3::ListTimeSeriesRequest::TimeSeriesView::FULL,
|
199
|
+
aggregation: aggregation
|
200
|
+
)
|
201
|
+
end
|
202
|
+
|
203
|
+
data = {}
|
204
|
+
result = results.first
|
205
|
+
if result
|
206
|
+
result.points.each do |point|
|
207
|
+
time = Time.at(point.interval.start_time.seconds)
|
208
|
+
value = point.value.double_value
|
209
|
+
value *= 100 if metric_name == "cpu/utilization"
|
210
|
+
data[time] = value
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
add_missing_data(data, start_time, end_time, period) if series
|
215
|
+
|
216
|
+
data
|
217
|
+
end
|
218
|
+
|
219
|
+
def system_stats(metric_key, **options)
|
220
|
+
case system_stats_provider
|
221
|
+
when :aws
|
222
|
+
metrics = {
|
223
|
+
cpu: "CPUUtilization",
|
224
|
+
connections: "DatabaseConnections",
|
225
|
+
replication_lag: "ReplicaLag",
|
226
|
+
read_iops: "ReadIOPS",
|
227
|
+
write_iops: "WriteIOPS",
|
228
|
+
free_space: "FreeStorageSpace"
|
229
|
+
}
|
230
|
+
rds_stats(metrics[metric_key], **options)
|
231
|
+
when :gcp
|
232
|
+
if metric_key == :free_space
|
233
|
+
quota = gcp_stats("disk/quota", **options)
|
234
|
+
used = gcp_stats("disk/bytes_used", **options)
|
235
|
+
free_space(quota, used)
|
236
|
+
else
|
237
|
+
metrics = {
|
238
|
+
cpu: "cpu/utilization",
|
239
|
+
connections: "postgresql/num_backends",
|
240
|
+
replication_lag: "replication/replica_lag",
|
241
|
+
read_iops: "disk/read_ops_count",
|
242
|
+
write_iops: "disk/write_ops_count"
|
243
|
+
}
|
244
|
+
gcp_stats(metrics[metric_key], **options)
|
245
|
+
end
|
246
|
+
when :azure
|
247
|
+
if metric_key == :free_space
|
248
|
+
quota = azure_stats("storage_limit", **options)
|
249
|
+
used = azure_stats("storage_used", **options)
|
250
|
+
free_space(quota, used)
|
251
|
+
else
|
252
|
+
# no read_iops, write_iops
|
253
|
+
# could add io_consumption_percent
|
254
|
+
metrics = {
|
255
|
+
cpu: "cpu_percent",
|
256
|
+
connections: "active_connections",
|
257
|
+
replication_lag: "pg_replica_log_delay_in_seconds"
|
258
|
+
}
|
259
|
+
raise Error, "Metric not supported" unless metrics[metric_key]
|
260
|
+
azure_stats(metrics[metric_key], **options)
|
261
|
+
end
|
262
|
+
else
|
263
|
+
raise NotEnabled, "System stats not enabled"
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
# only use data points included in both series
|
268
|
+
# this also eliminates need to align Time.now
|
269
|
+
def free_space(quota, used)
|
270
|
+
data = {}
|
271
|
+
quota.each do |k, v|
|
272
|
+
data[k] = v - used[k] if v && used[k]
|
273
|
+
end
|
274
|
+
data
|
275
|
+
end
|
276
|
+
|
277
|
+
def add_missing_data(data, start_time, end_time, period)
|
278
|
+
time = start_time
|
279
|
+
end_time = end_time
|
280
|
+
while time < end_time
|
281
|
+
data[time] ||= nil
|
282
|
+
time += period
|
283
|
+
end
|
72
284
|
end
|
73
285
|
end
|
74
286
|
end
|
data/lib/pghero/methods/users.rb
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
module PgHero
|
2
2
|
module Methods
|
3
3
|
module Users
|
4
|
+
# documented as unsafe to pass user input
|
5
|
+
# TODO quote in 3.0, but still not officially supported
|
4
6
|
def create_user(user, password: nil, schema: "public", database: nil, readonly: false, tables: nil)
|
5
7
|
password ||= random_password
|
6
8
|
database ||= connection_model.connection_config[:database]
|
@@ -39,6 +41,8 @@ module PgHero
|
|
39
41
|
{password: password}
|
40
42
|
end
|
41
43
|
|
44
|
+
# documented as unsafe to pass user input
|
45
|
+
# TODO quote in 3.0, but still not officially supported
|
42
46
|
def drop_user(user, schema: "public", database: nil)
|
43
47
|
database ||= connection_model.connection_config[:database]
|
44
48
|
|
data/lib/pghero/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: pghero
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.7.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andrew Kane
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2020-08-04 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -200,7 +200,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
200
200
|
- !ruby/object:Gem::Version
|
201
201
|
version: '0'
|
202
202
|
requirements: []
|
203
|
-
rubygems_version: 3.
|
203
|
+
rubygems_version: 3.1.2
|
204
204
|
signing_key:
|
205
205
|
specification_version: 4
|
206
206
|
summary: A performance dashboard for Postgres
|