pg_peek 0.1.0 → 0.2.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 +4 -4
- data/app/assets/stylesheets/pg_peek/application.css +37 -1
- data/app/controllers/pg_peek/databases_controller.rb +12 -9
- data/app/controllers/pg_peek/endpoints_controller.rb +15 -3
- data/app/controllers/pg_peek/jobs_controller.rb +1 -1
- data/app/controllers/pg_peek/pg_stat_statements_controller.rb +9 -3
- data/app/helpers/pg_peek/application_helper.rb +24 -9
- data/app/helpers/pg_peek/report_helper.rb +31 -7
- data/app/models/pg_peek/pg_stat_statements.rb +73 -7
- data/app/models/pg_peek/table.rb +4 -3
- data/app/queries/pg_peek/pg14/pg_stat_statements/outliers.sql +1 -1
- data/app/queries/pg_peek/pg14/pg_stat_statements/outliers_by_endpoint.sql +24 -0
- data/app/queries/pg_peek/pg14/pg_stat_statements/outliers_by_job.sql +1 -1
- data/app/queries/pg_peek/pg14/tables/dead_tuples.sql +7 -3
- data/app/queries/pg_peek/pg15/pg_stat_statements/outliers.sql +1 -1
- data/app/queries/pg_peek/pg15/pg_stat_statements/outliers_by_endpoint.sql +24 -0
- data/app/queries/pg_peek/pg15/pg_stat_statements/outliers_by_job.sql +1 -1
- data/app/queries/pg_peek/pg15/tables/dead_tuples.sql +7 -3
- data/app/queries/pg_peek/pg16/pg_stat_statements/outliers.sql +1 -1
- data/app/queries/pg_peek/pg16/pg_stat_statements/outliers_by_endpoint.sql +24 -0
- data/app/queries/pg_peek/pg16/pg_stat_statements/outliers_by_job.sql +1 -1
- data/app/queries/pg_peek/pg16/tables/dead_tuples.sql +7 -3
- data/app/queries/pg_peek/pg17/pg_stat_statements/outliers.sql +1 -1
- data/app/queries/pg_peek/pg17/pg_stat_statements/outliers_by_endpoint.sql +24 -0
- data/app/queries/pg_peek/pg17/pg_stat_statements/outliers_by_job.sql +1 -1
- data/app/queries/pg_peek/pg17/tables/dead_tuples.sql +7 -3
- data/app/queries/pg_peek/pg18/pg_stat_statements/outliers.sql +1 -1
- data/app/queries/pg_peek/pg18/pg_stat_statements/outliers_by_endpoint.sql +24 -0
- data/app/queries/pg_peek/pg18/pg_stat_statements/outliers_by_job.sql +1 -1
- data/app/queries/pg_peek/pg18/tables/dead_tuples.sql +7 -3
- data/app/reports/pg_peek/column.rb +6 -1
- data/app/reports/pg_peek/report.rb +1 -2
- data/app/reports/pg_peek/reports/endpoint_queries.rb +14 -0
- data/app/reports/pg_peek/reports/endpoints.rb +5 -8
- data/app/reports/pg_peek/reports/jobs.rb +7 -7
- data/app/reports/pg_peek/reports/queries.rb +2 -1
- data/app/views/layouts/pg_peek/application.html.erb +1 -0
- data/app/views/pg_peek/databases/show.html.erb +16 -4
- data/app/views/pg_peek/endpoints/index.html.erb +14 -1
- data/app/views/pg_peek/endpoints/show.html.erb +17 -0
- data/app/views/pg_peek/jobs/index.html.erb +1 -1
- data/app/views/pg_peek/jobs/show.html.erb +1 -1
- data/app/views/pg_peek/pg_stat_statements/_preload_instructions.html.erb +5 -1
- data/app/views/pg_peek/tables/show.html.erb +1 -1
- data/config/routes.rb +3 -0
- data/lib/generators/pg_peek/install/templates/initializer.rb +9 -0
- data/lib/pg_peek/configuration.rb +8 -1
- data/lib/pg_peek/version.rb +1 -1
- metadata +9 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 52d233ab8cafc3dae1e3386c44b9244549344b3b4bf97bc9f053cfdb843bd223
|
|
4
|
+
data.tar.gz: 3abf5a72e79b3ffff19323bb6fc7812c479bfd23c8bbee8667aff5a52732cb1f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 93640cabe15c0afecb89784c94623a77ccb118a1679a9fe821b58e4fb9f04f68de2e28b88222f475a5930f1daff722f577c65149159c498aece5b19e1fe20f9f
|
|
7
|
+
data.tar.gz: 37074ace65e4e13d9953f4d342a8b4011a0fa7903b7141e29b8961d3db44ea680273c78b4835157605275f4578878b23d04e28df1287a99ce3505703b33b11ab
|
|
@@ -149,6 +149,11 @@ pre {
|
|
|
149
149
|
|
|
150
150
|
pre code { background: none; border: 0; padding: 0; }
|
|
151
151
|
|
|
152
|
+
/* Names from the data (a job class, a table) keep their case inside the
|
|
153
|
+
lowercase headings, and read as the heading rather than as a code box. */
|
|
154
|
+
.name { text-transform: none; }
|
|
155
|
+
code.name { background: none; border: 0; padding: 0; }
|
|
156
|
+
|
|
152
157
|
/* Tables ------------------------------------------------------------------ */
|
|
153
158
|
|
|
154
159
|
.scroll { overflow-x: auto; }
|
|
@@ -194,7 +199,6 @@ tbody th + td { text-align: right; }
|
|
|
194
199
|
.nowrap { white-space: nowrap; }
|
|
195
200
|
.dim { color: var(--dim); }
|
|
196
201
|
.warn { color: var(--warn); }
|
|
197
|
-
.intensity { white-space: nowrap; }
|
|
198
202
|
.bar { letter-spacing: 0.08em; }
|
|
199
203
|
.bar-on { color: var(--accent); }
|
|
200
204
|
/* Brightness follows the value: a one-segment bar sits at 80%, five at full. */
|
|
@@ -204,6 +208,24 @@ tbody th + td { text-align: right; }
|
|
|
204
208
|
.bar-4 .bar-on { opacity: 0.95; }
|
|
205
209
|
.bar-5 .bar-on { opacity: 1; }
|
|
206
210
|
.bar-off { color: var(--line); }
|
|
211
|
+
/* An endpoint or job name takes the width the figures leave over, so a long
|
|
212
|
+
one is cut instead of pushing them out of view (max-width: 0 drops the name
|
|
213
|
+
from the table's width calculation). The dimmed namespace shrinks first and
|
|
214
|
+
is cut from the left, since the end of a name tells rows apart: rtl puts
|
|
215
|
+
the ellipsis on the left, the inner span keeps the text itself ltr so "::"
|
|
216
|
+
and "/" stay in place. A leaf wider than the cell is cut on the right. */
|
|
217
|
+
.name-column { width: 100%; max-width: 0; min-width: 12ch; }
|
|
218
|
+
.name-column > a { display: block; }
|
|
219
|
+
.qualified { display: flex; overflow: hidden; white-space: nowrap; }
|
|
220
|
+
.qualified .namespace {
|
|
221
|
+
min-width: 0;
|
|
222
|
+
overflow: hidden;
|
|
223
|
+
text-overflow: ellipsis;
|
|
224
|
+
direction: rtl;
|
|
225
|
+
opacity: 0.6;
|
|
226
|
+
}
|
|
227
|
+
.qualified .namespace > span { direction: ltr; unicode-bidi: embed; }
|
|
228
|
+
.qualified .leaf { flex-shrink: 0; max-width: 100%; overflow: hidden; text-overflow: ellipsis; }
|
|
207
229
|
.sql { word-break: break-all; white-space: pre-wrap; background: none; border: 0; padding: 0; }
|
|
208
230
|
|
|
209
231
|
/* What issued a query, from its SQLcommenter tags: a link line under the SQL,
|
|
@@ -280,6 +302,12 @@ button, .button {
|
|
|
280
302
|
button:hover { border-color: var(--fg); }
|
|
281
303
|
button.danger { color: var(--danger); border-color: var(--danger); }
|
|
282
304
|
button.danger:hover { background: var(--danger); color: var(--bg); }
|
|
305
|
+
button:disabled, button:disabled:hover {
|
|
306
|
+
background: var(--panel);
|
|
307
|
+
color: var(--dim);
|
|
308
|
+
border-color: var(--line);
|
|
309
|
+
cursor: not-allowed;
|
|
310
|
+
}
|
|
283
311
|
|
|
284
312
|
form.inline { display: inline; margin: 0; }
|
|
285
313
|
|
|
@@ -297,6 +325,13 @@ form.inline { display: inline; margin: 0; }
|
|
|
297
325
|
margin: 0.75rem 0;
|
|
298
326
|
}
|
|
299
327
|
|
|
328
|
+
.alert {
|
|
329
|
+
border: 1px solid var(--danger);
|
|
330
|
+
color: var(--danger);
|
|
331
|
+
padding: 0.4rem 0.75rem;
|
|
332
|
+
margin: 0.75rem 0;
|
|
333
|
+
}
|
|
334
|
+
|
|
300
335
|
mark { background: var(--accent); color: var(--bg); }
|
|
301
336
|
|
|
302
337
|
.grid {
|
|
@@ -340,6 +375,7 @@ footer.status button:hover { color: var(--fg); }
|
|
|
340
375
|
|
|
341
376
|
.vitals { margin: 0 0 1rem; }
|
|
342
377
|
.vitals span + span::before { content: " · "; color: var(--line); }
|
|
378
|
+
a.provisional { color: color-mix(in srgb, var(--warn) 70%, var(--dim)); }
|
|
343
379
|
|
|
344
380
|
.panels {
|
|
345
381
|
display: grid;
|
|
@@ -54,10 +54,18 @@ class PgPeek::DatabasesController < PgPeek::ApplicationController
|
|
|
54
54
|
href: database_table_path(@database, table["relname"]) }
|
|
55
55
|
end
|
|
56
56
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
57
|
+
# Scan counts restart at zero on a statistics reset, so right after one
|
|
58
|
+
# every index looks unused. Small ones cost too little to act on; the
|
|
59
|
+
# indexes page still lists them all.
|
|
60
|
+
reset_at = @database.stats_reset_at
|
|
61
|
+
if reset_at.nil? || reset_at < PgPeek.config.unused_index_min_stats_age.ago
|
|
62
|
+
unused = PgPeek::Reports::Indexes.new(database: @database).unused
|
|
63
|
+
.select { |row| row["size_bytes"].to_i >= PgPeek.config.unused_index_min_size }
|
|
64
|
+
if unused.any?
|
|
65
|
+
bytes = unused.sum { |row| row["size_bytes"].to_i }
|
|
66
|
+
items << { text: "#{helpers.pluralize(unused.size, "unused index")} · #{helpers.number_to_human_size(bytes)}",
|
|
67
|
+
href: database_indexes_path(@database) }
|
|
68
|
+
end
|
|
61
69
|
end
|
|
62
70
|
|
|
63
71
|
activity = database_activity_path(@database)
|
|
@@ -86,11 +94,6 @@ class PgPeek::DatabasesController < PgPeek::ApplicationController
|
|
|
86
94
|
items << { text: "#{used} of #{max} connections in use", href: activity }
|
|
87
95
|
end
|
|
88
96
|
|
|
89
|
-
if @stats_reset_at && @stats_reset_at > 1.hour.ago
|
|
90
|
-
items << { text: "statistics were reset #{helpers.time_ago_in_words(@stats_reset_at)} ago -- figures may not be representative yet",
|
|
91
|
-
href: database_path(@database, anchor: "pg_stat_statements") }
|
|
92
|
-
end
|
|
93
|
-
|
|
94
97
|
items
|
|
95
98
|
end
|
|
96
99
|
end
|
|
@@ -1,10 +1,22 @@
|
|
|
1
1
|
class PgPeek::EndpointsController < PgPeek::ApplicationController
|
|
2
|
-
|
|
3
|
-
@database = PgPeek::Database.find(params[:database_id])
|
|
4
|
-
@pg_stat_statements = PgPeek::PgStatStatements.new(database: @database)
|
|
2
|
+
before_action :set_database
|
|
5
3
|
|
|
4
|
+
def index
|
|
6
5
|
return render "pg_peek/pg_stat_statements/not_preloaded" unless @pg_stat_statements.usable?
|
|
7
6
|
|
|
8
7
|
@report = PgPeek::Reports::Endpoints.new(database: @database)
|
|
9
8
|
end
|
|
9
|
+
|
|
10
|
+
def show
|
|
11
|
+
@endpoint = "#{params[:endpoint_controller]}##{params[:endpoint_action]}"
|
|
12
|
+
return render "pg_peek/pg_stat_statements/not_preloaded" unless @pg_stat_statements.usable?
|
|
13
|
+
|
|
14
|
+
@report = PgPeek::Reports::EndpointQueries.new(database: @database, endpoint: @endpoint)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
def set_database
|
|
19
|
+
@database = PgPeek::Database.find(params[:database_id])
|
|
20
|
+
@pg_stat_statements = PgPeek::PgStatStatements.new(database: @database)
|
|
21
|
+
end
|
|
10
22
|
end
|
|
@@ -13,7 +13,7 @@ class PgPeek::JobsController < PgPeek::ApplicationController
|
|
|
13
13
|
end
|
|
14
14
|
|
|
15
15
|
def show
|
|
16
|
-
@job_class =
|
|
16
|
+
@job_class = params[:job_class]
|
|
17
17
|
return render "pg_peek/pg_stat_statements/not_preloaded" unless @pg_stat_statements.usable?
|
|
18
18
|
|
|
19
19
|
@report = PgPeek::Reports::JobQueries.new(database: @database, job_class: @job_class)
|
|
@@ -3,9 +3,15 @@
|
|
|
3
3
|
class PgPeek::PgStatStatementsController < PgPeek::ApplicationController
|
|
4
4
|
def reset
|
|
5
5
|
@database = PgPeek::Database.find(params[:database_id]) or raise ActiveRecord::RecordNotFound, "Database not found"
|
|
6
|
-
PgPeek::PgStatStatements.new(database: @database)
|
|
6
|
+
pg_stat_statements = PgPeek::PgStatStatements.new(database: @database)
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
if pg_stat_statements.resettable?
|
|
9
|
+
pg_stat_statements.reset!
|
|
10
|
+
flash[:notice] = "pg_stat_statements has been reset."
|
|
11
|
+
else
|
|
12
|
+
flash[:alert] = "This role may not reset pg_stat_statements; the statistics were left as they are."
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
redirect_to database_path(@database, anchor: "pg_stat_statements")
|
|
10
16
|
end
|
|
11
17
|
end
|
|
@@ -30,16 +30,23 @@ module PgPeek::ApplicationHelper
|
|
|
30
30
|
sql.gsub(SQLCOMMENTER_PATTERN, "").strip
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
-
# Where a query's SQLcommenter tags point, if anywhere: a job's own page,
|
|
34
|
-
#
|
|
35
|
-
#
|
|
33
|
+
# Where a query's SQLcommenter tags point, if anywhere: a job's own page, or
|
|
34
|
+
# a controller/action's own page. A controller without an action has no page,
|
|
35
|
+
# so it lands on the endpoints list.
|
|
36
36
|
def sqlcommenter_href(database, tags)
|
|
37
37
|
return database_job_path(database, tags["job"]) if tags["job"].present?
|
|
38
38
|
|
|
39
39
|
controller = tags["namespaced_controller"] || tags["controller"]
|
|
40
|
-
return
|
|
40
|
+
return nil if controller.blank?
|
|
41
|
+
return database_endpoints_path(database) if tags["action"].blank?
|
|
41
42
|
|
|
42
|
-
|
|
43
|
+
endpoint_path(database, "#{controller}##{tags["action"]}")
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# An endpoint's page, from its controller#action name.
|
|
47
|
+
def endpoint_path(database, endpoint)
|
|
48
|
+
controller, action = endpoint.split("#", 2)
|
|
49
|
+
database_endpoint_path(database, endpoint_controller: controller, endpoint_action: action)
|
|
43
50
|
end
|
|
44
51
|
|
|
45
52
|
# Format a rate with auto-scaled units based on magnitude
|
|
@@ -96,10 +103,15 @@ module PgPeek::ApplicationHelper
|
|
|
96
103
|
|
|
97
104
|
# Every pg_stat_statements figure is cumulative since the last reset. Each
|
|
98
105
|
# page says so in its header and links to the overview block that resets it.
|
|
106
|
+
# Within the first hour the figures cover too little to go by, and the link
|
|
107
|
+
# is marked provisional.
|
|
99
108
|
def stats_since_link(database, reset_at)
|
|
109
|
+
provisional = reset_at && reset_at > 1.hour.ago
|
|
110
|
+
|
|
100
111
|
link_to "stats since #{format_time_ago(reset_at)}",
|
|
101
112
|
database_path(database, anchor: "pg_stat_statements"),
|
|
102
|
-
|
|
113
|
+
class: ("provisional" if provisional),
|
|
114
|
+
title: provisional ? "figures may not be representative yet" : "figures are cumulative since the last pg_stat_statements reset"
|
|
103
115
|
end
|
|
104
116
|
|
|
105
117
|
# Format cache hit ratio for inline display
|
|
@@ -134,12 +146,15 @@ module PgPeek::ApplicationHelper
|
|
|
134
146
|
((value.to_f / max_value) * INTENSITY_SEGMENTS).ceil.clamp(0, INTENSITY_SEGMENTS)
|
|
135
147
|
end
|
|
136
148
|
|
|
137
|
-
# Durations pick their unit by magnitude: 210ms, 1.20s, 2m 05s,
|
|
138
|
-
# Values stay narrow and read at a glance, and a header sits over
|
|
139
|
-
# similar width. The exact figure belongs in a title, not the cell.
|
|
149
|
+
# Durations pick their unit by magnitude: <1ms, 210ms, 1.20s, 2m 05s,
|
|
150
|
+
# 1h 12m. Values stay narrow and read at a glance, and a header sits over
|
|
151
|
+
# data of similar width. The exact figure belongs in a title, not the cell.
|
|
152
|
+
# Anything above zero but under a millisecond is <1ms, so a fast query never
|
|
153
|
+
# reads as 0ms -- that is kept for a real zero.
|
|
140
154
|
def format_duration_from_ms(total_ms)
|
|
141
155
|
ms = total_ms.to_f
|
|
142
156
|
return "0ms" if ms <= 0
|
|
157
|
+
return "<1ms" if ms < 1
|
|
143
158
|
return format("%.1fms", ms) if ms < 10
|
|
144
159
|
return "#{ms.round}ms" if ms < 1000
|
|
145
160
|
|
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
# Turns a report cell into markup. The report is passed alongside the column
|
|
2
|
-
# because
|
|
3
|
-
#
|
|
2
|
+
# because a bar is relative -- it means nothing without the largest value in
|
|
3
|
+
# its column.
|
|
4
4
|
module PgPeek::ReportHelper
|
|
5
5
|
def format_cell(report, column, row)
|
|
6
6
|
value = row.value(column)
|
|
7
7
|
return "" if value.nil? || value.to_s.empty?
|
|
8
8
|
|
|
9
|
+
content = format_value(report, column, value)
|
|
10
|
+
return content unless column.bar?
|
|
11
|
+
|
|
12
|
+
safe_join([ content, intensity_bar(value.to_f, report.max_for(column)) ], " ")
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def format_value(report, column, value)
|
|
9
16
|
case column.format
|
|
10
17
|
when :number then format_number(value.to_i)
|
|
11
18
|
when :duration_ms then duration_cell(value.to_f)
|
|
@@ -13,14 +20,30 @@ module PgPeek::ReportHelper
|
|
|
13
20
|
when :percent then "#{value}%"
|
|
14
21
|
when :ratio then "~#{value}"
|
|
15
22
|
when :sql then sql_cell(report, value.to_s)
|
|
16
|
-
when :
|
|
23
|
+
when :name then name_cell(value.to_s)
|
|
17
24
|
else value.to_s
|
|
18
25
|
end
|
|
19
26
|
end
|
|
20
27
|
|
|
21
28
|
# The compact figure is what you scan; the exact one is a hover away.
|
|
22
29
|
def duration_cell(ms)
|
|
23
|
-
tag.span(format_duration_from_ms(ms), title: "#{number_with_delimiter(ms.round(
|
|
30
|
+
tag.span(format_duration_from_ms(ms), title: "#{number_with_delimiter(ms.round(3))} ms")
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# An endpoint or job name in its native form, the namespace dimmed so the
|
|
34
|
+
# eye goes to the last part. When the column is too narrow, the namespace
|
|
35
|
+
# is what gets cut; the full name is on hover.
|
|
36
|
+
def name_cell(name)
|
|
37
|
+
namespace, leaf = name_parts(name)
|
|
38
|
+
tag.span(class: "qualified", title: name) do
|
|
39
|
+
safe_join([ (tag.span(tag.span(namespace), class: "namespace") if namespace), tag.span(leaf, class: "leaf") ].compact)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Splits after the last "/" or "::": courier_company_app/dashboard/ and
|
|
44
|
+
# executions#show, or GDPR::OrderAnonymizationBatch:: and ProgressMonitorJob.
|
|
45
|
+
def name_parts(name)
|
|
46
|
+
name.match(%r{\A(.*(?:/|::))?(.*)\z}m).captures
|
|
24
47
|
end
|
|
25
48
|
|
|
26
49
|
# The SQL text, plus -- when SQLcommenter tagged it -- what issued it (a
|
|
@@ -50,9 +73,10 @@ module PgPeek::ReportHelper
|
|
|
50
73
|
end
|
|
51
74
|
|
|
52
75
|
def cell_class(column)
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
76
|
+
if column.format == :name
|
|
77
|
+
"name-column"
|
|
78
|
+
elsif column.numeric?
|
|
79
|
+
"num"
|
|
56
80
|
end
|
|
57
81
|
end
|
|
58
82
|
end
|
|
@@ -24,16 +24,34 @@ class PgPeek::PgStatStatements
|
|
|
24
24
|
# The extension can be created in the database while the module itself was
|
|
25
25
|
# never loaded at server start. Querying the view then raises
|
|
26
26
|
# PG::ObjectNotInPrerequisiteState, so every read has to check this first.
|
|
27
|
+
#
|
|
28
|
+
# Roles without pg_read_all_settings (dokku's postgres plugin, most managed
|
|
29
|
+
# hosts) cannot read shared_preload_libraries, so then the only way to tell
|
|
30
|
+
# is to ask the module itself.
|
|
27
31
|
def preloaded?
|
|
28
|
-
shared_preload_libraries
|
|
32
|
+
if shared_preload_libraries
|
|
33
|
+
shared_preload_libraries.split(",").map(&:strip).include?(LIBRARY_NAME)
|
|
34
|
+
else
|
|
35
|
+
installed? && module_loaded?
|
|
36
|
+
end
|
|
29
37
|
end
|
|
30
38
|
|
|
31
39
|
def usable?
|
|
32
40
|
installed? && preloaded?
|
|
33
41
|
end
|
|
34
42
|
|
|
43
|
+
# nil when this role may not examine the setting, as opposed to "" when
|
|
44
|
+
# nothing is preloaded.
|
|
35
45
|
def shared_preload_libraries
|
|
36
|
-
@shared_preload_libraries
|
|
46
|
+
return @shared_preload_libraries if defined?(@shared_preload_libraries)
|
|
47
|
+
|
|
48
|
+
@shared_preload_libraries = without_aborting_transaction do
|
|
49
|
+
connection.select_value(PgPeek::QueryLoader.mark("SHOW shared_preload_libraries")).to_s
|
|
50
|
+
end
|
|
51
|
+
rescue ActiveRecord::StatementInvalid => e
|
|
52
|
+
raise unless e.cause.is_a?(PG::InsufficientPrivilege)
|
|
53
|
+
|
|
54
|
+
@shared_preload_libraries = nil
|
|
37
55
|
end
|
|
38
56
|
|
|
39
57
|
def outdated?
|
|
@@ -42,12 +60,23 @@ class PgPeek::PgStatStatements
|
|
|
42
60
|
@installed_version != @default_version
|
|
43
61
|
end
|
|
44
62
|
|
|
63
|
+
# The extension revokes EXECUTE on the reset from PUBLIC, so only superusers
|
|
64
|
+
# and roles granted it may run one.
|
|
65
|
+
def resettable?
|
|
66
|
+
return @resettable if defined?(@resettable)
|
|
67
|
+
|
|
68
|
+
@resettable = usable? && connection.select_value(PgPeek::QueryLoader.mark(<<~SQL)) == true
|
|
69
|
+
SELECT bool_and(has_function_privilege(oid, 'EXECUTE'))
|
|
70
|
+
FROM pg_proc WHERE proname = 'pg_stat_statements_reset'
|
|
71
|
+
SQL
|
|
72
|
+
end
|
|
73
|
+
|
|
45
74
|
# Server-wide on purpose. pg_stat_statements_info keeps a single stats_reset
|
|
46
75
|
# for the whole server and only moves it when every entry is removed, so a
|
|
47
76
|
# reset scoped to this database would leave "stats since" pointing at the
|
|
48
77
|
# old time.
|
|
49
78
|
def reset!
|
|
50
|
-
return unless
|
|
79
|
+
return unless resettable?
|
|
51
80
|
|
|
52
81
|
connection.execute PgPeek::QueryLoader.mark(<<-SQL)
|
|
53
82
|
SELECT pg_stat_statements_reset();
|
|
@@ -104,10 +133,10 @@ class PgPeek::PgStatStatements
|
|
|
104
133
|
def outliers_by_job(job_class)
|
|
105
134
|
return unless usable?
|
|
106
135
|
|
|
107
|
-
#
|
|
108
|
-
#
|
|
109
|
-
|
|
110
|
-
pattern = "%job='#{
|
|
136
|
+
# Rails writes the job tag url-encoded (job='Reports%3A%3ADigestJob'), so
|
|
137
|
+
# match it encoded, with the % of the encoding escaped for LIKE.
|
|
138
|
+
encoded_job = ActiveRecord::Base.sanitize_sql_like(ERB::Util.url_encode(job_class))
|
|
139
|
+
pattern = "%job='#{encoded_job}'%"
|
|
111
140
|
|
|
112
141
|
query = PgPeek::QueryLoader.load("pg_stat_statements/outliers_by_job",
|
|
113
142
|
pg_version: database.major_version,
|
|
@@ -117,8 +146,45 @@ class PgPeek::PgStatStatements
|
|
|
117
146
|
result.to_a
|
|
118
147
|
end
|
|
119
148
|
|
|
149
|
+
# endpoint is controller#action, as the endpoints report names it.
|
|
150
|
+
def outliers_by_endpoint(endpoint)
|
|
151
|
+
return unless usable?
|
|
152
|
+
|
|
153
|
+
# Rails writes the tags url-encoded (namespaced_controller='staff%2Fprocesses'),
|
|
154
|
+
# so compare them encoded.
|
|
155
|
+
controller, action = endpoint.split("#", 2).map { |part| ERB::Util.url_encode(part) }
|
|
156
|
+
|
|
157
|
+
query = PgPeek::QueryLoader.load("pg_stat_statements/outliers_by_endpoint",
|
|
158
|
+
pg_version: database.major_version,
|
|
159
|
+
controller: connection.quote(controller),
|
|
160
|
+
action: connection.quote(action),
|
|
161
|
+
limit: PgPeek.config.outliers_limit)
|
|
162
|
+
connection.execute(query).to_a
|
|
163
|
+
end
|
|
164
|
+
|
|
120
165
|
private
|
|
121
166
|
|
|
167
|
+
# pg_stat_statements_info() raises PG::ObjectNotInPrerequisiteState unless
|
|
168
|
+
# the module was loaded at server start, and is readable by any role.
|
|
169
|
+
def module_loaded?
|
|
170
|
+
return @module_loaded if defined?(@module_loaded)
|
|
171
|
+
|
|
172
|
+
@module_loaded = without_aborting_transaction do
|
|
173
|
+
connection.select_value(PgPeek::QueryLoader.mark("SELECT 1 FROM pg_stat_statements_info"))
|
|
174
|
+
true
|
|
175
|
+
end
|
|
176
|
+
rescue ActiveRecord::StatementInvalid => e
|
|
177
|
+
raise unless e.cause.is_a?(PG::ObjectNotInPrerequisiteState)
|
|
178
|
+
|
|
179
|
+
@module_loaded = false
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# A failed statement inside an open transaction (a host app's, or a
|
|
183
|
+
# transactional test) would poison it; a savepoint confines the failure.
|
|
184
|
+
def without_aborting_transaction(&)
|
|
185
|
+
connection.transaction(requires_new: true, &)
|
|
186
|
+
end
|
|
187
|
+
|
|
122
188
|
def verify_installation
|
|
123
189
|
result = connection.execute PgPeek::QueryLoader.mark(<<-SQL)
|
|
124
190
|
SELECT default_version, installed_version FROM pg_available_extensions WHERE name = 'pg_stat_statements';
|
data/app/models/pg_peek/table.rb
CHANGED
|
@@ -147,10 +147,11 @@ class PgPeek::Table
|
|
|
147
147
|
(dead_tuples.to_f / total * 100).round(1)
|
|
148
148
|
end
|
|
149
149
|
|
|
150
|
+
# The same rule as the overview's attention list: past autovacuum's trigger
|
|
151
|
+
# point as well as the percentage threshold.
|
|
150
152
|
def dead_tuple_warning?
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
ratio >= PgPeek.config.dead_tuple_warning_threshold
|
|
153
|
+
database.tables_with_dead_tuples(PgPeek.config.dead_tuple_warning_threshold)
|
|
154
|
+
.any? { |table| table["relname"] == name }
|
|
154
155
|
end
|
|
155
156
|
|
|
156
157
|
def row_estimate
|
|
@@ -2,7 +2,7 @@ SELECT
|
|
|
2
2
|
interval '1 millisecond' * total_exec_time AS total_exec_time,
|
|
3
3
|
COALESCE(to_char((total_exec_time/NULLIF(sum(total_exec_time) OVER(), 0)) * 100, 'FM990D0'), '0') || '%' AS prop_exec_time,
|
|
4
4
|
to_char(calls, 'FM999G999G999G990') AS ncalls,
|
|
5
|
-
|
|
5
|
+
total_exec_time/NULLIF(calls, 0) AS avg_exec_ms,
|
|
6
6
|
interval '1 millisecond' * (blk_read_time + blk_write_time) AS sync_io_time,
|
|
7
7
|
query
|
|
8
8
|
FROM pg_stat_statements
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
-- The query shapes one controller#action issued. The endpoint is read from the
|
|
2
|
+
-- tags exactly as endpoints.sql reads it, so this page lists the same shapes
|
|
3
|
+
-- that endpoints.sql counts for it. An equality, not a LIKE: with both tags on,
|
|
4
|
+
-- controller='posts' also appears in the queries of admin/posts.
|
|
5
|
+
SELECT
|
|
6
|
+
interval '1 millisecond' * total_exec_time AS total_exec_time,
|
|
7
|
+
COALESCE(to_char((total_exec_time/NULLIF(sum(total_exec_time) OVER(), 0)) * 100, 'FM990D0'), '0') || '%' AS prop_exec_time,
|
|
8
|
+
to_char(calls, 'FM999G999G999G990') AS ncalls,
|
|
9
|
+
ROUND(total_exec_time/NULLIF(calls, 0)) AS avg_exec_ms,
|
|
10
|
+
interval '1 millisecond' * (blk_read_time + blk_write_time) AS sync_io_time,
|
|
11
|
+
query
|
|
12
|
+
FROM pg_stat_statements
|
|
13
|
+
WHERE userid = (SELECT usesysid FROM pg_user WHERE usename = current_user LIMIT 1)
|
|
14
|
+
AND dbid = (SELECT oid FROM pg_database WHERE datname = current_database())
|
|
15
|
+
AND query NOT LIKE '%/* pg_peek */%'
|
|
16
|
+
AND COALESCE(
|
|
17
|
+
substring(query from 'namespaced_controller=''([^'']+)'''),
|
|
18
|
+
substring(query from 'controller=''([^'']+)''')
|
|
19
|
+
) = {{controller}}
|
|
20
|
+
AND substring(query from 'action=''([^'']+)''') = {{action}}
|
|
21
|
+
AND query !~* 'pg_catalog|information_schema|pg_index|pg_class|pg_attribute'
|
|
22
|
+
AND query !~* '^\s*SHOW '
|
|
23
|
+
ORDER BY total_exec_time DESC
|
|
24
|
+
LIMIT {{limit}}
|
|
@@ -2,7 +2,7 @@ SELECT
|
|
|
2
2
|
interval '1 millisecond' * total_exec_time AS total_exec_time,
|
|
3
3
|
COALESCE(to_char((total_exec_time/NULLIF(sum(total_exec_time) OVER(), 0)) * 100, 'FM990D0'), '0') || '%' AS prop_exec_time,
|
|
4
4
|
to_char(calls, 'FM999G999G999G990') AS ncalls,
|
|
5
|
-
|
|
5
|
+
total_exec_time/NULLIF(calls, 0) AS avg_exec_ms,
|
|
6
6
|
interval '1 millisecond' * (blk_read_time + blk_write_time) AS sync_io_time,
|
|
7
7
|
query
|
|
8
8
|
FROM pg_stat_statements
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
-- Tables
|
|
1
|
+
-- Tables that autovacuum should have cleaned by now: more dead tuples than its
|
|
2
|
+
-- own trigger point (autovacuum_vacuum_threshold + scale_factor * live rows),
|
|
3
|
+
-- and a dead-tuple share at or above the warning threshold. Small tables never
|
|
4
|
+
-- reach the trigger point, so a high share alone is not a problem.
|
|
2
5
|
SELECT
|
|
3
6
|
relname,
|
|
4
7
|
n_live_tup,
|
|
@@ -6,6 +9,7 @@ SELECT
|
|
|
6
9
|
round(100.0 * n_dead_tup / NULLIF(n_live_tup + n_dead_tup, 0), 1) AS dead_ratio,
|
|
7
10
|
last_autovacuum
|
|
8
11
|
FROM pg_stat_user_tables
|
|
9
|
-
WHERE
|
|
10
|
-
|
|
12
|
+
WHERE n_dead_tup > current_setting('autovacuum_vacuum_threshold')::int
|
|
13
|
+
+ current_setting('autovacuum_vacuum_scale_factor')::float * n_live_tup
|
|
14
|
+
AND 100.0 * n_dead_tup / NULLIF(n_live_tup + n_dead_tup, 0) >= {{threshold}}
|
|
11
15
|
ORDER BY dead_ratio DESC
|
|
@@ -2,7 +2,7 @@ SELECT
|
|
|
2
2
|
interval '1 millisecond' * total_exec_time AS total_exec_time,
|
|
3
3
|
COALESCE(to_char((total_exec_time/NULLIF(sum(total_exec_time) OVER(), 0)) * 100, 'FM990D0'), '0') || '%' AS prop_exec_time,
|
|
4
4
|
to_char(calls, 'FM999G999G999G990') AS ncalls,
|
|
5
|
-
|
|
5
|
+
total_exec_time/NULLIF(calls, 0) AS avg_exec_ms,
|
|
6
6
|
interval '1 millisecond' * (blk_read_time + blk_write_time) AS sync_io_time,
|
|
7
7
|
query
|
|
8
8
|
FROM pg_stat_statements
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
-- The query shapes one controller#action issued. The endpoint is read from the
|
|
2
|
+
-- tags exactly as endpoints.sql reads it, so this page lists the same shapes
|
|
3
|
+
-- that endpoints.sql counts for it. An equality, not a LIKE: with both tags on,
|
|
4
|
+
-- controller='posts' also appears in the queries of admin/posts.
|
|
5
|
+
SELECT
|
|
6
|
+
interval '1 millisecond' * total_exec_time AS total_exec_time,
|
|
7
|
+
COALESCE(to_char((total_exec_time/NULLIF(sum(total_exec_time) OVER(), 0)) * 100, 'FM990D0'), '0') || '%' AS prop_exec_time,
|
|
8
|
+
to_char(calls, 'FM999G999G999G990') AS ncalls,
|
|
9
|
+
ROUND(total_exec_time/NULLIF(calls, 0)) AS avg_exec_ms,
|
|
10
|
+
interval '1 millisecond' * (blk_read_time + blk_write_time) AS sync_io_time,
|
|
11
|
+
query
|
|
12
|
+
FROM pg_stat_statements
|
|
13
|
+
WHERE userid = (SELECT usesysid FROM pg_user WHERE usename = current_user LIMIT 1)
|
|
14
|
+
AND dbid = (SELECT oid FROM pg_database WHERE datname = current_database())
|
|
15
|
+
AND query NOT LIKE '%/* pg_peek */%'
|
|
16
|
+
AND COALESCE(
|
|
17
|
+
substring(query from 'namespaced_controller=''([^'']+)'''),
|
|
18
|
+
substring(query from 'controller=''([^'']+)''')
|
|
19
|
+
) = {{controller}}
|
|
20
|
+
AND substring(query from 'action=''([^'']+)''') = {{action}}
|
|
21
|
+
AND query !~* 'pg_catalog|information_schema|pg_index|pg_class|pg_attribute'
|
|
22
|
+
AND query !~* '^\s*SHOW '
|
|
23
|
+
ORDER BY total_exec_time DESC
|
|
24
|
+
LIMIT {{limit}}
|
|
@@ -2,7 +2,7 @@ SELECT
|
|
|
2
2
|
interval '1 millisecond' * total_exec_time AS total_exec_time,
|
|
3
3
|
COALESCE(to_char((total_exec_time/NULLIF(sum(total_exec_time) OVER(), 0)) * 100, 'FM990D0'), '0') || '%' AS prop_exec_time,
|
|
4
4
|
to_char(calls, 'FM999G999G999G990') AS ncalls,
|
|
5
|
-
|
|
5
|
+
total_exec_time/NULLIF(calls, 0) AS avg_exec_ms,
|
|
6
6
|
interval '1 millisecond' * (blk_read_time + blk_write_time) AS sync_io_time,
|
|
7
7
|
query
|
|
8
8
|
FROM pg_stat_statements
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
-- Tables
|
|
1
|
+
-- Tables that autovacuum should have cleaned by now: more dead tuples than its
|
|
2
|
+
-- own trigger point (autovacuum_vacuum_threshold + scale_factor * live rows),
|
|
3
|
+
-- and a dead-tuple share at or above the warning threshold. Small tables never
|
|
4
|
+
-- reach the trigger point, so a high share alone is not a problem.
|
|
2
5
|
SELECT
|
|
3
6
|
relname,
|
|
4
7
|
n_live_tup,
|
|
@@ -6,6 +9,7 @@ SELECT
|
|
|
6
9
|
round(100.0 * n_dead_tup / NULLIF(n_live_tup + n_dead_tup, 0), 1) AS dead_ratio,
|
|
7
10
|
last_autovacuum
|
|
8
11
|
FROM pg_stat_user_tables
|
|
9
|
-
WHERE
|
|
10
|
-
|
|
12
|
+
WHERE n_dead_tup > current_setting('autovacuum_vacuum_threshold')::int
|
|
13
|
+
+ current_setting('autovacuum_vacuum_scale_factor')::float * n_live_tup
|
|
14
|
+
AND 100.0 * n_dead_tup / NULLIF(n_live_tup + n_dead_tup, 0) >= {{threshold}}
|
|
11
15
|
ORDER BY dead_ratio DESC
|
|
@@ -2,7 +2,7 @@ SELECT
|
|
|
2
2
|
interval '1 millisecond' * total_exec_time AS total_exec_time,
|
|
3
3
|
COALESCE(to_char((total_exec_time/NULLIF(sum(total_exec_time) OVER(), 0)) * 100, 'FM990D0'), '0') || '%' AS prop_exec_time,
|
|
4
4
|
to_char(calls, 'FM999G999G999G990') AS ncalls,
|
|
5
|
-
|
|
5
|
+
total_exec_time/NULLIF(calls, 0) AS avg_exec_ms,
|
|
6
6
|
interval '1 millisecond' * (blk_read_time + blk_write_time) AS sync_io_time,
|
|
7
7
|
query
|
|
8
8
|
FROM pg_stat_statements
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
-- The query shapes one controller#action issued. The endpoint is read from the
|
|
2
|
+
-- tags exactly as endpoints.sql reads it, so this page lists the same shapes
|
|
3
|
+
-- that endpoints.sql counts for it. An equality, not a LIKE: with both tags on,
|
|
4
|
+
-- controller='posts' also appears in the queries of admin/posts.
|
|
5
|
+
SELECT
|
|
6
|
+
interval '1 millisecond' * total_exec_time AS total_exec_time,
|
|
7
|
+
COALESCE(to_char((total_exec_time/NULLIF(sum(total_exec_time) OVER(), 0)) * 100, 'FM990D0'), '0') || '%' AS prop_exec_time,
|
|
8
|
+
to_char(calls, 'FM999G999G999G990') AS ncalls,
|
|
9
|
+
ROUND(total_exec_time/NULLIF(calls, 0)) AS avg_exec_ms,
|
|
10
|
+
interval '1 millisecond' * (blk_read_time + blk_write_time) AS sync_io_time,
|
|
11
|
+
query
|
|
12
|
+
FROM pg_stat_statements
|
|
13
|
+
WHERE userid = (SELECT usesysid FROM pg_user WHERE usename = current_user LIMIT 1)
|
|
14
|
+
AND dbid = (SELECT oid FROM pg_database WHERE datname = current_database())
|
|
15
|
+
AND query NOT LIKE '%/* pg_peek */%'
|
|
16
|
+
AND COALESCE(
|
|
17
|
+
substring(query from 'namespaced_controller=''([^'']+)'''),
|
|
18
|
+
substring(query from 'controller=''([^'']+)''')
|
|
19
|
+
) = {{controller}}
|
|
20
|
+
AND substring(query from 'action=''([^'']+)''') = {{action}}
|
|
21
|
+
AND query !~* 'pg_catalog|information_schema|pg_index|pg_class|pg_attribute'
|
|
22
|
+
AND query !~* '^\s*SHOW '
|
|
23
|
+
ORDER BY total_exec_time DESC
|
|
24
|
+
LIMIT {{limit}}
|
|
@@ -2,7 +2,7 @@ SELECT
|
|
|
2
2
|
interval '1 millisecond' * total_exec_time AS total_exec_time,
|
|
3
3
|
COALESCE(to_char((total_exec_time/NULLIF(sum(total_exec_time) OVER(), 0)) * 100, 'FM990D0'), '0') || '%' AS prop_exec_time,
|
|
4
4
|
to_char(calls, 'FM999G999G999G990') AS ncalls,
|
|
5
|
-
|
|
5
|
+
total_exec_time/NULLIF(calls, 0) AS avg_exec_ms,
|
|
6
6
|
interval '1 millisecond' * (blk_read_time + blk_write_time) AS sync_io_time,
|
|
7
7
|
query
|
|
8
8
|
FROM pg_stat_statements
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
-- Tables
|
|
1
|
+
-- Tables that autovacuum should have cleaned by now: more dead tuples than its
|
|
2
|
+
-- own trigger point (autovacuum_vacuum_threshold + scale_factor * live rows),
|
|
3
|
+
-- and a dead-tuple share at or above the warning threshold. Small tables never
|
|
4
|
+
-- reach the trigger point, so a high share alone is not a problem.
|
|
2
5
|
SELECT
|
|
3
6
|
relname,
|
|
4
7
|
n_live_tup,
|
|
@@ -6,6 +9,7 @@ SELECT
|
|
|
6
9
|
round(100.0 * n_dead_tup / NULLIF(n_live_tup + n_dead_tup, 0), 1) AS dead_ratio,
|
|
7
10
|
last_autovacuum
|
|
8
11
|
FROM pg_stat_user_tables
|
|
9
|
-
WHERE
|
|
10
|
-
|
|
12
|
+
WHERE n_dead_tup > current_setting('autovacuum_vacuum_threshold')::int
|
|
13
|
+
+ current_setting('autovacuum_vacuum_scale_factor')::float * n_live_tup
|
|
14
|
+
AND 100.0 * n_dead_tup / NULLIF(n_live_tup + n_dead_tup, 0) >= {{threshold}}
|
|
11
15
|
ORDER BY dead_ratio DESC
|
|
@@ -2,7 +2,7 @@ SELECT
|
|
|
2
2
|
interval '1 millisecond' * total_exec_time AS total_exec_time,
|
|
3
3
|
COALESCE(to_char((total_exec_time/NULLIF(sum(total_exec_time) OVER(), 0)) * 100, 'FM990D0'), '0') || '%' AS prop_exec_time,
|
|
4
4
|
to_char(calls, 'FM999G999G999G990') AS ncalls,
|
|
5
|
-
|
|
5
|
+
total_exec_time/NULLIF(calls, 0) AS avg_exec_ms,
|
|
6
6
|
interval '1 millisecond' * (shared_blk_read_time + shared_blk_write_time) AS sync_io_time,
|
|
7
7
|
query
|
|
8
8
|
FROM pg_stat_statements
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
-- The query shapes one controller#action issued. The endpoint is read from the
|
|
2
|
+
-- tags exactly as endpoints.sql reads it, so this page lists the same shapes
|
|
3
|
+
-- that endpoints.sql counts for it. An equality, not a LIKE: with both tags on,
|
|
4
|
+
-- controller='posts' also appears in the queries of admin/posts.
|
|
5
|
+
SELECT
|
|
6
|
+
interval '1 millisecond' * total_exec_time AS total_exec_time,
|
|
7
|
+
COALESCE(to_char((total_exec_time/NULLIF(sum(total_exec_time) OVER(), 0)) * 100, 'FM990D0'), '0') || '%' AS prop_exec_time,
|
|
8
|
+
to_char(calls, 'FM999G999G999G990') AS ncalls,
|
|
9
|
+
ROUND(total_exec_time/NULLIF(calls, 0)) AS avg_exec_ms,
|
|
10
|
+
interval '1 millisecond' * (shared_blk_read_time + shared_blk_write_time) AS sync_io_time,
|
|
11
|
+
query
|
|
12
|
+
FROM pg_stat_statements
|
|
13
|
+
WHERE userid = (SELECT usesysid FROM pg_user WHERE usename = current_user LIMIT 1)
|
|
14
|
+
AND dbid = (SELECT oid FROM pg_database WHERE datname = current_database())
|
|
15
|
+
AND query NOT LIKE '%/* pg_peek */%'
|
|
16
|
+
AND COALESCE(
|
|
17
|
+
substring(query from 'namespaced_controller=''([^'']+)'''),
|
|
18
|
+
substring(query from 'controller=''([^'']+)''')
|
|
19
|
+
) = {{controller}}
|
|
20
|
+
AND substring(query from 'action=''([^'']+)''') = {{action}}
|
|
21
|
+
AND query !~* 'pg_catalog|information_schema|pg_index|pg_class|pg_attribute'
|
|
22
|
+
AND query !~* '^\s*SHOW '
|
|
23
|
+
ORDER BY total_exec_time DESC
|
|
24
|
+
LIMIT {{limit}}
|
|
@@ -2,7 +2,7 @@ SELECT
|
|
|
2
2
|
interval '1 millisecond' * total_exec_time AS total_exec_time,
|
|
3
3
|
COALESCE(to_char((total_exec_time/NULLIF(sum(total_exec_time) OVER(), 0)) * 100, 'FM990D0'), '0') || '%' AS prop_exec_time,
|
|
4
4
|
to_char(calls, 'FM999G999G999G990') AS ncalls,
|
|
5
|
-
|
|
5
|
+
total_exec_time/NULLIF(calls, 0) AS avg_exec_ms,
|
|
6
6
|
interval '1 millisecond' * (shared_blk_read_time + shared_blk_write_time) AS sync_io_time,
|
|
7
7
|
query
|
|
8
8
|
FROM pg_stat_statements
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
-- Tables
|
|
1
|
+
-- Tables that autovacuum should have cleaned by now: more dead tuples than its
|
|
2
|
+
-- own trigger point (autovacuum_vacuum_threshold + scale_factor * live rows),
|
|
3
|
+
-- and a dead-tuple share at or above the warning threshold. Small tables never
|
|
4
|
+
-- reach the trigger point, so a high share alone is not a problem.
|
|
2
5
|
SELECT
|
|
3
6
|
relname,
|
|
4
7
|
n_live_tup,
|
|
@@ -6,6 +9,7 @@ SELECT
|
|
|
6
9
|
round(100.0 * n_dead_tup / NULLIF(n_live_tup + n_dead_tup, 0), 1) AS dead_ratio,
|
|
7
10
|
last_autovacuum
|
|
8
11
|
FROM pg_stat_user_tables
|
|
9
|
-
WHERE
|
|
10
|
-
|
|
12
|
+
WHERE n_dead_tup > current_setting('autovacuum_vacuum_threshold')::int
|
|
13
|
+
+ current_setting('autovacuum_vacuum_scale_factor')::float * n_live_tup
|
|
14
|
+
AND 100.0 * n_dead_tup / NULLIF(n_live_tup + n_dead_tup, 0) >= {{threshold}}
|
|
11
15
|
ORDER BY dead_ratio DESC
|
|
@@ -2,7 +2,7 @@ SELECT
|
|
|
2
2
|
interval '1 millisecond' * total_exec_time AS total_exec_time,
|
|
3
3
|
COALESCE(to_char((total_exec_time/NULLIF(sum(total_exec_time) OVER(), 0)) * 100, 'FM990D0'), '0') || '%' AS prop_exec_time,
|
|
4
4
|
to_char(calls, 'FM999G999G999G990') AS ncalls,
|
|
5
|
-
|
|
5
|
+
total_exec_time/NULLIF(calls, 0) AS avg_exec_ms,
|
|
6
6
|
interval '1 millisecond' * (shared_blk_read_time + shared_blk_write_time) AS sync_io_time,
|
|
7
7
|
query
|
|
8
8
|
FROM pg_stat_statements
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
-- The query shapes one controller#action issued. The endpoint is read from the
|
|
2
|
+
-- tags exactly as endpoints.sql reads it, so this page lists the same shapes
|
|
3
|
+
-- that endpoints.sql counts for it. An equality, not a LIKE: with both tags on,
|
|
4
|
+
-- controller='posts' also appears in the queries of admin/posts.
|
|
5
|
+
SELECT
|
|
6
|
+
interval '1 millisecond' * total_exec_time AS total_exec_time,
|
|
7
|
+
COALESCE(to_char((total_exec_time/NULLIF(sum(total_exec_time) OVER(), 0)) * 100, 'FM990D0'), '0') || '%' AS prop_exec_time,
|
|
8
|
+
to_char(calls, 'FM999G999G999G990') AS ncalls,
|
|
9
|
+
ROUND(total_exec_time/NULLIF(calls, 0)) AS avg_exec_ms,
|
|
10
|
+
interval '1 millisecond' * (shared_blk_read_time + shared_blk_write_time) AS sync_io_time,
|
|
11
|
+
query
|
|
12
|
+
FROM pg_stat_statements
|
|
13
|
+
WHERE userid = (SELECT usesysid FROM pg_user WHERE usename = current_user LIMIT 1)
|
|
14
|
+
AND dbid = (SELECT oid FROM pg_database WHERE datname = current_database())
|
|
15
|
+
AND query NOT LIKE '%/* pg_peek */%'
|
|
16
|
+
AND COALESCE(
|
|
17
|
+
substring(query from 'namespaced_controller=''([^'']+)'''),
|
|
18
|
+
substring(query from 'controller=''([^'']+)''')
|
|
19
|
+
) = {{controller}}
|
|
20
|
+
AND substring(query from 'action=''([^'']+)''') = {{action}}
|
|
21
|
+
AND query !~* 'pg_catalog|information_schema|pg_index|pg_class|pg_attribute'
|
|
22
|
+
AND query !~* '^\s*SHOW '
|
|
23
|
+
ORDER BY total_exec_time DESC
|
|
24
|
+
LIMIT {{limit}}
|
|
@@ -2,7 +2,7 @@ SELECT
|
|
|
2
2
|
interval '1 millisecond' * total_exec_time AS total_exec_time,
|
|
3
3
|
COALESCE(to_char((total_exec_time/NULLIF(sum(total_exec_time) OVER(), 0)) * 100, 'FM990D0'), '0') || '%' AS prop_exec_time,
|
|
4
4
|
to_char(calls, 'FM999G999G999G990') AS ncalls,
|
|
5
|
-
|
|
5
|
+
total_exec_time/NULLIF(calls, 0) AS avg_exec_ms,
|
|
6
6
|
interval '1 millisecond' * (shared_blk_read_time + shared_blk_write_time) AS sync_io_time,
|
|
7
7
|
query
|
|
8
8
|
FROM pg_stat_statements
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
-- Tables
|
|
1
|
+
-- Tables that autovacuum should have cleaned by now: more dead tuples than its
|
|
2
|
+
-- own trigger point (autovacuum_vacuum_threshold + scale_factor * live rows),
|
|
3
|
+
-- and a dead-tuple share at or above the warning threshold. Small tables never
|
|
4
|
+
-- reach the trigger point, so a high share alone is not a problem.
|
|
2
5
|
SELECT
|
|
3
6
|
relname,
|
|
4
7
|
n_live_tup,
|
|
@@ -6,6 +9,7 @@ SELECT
|
|
|
6
9
|
round(100.0 * n_dead_tup / NULLIF(n_live_tup + n_dead_tup, 0), 1) AS dead_ratio,
|
|
7
10
|
last_autovacuum
|
|
8
11
|
FROM pg_stat_user_tables
|
|
9
|
-
WHERE
|
|
10
|
-
|
|
12
|
+
WHERE n_dead_tup > current_setting('autovacuum_vacuum_threshold')::int
|
|
13
|
+
+ current_setting('autovacuum_vacuum_scale_factor')::float * n_live_tup
|
|
14
|
+
AND 100.0 * n_dead_tup / NULLIF(n_live_tup + n_dead_tup, 0) >= {{threshold}}
|
|
11
15
|
ORDER BY dead_ratio DESC
|
|
@@ -7,7 +7,9 @@ class PgPeek::Column
|
|
|
7
7
|
|
|
8
8
|
attr_reader :key, :align, :format, :title
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
# bar: draw the value with a bar next to it, scaled against the largest value
|
|
11
|
+
# in the column, so the biggest rows stand out without reading every figure.
|
|
12
|
+
def initialize(key, header: nil, align: :left, format: :text, title: nil, bar: false)
|
|
11
13
|
raise ArgumentError, "unknown alignment #{align.inspect}" unless ALIGNMENTS.include?(align)
|
|
12
14
|
|
|
13
15
|
@key = key
|
|
@@ -15,12 +17,15 @@ class PgPeek::Column
|
|
|
15
17
|
@align = align
|
|
16
18
|
@format = format
|
|
17
19
|
@title = title
|
|
20
|
+
@bar = bar
|
|
18
21
|
end
|
|
19
22
|
|
|
20
23
|
def header
|
|
21
24
|
@header || key.to_s.tr("_", " ")
|
|
22
25
|
end
|
|
23
26
|
|
|
27
|
+
def bar? = @bar
|
|
28
|
+
|
|
24
29
|
def numeric?
|
|
25
30
|
align == :right
|
|
26
31
|
end
|
|
@@ -43,8 +43,7 @@ class PgPeek::Report
|
|
|
43
43
|
|
|
44
44
|
def title = self.class.title
|
|
45
45
|
|
|
46
|
-
#
|
|
47
|
-
# the column, not just the cell.
|
|
46
|
+
# A bar is relative: it needs the extent of the column, not just the cell.
|
|
48
47
|
def max_for(column)
|
|
49
48
|
@maxima ||= {}
|
|
50
49
|
@maxima[column.key] ||= rows.filter_map { |row| row.value(column)&.to_f }.max
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# The query shapes one controller#action issues, most expensive first.
|
|
2
|
+
class PgPeek::Reports::EndpointQueries < PgPeek::Reports::Queries
|
|
3
|
+
attr_reader :endpoint
|
|
4
|
+
|
|
5
|
+
def initialize(database:, endpoint:)
|
|
6
|
+
super(database: database)
|
|
7
|
+
@endpoint = endpoint
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
def source_rows
|
|
12
|
+
pg_stat_statements.outliers_by_endpoint(endpoint)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -6,9 +6,9 @@ class PgPeek::Reports::Endpoints < PgPeek::Report
|
|
|
6
6
|
# the shape of an N+1 rather than a page that legitimately asks twice.
|
|
7
7
|
N_PLUS_ONE_THRESHOLD = 5
|
|
8
8
|
|
|
9
|
-
column :endpoint
|
|
10
|
-
column :total_exec_time_ms, header: "db time", align: :right, format: :duration_ms
|
|
11
|
-
|
|
9
|
+
column :endpoint, format: :name
|
|
10
|
+
column :total_exec_time_ms, header: "db time", align: :right, format: :duration_ms, bar: true,
|
|
11
|
+
title: "Database time; the bar compares it with the other endpoints listed"
|
|
12
12
|
column :request_count, header: "requests", align: :right, format: :number,
|
|
13
13
|
title: "Estimated from the least-called query of this endpoint"
|
|
14
14
|
column :total_calls, header: "queries", align: :right, format: :number
|
|
@@ -26,10 +26,7 @@ class PgPeek::Reports::Endpoints < PgPeek::Report
|
|
|
26
26
|
rows = pg_stat_statements.endpoints
|
|
27
27
|
return [] if rows.blank?
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
row["endpoint"] = CGI.unescape(row["endpoint"].to_s)
|
|
32
|
-
row["share"] = row["total_exec_time_ms"]
|
|
33
|
-
end
|
|
29
|
+
# namespaced_controller arrives url-encoded: staff%2Fprocesses#show.
|
|
30
|
+
rows.each { |row| row["endpoint"] = CGI.unescape(row["endpoint"].to_s) }
|
|
34
31
|
end
|
|
35
32
|
end
|
|
@@ -4,18 +4,18 @@
|
|
|
4
4
|
class PgPeek::Reports::Jobs < PgPeek::Report
|
|
5
5
|
title "Jobs"
|
|
6
6
|
|
|
7
|
-
column :job_class, header: "job"
|
|
8
|
-
column :total_exec_time_ms, header: "db time", align: :right, format: :duration_ms
|
|
9
|
-
|
|
7
|
+
column :job_class, header: "job", format: :name
|
|
8
|
+
column :total_exec_time_ms, header: "db time", align: :right, format: :duration_ms, bar: true,
|
|
9
|
+
title: "Database time; the bar compares it with the other jobs listed"
|
|
10
10
|
column :total_calls, header: "queries", align: :right, format: :number
|
|
11
11
|
column :query_count, header: "shapes", align: :right, format: :number,
|
|
12
12
|
title: "Distinct query shapes issued by this job"
|
|
13
13
|
|
|
14
14
|
private
|
|
15
15
|
def fetch_rows
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
pg_stat_statements.jobs.to_a.each do |row|
|
|
17
|
+
# The job tag arrives url-encoded: Reports%3A%3ADigestJob.
|
|
18
|
+
row["job_class"] = CGI.unescape(row["job_class"].to_s)
|
|
19
|
+
end
|
|
20
20
|
end
|
|
21
21
|
end
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
class PgPeek::Reports::Queries < PgPeek::Report
|
|
3
3
|
title "Queries"
|
|
4
4
|
|
|
5
|
-
column :calls, align: :right, format: :
|
|
5
|
+
column :calls, align: :right, format: :number, bar: true,
|
|
6
|
+
title: "Call count; the bar compares it with the other queries listed"
|
|
6
7
|
column :total_exec_time, header: "db time", align: :right, format: :duration
|
|
7
8
|
column :prop_exec_time, header: "share", align: :right
|
|
8
9
|
column :avg_exec_ms, header: "avg", align: :right, format: :duration_ms
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
<body>
|
|
16
16
|
<main>
|
|
17
17
|
<% if flash[:notice] %><p class="notice"><%= flash[:notice] %></p><% end %>
|
|
18
|
+
<% if flash[:alert] %><p class="alert"><%= flash[:alert] %></p><% end %>
|
|
18
19
|
<nav>
|
|
19
20
|
<ul>
|
|
20
21
|
<li><%= link_to "pg_peek", databases_path, class: "wordmark" %></li>
|
|
@@ -63,8 +63,10 @@ end</code></pre>
|
|
|
63
63
|
<p class="dim">No endpoint has run a query yet.</p>
|
|
64
64
|
<% else %>
|
|
65
65
|
<%= render "pg_peek/reports/report", report: @endpoints,
|
|
66
|
-
columns: %i[endpoint
|
|
67
|
-
limit: PgPeek::DatabasesController::OVERVIEW_ROWS
|
|
66
|
+
columns: %i[endpoint total_exec_time_ms max_queries_per_request],
|
|
67
|
+
limit: PgPeek::DatabasesController::OVERVIEW_ROWS,
|
|
68
|
+
link_column: :endpoint,
|
|
69
|
+
link: ->(row) { endpoint_path(@database, row["endpoint"]) } %>
|
|
68
70
|
<% end %>
|
|
69
71
|
</section>
|
|
70
72
|
|
|
@@ -74,8 +76,10 @@ end</code></pre>
|
|
|
74
76
|
<p class="dim">No job has run a query yet.</p>
|
|
75
77
|
<% else %>
|
|
76
78
|
<%= render "pg_peek/reports/report", report: @jobs,
|
|
77
|
-
columns: %i[job_class
|
|
78
|
-
limit: PgPeek::DatabasesController::OVERVIEW_ROWS
|
|
79
|
+
columns: %i[job_class total_exec_time_ms],
|
|
80
|
+
limit: PgPeek::DatabasesController::OVERVIEW_ROWS,
|
|
81
|
+
link_column: :job_class,
|
|
82
|
+
link: ->(row) { database_job_path(@database, row["job_class"]) } %>
|
|
79
83
|
<% end %>
|
|
80
84
|
</section>
|
|
81
85
|
|
|
@@ -142,8 +146,16 @@ end</code></pre>
|
|
|
142
146
|
reset_database_pg_stat_statements_path(@database),
|
|
143
147
|
method: :delete,
|
|
144
148
|
class: "danger",
|
|
149
|
+
disabled: !@pg_stat_statements.resettable?,
|
|
145
150
|
form_class: "inline",
|
|
146
151
|
form: { data: { confirm: "Reset pg_stat_statements? This clears statement statistics for every database on this server." } } %>
|
|
152
|
+
<% unless @pg_stat_statements.resettable? %>
|
|
153
|
+
<p class="dim">
|
|
154
|
+
<%= @connection_summary["username"] %> may not reset the statistics: the extension
|
|
155
|
+
allows it only to superusers and roles granted it. To allow it:
|
|
156
|
+
</p>
|
|
157
|
+
<pre><code>GRANT EXECUTE ON FUNCTION pg_stat_statements_reset TO <%= @connection_summary["username"] %>;</code></pre>
|
|
158
|
+
<% end %>
|
|
147
159
|
<% if @pg_stat_statements.outdated? %>
|
|
148
160
|
<p class="dim">
|
|
149
161
|
Version <%= @pg_stat_statements.default_version %> is available:
|
|
@@ -13,7 +13,20 @@
|
|
|
13
13
|
</p>
|
|
14
14
|
</article>
|
|
15
15
|
<% else %>
|
|
16
|
-
|
|
16
|
+
<input type="search"
|
|
17
|
+
placeholder="filter endpoints… (press / to focus)"
|
|
18
|
+
data-filter-input
|
|
19
|
+
data-filter-noun="endpoint"
|
|
20
|
+
autofocus
|
|
21
|
+
autocomplete="off">
|
|
22
|
+
<p class="dim" data-filter-count></p>
|
|
23
|
+
|
|
24
|
+
<%= render "pg_peek/reports/report",
|
|
25
|
+
report: @report,
|
|
26
|
+
link_column: :endpoint,
|
|
27
|
+
link: ->(row) { endpoint_path(@database, row["endpoint"]) } %>
|
|
28
|
+
|
|
29
|
+
<p class="dim" data-filter-empty hidden>No endpoints match that filter.</p>
|
|
17
30
|
|
|
18
31
|
<p class="dim">
|
|
19
32
|
<strong>q/req</strong> is an estimate: the request count is inferred from the
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<div class="toolbar">
|
|
2
|
+
<h1><code class="name"><%= @endpoint %></code></h1>
|
|
3
|
+
<%= link_to "← endpoints", database_endpoints_path(@database) %>
|
|
4
|
+
</div>
|
|
5
|
+
|
|
6
|
+
<% if @report.empty? %>
|
|
7
|
+
<article aria-label="No queries found">
|
|
8
|
+
<header>No queries found for this endpoint</header>
|
|
9
|
+
<p>This could mean:</p>
|
|
10
|
+
<ul>
|
|
11
|
+
<li>The endpoint hasn't executed any database queries since statistics were last reset</li>
|
|
12
|
+
<li>The endpoint'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 %>
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
<%= render "pg_peek/reports/report",
|
|
46
46
|
report: @report,
|
|
47
47
|
link_column: :job_class,
|
|
48
|
-
link: ->(row) { database_job_path(@database,
|
|
48
|
+
link: ->(row) { database_job_path(@database, row["job_class"]) } %>
|
|
49
49
|
|
|
50
50
|
<p class="dim" data-filter-empty hidden>No jobs match that filter.</p>
|
|
51
51
|
<% end %>
|
|
@@ -11,7 +11,11 @@
|
|
|
11
11
|
<footer>
|
|
12
12
|
<small>
|
|
13
13
|
A server restart is required — this setting cannot be reloaded.
|
|
14
|
-
|
|
14
|
+
<% if pg_stat_statements.shared_preload_libraries %>
|
|
15
|
+
Currently loaded: <code><%= pg_stat_statements.shared_preload_libraries.presence || "(none)" %></code>
|
|
16
|
+
<% else %>
|
|
17
|
+
This role may not read <code>shared_preload_libraries</code>; the module itself reported it is not loaded.
|
|
18
|
+
<% end %>
|
|
15
19
|
</small>
|
|
16
20
|
</footer>
|
|
17
21
|
</article>
|
data/config/routes.rb
CHANGED
|
@@ -3,6 +3,9 @@ PgPeek::Engine.routes.draw do
|
|
|
3
3
|
resources :queries, only: [ :index ]
|
|
4
4
|
resources :tables, only: [ :show ], param: :name
|
|
5
5
|
resources :endpoints, only: [ :index ]
|
|
6
|
+
# admin/posts#index lives at endpoints/admin/posts/index: the slashes of a
|
|
7
|
+
# namespace stay as they are, and no # needs encoding.
|
|
8
|
+
get "endpoints/*endpoint_controller/:endpoint_action", to: "endpoints#show", as: :endpoint, format: false
|
|
6
9
|
resources :indexes, only: [ :index ]
|
|
7
10
|
resource :activity, only: [ :show ], controller: "activity"
|
|
8
11
|
resources :jobs, only: [ :index, :show ], param: :job_class
|
|
@@ -29,6 +29,15 @@ Rails.application.config.pg_peek.tap do |config|
|
|
|
29
29
|
# Dead tuple ratio threshold (percentage) for warnings.
|
|
30
30
|
# config.dead_tuple_warning_threshold = 10
|
|
31
31
|
|
|
32
|
+
# Index scan counts start from zero when statistics are reset, so right
|
|
33
|
+
# after a reset every index looks unused. The overview waits this long
|
|
34
|
+
# before calling unused indexes out.
|
|
35
|
+
# config.unused_index_min_stats_age = 1.day
|
|
36
|
+
|
|
37
|
+
# Unused indexes smaller than this (in bytes) are left out of the overview;
|
|
38
|
+
# the indexes page still lists them all.
|
|
39
|
+
# config.unused_index_min_size = 1.megabyte
|
|
40
|
+
|
|
32
41
|
# A query running longer than this is called out on the overview.
|
|
33
42
|
# config.long_query_warning_seconds = 5
|
|
34
43
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
require "active_support/core_ext/integer/time"
|
|
2
|
+
|
|
1
3
|
module PgPeek
|
|
2
4
|
class Configuration
|
|
3
5
|
DEFAULT_EXCLUDED_TABLES = %w[schema_migrations ar_internal_metadata].freeze
|
|
@@ -6,10 +8,13 @@ module PgPeek
|
|
|
6
8
|
DEFAULT_CACHE_HIT_WARNING_THRESHOLD = 99 # percentage
|
|
7
9
|
DEFAULT_LONG_QUERY_WARNING_SECONDS = 5
|
|
8
10
|
DEFAULT_IDLE_IN_TRANSACTION_WARNING_SECONDS = 60
|
|
11
|
+
DEFAULT_UNUSED_INDEX_MIN_STATS_AGE = 1.day
|
|
12
|
+
DEFAULT_UNUSED_INDEX_MIN_SIZE = 1.megabyte
|
|
9
13
|
|
|
10
14
|
attr_accessor :connections, :excluded_tables, :dead_tuple_warning_threshold, :outliers_limit,
|
|
11
15
|
:cache_hit_warning_threshold, :long_query_warning_seconds,
|
|
12
|
-
:idle_in_transaction_warning_seconds, :
|
|
16
|
+
:idle_in_transaction_warning_seconds, :unused_index_min_stats_age,
|
|
17
|
+
:unused_index_min_size, :public_dashboard
|
|
13
18
|
attr_writer :username, :password
|
|
14
19
|
|
|
15
20
|
def initialize
|
|
@@ -20,6 +25,8 @@ module PgPeek
|
|
|
20
25
|
@cache_hit_warning_threshold = DEFAULT_CACHE_HIT_WARNING_THRESHOLD
|
|
21
26
|
@long_query_warning_seconds = DEFAULT_LONG_QUERY_WARNING_SECONDS
|
|
22
27
|
@idle_in_transaction_warning_seconds = DEFAULT_IDLE_IN_TRANSACTION_WARNING_SECONDS
|
|
28
|
+
@unused_index_min_stats_age = DEFAULT_UNUSED_INDEX_MIN_STATS_AGE
|
|
29
|
+
@unused_index_min_size = DEFAULT_UNUSED_INDEX_MIN_SIZE
|
|
23
30
|
@public_dashboard = false
|
|
24
31
|
end
|
|
25
32
|
|
data/lib/pg_peek/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: pg_peek
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Tomasz Mazur
|
|
@@ -78,6 +78,7 @@ files:
|
|
|
78
78
|
- app/queries/pg_peek/pg14/pg_stat_statements/endpoints.sql
|
|
79
79
|
- app/queries/pg_peek/pg14/pg_stat_statements/jobs.sql
|
|
80
80
|
- app/queries/pg_peek/pg14/pg_stat_statements/outliers.sql
|
|
81
|
+
- app/queries/pg_peek/pg14/pg_stat_statements/outliers_by_endpoint.sql
|
|
81
82
|
- app/queries/pg_peek/pg14/pg_stat_statements/outliers_by_job.sql
|
|
82
83
|
- app/queries/pg_peek/pg14/tables/dead_tuples.sql
|
|
83
84
|
- app/queries/pg_peek/pg14/tables/inline_stats.sql
|
|
@@ -90,6 +91,7 @@ files:
|
|
|
90
91
|
- app/queries/pg_peek/pg15/pg_stat_statements/endpoints.sql
|
|
91
92
|
- app/queries/pg_peek/pg15/pg_stat_statements/jobs.sql
|
|
92
93
|
- app/queries/pg_peek/pg15/pg_stat_statements/outliers.sql
|
|
94
|
+
- app/queries/pg_peek/pg15/pg_stat_statements/outliers_by_endpoint.sql
|
|
93
95
|
- app/queries/pg_peek/pg15/pg_stat_statements/outliers_by_job.sql
|
|
94
96
|
- app/queries/pg_peek/pg15/tables/dead_tuples.sql
|
|
95
97
|
- app/queries/pg_peek/pg15/tables/inline_stats.sql
|
|
@@ -102,6 +104,7 @@ files:
|
|
|
102
104
|
- app/queries/pg_peek/pg16/pg_stat_statements/endpoints.sql
|
|
103
105
|
- app/queries/pg_peek/pg16/pg_stat_statements/jobs.sql
|
|
104
106
|
- app/queries/pg_peek/pg16/pg_stat_statements/outliers.sql
|
|
107
|
+
- app/queries/pg_peek/pg16/pg_stat_statements/outliers_by_endpoint.sql
|
|
105
108
|
- app/queries/pg_peek/pg16/pg_stat_statements/outliers_by_job.sql
|
|
106
109
|
- app/queries/pg_peek/pg16/tables/dead_tuples.sql
|
|
107
110
|
- app/queries/pg_peek/pg16/tables/inline_stats.sql
|
|
@@ -114,6 +117,7 @@ files:
|
|
|
114
117
|
- app/queries/pg_peek/pg17/pg_stat_statements/endpoints.sql
|
|
115
118
|
- app/queries/pg_peek/pg17/pg_stat_statements/jobs.sql
|
|
116
119
|
- app/queries/pg_peek/pg17/pg_stat_statements/outliers.sql
|
|
120
|
+
- app/queries/pg_peek/pg17/pg_stat_statements/outliers_by_endpoint.sql
|
|
117
121
|
- app/queries/pg_peek/pg17/pg_stat_statements/outliers_by_job.sql
|
|
118
122
|
- app/queries/pg_peek/pg17/tables/dead_tuples.sql
|
|
119
123
|
- app/queries/pg_peek/pg17/tables/inline_stats.sql
|
|
@@ -126,6 +130,7 @@ files:
|
|
|
126
130
|
- app/queries/pg_peek/pg18/pg_stat_statements/endpoints.sql
|
|
127
131
|
- app/queries/pg_peek/pg18/pg_stat_statements/jobs.sql
|
|
128
132
|
- app/queries/pg_peek/pg18/pg_stat_statements/outliers.sql
|
|
133
|
+
- app/queries/pg_peek/pg18/pg_stat_statements/outliers_by_endpoint.sql
|
|
129
134
|
- app/queries/pg_peek/pg18/pg_stat_statements/outliers_by_job.sql
|
|
130
135
|
- app/queries/pg_peek/pg18/tables/dead_tuples.sql
|
|
131
136
|
- app/queries/pg_peek/pg18/tables/inline_stats.sql
|
|
@@ -134,6 +139,7 @@ files:
|
|
|
134
139
|
- app/reports/pg_peek/report.rb
|
|
135
140
|
- app/reports/pg_peek/reports/blocked.rb
|
|
136
141
|
- app/reports/pg_peek/reports/databases.rb
|
|
142
|
+
- app/reports/pg_peek/reports/endpoint_queries.rb
|
|
137
143
|
- app/reports/pg_peek/reports/endpoints.rb
|
|
138
144
|
- app/reports/pg_peek/reports/indexes.rb
|
|
139
145
|
- app/reports/pg_peek/reports/job_queries.rb
|
|
@@ -147,6 +153,7 @@ files:
|
|
|
147
153
|
- app/views/pg_peek/databases/index.html.erb
|
|
148
154
|
- app/views/pg_peek/databases/show.html.erb
|
|
149
155
|
- app/views/pg_peek/endpoints/index.html.erb
|
|
156
|
+
- app/views/pg_peek/endpoints/show.html.erb
|
|
150
157
|
- app/views/pg_peek/indexes/index.html.erb
|
|
151
158
|
- app/views/pg_peek/jobs/index.html.erb
|
|
152
159
|
- app/views/pg_peek/jobs/show.html.erb
|
|
@@ -171,6 +178,7 @@ licenses:
|
|
|
171
178
|
metadata:
|
|
172
179
|
homepage_uri: https://github.com/trix/pg_peek
|
|
173
180
|
source_code_uri: https://github.com/trix/pg_peek
|
|
181
|
+
changelog_uri: https://github.com/trix/pg_peek/releases
|
|
174
182
|
rubygems_mfa_required: 'true'
|
|
175
183
|
rdoc_options: []
|
|
176
184
|
require_paths:
|