pg_reports 0.9.0 → 1.0.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.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +82 -0
  3. data/README.md +12 -8
  4. data/app/controllers/pg_reports/dashboard_controller.rb +277 -221
  5. data/app/controllers/pg_reports/metrics_controller.rb +13 -4
  6. data/app/views/layouts/pg_reports/application.html.erb +222 -93
  7. data/app/views/pg_reports/dashboard/_settings_modal.html.erb +55 -0
  8. data/app/views/pg_reports/dashboard/_show_modals.html.erb +1 -42
  9. data/app/views/pg_reports/dashboard/_show_scripts.html.erb +75 -105
  10. data/app/views/pg_reports/dashboard/index.html.erb +346 -238
  11. data/app/views/pg_reports/dashboard/show.html.erb +1 -1
  12. data/bin/pg_reports +2 -2
  13. data/config/brakeman.ignore +10 -56
  14. data/config/locales/en.yml +45 -11
  15. data/config/locales/ru.yml +44 -11
  16. data/config/locales/uk.yml +44 -11
  17. data/config/routes.rb +2 -0
  18. data/lib/pg_reports/access_policy.rb +39 -0
  19. data/lib/pg_reports/compatibility.rb +10 -2
  20. data/lib/pg_reports/configuration.rb +33 -2
  21. data/lib/pg_reports/connection/error_translator.rb +21 -0
  22. data/lib/pg_reports/connection/target.rb +43 -10
  23. data/lib/pg_reports/csv_cell.rb +20 -0
  24. data/lib/pg_reports/dashboard/reports_registry.rb +37 -30
  25. data/lib/pg_reports/definitions/connections/connection_stats.yml +4 -0
  26. data/lib/pg_reports/definitions/connections/idle_connections.yml +4 -0
  27. data/lib/pg_reports/definitions/connections/locks.yml +1 -0
  28. data/lib/pg_reports/definitions/connections/long_running_queries.yml +0 -1
  29. data/lib/pg_reports/definitions/indexes/bloated_indexes.yml +1 -2
  30. data/lib/pg_reports/definitions/indexes/index_correlation.yml +2 -1
  31. data/lib/pg_reports/definitions/indexes/missing_indexes.yml +1 -0
  32. data/lib/pg_reports/definitions/queries/frequent_queries.yml +50 -0
  33. data/lib/pg_reports/definitions/system/cache_stats.yml +2 -1
  34. data/lib/pg_reports/definitions/tables/bloated_tables.yml +1 -2
  35. data/lib/pg_reports/definitions/tables/recently_modified.yml +3 -3
  36. data/lib/pg_reports/definitions/tables/unused_tables.yml +1 -1
  37. data/lib/pg_reports/drop_index_migration.rb +56 -0
  38. data/lib/pg_reports/engine.rb +4 -3
  39. data/lib/pg_reports/executor.rb +46 -9
  40. data/lib/pg_reports/modules/queries.rb +1 -0
  41. data/lib/pg_reports/query_monitor.rb +478 -192
  42. data/lib/pg_reports/rate_limiter.rb +63 -0
  43. data/lib/pg_reports/report.rb +1 -1
  44. data/lib/pg_reports/report_definition.rb +11 -1
  45. data/lib/pg_reports/sql/connections/connection_stats.sql +2 -1
  46. data/lib/pg_reports/sql/connections/idle_connections.sql +1 -0
  47. data/lib/pg_reports/sql/connections/locks.sql +1 -0
  48. data/lib/pg_reports/sql/indexes/bloated_indexes.sql +16 -8
  49. data/lib/pg_reports/sql/queries/activity_sample.sql +22 -0
  50. data/lib/pg_reports/sql/queries/frequent_queries.sql +42 -0
  51. data/lib/pg_reports/sql_redactor.rb +32 -0
  52. data/lib/pg_reports/standalone.rb +83 -20
  53. data/lib/pg_reports/version.rb +1 -1
  54. data/lib/pg_reports.rb +7 -2
  55. metadata +18 -9
@@ -7,25 +7,39 @@ module PgReports
7
7
  # context set by PgReports.with_target / with_database is honored even when an
8
8
  # Executor instance has been memoized at the module level.
9
9
  class Executor
10
+ # Marks the SQL as ours where a statement name cannot reach: inside a host
11
+ # application these queries share its connections and application_name, so
12
+ # this is all that tells them apart in pg_stat_activity.
13
+ QUERY_TAG = "/* pg_reports */"
14
+
15
+ # A report that needs a lock someone else holds has nothing to wait for: during
16
+ # an incident the answer is more use now than after the full statement timeout.
17
+ REPORT_LOCK_TIMEOUT_MS = 3_000
18
+
19
+ # Not the second colon of a :: cast, nor the middle of a word
20
+ PLACEHOLDER = /(?<![:\w]):([a-zA-Z_]\w*)/
21
+
10
22
  def initialize(connection: nil)
11
23
  @connection_override = connection
12
24
  end
13
25
 
14
- # Execute SQL from a file and return results as array of hashes
26
+ # Execute SQL from a file and return results as array of hashes.
27
+ #
28
+ # These files only read, so they run under the report time limits.
15
29
  def execute_from_file(category, name, **params)
16
30
  sql = SqlLoader.load(category, name)
17
- execute(sql, **params)
31
+ within_report_limits { execute(sql, **params) }
18
32
  end
19
33
 
20
34
  # Execute raw SQL and return results as array of hashes.
21
35
  #
22
36
  # Every query is tagged with the "PgReports" AR statement name so the Query
23
- # Monitor can skip our own queries by name (see QueryMonitor#should_skip?),
37
+ # Monitor can skip our own queries by name (see QueryMonitor#skip_by_payload?),
24
38
  # reliably and independent of backtrace depth — the internal live_metrics /
25
39
  # status polling would otherwise leak into the monitor's history.
26
40
  def execute(sql, **params)
27
41
  processed_sql = interpolate_params(sql, params)
28
- result = connection.exec_query(processed_sql, "PgReports")
42
+ result = connection.exec_query("#{QUERY_TAG} #{processed_sql}", "PgReports")
29
43
  result.to_a
30
44
  end
31
45
 
@@ -36,14 +50,37 @@ module PgReports
36
50
 
37
51
  private
38
52
 
39
- # Simple parameter interpolation for SQL
40
- # Replaces :param_name with quoted values
53
+ # The limits are local to a transaction, and a savepoint inside the host
54
+ # application's transaction would hand them on to it when released. Rolling back
55
+ # discards them in both cases, and there is nothing here to keep.
56
+ def within_report_limits
57
+ timeout_ms = PgReports.config.report_statement_timeout_ms.to_i
58
+ return yield unless timeout_ms.positive?
59
+
60
+ rows = nil
61
+ connection.transaction(requires_new: true) do
62
+ connection.exec_query(report_limits_sql(timeout_ms), "PgReports")
63
+ rows = yield
64
+ raise ActiveRecord::Rollback
65
+ end
66
+ rows
67
+ end
68
+
69
+ def report_limits_sql(timeout_ms)
70
+ lock_timeout_ms = [timeout_ms, REPORT_LOCK_TIMEOUT_MS].min
71
+
72
+ "#{QUERY_TAG} SELECT set_config('statement_timeout', '#{timeout_ms}', true), " \
73
+ "set_config('lock_timeout', '#{lock_timeout_ms}', true)"
74
+ end
75
+
76
+ # Replaces :param_name with quoted values in a single pass, so a value is never
77
+ # read back as SQL: one holding ":other" stays text, and :limit leaves :limit_x alone.
41
78
  def interpolate_params(sql, params)
42
79
  return sql if params.empty?
43
80
 
44
- params.reduce(sql) do |query, (key, value)|
45
- quoted_value = quote_value(value)
46
- query.gsub(":#{key}", quoted_value)
81
+ sql.gsub(PLACEHOLDER) do |placeholder|
82
+ key = Regexp.last_match(1).to_sym
83
+ params.key?(key) ? quote_value(params[key]) : placeholder
47
84
  end
48
85
  end
49
86
 
@@ -10,6 +10,7 @@ module PgReports
10
10
  # The following methods are auto-generated from YAML:
11
11
  # - slow_queries(limit: 20)
12
12
  # - heavy_queries(limit: 20)
13
+ # - frequent_queries(limit: 20)
13
14
  # - expensive_queries(limit: 20)
14
15
  # - missing_index_queries(limit: 20)
15
16
  # - low_cache_hit_queries(limit: 20, min_calls: 100)