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.

@@ -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
- rds_stats("CPUUtilization", options)
20
+ system_stats(:cpu, **options)
6
21
  end
7
22
 
8
23
  def connection_stats(**options)
9
- rds_stats("DatabaseConnections", options)
24
+ system_stats(:connections, **options)
10
25
  end
11
26
 
12
27
  def replication_lag_stats(**options)
13
- rds_stats("ReplicaLag", options)
28
+ system_stats(:replication_lag, **options)
14
29
  end
15
30
 
16
31
  def read_iops_stats(**options)
17
- rds_stats("ReadIOPS", options)
32
+ system_stats(:read_iops, **options)
18
33
  end
19
34
 
20
35
  def write_iops_stats(**options)
21
- rds_stats("WriteIOPS", options)
36
+ system_stats(:write_iops, **options)
22
37
  end
23
38
 
24
39
  def free_space_stats(**options)
25
- rds_stats("FreeStorageSpace", options)
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
- end_time = (Time.now - offset)
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: db_instance_identifier}],
55
- start_time: (end_time - duration).iso8601,
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 system_stats_enabled?
71
- !!((defined?(Aws) || defined?(AWS)) && db_instance_identifier)
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
@@ -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
 
@@ -1,3 +1,3 @@
1
1
  module PgHero
2
- VERSION = "2.4.1"
2
+ VERSION = "2.7.0"
3
3
  end
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.1
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: 2019-11-21 00:00:00.000000000 Z
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.0.6
203
+ rubygems_version: 3.1.2
204
204
  signing_key:
205
205
  specification_version: 4
206
206
  summary: A performance dashboard for Postgres