pghero 1.0.1 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of pghero might be problematic. Click here for more details.

checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 29a21b5bab969343ade74d9df1643ac808cf81c5
4
- data.tar.gz: cbfa3d06c92ec558cb28733fcfdf3a74ad56d295
3
+ metadata.gz: 1ed028006b69e4e3595170baffc6fb328de3cd3f
4
+ data.tar.gz: 05f3f70425c1506f3ba7e6106e1be4922b256fc6
5
5
  SHA512:
6
- metadata.gz: 9b42030927945b2e8a7e6fc802ab74e65a4afaf101e3ba21e3d8f35346bbae1184a4fba4e07d774d591351ca86925a3089d92966ea68f5263a6694dcbbcea7f5
7
- data.tar.gz: b44897c9ee84a0622c059f74254f0f15380ce6dba24bbe007a20cf037e884ace4255546c4b3c978edc769e6e96a134aaaa0425950c0963fae7903f632dd0c8f2
6
+ metadata.gz: a7d7323653811a623f0a7843fd5aaec408570f96909a7ccec71f2aaae343619f67eb8ff81878f1f86cc035f9a1cc2c20539470025136ac004765aa0d67c6e75b
7
+ data.tar.gz: 951cf7cc4359047ec384905e6dcbd0e8f6de9c205a32a09c399a38191f9edcf10492b07b05f8af6a80948d1366c34ca9edc7c7f26b20812fc39f7f8b90f43e9a
data/CHANGELOG.md CHANGED
@@ -1,3 +1,7 @@
1
+ ## 1.1.0
2
+
3
+ - Added historical query stats
4
+
1
5
  ## 1.0.1
2
6
 
3
7
  - Fixed connection bad errors
@@ -11,7 +11,7 @@ module PgHero
11
11
 
12
12
  def index
13
13
  @title = "Overview"
14
- @slow_queries = PgHero.slow_queries
14
+ @slow_queries = PgHero.slow_queries(historical: true, start_at: 3.hours.ago)
15
15
  @long_running_queries = PgHero.long_running_queries
16
16
  @index_hit_rate = PgHero.index_hit_rate
17
17
  @table_hit_rate = PgHero.table_hit_rate
@@ -46,7 +46,28 @@ module PgHero
46
46
 
47
47
  def queries
48
48
  @title = "Queries"
49
- @query_stats = PgHero.query_stats
49
+ @historical_query_stats_enabled = PgHero.historical_query_stats_enabled?
50
+
51
+ @query_stats =
52
+ begin
53
+ if @historical_query_stats_enabled
54
+ @start_at = params[:start_at] ? Time.zone.parse(params[:start_at]) : 24.hours.ago
55
+ @end_at = Time.zone.parse(params[:end_at]) if params[:end_at]
56
+ end
57
+
58
+ if @historical_query_stats_enabled && !request.xhr?
59
+ []
60
+ else
61
+ PgHero.query_stats(historical: true, start_at: @start_at, end_at: @end_at)
62
+ end
63
+ rescue
64
+ @error = true
65
+ []
66
+ end
67
+
68
+ if request.xhr?
69
+ render layout: false, partial: "queries_table", locals: {queries: @query_stats, xhr: true}
70
+ end
50
71
  end
51
72
 
52
73
  def system
@@ -120,6 +120,27 @@
120
120
  margin-bottom: 0;
121
121
  }
122
122
 
123
+ #slider-container {
124
+ padding: 6px 140px 20px 140px;
125
+ }
126
+
127
+ #slider {
128
+ margin-bottom: 20px;
129
+ }
130
+
131
+ #range-start {
132
+ min-height: 20px;
133
+ }
134
+
135
+ #range-end {
136
+ float: right;
137
+ }
138
+
139
+ .queries-info {
140
+ text-align: center;
141
+ margin-top: 40px;
142
+ }
143
+
123
144
  /* nav */
124
145
 
125
146
  .nav a {
@@ -137,6 +158,10 @@
137
158
  background-color: #ddd;
138
159
  }
139
160
 
161
+ .nav li.active-database a {
162
+ color: #999;
163
+ }
164
+
140
165
  .nav-header {
141
166
  font-weight: bold;
142
167
  color: #333;
@@ -391,11 +416,11 @@
391
416
  </ul>
392
417
 
393
418
  <% if @databases.size > 1 %>
394
- <p class="nav-header">Other Databases</p>
419
+ <p class="nav-header">Databases</p>
395
420
  <ul class="nav">
396
- <% (@databases - [PgHero.current_database]).each do |database| %>
397
- <li>
398
- <%= link_to database.titleize, root_path(database: database) %>
421
+ <% @databases.each do |database| %>
422
+ <li class="<%= ("active-database" if PgHero.current_database == database) %>">
423
+ <%= link_to database.titleize, database: database %>
399
424
  </li>
400
425
  <% end %>
401
426
  </ul>
@@ -1,12 +1,27 @@
1
1
  <table class="table">
2
- <thead>
3
- <tr>
4
- <th style="width: 33.33%;">Total Time</th>
5
- <th style="width: 33.33%;">Average Time</th>
6
- <th style="width: 33.33%;">Calls</th>
7
- </tr>
8
- </thead>
9
- <tbody>
2
+ <% unless local_assigns[:xhr] %>
3
+ <thead>
4
+ <tr>
5
+ <th style="width: 33.33%;">Total Time</th>
6
+ <th style="width: 33.33%;">Average Time</th>
7
+ <th style="width: 33.33%;">Calls</th>
8
+ </tr>
9
+ </thead>
10
+ <% end %>
11
+ <tbody id="queries">
12
+ <% if queries.empty? %>
13
+ <tr>
14
+ <td colspan="3">
15
+ <p class="queries-info text-muted">
16
+ <% if local_assigns[:xhr] %>
17
+ No data available for this time.
18
+ <% else %>
19
+ ...
20
+ <% end %>
21
+ </p>
22
+ </td>
23
+ </tr>
24
+ <% end %>
10
25
  <% queries.each do |query| %>
11
26
  <tr>
12
27
  <td>
@@ -0,0 +1,129 @@
1
+ <div id="slider-container">
2
+ <div id="slider"></div>
3
+ <div id="range-end"></div>
4
+ <div id="range-start"></div>
5
+ </div>
6
+
7
+ <%= stylesheet_link_tag "https://cdnjs.cloudflare.com/ajax/libs/noUiSlider/6.2.0/jquery.nouislider.min.css" %>
8
+ <%= javascript_include_tag "//ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js", "https://cdnjs.cloudflare.com/ajax/libs/noUiSlider/6.2.0/jquery.nouislider.min.js" %>
9
+
10
+ <style>
11
+ .noUi-connect {
12
+ box-shadow: none;
13
+ background: #5bc0de;
14
+ }
15
+
16
+ .noUi-handle {
17
+ box-shadow: none;
18
+ }
19
+
20
+ .noUi-target {
21
+ box-shadow: none;
22
+ border-color: #eee;
23
+ }
24
+
25
+ .noUi-background {
26
+ box-shadow: none;
27
+ background-color: #eee;
28
+ }
29
+
30
+ .noUi-origin {
31
+ border-radius: 0;
32
+ }
33
+ </style>
34
+
35
+ <script>
36
+ function roundTime(time) {
37
+ var period = 1000 * 60 * 5;
38
+ return new Date(Math.ceil(time.getTime() / period) * period);
39
+ }
40
+
41
+ function pad(num) {
42
+ return (num < 10) ? "0" + num : num;
43
+ }
44
+
45
+ var months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
46
+
47
+ var days = 1;
48
+ var now = new Date();
49
+ var sliderStartAt = roundTime(now) - days * 24 * 60 * 60 * 1000;
50
+ var sliderMax = 24 * 12 * days;
51
+
52
+ var startAt = <%= @start_at.to_i %> * 1000;
53
+ var min = (startAt > 0) ? (startAt - sliderStartAt) / (1000 * 60 * 5) : 0;
54
+
55
+ var endAt = <%= @end_at.to_i %> * 1000;
56
+ var max = (endAt > 0) ? (endAt - sliderStartAt) / (1000 * 60 * 5) : sliderMax;
57
+
58
+ var $slider = $("#slider");
59
+
60
+ $slider.noUiSlider({
61
+ range: {
62
+ min: 0,
63
+ max: sliderMax
64
+ },
65
+ step: 1,
66
+ connect: true,
67
+ start: [min, max]
68
+ });
69
+
70
+ function updateText() {
71
+ var values = $slider.val();
72
+ setText("#range-start", values[0]);
73
+ setText("#range-end", values[1]);
74
+ }
75
+
76
+ function setText(selector, offset) {
77
+ var time = timeAt(offset);
78
+
79
+ var html = "";
80
+ if (time == now) {
81
+ if (selector == "#range-end") {
82
+ html = "Now";
83
+ }
84
+ } else {
85
+ html = months[time.getMonth()] + " " + time.getDate() + ", " + pad(time.getHours()) + ":" + pad(time.getMinutes());
86
+ }
87
+ $(selector).html(html);
88
+ }
89
+
90
+ function timeAt(offset) {
91
+ var time = new Date(sliderStartAt + (offset * 5) * 60 * 1000);
92
+ return (time > now) ? now : time;
93
+ }
94
+
95
+ function timeParam(time) {
96
+ return time.toISOString();
97
+ }
98
+
99
+ function refreshStats(push) {
100
+ var values = $slider.val();
101
+ var startAt = timeAt(values[0]);
102
+ var endAt = timeAt(values[1]);
103
+
104
+ var params = {}
105
+ if (startAt > sliderStartAt) {
106
+ params.start_at = timeParam(startAt);
107
+ }
108
+ if (endAt < now) {
109
+ params.end_at = timeParam(endAt);
110
+ }
111
+
112
+ var path = "queries";
113
+ if (params.start_at || params.end_at) {
114
+ path += "?" + $.param(params);
115
+ }
116
+
117
+ $("#queries").html('<tr><td colspan="3"><p class="queries-info text-muted">...</p></td></tr>').load(path);
118
+
119
+ if (push && history.pushState) {
120
+ history.pushState(null, null, path);
121
+ }
122
+ }
123
+
124
+ $slider.on("slide", updateText).on("change", function () {
125
+ refreshStats(true);
126
+ });
127
+ updateText();
128
+ $(refreshStats);
129
+ </script>
@@ -3,10 +3,16 @@
3
3
  <%= button_to "Reset", reset_query_stats_path, class: "btn btn-danger", style: "float: right;" %>
4
4
  <% end %>
5
5
 
6
- <h1>Queries</h1>
6
+ <h1 style="float: left;">Queries</h1>
7
+
8
+ <% if @historical_query_stats_enabled %>
9
+ <%= render partial: "query_stats_slider" %>
10
+ <% end %>
7
11
 
8
12
  <% if @query_stats_enabled %>
9
- <% if @query_stats.any? %>
13
+ <% if @error %>
14
+ <div class="alert alert-danger">Cannot understand start or end time.</div>
15
+ <% elsif @query_stats.any? || @historical_query_stats_enabled %>
10
16
  <%= render partial: "queries_table", locals: {queries: @query_stats} %>
11
17
  <% else %>
12
18
  <p>Stats are not available yet. Come back soon!</p>
data/guides/Rails.md CHANGED
@@ -118,6 +118,35 @@ end
118
118
 
119
119
  Query stats can be enabled from the dashboard. If you run into issues, [view the guide](Query-Stats.md).
120
120
 
121
+ ## Historical Query Stats
122
+
123
+ To track query stats over time, run:
124
+
125
+ ```sh
126
+ rails generate pghero:query_stats
127
+ rake db:migrate
128
+ ```
129
+
130
+ And schedule the task below to run every 5 minutes.
131
+
132
+ ```sh
133
+ rake pghero:capture_query_stats
134
+ ```
135
+
136
+ Or with a scheduler like Clockwork, use:
137
+
138
+ ```ruby
139
+ PgHero.capture_query_stats
140
+ ```
141
+
142
+ After this, a time range slider will appear on the Queries tab.
143
+
144
+ By default, historical query stats are stored in your primary database. Change this with:
145
+
146
+ ```ruby
147
+ ENV["PGHERO_STATS_DATABASE_URL"]
148
+ ```
149
+
121
150
  ## System Stats
122
151
 
123
152
  CPU usage is available for Amazon RDS. Add these lines to your application’s Gemfile:
@@ -154,6 +183,12 @@ production:
154
183
  <<: *default
155
184
  ```
156
185
 
186
+ Specify a database with:
187
+
188
+ ```ruby
189
+ PgHero.with(:replica) { PgHero.running_queries }
190
+ ```
191
+
157
192
  ## Customize
158
193
 
159
194
  Minimum time for long running queries
@@ -0,0 +1,29 @@
1
+ # taken from https://github.com/collectiveidea/audited/blob/master/lib/generators/audited/install_generator.rb
2
+ require "rails/generators"
3
+ require "rails/generators/migration"
4
+ require "active_record"
5
+ require "rails/generators/active_record"
6
+
7
+ module Pghero
8
+ module Generators
9
+ class QueryStatsGenerator < Rails::Generators::Base
10
+ include Rails::Generators::Migration
11
+
12
+ source_root File.expand_path("../templates", __FILE__)
13
+
14
+ # Implement the required interface for Rails::Generators::Migration.
15
+ def self.next_migration_number(dirname) #:nodoc:
16
+ next_migration_number = current_migration_number(dirname) + 1
17
+ if ::ActiveRecord::Base.timestamped_migrations
18
+ [Time.now.utc.strftime("%Y%m%d%H%M%S"), "%.14d" % next_migration_number].max
19
+ else
20
+ "%.3d" % next_migration_number
21
+ end
22
+ end
23
+
24
+ def copy_migration
25
+ migration_template "query_stats.rb", "db/migrate/create_pghero_query_stats.rb"
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,13 @@
1
+ class <%= migration_class_name %> < ActiveRecord::Migration
2
+ def change
3
+ create_table :pghero_query_stats do |t|
4
+ t.text :database
5
+ t.text :query
6
+ t.float :total_time
7
+ t.integer :calls, limit: 8
8
+ t.timestamp :captured_at
9
+ end
10
+
11
+ add_index :pghero_query_stats, [:database, :captured_at]
12
+ end
13
+ end
data/lib/pghero.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  require "pghero/version"
2
2
  require "active_record"
3
3
  require "pghero/engine" if defined?(Rails)
4
+ require "pghero/tasks"
4
5
 
5
6
  module PgHero
6
7
  # hack for connection
@@ -8,6 +9,12 @@ module PgHero
8
9
  self.abstract_class = true
9
10
  end
10
11
 
12
+ class QueryStats < ActiveRecord::Base
13
+ self.abstract_class = true
14
+ self.table_name = "pghero_query_stats"
15
+ establish_connection ENV["PGHERO_STATS_DATABASE_URL"] if ENV["PGHERO_STATS_DATABASE_URL"]
16
+ end
17
+
11
18
  class << self
12
19
  attr_accessor :long_running_query_sec, :slow_query_ms, :slow_query_calls, :total_connections_threshold, :env
13
20
  end
@@ -33,7 +40,7 @@ module PgHero
33
40
  {
34
41
  "databases" => {
35
42
  "primary" => {
36
- "url" => ENV["PGHERO_DATABASE_URL"], # nil reverts to default config
43
+ "url" => ENV["PGHERO_DATABASE_URL"] || ActiveRecord::Base.connection_config,
37
44
  "db_instance_identifier" => ENV["PGHERO_DB_INSTANCE_IDENTIFIER"]
38
45
  }
39
46
  }
@@ -301,74 +308,28 @@ module PgHero
301
308
  true
302
309
  end
303
310
 
304
- # http://www.craigkerstiens.com/2013/01/10/more-on-postgres-performance/
305
- def query_stats
306
- if query_stats_enabled?
307
- select_all <<-SQL
308
- WITH query_stats AS (
309
- SELECT
310
- query,
311
- (total_time / 1000 / 60) as total_minutes,
312
- (total_time / calls) as average_time,
313
- calls
314
- FROM
315
- pg_stat_statements
316
- INNER JOIN
317
- pg_database ON pg_database.oid = pg_stat_statements.dbid
318
- WHERE
319
- pg_database.datname = current_database()
320
- )
321
- SELECT
322
- query,
323
- total_minutes,
324
- average_time,
325
- calls,
326
- total_minutes * 100.0 / (SELECT SUM(total_minutes) FROM query_stats) AS total_percent
327
- FROM
328
- query_stats
329
- ORDER BY
330
- total_minutes DESC
331
- LIMIT 100
332
- SQL
333
- else
334
- []
311
+ def query_stats(options = {})
312
+ current_query_stats = (options[:historical] && options[:end_at] && options[:end_at] < Time.now ? [] : current_query_stats(options)).index_by { |q| q["query"] }
313
+ historical_query_stats = (options[:historical] ? historical_query_stats(options) : []).index_by { |q| q["query"] }
314
+ current_query_stats.default = {}
315
+ historical_query_stats.default = {}
316
+
317
+ query_stats = []
318
+ (current_query_stats.keys + historical_query_stats.keys).uniq.each do |query|
319
+ value = {
320
+ "query" => query,
321
+ "total_minutes" => current_query_stats[query]["total_minutes"].to_f + historical_query_stats[query]["total_minutes"].to_f,
322
+ "calls" => current_query_stats[query]["calls"].to_i + historical_query_stats[query]["calls"].to_i
323
+ }
324
+ value["average_time"] = value["total_minutes"] * 1000 * 60 / value["calls"]
325
+ value["total_percent"] = value["total_minutes"] * 100.0 / (current_query_stats[query]["all_queries_total_minutes"].to_f + historical_query_stats[query]["all_queries_total_minutes"].to_f)
326
+ query_stats << value
335
327
  end
328
+ query_stats.sort_by { |q| -q["total_minutes"] }.first(100)
336
329
  end
337
330
 
338
- def slow_queries
339
- if query_stats_enabled?
340
- select_all <<-SQL
341
- WITH query_stats AS (
342
- SELECT
343
- query,
344
- (total_time / 1000 / 60) as total_minutes,
345
- (total_time / calls) as average_time,
346
- calls
347
- FROM
348
- pg_stat_statements
349
- INNER JOIN
350
- pg_database ON pg_database.oid = pg_stat_statements.dbid
351
- WHERE
352
- pg_database.datname = current_database()
353
- )
354
- SELECT
355
- query,
356
- total_minutes,
357
- average_time,
358
- calls,
359
- total_minutes * 100.0 / (SELECT SUM(total_minutes) FROM query_stats) AS total_percent
360
- FROM
361
- query_stats
362
- WHERE
363
- calls >= #{slow_query_calls.to_i}
364
- AND average_time >= #{slow_query_ms.to_i}
365
- ORDER BY
366
- total_minutes DESC
367
- LIMIT 100
368
- SQL
369
- else
370
- []
371
- end
331
+ def slow_queries(options = {})
332
+ query_stats(options).select { |q| q["calls"].to_i >= slow_query_calls.to_i && q["average_time"].to_i >= slow_query_ms.to_i }
372
333
  end
373
334
 
374
335
  def query_stats_available?
@@ -403,6 +364,53 @@ module PgHero
403
364
  end
404
365
  end
405
366
 
367
+ def capture_query_stats
368
+ config["databases"].keys.each do |database|
369
+ with(database) do
370
+ now = Time.now
371
+ query_stats = self.query_stats(limit: 1000000)
372
+ if query_stats.any? && reset_query_stats
373
+ values =
374
+ query_stats.map do |qs|
375
+ [
376
+ database,
377
+ qs["query"],
378
+ qs["total_minutes"].to_f * 60 * 1000,
379
+ qs["calls"],
380
+ now
381
+ ].map { |v| quote(v) }.join(",")
382
+ end.map { |v| "(#{v})" }.join(",")
383
+
384
+ stats_connection.execute("INSERT INTO pghero_query_stats (database, query, total_time, calls, captured_at) VALUES #{values}")
385
+ end
386
+ end
387
+ end
388
+ end
389
+
390
+ # http://stackoverflow.com/questions/20582500/how-to-check-if-a-table-exists-in-a-given-schema
391
+ def historical_query_stats_enabled?
392
+ # TODO use schema from config
393
+ stats_connection.select_all( squish <<-SQL
394
+ SELECT EXISTS (
395
+ SELECT
396
+ 1
397
+ FROM
398
+ pg_catalog.pg_class c
399
+ INNER JOIN
400
+ pg_catalog.pg_namespace n ON n.oid = c.relnamespace
401
+ WHERE
402
+ n.nspname = 'public'
403
+ AND c.relname = 'pghero_query_stats'
404
+ AND c.relkind = 'r'
405
+ )
406
+ SQL
407
+ ).to_a.first["exists"] == "t"
408
+ end
409
+
410
+ def stats_connection
411
+ QueryStats.connection
412
+ end
413
+
406
414
  def ssl_used?
407
415
  ssl_used = nil
408
416
  Connection.transaction do
@@ -464,7 +472,7 @@ module PgHero
464
472
 
465
473
  commands =
466
474
  [
467
- "CREATE ROLE #{user} LOGIN PASSWORD #{connection.quote(password)}",
475
+ "CREATE ROLE #{user} LOGIN PASSWORD #{quote(password)}",
468
476
  "GRANT CONNECT ON DATABASE #{database} TO #{user}",
469
477
  "GRANT USAGE ON SCHEMA #{schema} TO #{user}"
470
478
  ]
@@ -571,6 +579,80 @@ module PgHero
571
579
  select_all("SELECT EXTRACT(EPOCH FROM NOW() - pg_last_xact_replay_timestamp()) AS replication_lag").first["replication_lag"].to_f
572
580
  end
573
581
 
582
+ private
583
+
584
+ # http://www.craigkerstiens.com/2013/01/10/more-on-postgres-performance/
585
+ def current_query_stats(options = {})
586
+ if query_stats_enabled?
587
+ limit = options[:limit] || 100
588
+ select_all <<-SQL
589
+ WITH query_stats AS (
590
+ SELECT
591
+ query,
592
+ (total_time / 1000 / 60) as total_minutes,
593
+ (total_time / calls) as average_time,
594
+ calls
595
+ FROM
596
+ pg_stat_statements
597
+ INNER JOIN
598
+ pg_database ON pg_database.oid = pg_stat_statements.dbid
599
+ WHERE
600
+ pg_database.datname = current_database()
601
+ )
602
+ SELECT
603
+ query,
604
+ total_minutes,
605
+ average_time,
606
+ calls,
607
+ total_minutes * 100.0 / (SELECT SUM(total_minutes) FROM query_stats) AS total_percent,
608
+ (SELECT SUM(total_minutes) FROM query_stats) AS all_queries_total_minutes
609
+ FROM
610
+ query_stats
611
+ ORDER BY
612
+ total_minutes DESC
613
+ LIMIT #{limit.to_i}
614
+ SQL
615
+ else
616
+ []
617
+ end
618
+ end
619
+
620
+ def historical_query_stats(options = {})
621
+ if historical_query_stats_enabled?
622
+ stats_connection.select_all squish <<-SQL
623
+ WITH query_stats AS (
624
+ SELECT
625
+ query,
626
+ (SUM(total_time) / 1000 / 60) as total_minutes,
627
+ (SUM(total_time) / SUM(calls)) as average_time,
628
+ SUM(calls) as calls
629
+ FROM
630
+ pghero_query_stats
631
+ WHERE
632
+ database = #{quote(current_database)}
633
+ #{options[:start_at] ? "AND captured_at >= #{quote(options[:start_at])}" : ""}
634
+ #{options[:end_at] ? "AND captured_at <= #{quote(options[:end_at])}" : ""}
635
+ GROUP BY
636
+ query
637
+ )
638
+ SELECT
639
+ query,
640
+ total_minutes,
641
+ average_time,
642
+ calls,
643
+ total_minutes * 100.0 / (SELECT SUM(total_minutes) FROM query_stats) AS total_percent,
644
+ (SELECT SUM(total_minutes) FROM query_stats) AS all_queries_total_minutes
645
+ FROM
646
+ query_stats
647
+ ORDER BY
648
+ total_minutes DESC
649
+ LIMIT 100
650
+ SQL
651
+ else
652
+ []
653
+ end
654
+ end
655
+
574
656
  def friendly_value(setting, unit)
575
657
  if %w(kB 8kB).include?(unit)
576
658
  value = setting.to_i
@@ -609,5 +691,9 @@ module PgHero
609
691
  def squish(str)
610
692
  str.to_s.gsub(/\A[[:space:]]+/, "").gsub(/[[:space:]]+\z/, "").gsub(/[[:space:]]+/, " ")
611
693
  end
694
+
695
+ def quote(value)
696
+ connection.quote(value)
697
+ end
612
698
  end
613
699
  end
@@ -0,0 +1,8 @@
1
+ require "rake"
2
+
3
+ namespace :pghero do
4
+ desc "capture query stats"
5
+ task capture_query_stats: :environment do
6
+ PgHero.capture_query_stats
7
+ end
8
+ end
@@ -1,3 +1,3 @@
1
1
  module PgHero
2
- VERSION = "1.0.1"
2
+ VERSION = "1.1.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pghero
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Kane
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-04-17 00:00:00.000000000 Z
11
+ date: 2015-06-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -98,6 +98,7 @@ files:
98
98
  - app/views/pg_hero/home/_connections_table.html.erb
99
99
  - app/views/pg_hero/home/_live_queries_table.html.erb
100
100
  - app/views/pg_hero/home/_queries_table.html.erb
101
+ - app/views/pg_hero/home/_query_stats_slider.html.erb
101
102
  - app/views/pg_hero/home/connections.html.erb
102
103
  - app/views/pg_hero/home/explain.html.erb
103
104
  - app/views/pg_hero/home/index.html.erb
@@ -116,8 +117,11 @@ files:
116
117
  - guides/Linux.md
117
118
  - guides/Query-Stats.md
118
119
  - guides/Rails.md
120
+ - lib/generators/pghero/query_stats_generator.rb
121
+ - lib/generators/pghero/templates/query_stats.rb
119
122
  - lib/pghero.rb
120
123
  - lib/pghero/engine.rb
124
+ - lib/pghero/tasks.rb
121
125
  - lib/pghero/version.rb
122
126
  - pghero.gemspec
123
127
  - test/pghero_test.rb