pg_peek 0.1.1 → 0.2.1
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/javascripts/pg_peek/application.js +25 -1
- data/app/assets/stylesheets/pg_peek/application.css +48 -2
- data/app/controllers/pg_peek/databases_controller.rb +12 -10
- 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 +17 -6
- data/app/controllers/pg_peek/tables_controller.rb +11 -1
- data/app/helpers/pg_peek/application_helper.rb +27 -11
- data/app/helpers/pg_peek/report_helper.rb +32 -8
- data/app/models/pg_peek/pg_stat_statements.rb +51 -4
- data/app/models/pg_peek/sql_format.rb +74 -0
- 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 +8 -60
- data/app/views/pg_peek/endpoints/index.html.erb +14 -1
- data/app/views/pg_peek/endpoints/show.html.erb +18 -0
- data/app/views/pg_peek/jobs/index.html.erb +1 -8
- data/app/views/pg_peek/jobs/show.html.erb +2 -1
- data/app/views/pg_peek/pg_stat_statements/show.html.erb +87 -0
- data/app/views/pg_peek/tables/index.html.erb +18 -0
- data/app/views/pg_peek/tables/show.html.erb +2 -2
- data/config/routes.rb +5 -2
- 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 +12 -2
|
@@ -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
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
<li><%= link_to "queries", database_queries_path(@database) %></li>
|
|
27
27
|
<li><%= link_to "endpoints", database_endpoints_path(@database) %></li>
|
|
28
28
|
<li><%= link_to "jobs", database_jobs_path(@database) %></li>
|
|
29
|
+
<li><%= link_to "tables", database_tables_path(@database) %></li>
|
|
29
30
|
<li><%= link_to "indexes", database_indexes_path(@database) %></li>
|
|
30
31
|
<li><%= link_to "activity", database_activity_path(@database) %></li>
|
|
31
32
|
<li class="current-database">
|
|
@@ -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
|
|
|
@@ -103,64 +107,8 @@ end</code></pre>
|
|
|
103
107
|
limit: PgPeek::DatabasesController::OVERVIEW_ROWS %>
|
|
104
108
|
<% end %>
|
|
105
109
|
</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
110
|
</div>
|
|
124
111
|
|
|
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
|
-
disabled: !@pg_stat_statements.resettable?,
|
|
146
|
-
form_class: "inline",
|
|
147
|
-
form: { data: { confirm: "Reset pg_stat_statements? This clears statement statistics for every database on this server." } } %>
|
|
148
|
-
<% unless @pg_stat_statements.resettable? %>
|
|
149
|
-
<p class="dim">
|
|
150
|
-
<%= @connection_summary["username"] %> may not reset the statistics: the extension
|
|
151
|
-
allows it only to superusers and roles granted it. To allow it:
|
|
152
|
-
</p>
|
|
153
|
-
<pre><code>GRANT EXECUTE ON FUNCTION pg_stat_statements_reset TO <%= @connection_summary["username"] %>;</code></pre>
|
|
154
|
-
<% end %>
|
|
155
|
-
<% if @pg_stat_statements.outdated? %>
|
|
156
|
-
<p class="dim">
|
|
157
|
-
Version <%= @pg_stat_statements.default_version %> is available:
|
|
158
|
-
<code>ALTER EXTENSION pg_stat_statements UPDATE;</code>
|
|
159
|
-
</p>
|
|
160
|
-
<% end %>
|
|
161
|
-
</section>
|
|
162
|
-
<% end %>
|
|
163
|
-
|
|
164
112
|
<details>
|
|
165
113
|
<summary>database details</summary>
|
|
166
114
|
<pre>name: <%= @database.name %>
|
|
@@ -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,18 @@
|
|
|
1
|
+
<div class="toolbar">
|
|
2
|
+
<h1><code class="name"><%= @endpoint %></code></h1>
|
|
3
|
+
<small class="dim"><%= stats_since_link(@database, @pg_stat_statements.reset_at) %></small>
|
|
4
|
+
<%= link_to "← endpoints", database_endpoints_path(@database) %>
|
|
5
|
+
</div>
|
|
6
|
+
|
|
7
|
+
<% if @report.empty? %>
|
|
8
|
+
<article aria-label="No queries found">
|
|
9
|
+
<header>No queries found for this endpoint</header>
|
|
10
|
+
<p>This could mean:</p>
|
|
11
|
+
<ul>
|
|
12
|
+
<li>The endpoint hasn't executed any database queries since statistics were last reset</li>
|
|
13
|
+
<li>The endpoint's queries are not among the top outliers</li>
|
|
14
|
+
</ul>
|
|
15
|
+
</article>
|
|
16
|
+
<% else %>
|
|
17
|
+
<%= render "pg_peek/reports/report", report: @report %>
|
|
18
|
+
<% end %>
|
|
@@ -24,13 +24,6 @@
|
|
|
24
24
|
collecting, so this page is working — there is simply nothing in it
|
|
25
25
|
yet. Jobs appear as they execute their first database query.
|
|
26
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
27
|
</article>
|
|
35
28
|
<% end %>
|
|
36
29
|
<% else %>
|
|
@@ -45,7 +38,7 @@
|
|
|
45
38
|
<%= render "pg_peek/reports/report",
|
|
46
39
|
report: @report,
|
|
47
40
|
link_column: :job_class,
|
|
48
|
-
link: ->(row) { database_job_path(@database,
|
|
41
|
+
link: ->(row) { database_job_path(@database, row["job_class"]) } %>
|
|
49
42
|
|
|
50
43
|
<p class="dim" data-filter-empty hidden>No jobs match that filter.</p>
|
|
51
44
|
<% end %>
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
<% reset_at = @pg_stat_statements.reset_at %>
|
|
2
|
+
<% settings = @pg_stat_statements.settings %>
|
|
3
|
+
<% dealloc = @pg_stat_statements.dealloc %>
|
|
4
|
+
|
|
5
|
+
<div class="toolbar">
|
|
6
|
+
<h1>pg_stat_statements <small class="dim"><%= @pg_stat_statements.installed_version %></small></h1>
|
|
7
|
+
</div>
|
|
8
|
+
|
|
9
|
+
<section>
|
|
10
|
+
<h2>collection</h2>
|
|
11
|
+
<table>
|
|
12
|
+
<tbody>
|
|
13
|
+
<tr>
|
|
14
|
+
<th>stats since</th>
|
|
15
|
+
<td><%= reset_at.strftime("%Y-%m-%d %H:%M:%S %Z") %> (<%= format_time_ago(reset_at) %>)</td>
|
|
16
|
+
</tr>
|
|
17
|
+
<tr>
|
|
18
|
+
<th>statements tracked</th>
|
|
19
|
+
<td>
|
|
20
|
+
<%= number_with_delimiter(@pg_stat_statements.server_statement_count) %>
|
|
21
|
+
<% if settings["max"] %>of <%= number_with_delimiter(settings["max"].to_i) %><% end %>
|
|
22
|
+
<span class="dim">(this database: <%= number_with_delimiter(@pg_stat_statements.statement_count) %>)</span>
|
|
23
|
+
</td>
|
|
24
|
+
</tr>
|
|
25
|
+
<tr>
|
|
26
|
+
<th>dropped when full</th>
|
|
27
|
+
<td><%= number_with_delimiter(dealloc) %></td>
|
|
28
|
+
</tr>
|
|
29
|
+
</tbody>
|
|
30
|
+
</table>
|
|
31
|
+
<% if dealloc.positive? %>
|
|
32
|
+
<p class="dim">
|
|
33
|
+
The server dropped statements to make room since the last reset, so every
|
|
34
|
+
total here undercounts. Raise <code>pg_stat_statements.max</code>
|
|
35
|
+
(needs a server restart) or reset more often.
|
|
36
|
+
</p>
|
|
37
|
+
<% end %>
|
|
38
|
+
</section>
|
|
39
|
+
|
|
40
|
+
<section>
|
|
41
|
+
<h2>settings</h2>
|
|
42
|
+
<table>
|
|
43
|
+
<tbody>
|
|
44
|
+
<% settings.each do |name, value| %>
|
|
45
|
+
<tr>
|
|
46
|
+
<th><code>pg_stat_statements.<%= name %></code></th>
|
|
47
|
+
<td><%= value %></td>
|
|
48
|
+
</tr>
|
|
49
|
+
<% end %>
|
|
50
|
+
</tbody>
|
|
51
|
+
</table>
|
|
52
|
+
<% if @pg_stat_statements.outdated? %>
|
|
53
|
+
<p class="dim">
|
|
54
|
+
Version <%= @pg_stat_statements.default_version %> is available:
|
|
55
|
+
<code>ALTER EXTENSION pg_stat_statements UPDATE;</code>
|
|
56
|
+
</p>
|
|
57
|
+
<% end %>
|
|
58
|
+
</section>
|
|
59
|
+
|
|
60
|
+
<section id="reset">
|
|
61
|
+
<h2>reset</h2>
|
|
62
|
+
<p class="dim">
|
|
63
|
+
Queries, endpoints and jobs are cumulative since the last reset. A reset
|
|
64
|
+
starts a fresh window: exact counts and attribution, at the price of the
|
|
65
|
+
history so far. Restarting PostgreSQL clears them too.
|
|
66
|
+
<% peers = @database.server_peers %>
|
|
67
|
+
<% if peers.any? %>
|
|
68
|
+
It also clears <%= peers.map(&:name).to_sentence %>, which
|
|
69
|
+
<%= peers.one? ? "shares" : "share" %> this server: PostgreSQL keeps
|
|
70
|
+
one reset time for all of them.
|
|
71
|
+
<% end %>
|
|
72
|
+
</p>
|
|
73
|
+
<%= button_to "reset statistics",
|
|
74
|
+
reset_database_pg_stat_statements_path(@database),
|
|
75
|
+
method: :delete,
|
|
76
|
+
class: "danger",
|
|
77
|
+
disabled: !@pg_stat_statements.resettable?,
|
|
78
|
+
form_class: "inline",
|
|
79
|
+
form: { data: { confirm: "Reset pg_stat_statements? This clears statement statistics for every database on this server." } } %>
|
|
80
|
+
<% unless @pg_stat_statements.resettable? %>
|
|
81
|
+
<p class="dim">
|
|
82
|
+
<%= @connection_summary["username"] %> may not reset the statistics: the extension
|
|
83
|
+
allows it only to superusers and roles granted it. To allow it:
|
|
84
|
+
</p>
|
|
85
|
+
<pre><code>GRANT EXECUTE ON FUNCTION pg_stat_statements_reset TO <%= @connection_summary["username"] %>;</code></pre>
|
|
86
|
+
<% end %>
|
|
87
|
+
</section>
|