pghero 1.0.1 → 1.1.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 +4 -0
- data/app/controllers/pg_hero/home_controller.rb +23 -2
- data/app/views/layouts/pg_hero/application.html.erb +29 -4
- data/app/views/pg_hero/home/_queries_table.html.erb +23 -8
- data/app/views/pg_hero/home/_query_stats_slider.html.erb +129 -0
- data/app/views/pg_hero/home/queries.html.erb +8 -2
- data/guides/Rails.md +35 -0
- data/lib/generators/pghero/query_stats_generator.rb +29 -0
- data/lib/generators/pghero/templates/query_stats.rb +13 -0
- data/lib/pghero.rb +153 -67
- data/lib/pghero/tasks.rb +8 -0
- data/lib/pghero/version.rb +1 -1
- metadata +6 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1ed028006b69e4e3595170baffc6fb328de3cd3f
|
4
|
+
data.tar.gz: 05f3f70425c1506f3ba7e6106e1be4922b256fc6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a7d7323653811a623f0a7843fd5aaec408570f96909a7ccec71f2aaae343619f67eb8ff81878f1f86cc035f9a1cc2c20539470025136ac004765aa0d67c6e75b
|
7
|
+
data.tar.gz: 951cf7cc4359047ec384905e6dcbd0e8f6de9c205a32a09c399a38191f9edcf10492b07b05f8af6a80948d1366c34ca9edc7c7f26b20812fc39f7f8b90f43e9a
|
data/CHANGELOG.md
CHANGED
@@ -11,7 +11,7 @@ module PgHero
|
|
11
11
|
|
12
12
|
def index
|
13
13
|
@title = "Overview"
|
14
|
-
@slow_queries = PgHero.slow_queries
|
14
|
+
@slow_queries = PgHero.slow_queries(historical: true, start_at: 3.hours.ago)
|
15
15
|
@long_running_queries = PgHero.long_running_queries
|
16
16
|
@index_hit_rate = PgHero.index_hit_rate
|
17
17
|
@table_hit_rate = PgHero.table_hit_rate
|
@@ -46,7 +46,28 @@ module PgHero
|
|
46
46
|
|
47
47
|
def queries
|
48
48
|
@title = "Queries"
|
49
|
-
@
|
49
|
+
@historical_query_stats_enabled = PgHero.historical_query_stats_enabled?
|
50
|
+
|
51
|
+
@query_stats =
|
52
|
+
begin
|
53
|
+
if @historical_query_stats_enabled
|
54
|
+
@start_at = params[:start_at] ? Time.zone.parse(params[:start_at]) : 24.hours.ago
|
55
|
+
@end_at = Time.zone.parse(params[:end_at]) if params[:end_at]
|
56
|
+
end
|
57
|
+
|
58
|
+
if @historical_query_stats_enabled && !request.xhr?
|
59
|
+
[]
|
60
|
+
else
|
61
|
+
PgHero.query_stats(historical: true, start_at: @start_at, end_at: @end_at)
|
62
|
+
end
|
63
|
+
rescue
|
64
|
+
@error = true
|
65
|
+
[]
|
66
|
+
end
|
67
|
+
|
68
|
+
if request.xhr?
|
69
|
+
render layout: false, partial: "queries_table", locals: {queries: @query_stats, xhr: true}
|
70
|
+
end
|
50
71
|
end
|
51
72
|
|
52
73
|
def system
|
@@ -120,6 +120,27 @@
|
|
120
120
|
margin-bottom: 0;
|
121
121
|
}
|
122
122
|
|
123
|
+
#slider-container {
|
124
|
+
padding: 6px 140px 20px 140px;
|
125
|
+
}
|
126
|
+
|
127
|
+
#slider {
|
128
|
+
margin-bottom: 20px;
|
129
|
+
}
|
130
|
+
|
131
|
+
#range-start {
|
132
|
+
min-height: 20px;
|
133
|
+
}
|
134
|
+
|
135
|
+
#range-end {
|
136
|
+
float: right;
|
137
|
+
}
|
138
|
+
|
139
|
+
.queries-info {
|
140
|
+
text-align: center;
|
141
|
+
margin-top: 40px;
|
142
|
+
}
|
143
|
+
|
123
144
|
/* nav */
|
124
145
|
|
125
146
|
.nav a {
|
@@ -137,6 +158,10 @@
|
|
137
158
|
background-color: #ddd;
|
138
159
|
}
|
139
160
|
|
161
|
+
.nav li.active-database a {
|
162
|
+
color: #999;
|
163
|
+
}
|
164
|
+
|
140
165
|
.nav-header {
|
141
166
|
font-weight: bold;
|
142
167
|
color: #333;
|
@@ -391,11 +416,11 @@
|
|
391
416
|
</ul>
|
392
417
|
|
393
418
|
<% if @databases.size > 1 %>
|
394
|
-
<p class="nav-header">
|
419
|
+
<p class="nav-header">Databases</p>
|
395
420
|
<ul class="nav">
|
396
|
-
<%
|
397
|
-
<li>
|
398
|
-
<%= link_to database.titleize,
|
421
|
+
<% @databases.each do |database| %>
|
422
|
+
<li class="<%= ("active-database" if PgHero.current_database == database) %>">
|
423
|
+
<%= link_to database.titleize, database: database %>
|
399
424
|
</li>
|
400
425
|
<% end %>
|
401
426
|
</ul>
|
@@ -1,12 +1,27 @@
|
|
1
1
|
<table class="table">
|
2
|
-
|
3
|
-
<
|
4
|
-
<
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
2
|
+
<% unless local_assigns[:xhr] %>
|
3
|
+
<thead>
|
4
|
+
<tr>
|
5
|
+
<th style="width: 33.33%;">Total Time</th>
|
6
|
+
<th style="width: 33.33%;">Average Time</th>
|
7
|
+
<th style="width: 33.33%;">Calls</th>
|
8
|
+
</tr>
|
9
|
+
</thead>
|
10
|
+
<% end %>
|
11
|
+
<tbody id="queries">
|
12
|
+
<% if queries.empty? %>
|
13
|
+
<tr>
|
14
|
+
<td colspan="3">
|
15
|
+
<p class="queries-info text-muted">
|
16
|
+
<% if local_assigns[:xhr] %>
|
17
|
+
No data available for this time.
|
18
|
+
<% else %>
|
19
|
+
...
|
20
|
+
<% end %>
|
21
|
+
</p>
|
22
|
+
</td>
|
23
|
+
</tr>
|
24
|
+
<% end %>
|
10
25
|
<% queries.each do |query| %>
|
11
26
|
<tr>
|
12
27
|
<td>
|
@@ -0,0 +1,129 @@
|
|
1
|
+
<div id="slider-container">
|
2
|
+
<div id="slider"></div>
|
3
|
+
<div id="range-end"></div>
|
4
|
+
<div id="range-start"></div>
|
5
|
+
</div>
|
6
|
+
|
7
|
+
<%= stylesheet_link_tag "https://cdnjs.cloudflare.com/ajax/libs/noUiSlider/6.2.0/jquery.nouislider.min.css" %>
|
8
|
+
<%= javascript_include_tag "//ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js", "https://cdnjs.cloudflare.com/ajax/libs/noUiSlider/6.2.0/jquery.nouislider.min.js" %>
|
9
|
+
|
10
|
+
<style>
|
11
|
+
.noUi-connect {
|
12
|
+
box-shadow: none;
|
13
|
+
background: #5bc0de;
|
14
|
+
}
|
15
|
+
|
16
|
+
.noUi-handle {
|
17
|
+
box-shadow: none;
|
18
|
+
}
|
19
|
+
|
20
|
+
.noUi-target {
|
21
|
+
box-shadow: none;
|
22
|
+
border-color: #eee;
|
23
|
+
}
|
24
|
+
|
25
|
+
.noUi-background {
|
26
|
+
box-shadow: none;
|
27
|
+
background-color: #eee;
|
28
|
+
}
|
29
|
+
|
30
|
+
.noUi-origin {
|
31
|
+
border-radius: 0;
|
32
|
+
}
|
33
|
+
</style>
|
34
|
+
|
35
|
+
<script>
|
36
|
+
function roundTime(time) {
|
37
|
+
var period = 1000 * 60 * 5;
|
38
|
+
return new Date(Math.ceil(time.getTime() / period) * period);
|
39
|
+
}
|
40
|
+
|
41
|
+
function pad(num) {
|
42
|
+
return (num < 10) ? "0" + num : num;
|
43
|
+
}
|
44
|
+
|
45
|
+
var months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
46
|
+
|
47
|
+
var days = 1;
|
48
|
+
var now = new Date();
|
49
|
+
var sliderStartAt = roundTime(now) - days * 24 * 60 * 60 * 1000;
|
50
|
+
var sliderMax = 24 * 12 * days;
|
51
|
+
|
52
|
+
var startAt = <%= @start_at.to_i %> * 1000;
|
53
|
+
var min = (startAt > 0) ? (startAt - sliderStartAt) / (1000 * 60 * 5) : 0;
|
54
|
+
|
55
|
+
var endAt = <%= @end_at.to_i %> * 1000;
|
56
|
+
var max = (endAt > 0) ? (endAt - sliderStartAt) / (1000 * 60 * 5) : sliderMax;
|
57
|
+
|
58
|
+
var $slider = $("#slider");
|
59
|
+
|
60
|
+
$slider.noUiSlider({
|
61
|
+
range: {
|
62
|
+
min: 0,
|
63
|
+
max: sliderMax
|
64
|
+
},
|
65
|
+
step: 1,
|
66
|
+
connect: true,
|
67
|
+
start: [min, max]
|
68
|
+
});
|
69
|
+
|
70
|
+
function updateText() {
|
71
|
+
var values = $slider.val();
|
72
|
+
setText("#range-start", values[0]);
|
73
|
+
setText("#range-end", values[1]);
|
74
|
+
}
|
75
|
+
|
76
|
+
function setText(selector, offset) {
|
77
|
+
var time = timeAt(offset);
|
78
|
+
|
79
|
+
var html = "";
|
80
|
+
if (time == now) {
|
81
|
+
if (selector == "#range-end") {
|
82
|
+
html = "Now";
|
83
|
+
}
|
84
|
+
} else {
|
85
|
+
html = months[time.getMonth()] + " " + time.getDate() + ", " + pad(time.getHours()) + ":" + pad(time.getMinutes());
|
86
|
+
}
|
87
|
+
$(selector).html(html);
|
88
|
+
}
|
89
|
+
|
90
|
+
function timeAt(offset) {
|
91
|
+
var time = new Date(sliderStartAt + (offset * 5) * 60 * 1000);
|
92
|
+
return (time > now) ? now : time;
|
93
|
+
}
|
94
|
+
|
95
|
+
function timeParam(time) {
|
96
|
+
return time.toISOString();
|
97
|
+
}
|
98
|
+
|
99
|
+
function refreshStats(push) {
|
100
|
+
var values = $slider.val();
|
101
|
+
var startAt = timeAt(values[0]);
|
102
|
+
var endAt = timeAt(values[1]);
|
103
|
+
|
104
|
+
var params = {}
|
105
|
+
if (startAt > sliderStartAt) {
|
106
|
+
params.start_at = timeParam(startAt);
|
107
|
+
}
|
108
|
+
if (endAt < now) {
|
109
|
+
params.end_at = timeParam(endAt);
|
110
|
+
}
|
111
|
+
|
112
|
+
var path = "queries";
|
113
|
+
if (params.start_at || params.end_at) {
|
114
|
+
path += "?" + $.param(params);
|
115
|
+
}
|
116
|
+
|
117
|
+
$("#queries").html('<tr><td colspan="3"><p class="queries-info text-muted">...</p></td></tr>').load(path);
|
118
|
+
|
119
|
+
if (push && history.pushState) {
|
120
|
+
history.pushState(null, null, path);
|
121
|
+
}
|
122
|
+
}
|
123
|
+
|
124
|
+
$slider.on("slide", updateText).on("change", function () {
|
125
|
+
refreshStats(true);
|
126
|
+
});
|
127
|
+
updateText();
|
128
|
+
$(refreshStats);
|
129
|
+
</script>
|
@@ -3,10 +3,16 @@
|
|
3
3
|
<%= button_to "Reset", reset_query_stats_path, class: "btn btn-danger", style: "float: right;" %>
|
4
4
|
<% end %>
|
5
5
|
|
6
|
-
<h1>Queries</h1>
|
6
|
+
<h1 style="float: left;">Queries</h1>
|
7
|
+
|
8
|
+
<% if @historical_query_stats_enabled %>
|
9
|
+
<%= render partial: "query_stats_slider" %>
|
10
|
+
<% end %>
|
7
11
|
|
8
12
|
<% if @query_stats_enabled %>
|
9
|
-
<% if @
|
13
|
+
<% if @error %>
|
14
|
+
<div class="alert alert-danger">Cannot understand start or end time.</div>
|
15
|
+
<% elsif @query_stats.any? || @historical_query_stats_enabled %>
|
10
16
|
<%= render partial: "queries_table", locals: {queries: @query_stats} %>
|
11
17
|
<% else %>
|
12
18
|
<p>Stats are not available yet. Come back soon!</p>
|
data/guides/Rails.md
CHANGED
@@ -118,6 +118,35 @@ end
|
|
118
118
|
|
119
119
|
Query stats can be enabled from the dashboard. If you run into issues, [view the guide](Query-Stats.md).
|
120
120
|
|
121
|
+
## Historical Query Stats
|
122
|
+
|
123
|
+
To track query stats over time, run:
|
124
|
+
|
125
|
+
```sh
|
126
|
+
rails generate pghero:query_stats
|
127
|
+
rake db:migrate
|
128
|
+
```
|
129
|
+
|
130
|
+
And schedule the task below to run every 5 minutes.
|
131
|
+
|
132
|
+
```sh
|
133
|
+
rake pghero:capture_query_stats
|
134
|
+
```
|
135
|
+
|
136
|
+
Or with a scheduler like Clockwork, use:
|
137
|
+
|
138
|
+
```ruby
|
139
|
+
PgHero.capture_query_stats
|
140
|
+
```
|
141
|
+
|
142
|
+
After this, a time range slider will appear on the Queries tab.
|
143
|
+
|
144
|
+
By default, historical query stats are stored in your primary database. Change this with:
|
145
|
+
|
146
|
+
```ruby
|
147
|
+
ENV["PGHERO_STATS_DATABASE_URL"]
|
148
|
+
```
|
149
|
+
|
121
150
|
## System Stats
|
122
151
|
|
123
152
|
CPU usage is available for Amazon RDS. Add these lines to your application’s Gemfile:
|
@@ -154,6 +183,12 @@ production:
|
|
154
183
|
<<: *default
|
155
184
|
```
|
156
185
|
|
186
|
+
Specify a database with:
|
187
|
+
|
188
|
+
```ruby
|
189
|
+
PgHero.with(:replica) { PgHero.running_queries }
|
190
|
+
```
|
191
|
+
|
157
192
|
## Customize
|
158
193
|
|
159
194
|
Minimum time for long running queries
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# taken from https://github.com/collectiveidea/audited/blob/master/lib/generators/audited/install_generator.rb
|
2
|
+
require "rails/generators"
|
3
|
+
require "rails/generators/migration"
|
4
|
+
require "active_record"
|
5
|
+
require "rails/generators/active_record"
|
6
|
+
|
7
|
+
module Pghero
|
8
|
+
module Generators
|
9
|
+
class QueryStatsGenerator < Rails::Generators::Base
|
10
|
+
include Rails::Generators::Migration
|
11
|
+
|
12
|
+
source_root File.expand_path("../templates", __FILE__)
|
13
|
+
|
14
|
+
# Implement the required interface for Rails::Generators::Migration.
|
15
|
+
def self.next_migration_number(dirname) #:nodoc:
|
16
|
+
next_migration_number = current_migration_number(dirname) + 1
|
17
|
+
if ::ActiveRecord::Base.timestamped_migrations
|
18
|
+
[Time.now.utc.strftime("%Y%m%d%H%M%S"), "%.14d" % next_migration_number].max
|
19
|
+
else
|
20
|
+
"%.3d" % next_migration_number
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def copy_migration
|
25
|
+
migration_template "query_stats.rb", "db/migrate/create_pghero_query_stats.rb"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
class <%= migration_class_name %> < ActiveRecord::Migration
|
2
|
+
def change
|
3
|
+
create_table :pghero_query_stats do |t|
|
4
|
+
t.text :database
|
5
|
+
t.text :query
|
6
|
+
t.float :total_time
|
7
|
+
t.integer :calls, limit: 8
|
8
|
+
t.timestamp :captured_at
|
9
|
+
end
|
10
|
+
|
11
|
+
add_index :pghero_query_stats, [:database, :captured_at]
|
12
|
+
end
|
13
|
+
end
|
data/lib/pghero.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
require "pghero/version"
|
2
2
|
require "active_record"
|
3
3
|
require "pghero/engine" if defined?(Rails)
|
4
|
+
require "pghero/tasks"
|
4
5
|
|
5
6
|
module PgHero
|
6
7
|
# hack for connection
|
@@ -8,6 +9,12 @@ module PgHero
|
|
8
9
|
self.abstract_class = true
|
9
10
|
end
|
10
11
|
|
12
|
+
class QueryStats < ActiveRecord::Base
|
13
|
+
self.abstract_class = true
|
14
|
+
self.table_name = "pghero_query_stats"
|
15
|
+
establish_connection ENV["PGHERO_STATS_DATABASE_URL"] if ENV["PGHERO_STATS_DATABASE_URL"]
|
16
|
+
end
|
17
|
+
|
11
18
|
class << self
|
12
19
|
attr_accessor :long_running_query_sec, :slow_query_ms, :slow_query_calls, :total_connections_threshold, :env
|
13
20
|
end
|
@@ -33,7 +40,7 @@ module PgHero
|
|
33
40
|
{
|
34
41
|
"databases" => {
|
35
42
|
"primary" => {
|
36
|
-
"url" => ENV["PGHERO_DATABASE_URL"]
|
43
|
+
"url" => ENV["PGHERO_DATABASE_URL"] || ActiveRecord::Base.connection_config,
|
37
44
|
"db_instance_identifier" => ENV["PGHERO_DB_INSTANCE_IDENTIFIER"]
|
38
45
|
}
|
39
46
|
}
|
@@ -301,74 +308,28 @@ module PgHero
|
|
301
308
|
true
|
302
309
|
end
|
303
310
|
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
)
|
321
|
-
SELECT
|
322
|
-
query,
|
323
|
-
total_minutes,
|
324
|
-
average_time,
|
325
|
-
calls,
|
326
|
-
total_minutes * 100.0 / (SELECT SUM(total_minutes) FROM query_stats) AS total_percent
|
327
|
-
FROM
|
328
|
-
query_stats
|
329
|
-
ORDER BY
|
330
|
-
total_minutes DESC
|
331
|
-
LIMIT 100
|
332
|
-
SQL
|
333
|
-
else
|
334
|
-
[]
|
311
|
+
def query_stats(options = {})
|
312
|
+
current_query_stats = (options[:historical] && options[:end_at] && options[:end_at] < Time.now ? [] : current_query_stats(options)).index_by { |q| q["query"] }
|
313
|
+
historical_query_stats = (options[:historical] ? historical_query_stats(options) : []).index_by { |q| q["query"] }
|
314
|
+
current_query_stats.default = {}
|
315
|
+
historical_query_stats.default = {}
|
316
|
+
|
317
|
+
query_stats = []
|
318
|
+
(current_query_stats.keys + historical_query_stats.keys).uniq.each do |query|
|
319
|
+
value = {
|
320
|
+
"query" => query,
|
321
|
+
"total_minutes" => current_query_stats[query]["total_minutes"].to_f + historical_query_stats[query]["total_minutes"].to_f,
|
322
|
+
"calls" => current_query_stats[query]["calls"].to_i + historical_query_stats[query]["calls"].to_i
|
323
|
+
}
|
324
|
+
value["average_time"] = value["total_minutes"] * 1000 * 60 / value["calls"]
|
325
|
+
value["total_percent"] = value["total_minutes"] * 100.0 / (current_query_stats[query]["all_queries_total_minutes"].to_f + historical_query_stats[query]["all_queries_total_minutes"].to_f)
|
326
|
+
query_stats << value
|
335
327
|
end
|
328
|
+
query_stats.sort_by { |q| -q["total_minutes"] }.first(100)
|
336
329
|
end
|
337
330
|
|
338
|
-
def slow_queries
|
339
|
-
|
340
|
-
select_all <<-SQL
|
341
|
-
WITH query_stats AS (
|
342
|
-
SELECT
|
343
|
-
query,
|
344
|
-
(total_time / 1000 / 60) as total_minutes,
|
345
|
-
(total_time / calls) as average_time,
|
346
|
-
calls
|
347
|
-
FROM
|
348
|
-
pg_stat_statements
|
349
|
-
INNER JOIN
|
350
|
-
pg_database ON pg_database.oid = pg_stat_statements.dbid
|
351
|
-
WHERE
|
352
|
-
pg_database.datname = current_database()
|
353
|
-
)
|
354
|
-
SELECT
|
355
|
-
query,
|
356
|
-
total_minutes,
|
357
|
-
average_time,
|
358
|
-
calls,
|
359
|
-
total_minutes * 100.0 / (SELECT SUM(total_minutes) FROM query_stats) AS total_percent
|
360
|
-
FROM
|
361
|
-
query_stats
|
362
|
-
WHERE
|
363
|
-
calls >= #{slow_query_calls.to_i}
|
364
|
-
AND average_time >= #{slow_query_ms.to_i}
|
365
|
-
ORDER BY
|
366
|
-
total_minutes DESC
|
367
|
-
LIMIT 100
|
368
|
-
SQL
|
369
|
-
else
|
370
|
-
[]
|
371
|
-
end
|
331
|
+
def slow_queries(options = {})
|
332
|
+
query_stats(options).select { |q| q["calls"].to_i >= slow_query_calls.to_i && q["average_time"].to_i >= slow_query_ms.to_i }
|
372
333
|
end
|
373
334
|
|
374
335
|
def query_stats_available?
|
@@ -403,6 +364,53 @@ module PgHero
|
|
403
364
|
end
|
404
365
|
end
|
405
366
|
|
367
|
+
def capture_query_stats
|
368
|
+
config["databases"].keys.each do |database|
|
369
|
+
with(database) do
|
370
|
+
now = Time.now
|
371
|
+
query_stats = self.query_stats(limit: 1000000)
|
372
|
+
if query_stats.any? && reset_query_stats
|
373
|
+
values =
|
374
|
+
query_stats.map do |qs|
|
375
|
+
[
|
376
|
+
database,
|
377
|
+
qs["query"],
|
378
|
+
qs["total_minutes"].to_f * 60 * 1000,
|
379
|
+
qs["calls"],
|
380
|
+
now
|
381
|
+
].map { |v| quote(v) }.join(",")
|
382
|
+
end.map { |v| "(#{v})" }.join(",")
|
383
|
+
|
384
|
+
stats_connection.execute("INSERT INTO pghero_query_stats (database, query, total_time, calls, captured_at) VALUES #{values}")
|
385
|
+
end
|
386
|
+
end
|
387
|
+
end
|
388
|
+
end
|
389
|
+
|
390
|
+
# http://stackoverflow.com/questions/20582500/how-to-check-if-a-table-exists-in-a-given-schema
|
391
|
+
def historical_query_stats_enabled?
|
392
|
+
# TODO use schema from config
|
393
|
+
stats_connection.select_all( squish <<-SQL
|
394
|
+
SELECT EXISTS (
|
395
|
+
SELECT
|
396
|
+
1
|
397
|
+
FROM
|
398
|
+
pg_catalog.pg_class c
|
399
|
+
INNER JOIN
|
400
|
+
pg_catalog.pg_namespace n ON n.oid = c.relnamespace
|
401
|
+
WHERE
|
402
|
+
n.nspname = 'public'
|
403
|
+
AND c.relname = 'pghero_query_stats'
|
404
|
+
AND c.relkind = 'r'
|
405
|
+
)
|
406
|
+
SQL
|
407
|
+
).to_a.first["exists"] == "t"
|
408
|
+
end
|
409
|
+
|
410
|
+
def stats_connection
|
411
|
+
QueryStats.connection
|
412
|
+
end
|
413
|
+
|
406
414
|
def ssl_used?
|
407
415
|
ssl_used = nil
|
408
416
|
Connection.transaction do
|
@@ -464,7 +472,7 @@ module PgHero
|
|
464
472
|
|
465
473
|
commands =
|
466
474
|
[
|
467
|
-
"CREATE ROLE #{user} LOGIN PASSWORD #{
|
475
|
+
"CREATE ROLE #{user} LOGIN PASSWORD #{quote(password)}",
|
468
476
|
"GRANT CONNECT ON DATABASE #{database} TO #{user}",
|
469
477
|
"GRANT USAGE ON SCHEMA #{schema} TO #{user}"
|
470
478
|
]
|
@@ -571,6 +579,80 @@ module PgHero
|
|
571
579
|
select_all("SELECT EXTRACT(EPOCH FROM NOW() - pg_last_xact_replay_timestamp()) AS replication_lag").first["replication_lag"].to_f
|
572
580
|
end
|
573
581
|
|
582
|
+
private
|
583
|
+
|
584
|
+
# http://www.craigkerstiens.com/2013/01/10/more-on-postgres-performance/
|
585
|
+
def current_query_stats(options = {})
|
586
|
+
if query_stats_enabled?
|
587
|
+
limit = options[:limit] || 100
|
588
|
+
select_all <<-SQL
|
589
|
+
WITH query_stats AS (
|
590
|
+
SELECT
|
591
|
+
query,
|
592
|
+
(total_time / 1000 / 60) as total_minutes,
|
593
|
+
(total_time / calls) as average_time,
|
594
|
+
calls
|
595
|
+
FROM
|
596
|
+
pg_stat_statements
|
597
|
+
INNER JOIN
|
598
|
+
pg_database ON pg_database.oid = pg_stat_statements.dbid
|
599
|
+
WHERE
|
600
|
+
pg_database.datname = current_database()
|
601
|
+
)
|
602
|
+
SELECT
|
603
|
+
query,
|
604
|
+
total_minutes,
|
605
|
+
average_time,
|
606
|
+
calls,
|
607
|
+
total_minutes * 100.0 / (SELECT SUM(total_minutes) FROM query_stats) AS total_percent,
|
608
|
+
(SELECT SUM(total_minutes) FROM query_stats) AS all_queries_total_minutes
|
609
|
+
FROM
|
610
|
+
query_stats
|
611
|
+
ORDER BY
|
612
|
+
total_minutes DESC
|
613
|
+
LIMIT #{limit.to_i}
|
614
|
+
SQL
|
615
|
+
else
|
616
|
+
[]
|
617
|
+
end
|
618
|
+
end
|
619
|
+
|
620
|
+
def historical_query_stats(options = {})
|
621
|
+
if historical_query_stats_enabled?
|
622
|
+
stats_connection.select_all squish <<-SQL
|
623
|
+
WITH query_stats AS (
|
624
|
+
SELECT
|
625
|
+
query,
|
626
|
+
(SUM(total_time) / 1000 / 60) as total_minutes,
|
627
|
+
(SUM(total_time) / SUM(calls)) as average_time,
|
628
|
+
SUM(calls) as calls
|
629
|
+
FROM
|
630
|
+
pghero_query_stats
|
631
|
+
WHERE
|
632
|
+
database = #{quote(current_database)}
|
633
|
+
#{options[:start_at] ? "AND captured_at >= #{quote(options[:start_at])}" : ""}
|
634
|
+
#{options[:end_at] ? "AND captured_at <= #{quote(options[:end_at])}" : ""}
|
635
|
+
GROUP BY
|
636
|
+
query
|
637
|
+
)
|
638
|
+
SELECT
|
639
|
+
query,
|
640
|
+
total_minutes,
|
641
|
+
average_time,
|
642
|
+
calls,
|
643
|
+
total_minutes * 100.0 / (SELECT SUM(total_minutes) FROM query_stats) AS total_percent,
|
644
|
+
(SELECT SUM(total_minutes) FROM query_stats) AS all_queries_total_minutes
|
645
|
+
FROM
|
646
|
+
query_stats
|
647
|
+
ORDER BY
|
648
|
+
total_minutes DESC
|
649
|
+
LIMIT 100
|
650
|
+
SQL
|
651
|
+
else
|
652
|
+
[]
|
653
|
+
end
|
654
|
+
end
|
655
|
+
|
574
656
|
def friendly_value(setting, unit)
|
575
657
|
if %w(kB 8kB).include?(unit)
|
576
658
|
value = setting.to_i
|
@@ -609,5 +691,9 @@ module PgHero
|
|
609
691
|
def squish(str)
|
610
692
|
str.to_s.gsub(/\A[[:space:]]+/, "").gsub(/[[:space:]]+\z/, "").gsub(/[[:space:]]+/, " ")
|
611
693
|
end
|
694
|
+
|
695
|
+
def quote(value)
|
696
|
+
connection.quote(value)
|
697
|
+
end
|
612
698
|
end
|
613
699
|
end
|
data/lib/pghero/tasks.rb
ADDED
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: 1.0
|
4
|
+
version: 1.1.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: 2015-
|
11
|
+
date: 2015-06-26 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -98,6 +98,7 @@ files:
|
|
98
98
|
- app/views/pg_hero/home/_connections_table.html.erb
|
99
99
|
- app/views/pg_hero/home/_live_queries_table.html.erb
|
100
100
|
- app/views/pg_hero/home/_queries_table.html.erb
|
101
|
+
- app/views/pg_hero/home/_query_stats_slider.html.erb
|
101
102
|
- app/views/pg_hero/home/connections.html.erb
|
102
103
|
- app/views/pg_hero/home/explain.html.erb
|
103
104
|
- app/views/pg_hero/home/index.html.erb
|
@@ -116,8 +117,11 @@ files:
|
|
116
117
|
- guides/Linux.md
|
117
118
|
- guides/Query-Stats.md
|
118
119
|
- guides/Rails.md
|
120
|
+
- lib/generators/pghero/query_stats_generator.rb
|
121
|
+
- lib/generators/pghero/templates/query_stats.rb
|
119
122
|
- lib/pghero.rb
|
120
123
|
- lib/pghero/engine.rb
|
124
|
+
- lib/pghero/tasks.rb
|
121
125
|
- lib/pghero/version.rb
|
122
126
|
- pghero.gemspec
|
123
127
|
- test/pghero_test.rb
|