pg_peek 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +217 -0
- data/Rakefile +16 -0
- data/app/assets/javascripts/pg_peek/application.js +147 -0
- data/app/assets/stylesheets/pg_peek/application.css +362 -0
- data/app/controllers/pg_peek/activity_controller.rb +16 -0
- data/app/controllers/pg_peek/application_controller.rb +36 -0
- data/app/controllers/pg_peek/databases_controller.rb +96 -0
- data/app/controllers/pg_peek/endpoints_controller.rb +10 -0
- data/app/controllers/pg_peek/indexes_controller.rb +7 -0
- data/app/controllers/pg_peek/jobs_controller.rb +44 -0
- data/app/controllers/pg_peek/pg_stat_statements_controller.rb +11 -0
- data/app/controllers/pg_peek/queries_controller.rb +13 -0
- data/app/controllers/pg_peek/tables_controller.rb +6 -0
- data/app/helpers/pg_peek/application_helper.rb +179 -0
- data/app/helpers/pg_peek/report_helper.rb +58 -0
- data/app/jobs/pg_peek/application_job.rb +4 -0
- data/app/mailers/pg_peek/application_mailer.rb +6 -0
- data/app/models/pg_peek/application_record.rb +3 -0
- data/app/models/pg_peek/database.rb +131 -0
- data/app/models/pg_peek/pg_stat_statements.rb +132 -0
- data/app/models/pg_peek/sql_comment.rb +25 -0
- data/app/models/pg_peek/table.rb +266 -0
- data/app/queries/pg_peek/pg14/activity/sessions.sql +26 -0
- data/app/queries/pg_peek/pg14/activity/summary.sql +8 -0
- data/app/queries/pg_peek/pg14/activity/waiting_locks.sql +11 -0
- data/app/queries/pg_peek/pg14/database/vitals.sql +11 -0
- data/app/queries/pg_peek/pg14/indexes/all.sql +17 -0
- data/app/queries/pg_peek/pg14/pg_stat_statements/endpoints.sql +43 -0
- data/app/queries/pg_peek/pg14/pg_stat_statements/jobs.sql +12 -0
- data/app/queries/pg_peek/pg14/pg_stat_statements/outliers.sql +13 -0
- data/app/queries/pg_peek/pg14/pg_stat_statements/outliers_by_job.sql +14 -0
- data/app/queries/pg_peek/pg14/tables/dead_tuples.sql +11 -0
- data/app/queries/pg_peek/pg14/tables/inline_stats.sql +9 -0
- data/app/queries/pg_peek/pg14/tables/stats.sql +32 -0
- data/app/queries/pg_peek/pg15/activity/sessions.sql +26 -0
- data/app/queries/pg_peek/pg15/activity/summary.sql +8 -0
- data/app/queries/pg_peek/pg15/activity/waiting_locks.sql +11 -0
- data/app/queries/pg_peek/pg15/database/vitals.sql +11 -0
- data/app/queries/pg_peek/pg15/indexes/all.sql +17 -0
- data/app/queries/pg_peek/pg15/pg_stat_statements/endpoints.sql +43 -0
- data/app/queries/pg_peek/pg15/pg_stat_statements/jobs.sql +12 -0
- data/app/queries/pg_peek/pg15/pg_stat_statements/outliers.sql +13 -0
- data/app/queries/pg_peek/pg15/pg_stat_statements/outliers_by_job.sql +14 -0
- data/app/queries/pg_peek/pg15/tables/dead_tuples.sql +11 -0
- data/app/queries/pg_peek/pg15/tables/inline_stats.sql +9 -0
- data/app/queries/pg_peek/pg15/tables/stats.sql +32 -0
- data/app/queries/pg_peek/pg16/activity/sessions.sql +26 -0
- data/app/queries/pg_peek/pg16/activity/summary.sql +8 -0
- data/app/queries/pg_peek/pg16/activity/waiting_locks.sql +11 -0
- data/app/queries/pg_peek/pg16/database/vitals.sql +11 -0
- data/app/queries/pg_peek/pg16/indexes/all.sql +17 -0
- data/app/queries/pg_peek/pg16/pg_stat_statements/endpoints.sql +43 -0
- data/app/queries/pg_peek/pg16/pg_stat_statements/jobs.sql +12 -0
- data/app/queries/pg_peek/pg16/pg_stat_statements/outliers.sql +13 -0
- data/app/queries/pg_peek/pg16/pg_stat_statements/outliers_by_job.sql +14 -0
- data/app/queries/pg_peek/pg16/tables/dead_tuples.sql +11 -0
- data/app/queries/pg_peek/pg16/tables/inline_stats.sql +9 -0
- data/app/queries/pg_peek/pg16/tables/stats.sql +32 -0
- data/app/queries/pg_peek/pg17/activity/sessions.sql +26 -0
- data/app/queries/pg_peek/pg17/activity/summary.sql +8 -0
- data/app/queries/pg_peek/pg17/activity/waiting_locks.sql +11 -0
- data/app/queries/pg_peek/pg17/database/vitals.sql +11 -0
- data/app/queries/pg_peek/pg17/indexes/all.sql +17 -0
- data/app/queries/pg_peek/pg17/pg_stat_statements/endpoints.sql +43 -0
- data/app/queries/pg_peek/pg17/pg_stat_statements/jobs.sql +12 -0
- data/app/queries/pg_peek/pg17/pg_stat_statements/outliers.sql +13 -0
- data/app/queries/pg_peek/pg17/pg_stat_statements/outliers_by_job.sql +14 -0
- data/app/queries/pg_peek/pg17/tables/dead_tuples.sql +11 -0
- data/app/queries/pg_peek/pg17/tables/inline_stats.sql +9 -0
- data/app/queries/pg_peek/pg17/tables/stats.sql +32 -0
- data/app/queries/pg_peek/pg18/activity/sessions.sql +26 -0
- data/app/queries/pg_peek/pg18/activity/summary.sql +8 -0
- data/app/queries/pg_peek/pg18/activity/waiting_locks.sql +11 -0
- data/app/queries/pg_peek/pg18/database/vitals.sql +11 -0
- data/app/queries/pg_peek/pg18/indexes/all.sql +17 -0
- data/app/queries/pg_peek/pg18/pg_stat_statements/endpoints.sql +43 -0
- data/app/queries/pg_peek/pg18/pg_stat_statements/jobs.sql +12 -0
- data/app/queries/pg_peek/pg18/pg_stat_statements/outliers.sql +13 -0
- data/app/queries/pg_peek/pg18/pg_stat_statements/outliers_by_job.sql +14 -0
- data/app/queries/pg_peek/pg18/tables/dead_tuples.sql +11 -0
- data/app/queries/pg_peek/pg18/tables/inline_stats.sql +9 -0
- data/app/queries/pg_peek/pg18/tables/stats.sql +32 -0
- data/app/reports/pg_peek/column.rb +27 -0
- data/app/reports/pg_peek/report.rb +63 -0
- data/app/reports/pg_peek/reports/blocked.rb +60 -0
- data/app/reports/pg_peek/reports/databases.rb +47 -0
- data/app/reports/pg_peek/reports/endpoints.rb +35 -0
- data/app/reports/pg_peek/reports/indexes.rb +44 -0
- data/app/reports/pg_peek/reports/job_queries.rb +14 -0
- data/app/reports/pg_peek/reports/jobs.rb +21 -0
- data/app/reports/pg_peek/reports/queries.rb +23 -0
- data/app/reports/pg_peek/reports/sessions.rb +88 -0
- data/app/reports/pg_peek/reports/tables.rb +25 -0
- data/app/views/layouts/pg_peek/application.html.erb +56 -0
- data/app/views/pg_peek/activity/show.html.erb +70 -0
- data/app/views/pg_peek/authentication_required.html.erb +31 -0
- data/app/views/pg_peek/databases/index.html.erb +43 -0
- data/app/views/pg_peek/databases/show.html.erb +169 -0
- data/app/views/pg_peek/endpoints/index.html.erb +24 -0
- data/app/views/pg_peek/indexes/index.html.erb +31 -0
- data/app/views/pg_peek/jobs/index.html.erb +51 -0
- data/app/views/pg_peek/jobs/show.html.erb +17 -0
- data/app/views/pg_peek/pg_stat_statements/_preload_instructions.html.erb +17 -0
- data/app/views/pg_peek/pg_stat_statements/not_preloaded.html.erb +5 -0
- data/app/views/pg_peek/queries/index.html.erb +10 -0
- data/app/views/pg_peek/reports/_report.html.erb +32 -0
- data/app/views/pg_peek/tables/show.html.erb +128 -0
- data/config/routes.rb +16 -0
- data/lib/generators/pg_peek/install/install_generator.rb +40 -0
- data/lib/generators/pg_peek/install/templates/README +31 -0
- data/lib/generators/pg_peek/install/templates/initializer.rb +38 -0
- data/lib/pg_peek/configuration.rb +59 -0
- data/lib/pg_peek/engine.rb +7 -0
- data/lib/pg_peek/query_loader.rb +59 -0
- data/lib/pg_peek/version.rb +3 -0
- data/lib/pg_peek.rb +10 -0
- data/lib/tasks/pg_peek_tasks.rake +4 -0
- metadata +192 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
<h1>Databases</h1>
|
|
2
|
+
|
|
3
|
+
<%# An empty list means one of two quite different things, and the engine knows
|
|
4
|
+
which: PostgreSQL databases exist but none is mapped to a connection class,
|
|
5
|
+
or there are no PostgreSQL databases to show at all. %>
|
|
6
|
+
<% if @configured_databases.empty? && @databases.any? %>
|
|
7
|
+
<article aria-label="No database connections configured">
|
|
8
|
+
<header>pg_peek has no database connections configured</header>
|
|
9
|
+
<p>
|
|
10
|
+
Found <%= pluralize(@databases.size, "PostgreSQL database") %> in
|
|
11
|
+
<code>config/database.yml</code>
|
|
12
|
+
(<%= safe_join(@databases.map { |database| content_tag(:code, database.name) }, ", ") %>),
|
|
13
|
+
but <code>config.pg_peek.connections</code> is empty, so none of them can be opened.
|
|
14
|
+
</p>
|
|
15
|
+
<p>Generate the initializer:</p>
|
|
16
|
+
<pre><code>bin/rails generate pg_peek:install</code></pre>
|
|
17
|
+
<p>Or add <code>config/initializers/pg_peek.rb</code> yourself:</p>
|
|
18
|
+
<pre><code>Rails.application.config.pg_peek.tap do |config|
|
|
19
|
+
config.connections = {
|
|
20
|
+
<%= @databases.map { |database|
|
|
21
|
+
klass = database.primary ? "ApplicationRecord" : "#{database.name.camelize}Record"
|
|
22
|
+
%( "#{database.name}" => "#{klass}")
|
|
23
|
+
}.join(",\n") %>
|
|
24
|
+
}
|
|
25
|
+
end</code></pre>
|
|
26
|
+
</article>
|
|
27
|
+
<% elsif @databases.empty? %>
|
|
28
|
+
<article aria-label="No PostgreSQL databases">
|
|
29
|
+
<header>No PostgreSQL databases found</header>
|
|
30
|
+
<p>
|
|
31
|
+
No entry in <code>config/database.yml</code> for the
|
|
32
|
+
<code><%= Rails.env %></code> environment uses the
|
|
33
|
+
<code>postgresql</code> adapter, so there is nothing for pg_peek to inspect.
|
|
34
|
+
</p>
|
|
35
|
+
</article>
|
|
36
|
+
<% end %>
|
|
37
|
+
|
|
38
|
+
<% if @configured_databases.any? %>
|
|
39
|
+
<%= render "pg_peek/reports/report",
|
|
40
|
+
report: @report,
|
|
41
|
+
link_column: :name,
|
|
42
|
+
link: ->(row) { database_path(row["database"]) } %>
|
|
43
|
+
<% end %>
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
<h1><%= @database.name %> <small class="dim">overview</small></h1>
|
|
2
|
+
|
|
3
|
+
<p class="vitals dim">
|
|
4
|
+
<span>postgres <%= @database.version %></span>
|
|
5
|
+
<% if @pg_stat_statements.usable? %>
|
|
6
|
+
<span>pg_stat_statements <%= @pg_stat_statements.installed_version %></span>
|
|
7
|
+
<span><%= stats_since_link(@database, @stats_reset_at) %></span>
|
|
8
|
+
<span><%= pluralize(number_with_delimiter(@pg_stat_statements.statement_count), "statement") %></span>
|
|
9
|
+
<% end %>
|
|
10
|
+
<% if @vitals["cache_hit_ratio"] %><span>cache hit <%= @vitals["cache_hit_ratio"] %>%</span><% end %>
|
|
11
|
+
<% if @vitals["connections"] %><span><%= pluralize(@vitals["connections"].to_i, "connection") %></span><% end %>
|
|
12
|
+
</p>
|
|
13
|
+
|
|
14
|
+
<% if @pg_stat_statements.usable? %>
|
|
15
|
+
<% elsif @pg_stat_statements.installed? %>
|
|
16
|
+
<%= render "pg_peek/pg_stat_statements/preload_instructions",
|
|
17
|
+
database: @database,
|
|
18
|
+
pg_stat_statements: @pg_stat_statements %>
|
|
19
|
+
<% elsif @pg_stat_statements.available? %>
|
|
20
|
+
<article aria-label="Extension pg_stat_statements is not enabled">
|
|
21
|
+
<header>pg_stat_statements extension is not enabled</header>
|
|
22
|
+
<p>Create it with a migration, so it is tracked in your schema:</p>
|
|
23
|
+
<% if @database.primary %>
|
|
24
|
+
<pre><code>bin/rails generate migration EnablePgStatStatements</code></pre>
|
|
25
|
+
<% else %>
|
|
26
|
+
<pre><code>bin/rails generate migration EnablePgStatStatements --database <%= @database.name %></code></pre>
|
|
27
|
+
<% end %>
|
|
28
|
+
<pre><code>def change
|
|
29
|
+
enable_extension "pg_stat_statements"
|
|
30
|
+
end</code></pre>
|
|
31
|
+
<p>Then <code>bin/rails db:migrate</code>.</p>
|
|
32
|
+
</article>
|
|
33
|
+
<% else %>
|
|
34
|
+
<article aria-label="Extension pg_stat_statements is not available">
|
|
35
|
+
<header>pg_stat_statements extension is not available</header>
|
|
36
|
+
<p>The pg_stat_statements extension is not available on this PostgreSQL server.</p>
|
|
37
|
+
<p>To make it available, add to <code>postgresql.conf</code>:</p>
|
|
38
|
+
<pre><code>shared_preload_libraries = 'pg_stat_statements'</code></pre>
|
|
39
|
+
<footer>
|
|
40
|
+
<small>A server restart is required after this change.</small>
|
|
41
|
+
</footer>
|
|
42
|
+
</article>
|
|
43
|
+
<% end %>
|
|
44
|
+
|
|
45
|
+
<div class="panels">
|
|
46
|
+
<section class="wide">
|
|
47
|
+
<h2>attention</h2>
|
|
48
|
+
<ul class="attention">
|
|
49
|
+
<% if @attention.empty? %>
|
|
50
|
+
<li class="ok">nothing needs attention</li>
|
|
51
|
+
<% else %>
|
|
52
|
+
<% @attention.each do |item| %>
|
|
53
|
+
<li><%= item[:href] ? link_to(item[:text], item[:href]) : item[:text] %></li>
|
|
54
|
+
<% end %>
|
|
55
|
+
<% end %>
|
|
56
|
+
</ul>
|
|
57
|
+
</section>
|
|
58
|
+
|
|
59
|
+
<% if @pg_stat_statements.usable? %>
|
|
60
|
+
<section>
|
|
61
|
+
<h2>endpoints <%= link_to "all →", database_endpoints_path(@database) %></h2>
|
|
62
|
+
<% if @endpoints.empty? %>
|
|
63
|
+
<p class="dim">No endpoint has run a query yet.</p>
|
|
64
|
+
<% else %>
|
|
65
|
+
<%= render "pg_peek/reports/report", report: @endpoints,
|
|
66
|
+
columns: %i[endpoint share total_exec_time_ms max_queries_per_request],
|
|
67
|
+
limit: PgPeek::DatabasesController::OVERVIEW_ROWS %>
|
|
68
|
+
<% end %>
|
|
69
|
+
</section>
|
|
70
|
+
|
|
71
|
+
<section>
|
|
72
|
+
<h2>jobs <%= link_to "all →", database_jobs_path(@database) %></h2>
|
|
73
|
+
<% if @jobs.empty? %>
|
|
74
|
+
<p class="dim">No job has run a query yet.</p>
|
|
75
|
+
<% else %>
|
|
76
|
+
<%= render "pg_peek/reports/report", report: @jobs,
|
|
77
|
+
columns: %i[job_class share total_exec_time_ms],
|
|
78
|
+
limit: PgPeek::DatabasesController::OVERVIEW_ROWS %>
|
|
79
|
+
<% end %>
|
|
80
|
+
</section>
|
|
81
|
+
|
|
82
|
+
<section class="wide">
|
|
83
|
+
<h2>slowest queries <%= link_to "all →", database_queries_path(@database) %></h2>
|
|
84
|
+
<% if @queries.empty? %>
|
|
85
|
+
<p class="dim">No statements recorded yet.</p>
|
|
86
|
+
<% else %>
|
|
87
|
+
<%= render "pg_peek/reports/report", report: @queries, limit: PgPeek::DatabasesController::OVERVIEW_ROWS %>
|
|
88
|
+
<% end %>
|
|
89
|
+
</section>
|
|
90
|
+
<% end %>
|
|
91
|
+
|
|
92
|
+
<section class="wide">
|
|
93
|
+
<h2>activity <%= link_to "now →", database_activity_path(@database) %></h2>
|
|
94
|
+
<p class="vitals dim">
|
|
95
|
+
<span><%= @connection_summary["client_connections"] %> of <%= @connection_summary["max_connections"] %> server connections in use</span>
|
|
96
|
+
<span>this database: <%= @sessions.active.size %> active</span>
|
|
97
|
+
<span><%= @sessions.in_transaction.size %> idle in transaction</span>
|
|
98
|
+
<span><%= @sessions.idle.size %> idle</span>
|
|
99
|
+
<% if @sessions.blocked.any? %><span><%= @sessions.blocked.size %> blocked</span><% end %>
|
|
100
|
+
</p>
|
|
101
|
+
<% if @sessions.busy.any? %>
|
|
102
|
+
<%= render "pg_peek/reports/report", report: @sessions, rows: @sessions.busy,
|
|
103
|
+
limit: PgPeek::DatabasesController::OVERVIEW_ROWS %>
|
|
104
|
+
<% end %>
|
|
105
|
+
</section>
|
|
106
|
+
|
|
107
|
+
<section class="wide" id="tables" data-filter>
|
|
108
|
+
<h2>tables (<%= @tables.rows.size %>)</h2>
|
|
109
|
+
<input type="search"
|
|
110
|
+
placeholder="filter tables… (press / to focus)"
|
|
111
|
+
data-filter-input
|
|
112
|
+
data-filter-noun="table"
|
|
113
|
+
autocomplete="off">
|
|
114
|
+
<p class="dim" data-filter-count></p>
|
|
115
|
+
|
|
116
|
+
<%= render "pg_peek/reports/report",
|
|
117
|
+
report: @tables,
|
|
118
|
+
link_column: :name,
|
|
119
|
+
link: ->(row) { database_table_path(@database, row["name"]) } %>
|
|
120
|
+
|
|
121
|
+
<p class="dim" data-filter-empty hidden>No tables match that filter.</p>
|
|
122
|
+
</section>
|
|
123
|
+
</div>
|
|
124
|
+
|
|
125
|
+
<% if @pg_stat_statements.usable? %>
|
|
126
|
+
<section id="pg_stat_statements">
|
|
127
|
+
<h2>pg_stat_statements <%= @pg_stat_statements.installed_version %></h2>
|
|
128
|
+
<p class="dim">
|
|
129
|
+
Queries, endpoints and jobs are cumulative since
|
|
130
|
+
<%= @stats_reset_at.strftime("%Y-%m-%d %H:%M:%S %Z") %> (<%= format_time_ago(@stats_reset_at) %>),
|
|
131
|
+
<%= pluralize(number_with_delimiter(@pg_stat_statements.statement_count), "statement") %> tracked.
|
|
132
|
+
A reset starts a fresh window: exact counts and attribution, at the
|
|
133
|
+
price of the history so far.
|
|
134
|
+
<% peers = @database.server_peers %>
|
|
135
|
+
<% if peers.any? %>
|
|
136
|
+
It also clears <%= peers.map(&:name).to_sentence %>, which
|
|
137
|
+
<%= peers.one? ? "shares" : "share" %> this server: PostgreSQL keeps
|
|
138
|
+
one reset time for all of them.
|
|
139
|
+
<% end %>
|
|
140
|
+
</p>
|
|
141
|
+
<%= button_to "reset statistics",
|
|
142
|
+
reset_database_pg_stat_statements_path(@database),
|
|
143
|
+
method: :delete,
|
|
144
|
+
class: "danger",
|
|
145
|
+
form_class: "inline",
|
|
146
|
+
form: { data: { confirm: "Reset pg_stat_statements? This clears statement statistics for every database on this server." } } %>
|
|
147
|
+
<% if @pg_stat_statements.outdated? %>
|
|
148
|
+
<p class="dim">
|
|
149
|
+
Version <%= @pg_stat_statements.default_version %> is available:
|
|
150
|
+
<code>ALTER EXTENSION pg_stat_statements UPDATE;</code>
|
|
151
|
+
</p>
|
|
152
|
+
<% end %>
|
|
153
|
+
</section>
|
|
154
|
+
<% end %>
|
|
155
|
+
|
|
156
|
+
<details>
|
|
157
|
+
<summary>database details</summary>
|
|
158
|
+
<pre>name: <%= @database.name %>
|
|
159
|
+
primary: <%= @database.primary %>
|
|
160
|
+
replica: <%= @database.replica %>
|
|
161
|
+
schema_format: <%= @database.schema_format %>
|
|
162
|
+
version: <%= @database.version_full %></pre>
|
|
163
|
+
</details>
|
|
164
|
+
|
|
165
|
+
<details>
|
|
166
|
+
<summary>installed extensions</summary>
|
|
167
|
+
<pre><% @database.installed_extensions.each do |extension| %><%= extension['name'] %> <%= extension['installed_version'] %>
|
|
168
|
+
<% end %></pre>
|
|
169
|
+
</details>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<div class="toolbar">
|
|
2
|
+
<h1>endpoints</h1>
|
|
3
|
+
<small class="dim"><%= stats_since_link(@database, @pg_stat_statements.reset_at) %></small>
|
|
4
|
+
</div>
|
|
5
|
+
|
|
6
|
+
<% if @report.empty? %>
|
|
7
|
+
<article aria-label="No endpoints found">
|
|
8
|
+
<header>No controller action has run a query yet</header>
|
|
9
|
+
<p>
|
|
10
|
+
Endpoints appear once requests have run database queries with
|
|
11
|
+
<code>:controller</code> (or <code>:namespaced_controller</code>) and
|
|
12
|
+
<code>:action</code> among <code>config.active_record.query_log_tags</code>.
|
|
13
|
+
</p>
|
|
14
|
+
</article>
|
|
15
|
+
<% else %>
|
|
16
|
+
<%= render "pg_peek/reports/report", report: @report %>
|
|
17
|
+
|
|
18
|
+
<p class="dim">
|
|
19
|
+
<strong>q/req</strong> is an estimate: the request count is inferred from the
|
|
20
|
+
least-called query of each endpoint, so an action whose every query is
|
|
21
|
+
conditional will read low. A value above
|
|
22
|
+
<%= PgPeek::Reports::Endpoints::N_PLUS_ONE_THRESHOLD %> is the shape of an N+1.
|
|
23
|
+
</p>
|
|
24
|
+
<% end %>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<div class="toolbar">
|
|
2
|
+
<h1>indexes</h1>
|
|
3
|
+
<small class="dim">
|
|
4
|
+
<% if @stats_reset_at %>
|
|
5
|
+
scan counts since <%= format_time_ago(@stats_reset_at) %>
|
|
6
|
+
<% else %>
|
|
7
|
+
scan counts since statistics began — never reset
|
|
8
|
+
<% end %>
|
|
9
|
+
</small>
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
<% if @report.unused.any? %>
|
|
13
|
+
<article aria-label="Unused indexes">
|
|
14
|
+
<header>
|
|
15
|
+
<%= pluralize(@report.unused.size, "index") %> never scanned,
|
|
16
|
+
<%= number_to_human_size(@report.unused_bytes) %> of disk and write cost
|
|
17
|
+
</header>
|
|
18
|
+
<p>
|
|
19
|
+
Not read by any query since statistics were last reset. Primary keys and
|
|
20
|
+
unique indexes are not counted: they enforce constraints whether or not a
|
|
21
|
+
query reads them. Before dropping one, make sure the window covers your
|
|
22
|
+
rarest workloads — a month-end report is still a workload.
|
|
23
|
+
</p>
|
|
24
|
+
</article>
|
|
25
|
+
<% end %>
|
|
26
|
+
|
|
27
|
+
<% if @report.empty? %>
|
|
28
|
+
<p class="dim">No indexes on user tables.</p>
|
|
29
|
+
<% else %>
|
|
30
|
+
<%= render "pg_peek/reports/report", report: @report %>
|
|
31
|
+
<% end %>
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
<div class="toolbar">
|
|
2
|
+
<h1>jobs</h1>
|
|
3
|
+
<small class="dim"><%= stats_since_link(@database, @stats_reset_at) %></small>
|
|
4
|
+
</div>
|
|
5
|
+
|
|
6
|
+
<% if @report.empty? %>
|
|
7
|
+
<% if !@job_tag_configured %>
|
|
8
|
+
<article aria-label="Job tag not configured">
|
|
9
|
+
<header>Queries are not tagged with the job that ran them</header>
|
|
10
|
+
<p>
|
|
11
|
+
<code>config.active_record.query_log_tags</code> is
|
|
12
|
+
<code><%= Rails.application.config.active_record.query_log_tags.to_a.inspect %></code>,
|
|
13
|
+
which does not include <code>:job</code>. Until it does, no query can be
|
|
14
|
+
attributed to a job and this page cannot fill, however many jobs run.
|
|
15
|
+
</p>
|
|
16
|
+
<p>In <code>config/application.rb</code>:</p>
|
|
17
|
+
<pre><code>config.active_record.query_log_tags = <%= (Rails.application.config.active_record.query_log_tags.to_a + [ :job ]).inspect %></code></pre>
|
|
18
|
+
</article>
|
|
19
|
+
<% else %>
|
|
20
|
+
<article aria-label="No jobs have run yet">
|
|
21
|
+
<header>No job has run since statistics were last collected</header>
|
|
22
|
+
<p>
|
|
23
|
+
Query log tags include <code>:job</code> and pg_stat_statements is
|
|
24
|
+
collecting, so this page is working — there is simply nothing in it
|
|
25
|
+
yet. Jobs appear as they execute their first database query.
|
|
26
|
+
</p>
|
|
27
|
+
<% if @stats_reset_at %>
|
|
28
|
+
<p>
|
|
29
|
+
Statistics were last reset <strong><%= format_time_ago(@stats_reset_at) %></strong>
|
|
30
|
+
(<%= @stats_reset_at %>), so anything that ran before then is gone.
|
|
31
|
+
Restarting PostgreSQL clears them too.
|
|
32
|
+
</p>
|
|
33
|
+
<% end %>
|
|
34
|
+
</article>
|
|
35
|
+
<% end %>
|
|
36
|
+
<% else %>
|
|
37
|
+
<input type="search"
|
|
38
|
+
placeholder="filter jobs… (press / to focus)"
|
|
39
|
+
data-filter-input
|
|
40
|
+
data-filter-noun="job"
|
|
41
|
+
autofocus
|
|
42
|
+
autocomplete="off">
|
|
43
|
+
<p class="dim" data-filter-count></p>
|
|
44
|
+
|
|
45
|
+
<%= render "pg_peek/reports/report",
|
|
46
|
+
report: @report,
|
|
47
|
+
link_column: :job_class,
|
|
48
|
+
link: ->(row) { database_job_path(@database, ERB::Util.url_encode(row["job_class"])) } %>
|
|
49
|
+
|
|
50
|
+
<p class="dim" data-filter-empty hidden>No jobs match that filter.</p>
|
|
51
|
+
<% end %>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<div class="toolbar">
|
|
2
|
+
<h1><%= @job_class %></h1>
|
|
3
|
+
<%= link_to "← jobs", database_jobs_path(@database) %>
|
|
4
|
+
</div>
|
|
5
|
+
|
|
6
|
+
<% if @report.empty? %>
|
|
7
|
+
<article aria-label="No queries found">
|
|
8
|
+
<header>No queries found for this job</header>
|
|
9
|
+
<p>This could mean:</p>
|
|
10
|
+
<ul>
|
|
11
|
+
<li>The job hasn't executed any database queries since statistics were last reset</li>
|
|
12
|
+
<li>The job's queries are not among the top outliers</li>
|
|
13
|
+
</ul>
|
|
14
|
+
</article>
|
|
15
|
+
<% else %>
|
|
16
|
+
<%= render "pg_peek/reports/report", report: @report %>
|
|
17
|
+
<% end %>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<article aria-label="Extension pg_stat_statements is not preloaded">
|
|
2
|
+
<header>pg_stat_statements is not loaded</header>
|
|
3
|
+
<p>
|
|
4
|
+
The extension is installed on <strong><%= database.name %></strong>, but the module was not
|
|
5
|
+
loaded when the server started, so it collects no statistics and its views cannot be queried.
|
|
6
|
+
</p>
|
|
7
|
+
<p>Add it to <code>postgresql.conf</code>:</p>
|
|
8
|
+
<pre><code>shared_preload_libraries = 'pg_stat_statements'</code></pre>
|
|
9
|
+
<p>Or set it without editing the file by hand:</p>
|
|
10
|
+
<pre><code>ALTER SYSTEM SET shared_preload_libraries = 'pg_stat_statements';</code></pre>
|
|
11
|
+
<footer>
|
|
12
|
+
<small>
|
|
13
|
+
A server restart is required — this setting cannot be reloaded.
|
|
14
|
+
Currently loaded: <code><%= pg_stat_statements.shared_preload_libraries.presence || "(none)" %></code>
|
|
15
|
+
</small>
|
|
16
|
+
</footer>
|
|
17
|
+
</article>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<div class="toolbar">
|
|
2
|
+
<h1>queries</h1>
|
|
3
|
+
<small class="dim"><%= stats_since_link(@database, @pg_stat_statements.reset_at) %></small>
|
|
4
|
+
</div>
|
|
5
|
+
|
|
6
|
+
<% if @queries.empty? %>
|
|
7
|
+
<p class="dim">No statements recorded since the last reset.</p>
|
|
8
|
+
<% else %>
|
|
9
|
+
<%= render "pg_peek/reports/report", report: @queries %>
|
|
10
|
+
<% end %>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<%# Draws any report. Pass columns: [keys], rows: [subset] and/or limit: n to show a projection. %>
|
|
2
|
+
<% columns = local_assigns[:columns] ? report.columns.select { |c| local_assigns[:columns].include?(c.key) } : report.columns %>
|
|
3
|
+
<% rows = local_assigns[:rows] || report.rows %>
|
|
4
|
+
<% rows = rows.first(local_assigns[:limit]) if local_assigns[:limit] %>
|
|
5
|
+
<div class="scroll">
|
|
6
|
+
<table>
|
|
7
|
+
<thead>
|
|
8
|
+
<tr>
|
|
9
|
+
<% columns.each do |column| %>
|
|
10
|
+
<th class="<%= cell_class(column) %>" <%= "title=#{column.title.inspect}".html_safe if column.title %>>
|
|
11
|
+
<%= column.header %>
|
|
12
|
+
</th>
|
|
13
|
+
<% end %>
|
|
14
|
+
</tr>
|
|
15
|
+
</thead>
|
|
16
|
+
<tbody>
|
|
17
|
+
<% rows.each do |row| %>
|
|
18
|
+
<tr <%= "data-filter-value=#{row.value(report.columns.first).to_s.downcase.inspect}".html_safe %>>
|
|
19
|
+
<% columns.each do |column| %>
|
|
20
|
+
<td class="<%= cell_class(column) %>">
|
|
21
|
+
<% if local_assigns[:link] && column.key == local_assigns[:link_column] %>
|
|
22
|
+
<%= link_to format_cell(report, column, row), local_assigns[:link].call(row) %>
|
|
23
|
+
<% else %>
|
|
24
|
+
<%= format_cell(report, column, row) %>
|
|
25
|
+
<% end %>
|
|
26
|
+
</td>
|
|
27
|
+
<% end %>
|
|
28
|
+
</tr>
|
|
29
|
+
<% end %>
|
|
30
|
+
</tbody>
|
|
31
|
+
</table>
|
|
32
|
+
</div>
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
<div class="toolbar">
|
|
2
|
+
<h1><%= @table.name %></h1>
|
|
3
|
+
<%= link_to "← tables", database_path(@database, anchor: "tables") %>
|
|
4
|
+
</div>
|
|
5
|
+
|
|
6
|
+
<% seconds_elapsed = @table.seconds_since_reset %>
|
|
7
|
+
|
|
8
|
+
<section>
|
|
9
|
+
<h2>cache performance</h2>
|
|
10
|
+
<table>
|
|
11
|
+
<tbody>
|
|
12
|
+
<tr>
|
|
13
|
+
<th>heap cache hit</th>
|
|
14
|
+
<td><%= format_percentage(@table.heap_cache_hit_ratio) %></td>
|
|
15
|
+
</tr>
|
|
16
|
+
<tr>
|
|
17
|
+
<th>index cache hit</th>
|
|
18
|
+
<td><%= format_percentage(@table.index_cache_hit_ratio) %></td>
|
|
19
|
+
</tr>
|
|
20
|
+
<tr>
|
|
21
|
+
<th>toast cache hit</th>
|
|
22
|
+
<td><%= format_percentage(@table.toast_cache_hit_ratio) %></td>
|
|
23
|
+
</tr>
|
|
24
|
+
<tr>
|
|
25
|
+
<th>toast index cache hit</th>
|
|
26
|
+
<td><%= format_percentage(@table.toast_index_cache_hit_ratio) %></td>
|
|
27
|
+
</tr>
|
|
28
|
+
</tbody>
|
|
29
|
+
</table>
|
|
30
|
+
</section>
|
|
31
|
+
|
|
32
|
+
<section>
|
|
33
|
+
<h2>row activity</h2>
|
|
34
|
+
<table>
|
|
35
|
+
<tbody>
|
|
36
|
+
<tr>
|
|
37
|
+
<th>estimated rows</th>
|
|
38
|
+
<td><%= format_number(@table.row_estimate) %></td>
|
|
39
|
+
</tr>
|
|
40
|
+
<tr>
|
|
41
|
+
<th>sequential scans</th>
|
|
42
|
+
<td>
|
|
43
|
+
<%= format_number(@table.seq_scan) %>
|
|
44
|
+
<% if @table.seq_scan_percentage %><span class="dim">(<%= @table.seq_scan_percentage %>%)</span><% end %>
|
|
45
|
+
</td>
|
|
46
|
+
</tr>
|
|
47
|
+
<tr>
|
|
48
|
+
<th>index scans</th>
|
|
49
|
+
<td>
|
|
50
|
+
<%= format_number(@table.idx_scan) %>
|
|
51
|
+
<% if @table.idx_scan_percentage %><span class="dim">(<%= @table.idx_scan_percentage %>%)</span><% end %>
|
|
52
|
+
</td>
|
|
53
|
+
</tr>
|
|
54
|
+
<tr>
|
|
55
|
+
<th>index usage</th>
|
|
56
|
+
<td><%= format_percentage(@table.index_usage_percentage) %></td>
|
|
57
|
+
</tr>
|
|
58
|
+
<tr>
|
|
59
|
+
<th>rows fetched</th>
|
|
60
|
+
<td><%= format_count_with_rate(@table.rows_fetched, seconds_elapsed) %></td>
|
|
61
|
+
</tr>
|
|
62
|
+
<tr>
|
|
63
|
+
<th>rows inserted</th>
|
|
64
|
+
<td><%= format_count_with_rate(@table.rows_inserted, seconds_elapsed) %></td>
|
|
65
|
+
</tr>
|
|
66
|
+
<tr>
|
|
67
|
+
<th>rows updated</th>
|
|
68
|
+
<td><%= format_count_with_rate(@table.rows_updated, seconds_elapsed) %></td>
|
|
69
|
+
</tr>
|
|
70
|
+
<tr>
|
|
71
|
+
<th>rows deleted</th>
|
|
72
|
+
<td><%= format_count_with_rate(@table.rows_deleted, seconds_elapsed) %></td>
|
|
73
|
+
</tr>
|
|
74
|
+
<tr>
|
|
75
|
+
<th>live tuples</th>
|
|
76
|
+
<td><%= format_number(@table.live_tuples) %></td>
|
|
77
|
+
</tr>
|
|
78
|
+
<tr>
|
|
79
|
+
<th>dead tuples</th>
|
|
80
|
+
<td>
|
|
81
|
+
<%= format_number(@table.dead_tuples) %>
|
|
82
|
+
<% if @table.dead_tuple_ratio %><span class="dim">(<%= @table.dead_tuple_ratio %>% of total)</span><% end %>
|
|
83
|
+
<% if @table.dead_tuple_warning? %><br><span class="warn">consider running VACUUM</span><% end %>
|
|
84
|
+
</td>
|
|
85
|
+
</tr>
|
|
86
|
+
</tbody>
|
|
87
|
+
</table>
|
|
88
|
+
</section>
|
|
89
|
+
|
|
90
|
+
<section>
|
|
91
|
+
<h2>maintenance</h2>
|
|
92
|
+
<table>
|
|
93
|
+
<tbody>
|
|
94
|
+
<tr>
|
|
95
|
+
<th>last vacuum</th>
|
|
96
|
+
<td><%= format_time_ago(@table.last_vacuum) %></td>
|
|
97
|
+
</tr>
|
|
98
|
+
<tr>
|
|
99
|
+
<th>last autovacuum</th>
|
|
100
|
+
<td><%= format_time_ago(@table.last_autovacuum) %></td>
|
|
101
|
+
</tr>
|
|
102
|
+
<tr>
|
|
103
|
+
<th>last analyze</th>
|
|
104
|
+
<td><%= format_time_ago(@table.last_analyze) %></td>
|
|
105
|
+
</tr>
|
|
106
|
+
<tr>
|
|
107
|
+
<th>last autoanalyze</th>
|
|
108
|
+
<td><%= format_time_ago(@table.last_autoanalyze) %></td>
|
|
109
|
+
</tr>
|
|
110
|
+
<tr>
|
|
111
|
+
<th>vacuum count</th>
|
|
112
|
+
<td><%= format_number(@table.vacuum_count) %></td>
|
|
113
|
+
</tr>
|
|
114
|
+
<tr>
|
|
115
|
+
<th>autovacuum count</th>
|
|
116
|
+
<td><%= format_number(@table.autovacuum_count) %></td>
|
|
117
|
+
</tr>
|
|
118
|
+
<tr>
|
|
119
|
+
<th>analyze count</th>
|
|
120
|
+
<td><%= format_number(@table.analyze_count) %></td>
|
|
121
|
+
</tr>
|
|
122
|
+
<tr>
|
|
123
|
+
<th>autoanalyze count</th>
|
|
124
|
+
<td><%= format_number(@table.autoanalyze_count) %></td>
|
|
125
|
+
</tr>
|
|
126
|
+
</tbody>
|
|
127
|
+
</table>
|
|
128
|
+
</section>
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
PgPeek::Engine.routes.draw do
|
|
2
|
+
resources :databases, only: [ :index, :show ] do
|
|
3
|
+
resources :queries, only: [ :index ]
|
|
4
|
+
resources :tables, only: [ :show ], param: :name
|
|
5
|
+
resources :endpoints, only: [ :index ]
|
|
6
|
+
resources :indexes, only: [ :index ]
|
|
7
|
+
resource :activity, only: [ :show ], controller: "activity"
|
|
8
|
+
resources :jobs, only: [ :index, :show ], param: :job_class
|
|
9
|
+
|
|
10
|
+
resource :pg_stat_statements, only: [] do
|
|
11
|
+
delete :reset
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
root to: "databases#home"
|
|
16
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
module PgPeek
|
|
2
|
+
module Generators
|
|
3
|
+
class InstallGenerator < Rails::Generators::Base
|
|
4
|
+
source_root File.expand_path("templates", __dir__)
|
|
5
|
+
|
|
6
|
+
desc "Creates a PgPeek initializer and mounts the engine."
|
|
7
|
+
|
|
8
|
+
class_option :skip_routes, type: :boolean, default: false,
|
|
9
|
+
desc: "Don't mount the engine in config/routes.rb"
|
|
10
|
+
|
|
11
|
+
def copy_initializer
|
|
12
|
+
template "initializer.rb", "config/initializers/pg_peek.rb"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def mount_engine
|
|
16
|
+
return if options[:skip_routes]
|
|
17
|
+
|
|
18
|
+
if engine_mounted?
|
|
19
|
+
say_status :identical, "config/routes.rb (PgPeek::Engine already mounted)", :blue
|
|
20
|
+
return
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
route %(mount PgPeek::Engine, at: "/pg_peek")
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def show_readme
|
|
27
|
+
readme "README" if behavior == :invoke
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
# Rails' route action appends unconditionally, so re-running the
|
|
32
|
+
# generator would mount the engine a second time.
|
|
33
|
+
def engine_mounted?
|
|
34
|
+
routes = File.join(destination_root, "config/routes.rb")
|
|
35
|
+
|
|
36
|
+
File.exist?(routes) && File.read(routes).include?("PgPeek::Engine")
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
===============================================================================
|
|
2
|
+
|
|
3
|
+
PgPeek has been installed and mounted at /pg_peek.
|
|
4
|
+
|
|
5
|
+
Next steps:
|
|
6
|
+
|
|
7
|
+
1. Map your databases to their ActiveRecord classes in
|
|
8
|
+
config/initializers/pg_peek.rb:
|
|
9
|
+
|
|
10
|
+
config.connections = { "primary" => "ApplicationRecord" }
|
|
11
|
+
|
|
12
|
+
Every database you want to inspect needs an entry, or it will not be listed.
|
|
13
|
+
|
|
14
|
+
2. Visit /pg_peek to view your PostgreSQL databases
|
|
15
|
+
|
|
16
|
+
Before deploying:
|
|
17
|
+
|
|
18
|
+
pg_peek exposes your schema, every normalized query in the database, and an
|
|
19
|
+
endpoint that resets query statistics. Outside development and test it
|
|
20
|
+
refuses to serve until you say how it is protected.
|
|
21
|
+
|
|
22
|
+
Give it credentials, and it will ask for them:
|
|
23
|
+
|
|
24
|
+
PG_PEEK_USERNAME=... PG_PEEK_PASSWORD=...
|
|
25
|
+
|
|
26
|
+
Or, if you mount it behind authentication you already have, say so in
|
|
27
|
+
config/initializers/pg_peek.rb:
|
|
28
|
+
|
|
29
|
+
config.public_dashboard = true
|
|
30
|
+
|
|
31
|
+
===============================================================================
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# PgPeek configuration
|
|
2
|
+
# See https://github.com/trix/pg_peek for more information.
|
|
3
|
+
|
|
4
|
+
Rails.application.config.pg_peek.tap do |config|
|
|
5
|
+
# Authentication.
|
|
6
|
+
#
|
|
7
|
+
# Outside development and test, pg_peek refuses to serve until it knows how it
|
|
8
|
+
# is protected. Give it credentials with PG_PEEK_USERNAME / PG_PEEK_PASSWORD or
|
|
9
|
+
# a pg_peek: entry in credentials.yml.enc and it asks for them over HTTP basic.
|
|
10
|
+
# Credentials, once set, apply in every environment -- including a development
|
|
11
|
+
# server exposed through a tunnel.
|
|
12
|
+
#
|
|
13
|
+
# If you mount the engine behind your own authentication instead, say so:
|
|
14
|
+
# config.public_dashboard = true
|
|
15
|
+
|
|
16
|
+
# Database connections mapping.
|
|
17
|
+
# Maps database names (from database.yml) to their ActiveRecord base class.
|
|
18
|
+
config.connections = {
|
|
19
|
+
"primary" => "ApplicationRecord"
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
# Tables to exclude from the tables list.
|
|
23
|
+
# Supports strings and regular expressions.
|
|
24
|
+
config.excluded_tables = %w[schema_migrations ar_internal_metadata]
|
|
25
|
+
|
|
26
|
+
# Maximum number of outlier queries to display.
|
|
27
|
+
# config.outliers_limit = 20
|
|
28
|
+
|
|
29
|
+
# Dead tuple ratio threshold (percentage) for warnings.
|
|
30
|
+
# config.dead_tuple_warning_threshold = 10
|
|
31
|
+
|
|
32
|
+
# A query running longer than this is called out on the overview.
|
|
33
|
+
# config.long_query_warning_seconds = 5
|
|
34
|
+
|
|
35
|
+
# A session idle in transaction longer than this holds locks and blocks
|
|
36
|
+
# vacuum, and is called out on the overview.
|
|
37
|
+
# config.idle_in_transaction_warning_seconds = 60
|
|
38
|
+
end
|