pgdexter 0.1.4 → 0.1.5

Sign up to get free protection for your applications and to get access to all the features.
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