pgdexter 0.1.4 → 0.1.5

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: d08be58d7de049df2c11f33211c0ada165f4a363
4
- data.tar.gz: ce17e302b12f3b7babe38de9618b76bab982b9d9
3
+ metadata.gz: fbc105f249fc27a10fcd47fa16ba9d732f0cd933
4
+ data.tar.gz: 20929a8fd1d268194b9bc2c7bab71130a2eabc43
5
5
  SHA512:
6
- metadata.gz: 7e0974f0f8c39ec8039749a75a3fb61f7613c6931db15f191eba2dda2a794cf57e8875f6aff4465474a9c608727deef08fd7a5833e16ba96e2d1d5a02e3e7f7b
7
- data.tar.gz: 73668e8c819b4334888bec707db491453ab2cc03a39e189e451738bb7606b8fe329912e2520d6e2b6b7aeb9634a8e2200b267887257c26428bf453fde74cc54e
6
+ metadata.gz: b01d8e32db41bdf7fa0dd2412b4b490318d8574fbfba5ed0357f1aa9f3e8dcc2e9a0fbef9ee80376678c184f8840a5bf5e52fef5b325cdbffb2b31106d5c2838
7
+ data.tar.gz: 2e9808dba0e161eb184ddc180ee915f0c49097225b2539a11916eaf5dd32e9bedce8ef07763391f758d6f4627258c90e6af4f7c158b473323d398050e64a9f75
data/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ ## 0.1.5
2
+
3
+ - Added support for non-`SELECT` queries
4
+ - Added `--pg-stat-statements` option
5
+ - Added advisory locks
6
+ - Added support for running as a non-superuser
7
+
1
8
  ## 0.1.4
2
9
 
3
10
  - Added support for multicolumn indexes
data/README.md CHANGED
@@ -37,7 +37,7 @@ The command line tool is also available as a [Linux package](guides/Linux.md).
37
37
  Dexter needs a connection to your database and a log file to process.
38
38
 
39
39
  ```sh
40
- tail -F -n +1 <log-file> | dexter <database-url>
40
+ tail -F -n +1 <log-file> | dexter <connection-string>
41
41
  ```
42
42
 
43
43
  This finds slow queries and generates output like:
@@ -62,12 +62,18 @@ To be safe, Dexter will not create indexes unless you pass the `--create` flag.
62
62
  2017-06-25T17:52:37+00:00 Index created: 15243 ms
63
63
  ```
64
64
 
65
- ## Single Statement Mode
65
+ ## Connection String
66
66
 
67
- You can also pass a single statement with:
67
+ The connection string is a URI with the format:
68
68
 
69
- ```sh
70
- dexter <database-url> -s "SELECT * FROM ..."
69
+ ```
70
+ postgresql://user:pass@host:5432/dbname
71
+ ```
72
+
73
+ To connect through a socket, just pass the database name.
74
+
75
+ ```
76
+ dbname
71
77
  ```
72
78
 
73
79
  ## Options
@@ -80,6 +86,28 @@ log-level | `debug` gives additional info for suggested indexes<br />`debug2` gi
80
86
  log-sql | log SQL statements executed | false
81
87
  min-time | only process queries consuming a min amount of DB time, in minutes | 0
82
88
 
89
+ ## Single Statement Mode
90
+
91
+ You can pass a single statement with:
92
+
93
+ ```sh
94
+ dexter <connection-string> -s "SELECT * FROM ..."
95
+ ```
96
+
97
+ ## Examples
98
+
99
+ Ubuntu with PostgreSQL 9.6
100
+
101
+ ```sh
102
+ tail -F -n +1 /var/log/postgresql/postgresql-9.6-main.log | sudo -u postgres dexter dbname
103
+ ```
104
+
105
+ Homebrew on Mac
106
+
107
+ ```sh
108
+ tail -F -n +1 /usr/local/var/postgres/server.log | dexter dbname
109
+ ```
110
+
83
111
  ## Future Work
84
112
 
85
113
  [Here are some ideas](https://github.com/ankane/dexter/issues/1)
data/guides/Linux.md CHANGED
@@ -11,8 +11,9 @@ Distributions
11
11
  ### Ubuntu 16.04 (Xenial)
12
12
 
13
13
  ```sh
14
- wget -qO - https://deb.packager.io/key | sudo apt-key add -
15
- echo "deb https://deb.packager.io/gh/pghero/dexter xenial master" | sudo tee /etc/apt/sources.list.d/dexter.list
14
+ wget -qO- https://dl.packager.io/srv/pghero/dexter/key | sudo apt-key add -
15
+ sudo wget -O /etc/apt/sources.list.d/dexter.list \
16
+ https://dl.packager.io/srv/pghero/dexter/master/installer/ubuntu/16.04.repo
16
17
  sudo apt-get update
17
18
  sudo apt-get -y install dexter
18
19
  ```
@@ -20,8 +21,9 @@ sudo apt-get -y install dexter
20
21
  ### Ubuntu 14.04 (Trusty)
21
22
 
22
23
  ```sh
23
- wget -qO - https://deb.packager.io/key | sudo apt-key add -
24
- echo "deb https://deb.packager.io/gh/pghero/dexter trusty master" | sudo tee /etc/apt/sources.list.d/dexter.list
24
+ wget -qO- https://dl.packager.io/srv/pghero/dexter/key | sudo apt-key add -
25
+ sudo wget -O /etc/apt/sources.list.d/dexter.list \
26
+ https://dl.packager.io/srv/pghero/dexter/master/installer/ubuntu/14.04.repo
25
27
  sudo apt-get update
26
28
  sudo apt-get install dexter
27
29
  ```
@@ -29,8 +31,9 @@ sudo apt-get install dexter
29
31
  ### Debian 8 (Jesse)
30
32
 
31
33
  ```sh
32
- wget -qO - https://deb.packager.io/key | sudo apt-key add -
33
- echo "deb https://deb.packager.io/gh/pghero/dexter jessie master" | sudo tee /etc/apt/sources.list.d/dexter.list
34
+ wget -qO- https://dl.packager.io/srv/pghero/dexter/key | sudo apt-key add -
35
+ sudo wget -O /etc/apt/sources.list.d/dexter.list \
36
+ https://dl.packager.io/srv/pghero/dexter/master/installer/debian/8.repo
34
37
  sudo apt-get update
35
38
  sudo apt-get install dexter
36
39
  ```
@@ -38,19 +41,16 @@ sudo apt-get install dexter
38
41
  ### CentOS / RHEL 7
39
42
 
40
43
  ```sh
41
- sudo rpm --import https://rpm.packager.io/key
42
- echo "[dexter]
43
- name=Repository for pghero/dexter application.
44
- baseurl=https://rpm.packager.io/gh/pghero/dexter/centos7/master
45
- enabled=1" | sudo tee /etc/yum.repos.d/dexter.repo
44
+ sudo wget -O /etc/yum.repos.d/dexter.repo \
45
+ https://dl.packager.io/srv/pghero/dexter/master/installer/el/7.repo
46
46
  sudo yum install dexter
47
47
  ```
48
48
 
49
49
  ### SUSE Linux Enterprise Server 12
50
50
 
51
51
  ```sh
52
- sudo rpm --import https://rpm.packager.io/key
53
- sudo zypper addrepo "https://rpm.packager.io/gh/pghero/dexter/sles12/master" "dexter"
52
+ sudo wget -O /etc/zypp/repos.d/dexter.repo \
53
+ https://dl.packager.io/srv/pghero/dexter/master/installer/sles/12.repo
54
54
  sudo zypper install dexter
55
55
  ```
56
56
 
data/lib/dexter/client.rb CHANGED
@@ -11,9 +11,10 @@ module Dexter
11
11
  STDERR.sync = true
12
12
 
13
13
  if options[:statement]
14
- fingerprint = PgQuery.fingerprint(options[:statement]) rescue "unknown"
15
- query = Query.new(options[:statement], fingerprint)
14
+ query = Query.new(options[:statement])
16
15
  Indexer.new(arguments[0], options).process_queries([query])
16
+ elsif options[:pg_stat_statements]
17
+ Indexer.new(arguments[0], options).process_stat_statements
17
18
  elsif arguments[1]
18
19
  Processor.new(arguments[0], arguments[1], options).perform
19
20
  else
@@ -31,6 +32,7 @@ Options:)
31
32
  o.array "--exclude", "prevent specific tables from being indexed"
32
33
  o.integer "--interval", "time to wait between processing queries, in seconds", default: 60
33
34
  o.float "--min-time", "only process queries that have consumed a certain amount of DB time, in minutes", default: 0
35
+ o.boolean "--pg-stat-statements", "use pg_stat_statements", default: false, help: false
34
36
  o.boolean "--log-explain", "log explain", default: false, help: false
35
37
  o.string "--log-level", "log level", default: "info"
36
38
  o.boolean "--log-sql", "log sql", default: false
@@ -9,8 +9,15 @@ module Dexter
9
9
  @exclude_tables = options[:exclude]
10
10
  @log_sql = options[:log_sql]
11
11
  @log_explain = options[:log_explain]
12
+ @min_time = options[:min_time] || 0
12
13
 
13
- create_extension
14
+ create_extension unless extension_exists?
15
+ end
16
+
17
+ def process_stat_statements
18
+ queries = stat_statements.map { |q| Query.new(q) }.sort_by(&:fingerprint).group_by(&:fingerprint).map { |_, v| v.first }
19
+ log "Processing #{queries.size} new query fingerprints"
20
+ process_queries(queries)
14
21
  end
15
22
 
16
23
  def process_queries(queries)
@@ -46,7 +53,15 @@ module Dexter
46
53
 
47
54
  def create_extension
48
55
  execute("SET client_min_messages = warning")
49
- execute("CREATE EXTENSION IF NOT EXISTS hypopg")
56
+ begin
57
+ execute("CREATE EXTENSION IF NOT EXISTS hypopg")
58
+ rescue PG::InsufficientPrivilege
59
+ abort "Use a superuser to run: CREATE EXTENSION hypopg"
60
+ end
61
+ end
62
+
63
+ def extension_exists?
64
+ execute("SELECT * FROM pg_available_extensions WHERE name = 'hypopg' AND installed_version IS NOT NULL").any?
50
65
  end
51
66
 
52
67
  def reset_hypothetical_indexes
@@ -208,17 +223,20 @@ module Dexter
208
223
  end
209
224
 
210
225
  if @create
211
- # TODO use advisory locks
212
226
  # 1. create lock
213
227
  # 2. refresh existing index list
214
228
  # 3. create indexes that still don't exist
215
229
  # 4. release lock
216
- new_indexes.each do |index|
217
- statement = "CREATE INDEX CONCURRENTLY ON #{quote_ident(index[:table])} (#{index[:columns].map { |c| quote_ident(c) }.join(", ")})"
218
- log "Creating index: #{statement}"
219
- started_at = Time.now
220
- execute(statement)
221
- log "Index created: #{((Time.now - started_at) * 1000).to_i} ms"
230
+ with_advisory_lock do
231
+ new_indexes.each do |index|
232
+ unless index_exists?(index)
233
+ statement = "CREATE INDEX CONCURRENTLY ON #{quote_ident(index[:table])} (#{index[:columns].map { |c| quote_ident(c) }.join(", ")})"
234
+ log "Creating index: #{statement}"
235
+ started_at = Time.now
236
+ execute(statement)
237
+ log "Index created: #{((Time.now - started_at) * 1000).to_i} ms"
238
+ end
239
+ end
222
240
  end
223
241
  end
224
242
  else
@@ -242,7 +260,7 @@ module Dexter
242
260
  PG::Connection.new(config)
243
261
  end
244
262
  rescue PG::ConnectionBad
245
- abort "Bad database url"
263
+ abort "Can't connect to database"
246
264
  end
247
265
 
248
266
  def execute(query)
@@ -284,14 +302,60 @@ module Dexter
284
302
  WHERE
285
303
  table_catalog = current_database() AND
286
304
  table_schema NOT IN ('pg_catalog', 'information_schema')
305
+ AND table_type = 'BASE TABLE'
287
306
  SQL
288
307
  result.map { |r| r["table_name"] }
289
308
  end
290
309
 
310
+ def stat_statements
311
+ result = execute <<-SQL
312
+ SELECT
313
+ DISTINCT query
314
+ FROM
315
+ pg_stat_statements
316
+ INNER JOIN
317
+ pg_database ON pg_database.oid = pg_stat_statements.dbid
318
+ WHERE
319
+ datname = current_database()
320
+ AND total_time >= #{@min_time * 60000}
321
+ ORDER BY
322
+ 1
323
+ SQL
324
+ result.map { |q| q["query"] }
325
+ end
326
+
291
327
  def possible_tables(queries)
292
328
  Set.new(queries.flat_map(&:tables).uniq & database_tables)
293
329
  end
294
330
 
331
+ def with_advisory_lock
332
+ lock_id = 123456
333
+ first_time = true
334
+ while execute("SELECT pg_try_advisory_lock(#{lock_id})").first["pg_try_advisory_lock"] != "t"
335
+ if first_time
336
+ log "Waiting for lock..."
337
+ first_time = false
338
+ end
339
+ sleep(1)
340
+ end
341
+ yield
342
+ ensure
343
+ with_min_messages("error") do
344
+ execute("SELECT pg_advisory_unlock(#{lock_id})")
345
+ end
346
+ end
347
+
348
+ def with_min_messages(value)
349
+ execute("SET client_min_messages = #{quote(value)}")
350
+ yield
351
+ ensure
352
+ execute("SET client_min_messages = warning")
353
+ end
354
+
355
+ def index_exists?(index)
356
+ indexes([index[:table]]).find { |i| i["columns"] == index[:columns] }
357
+ end
358
+
295
359
  def columns(tables)
296
360
  columns = execute <<-SQL
297
361
  SELECT
@@ -49,7 +49,7 @@ module Dexter
49
49
  end
50
50
 
51
51
  def process_entry(query, duration)
52
- @collector.add(query, duration) if query =~ /SELECT/i
52
+ @collector.add(query, duration)
53
53
  end
54
54
  end
55
55
  end
data/lib/dexter/query.rb CHANGED
@@ -3,8 +3,11 @@ module Dexter
3
3
  attr_reader :statement, :fingerprint, :plans
4
4
  attr_accessor :missing_tables, :new_cost
5
5
 
6
- def initialize(statement, fingerprint)
6
+ def initialize(statement, fingerprint = nil)
7
7
  @statement = statement
8
+ unless fingerprint
9
+ fingerprint = PgQuery.fingerprint(statement) rescue "unknown"
10
+ end
8
11
  @fingerprint = fingerprint
9
12
  @plans = []
10
13
  end
@@ -1,3 +1,3 @@
1
1
  module Dexter
2
- VERSION = "0.1.4"
2
+ VERSION = "0.1.5"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pgdexter
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.4
4
+ version: 0.1.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Kane
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2017-07-03 00:00:00.000000000 Z
11
+ date: 2017-08-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: slop