pg_reports 0.6.2 → 0.8.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +31 -0
- data/README.md +71 -86
- data/app/controllers/pg_reports/dashboard_controller.rb +142 -2
- data/app/controllers/pg_reports/metrics_controller.rb +27 -0
- data/app/views/layouts/pg_reports/application.html.erb +125 -0
- data/app/views/pg_reports/dashboard/_database_selector.html.erb +39 -0
- data/app/views/pg_reports/dashboard/_target_selector.html.erb +27 -0
- data/app/views/pg_reports/dashboard/index.html.erb +43 -29
- data/app/views/pg_reports/dashboard/show.html.erb +4 -0
- data/config/locales/en.yml +9 -2
- data/config/locales/ru.yml +9 -2
- data/config/locales/uk.yml +9 -2
- data/config/routes.rb +5 -0
- data/lib/pg_reports/configuration.rb +42 -2
- data/lib/pg_reports/connection/error_translator.rb +109 -0
- data/lib/pg_reports/connection/registry.rb +150 -0
- data/lib/pg_reports/connection/target.rb +111 -0
- data/lib/pg_reports/dashboard/reports_registry.rb +22 -8
- data/lib/pg_reports/executor.rb +14 -5
- data/lib/pg_reports/grafana/dashboard_builder.rb +277 -0
- data/lib/pg_reports/grafana/exporter.rb +240 -0
- data/lib/pg_reports/module_generator.rb +29 -28
- data/lib/pg_reports/modules/schema_analysis.rb +1 -1
- data/lib/pg_reports/modules/system.rb +4 -1
- data/lib/pg_reports/query_monitor.rb +10 -7
- data/lib/pg_reports/version.rb +1 -1
- data/lib/pg_reports.rb +57 -0
- data/lib/tasks/pg_reports.rake +36 -0
- metadata +10 -1
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PgReports
|
|
4
|
+
module Grafana
|
|
5
|
+
# Renders selected reports in Prometheus exposition format.
|
|
6
|
+
# Severity is derived from REPORT_CONFIG thresholds in Dashboard::ReportsRegistry.
|
|
7
|
+
class Exporter
|
|
8
|
+
SEVERITY_ORDER = {"ok" => 0, "warning" => 1, "critical" => 2}.freeze
|
|
9
|
+
MAX_LABEL_VALUE_LENGTH = 200
|
|
10
|
+
RESERVED_LABEL_NAMES = %w[report severity row error].freeze
|
|
11
|
+
|
|
12
|
+
MODULES = {
|
|
13
|
+
queries: -> { Modules::Queries },
|
|
14
|
+
indexes: -> { Modules::Indexes },
|
|
15
|
+
tables: -> { Modules::Tables },
|
|
16
|
+
connections: -> { Modules::Connections },
|
|
17
|
+
system: -> { Modules::System },
|
|
18
|
+
schema_analysis: -> { Modules::SchemaAnalysis }
|
|
19
|
+
}.freeze
|
|
20
|
+
|
|
21
|
+
def self.render
|
|
22
|
+
new.render
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def initialize(favorites: PgReports.config.grafana_favorites,
|
|
26
|
+
cache_ttl: PgReports.config.grafana_cache_ttl,
|
|
27
|
+
clock: Time)
|
|
28
|
+
@favorites = normalize(favorites)
|
|
29
|
+
@cache_ttl = cache_ttl
|
|
30
|
+
@clock = clock
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def render
|
|
34
|
+
results = @favorites.map { |key, opts| collect(key, opts) }
|
|
35
|
+
|
|
36
|
+
lines = []
|
|
37
|
+
emit(lines, "pg_reports_issues", "Number of rows by severity for the report") do |emit|
|
|
38
|
+
results.each do |r|
|
|
39
|
+
next unless r[:ok]
|
|
40
|
+
r[:severities].each { |sev, count| emit.call({report: r[:key], severity: sev}, count) }
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
emit(lines, "pg_reports_rows", "Total rows returned by the report") do |emit|
|
|
45
|
+
results.each { |r| emit.call({report: r[:key]}, r[:rows]) if r[:ok] }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
emit(lines, "pg_reports_run_seconds", "Time spent collecting the report") do |emit|
|
|
49
|
+
results.each { |r| emit.call({report: r[:key]}, r[:duration].round(4)) if r[:ok] }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
emit(lines, "pg_reports_last_run_timestamp", "Unix timestamp of last collection") do |emit|
|
|
53
|
+
results.each { |r| emit.call({report: r[:key]}, r[:timestamp]) if r[:ok] }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
emit(lines, "pg_reports_up", "Whether collection succeeded (1) or failed (0)") do |emit|
|
|
57
|
+
results.each do |r|
|
|
58
|
+
labels = {report: r[:key]}
|
|
59
|
+
labels[:error] = r[:error] unless r[:ok]
|
|
60
|
+
emit.call(labels, r[:ok] ? 1 : 0)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
emit(lines, "pg_reports_row", "One series per row of the report (drives Grafana table panels). Each row column becomes a label.") do |emit|
|
|
65
|
+
results.each do |r|
|
|
66
|
+
next unless r[:ok] && r[:rows_data]
|
|
67
|
+
r[:rows_data].each_with_index do |row_labels, idx|
|
|
68
|
+
emit.call(row_labels.merge(report: r[:key], row: idx), 1)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
(lines << "").join("\n")
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
def normalize(favorites)
|
|
79
|
+
case favorites
|
|
80
|
+
when Hash
|
|
81
|
+
favorites.each_with_object({}) { |(k, v), h| h[k.to_sym] = (v || {}).symbolize_keys }
|
|
82
|
+
when Array
|
|
83
|
+
favorites.each_with_object({}) { |k, h| h[k.to_sym] = {} }
|
|
84
|
+
else
|
|
85
|
+
{}
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def collect(key, opts)
|
|
90
|
+
cached(key, opts) { run(key, opts) }
|
|
91
|
+
rescue => e
|
|
92
|
+
{key: key, ok: false, error: e.class.name, message: e.message}
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def cached(key, opts)
|
|
96
|
+
ttl = opts[:ttl] || @cache_ttl
|
|
97
|
+
if ttl && defined?(Rails) && Rails.respond_to?(:cache) && Rails.cache
|
|
98
|
+
Rails.cache.fetch("pg_reports/grafana/#{key}", expires_in: ttl) { yield }
|
|
99
|
+
else
|
|
100
|
+
yield
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def run(key, opts)
|
|
105
|
+
mod = module_for(key) or raise ArgumentError, "Unknown report: #{key}"
|
|
106
|
+
|
|
107
|
+
started = @clock.now
|
|
108
|
+
report = mod.public_send(key, **report_args(opts))
|
|
109
|
+
finished = @clock.now
|
|
110
|
+
|
|
111
|
+
{
|
|
112
|
+
key: key,
|
|
113
|
+
ok: true,
|
|
114
|
+
rows: report.size,
|
|
115
|
+
severities: severity_counts(key, report),
|
|
116
|
+
rows_data: opts.fetch(:expose_rows, true) ? row_label_sets(report) : nil,
|
|
117
|
+
duration: finished - started,
|
|
118
|
+
timestamp: finished.to_i
|
|
119
|
+
}
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def row_label_sets(report)
|
|
123
|
+
report.map { |row| row_to_labels(row) }
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def row_to_labels(row)
|
|
127
|
+
labels = {}
|
|
128
|
+
row.each do |column, value|
|
|
129
|
+
next if value.nil?
|
|
130
|
+
|
|
131
|
+
name = sanitize_label_name(column.to_s)
|
|
132
|
+
next if name.empty? || RESERVED_LABEL_NAMES.include?(name)
|
|
133
|
+
|
|
134
|
+
formatted = format_label_value(value)
|
|
135
|
+
next if formatted.length > MAX_LABEL_VALUE_LENGTH
|
|
136
|
+
|
|
137
|
+
labels[name] = formatted
|
|
138
|
+
end
|
|
139
|
+
labels
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def sanitize_label_name(name)
|
|
143
|
+
cleaned = name.gsub(/[^a-zA-Z0-9_]/, "_")
|
|
144
|
+
cleaned = "_#{cleaned}" if cleaned.match?(/\A[0-9]/)
|
|
145
|
+
cleaned
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def format_label_value(value)
|
|
149
|
+
case value
|
|
150
|
+
when Float then format("%g", value)
|
|
151
|
+
when Time, DateTime then value.iso8601
|
|
152
|
+
else value.to_s
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def module_for(key)
|
|
157
|
+
Dashboard::ReportsRegistry::REPORTS.each do |category, info|
|
|
158
|
+
next unless info[:reports].key?(key.to_sym)
|
|
159
|
+
factory = MODULES[category] or return nil
|
|
160
|
+
return factory.call
|
|
161
|
+
end
|
|
162
|
+
nil
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def report_args(opts)
|
|
166
|
+
# Only forward kwargs that report methods accept; keep the surface tiny.
|
|
167
|
+
opts.slice(:limit).compact
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def severity_counts(key, report)
|
|
171
|
+
thresholds = Dashboard::ReportsRegistry.thresholds(key)
|
|
172
|
+
counts = Hash.new(0)
|
|
173
|
+
|
|
174
|
+
if thresholds.empty?
|
|
175
|
+
counts["ok"] = report.size
|
|
176
|
+
return counts
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
report.each { |row| counts[row_severity(row, thresholds)] += 1 }
|
|
180
|
+
counts
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def row_severity(row, thresholds)
|
|
184
|
+
worst = "ok"
|
|
185
|
+
thresholds.each do |field, t|
|
|
186
|
+
value = row[field.to_s] || row[field]
|
|
187
|
+
next if value.nil?
|
|
188
|
+
|
|
189
|
+
worst = max_severity(worst, severity_for(value.to_f, t))
|
|
190
|
+
end
|
|
191
|
+
worst
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def severity_for(value, thresholds)
|
|
195
|
+
critical = thresholds[:critical]
|
|
196
|
+
warning = thresholds[:warning]
|
|
197
|
+
|
|
198
|
+
if thresholds[:inverted]
|
|
199
|
+
return "critical" if critical && value <= critical
|
|
200
|
+
return "warning" if warning && value <= warning
|
|
201
|
+
else
|
|
202
|
+
return "critical" if critical && value >= critical
|
|
203
|
+
return "warning" if warning && value >= warning
|
|
204
|
+
end
|
|
205
|
+
"ok"
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def max_severity(a, b)
|
|
209
|
+
(SEVERITY_ORDER[a] >= SEVERITY_ORDER[b]) ? a : b
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def emit(lines, metric, help)
|
|
213
|
+
buffer = []
|
|
214
|
+
emitter = ->(labels, value) {
|
|
215
|
+
buffer << "#{metric}#{format_labels(labels)} #{value}"
|
|
216
|
+
}
|
|
217
|
+
yield emitter
|
|
218
|
+
return if buffer.empty?
|
|
219
|
+
|
|
220
|
+
lines << "# HELP #{metric} #{help}"
|
|
221
|
+
lines << "# TYPE #{metric} gauge"
|
|
222
|
+
lines.concat(buffer)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def format_labels(labels)
|
|
226
|
+
return "" if labels.nil? || labels.empty?
|
|
227
|
+
|
|
228
|
+
pairs = labels.map { |k, v| %(#{k}="#{escape_label(v)}") }
|
|
229
|
+
"{#{pairs.join(",")}}"
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def escape_label(value)
|
|
233
|
+
value.to_s
|
|
234
|
+
.gsub("\\", "\\\\\\\\")
|
|
235
|
+
.gsub('"', '\\"')
|
|
236
|
+
.gsub("\n", '\\n')
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
end
|
|
@@ -3,42 +3,43 @@
|
|
|
3
3
|
module PgReports
|
|
4
4
|
# Generates module methods dynamically from YAML report definitions
|
|
5
5
|
class ModuleGenerator
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
6
|
+
class << self
|
|
7
|
+
def generate!
|
|
8
|
+
ReportLoader.load_all.each do |module_name, reports|
|
|
9
|
+
module_class = get_module(module_name)
|
|
10
|
+
next unless module_class
|
|
11
|
+
|
|
12
|
+
reports.each do |report_name, definition|
|
|
13
|
+
define_report_method(module_class, report_name, definition)
|
|
14
|
+
end
|
|
13
15
|
end
|
|
14
16
|
end
|
|
15
|
-
end
|
|
16
17
|
|
|
17
|
-
|
|
18
|
+
private
|
|
18
19
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
20
|
+
def get_module(module_name)
|
|
21
|
+
const_name = module_name.to_s.split("_").map(&:capitalize).join
|
|
22
|
+
PgReports::Modules.const_get(const_name)
|
|
23
|
+
rescue NameError
|
|
24
|
+
# Module doesn't exist, skip it
|
|
25
|
+
# We don't auto-create modules to avoid conflicts
|
|
26
|
+
nil
|
|
27
|
+
end
|
|
27
28
|
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
def define_report_method(module_class, report_name, definition)
|
|
30
|
+
params_config = definition.config["parameters"] || {}
|
|
30
31
|
|
|
31
|
-
|
|
32
|
-
|
|
32
|
+
# Extract default parameter values
|
|
33
|
+
defaults = params_config.transform_values { |v| v["default"] }
|
|
33
34
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
captured_defaults = defaults
|
|
35
|
+
# Capture definition + defaults so the singleton method closes over them
|
|
36
|
+
captured_definition = definition
|
|
37
|
+
captured_defaults = defaults
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
module_class.define_singleton_method(report_name) do |**params|
|
|
40
|
+
merged_params = captured_defaults.merge(params)
|
|
41
|
+
captured_definition.generate_report(**merged_params)
|
|
42
|
+
end
|
|
42
43
|
end
|
|
43
44
|
end
|
|
44
45
|
end
|
|
@@ -112,7 +112,7 @@ module PgReports
|
|
|
112
112
|
counter_belongs_to(model).each do |assoc|
|
|
113
113
|
counter_col = counter_cache_column_name(model, assoc)
|
|
114
114
|
parent = parent_class_for(assoc)
|
|
115
|
-
next unless parent
|
|
115
|
+
next unless parent&.table_exists?
|
|
116
116
|
|
|
117
117
|
unless parent.column_names.include?(counter_col)
|
|
118
118
|
results << {
|
|
@@ -173,7 +173,10 @@ module PgReports
|
|
|
173
173
|
private
|
|
174
174
|
|
|
175
175
|
def pg_version
|
|
176
|
-
|
|
176
|
+
# Cache per-connection so switching targets/databases re-resolves the version.
|
|
177
|
+
cache = (Thread.current[:pg_reports_pg_version_cache] ||= {})
|
|
178
|
+
key = executor.connection.object_id
|
|
179
|
+
cache[key] ||= begin
|
|
177
180
|
result = executor.execute("SELECT current_setting('server_version_num')::int AS v")
|
|
178
181
|
result.first&.fetch("v", 0).to_i
|
|
179
182
|
end
|
|
@@ -290,13 +290,16 @@ module PgReports
|
|
|
290
290
|
# when gem is installed from RubyGems
|
|
291
291
|
next if path.include?("/query_monitor.rb")
|
|
292
292
|
|
|
293
|
-
# Filter queries from pg_reports internal
|
|
294
|
-
# - Installed gem: /gems/pg_reports-X.Y.Z/lib/
|
|
295
|
-
# - Local gem: /pg_reports/lib/pg_reports/
|
|
296
|
-
#
|
|
297
|
-
#
|
|
298
|
-
|
|
299
|
-
|
|
293
|
+
# Filter queries from pg_reports internal code:
|
|
294
|
+
# - Installed gem: /gems/pg_reports-X.Y.Z/lib/ or /gems/pg_reports-X.Y.Z/app/
|
|
295
|
+
# - Local gem: /pg_reports/lib/pg_reports/ or /pg_reports/app/(controllers|views)/pg_reports/
|
|
296
|
+
# This includes the dashboard controller's own SQL execution endpoints
|
|
297
|
+
# (execute_query, explain_analyze) which call AR directly without going
|
|
298
|
+
# through Executor — without this match nothing in the caller stack
|
|
299
|
+
# would identify the query as ours.
|
|
300
|
+
path.match?(%r{/gems/pg_reports[-\d.]+/(lib|app)/}) ||
|
|
301
|
+
path.match?(%r{/pg_reports/lib/pg_reports/}) ||
|
|
302
|
+
path.match?(%r{/pg_reports/app/(controllers|views)/pg_reports/})
|
|
300
303
|
end
|
|
301
304
|
end
|
|
302
305
|
|
data/lib/pg_reports/version.rb
CHANGED
data/lib/pg_reports.rb
CHANGED
|
@@ -6,6 +6,9 @@ require "active_record"
|
|
|
6
6
|
|
|
7
7
|
require_relative "pg_reports/version"
|
|
8
8
|
require_relative "pg_reports/error"
|
|
9
|
+
require_relative "pg_reports/connection/target"
|
|
10
|
+
require_relative "pg_reports/connection/registry"
|
|
11
|
+
require_relative "pg_reports/connection/error_translator"
|
|
9
12
|
require_relative "pg_reports/compatibility"
|
|
10
13
|
require_relative "pg_reports/configuration"
|
|
11
14
|
require_relative "pg_reports/sql_loader"
|
|
@@ -33,6 +36,10 @@ require_relative "pg_reports/modules/schema_analysis"
|
|
|
33
36
|
# Dashboard
|
|
34
37
|
require_relative "pg_reports/dashboard/reports_registry"
|
|
35
38
|
|
|
39
|
+
# Grafana / Prometheus exporter
|
|
40
|
+
require_relative "pg_reports/grafana/exporter"
|
|
41
|
+
require_relative "pg_reports/grafana/dashboard_builder"
|
|
42
|
+
|
|
36
43
|
# Rails Engine
|
|
37
44
|
require_relative "pg_reports/engine" if defined?(Rails::Engine)
|
|
38
45
|
|
|
@@ -134,6 +141,56 @@ module PgReports
|
|
|
134
141
|
ReportLoader.reload!
|
|
135
142
|
ModuleGenerator.generate!
|
|
136
143
|
end
|
|
144
|
+
|
|
145
|
+
# Connection registry — multi-target / multi-database support.
|
|
146
|
+
# The :primary target is auto-discovered from ActiveRecord on first access.
|
|
147
|
+
def connection_registry
|
|
148
|
+
@connection_registry ||= Connection::Registry.new
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Run a block against a specific target (and optionally a specific database
|
|
152
|
+
# on that target). Honored by Executor and any code routing through
|
|
153
|
+
# PgReports.config.connection.
|
|
154
|
+
#
|
|
155
|
+
# PgReports.with_target(:analytics) { PgReports.slow_queries }
|
|
156
|
+
# PgReports.with_target(:primary, database: "logs") { PgReports.table_sizes }
|
|
157
|
+
def with_target(name, database: nil, &block)
|
|
158
|
+
connection_registry.with_context(target: name, database: database, &block)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Switch only the database on whatever target is currently active
|
|
162
|
+
# (defaults to the registry's default target).
|
|
163
|
+
#
|
|
164
|
+
# PgReports.with_database("reporting") { PgReports.database_sizes }
|
|
165
|
+
def with_database(database, &block)
|
|
166
|
+
target = connection_registry.current_name || connection_registry.default_name
|
|
167
|
+
connection_registry.with_context(target: target, database: database, &block)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Name of the currently effective target (taking with_target into account).
|
|
171
|
+
def current_target_name
|
|
172
|
+
connection_registry.current_name || connection_registry.default_name
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Name of the currently effective database (taking with_database into account).
|
|
176
|
+
def current_database_name
|
|
177
|
+
connection_registry.current_database_name
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# List databases on the currently active target's cluster.
|
|
181
|
+
# Each row: { "name" => String, "size" => String, "current" => Boolean }
|
|
182
|
+
def list_databases
|
|
183
|
+
target = connection_registry.fetch
|
|
184
|
+
target.list_databases(current: current_database_name)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# List of registered targets, each as { name:, default_database:, current: }.
|
|
188
|
+
def list_targets
|
|
189
|
+
current = current_target_name
|
|
190
|
+
connection_registry.targets.map do |t|
|
|
191
|
+
{name: t.name, default_database: t.default_database, current: t.name == current}
|
|
192
|
+
end
|
|
193
|
+
end
|
|
137
194
|
end
|
|
138
195
|
end
|
|
139
196
|
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
namespace :pg_reports do
|
|
6
|
+
namespace :grafana do
|
|
7
|
+
desc "Write importable Grafana dashboard JSON for the configured grafana_favorites. " \
|
|
8
|
+
"Defaults to pg_reports.json in pwd. Override with OUTPUT=, FAVORITES=, TITLE=, UID=, REFRESH=, TIME_FROM=."
|
|
9
|
+
task dashboard: :environment do
|
|
10
|
+
favorites = if ENV["FAVORITES"]
|
|
11
|
+
ENV["FAVORITES"].split(",").map { |k| k.strip.to_sym }
|
|
12
|
+
else
|
|
13
|
+
PgReports.config.grafana_favorites
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
builder = PgReports::Grafana::DashboardBuilder.new(
|
|
17
|
+
favorites: favorites,
|
|
18
|
+
title: ENV.fetch("TITLE", PgReports::Grafana::DashboardBuilder::DEFAULT_TITLE),
|
|
19
|
+
uid: ENV.fetch("UID", PgReports::Grafana::DashboardBuilder::DEFAULT_UID),
|
|
20
|
+
refresh: ENV.fetch("REFRESH", "1m"),
|
|
21
|
+
time_from: ENV.fetch("TIME_FROM", "now-6h")
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
output_path = ENV.fetch("OUTPUT", "pg_reports.json")
|
|
25
|
+
File.write(output_path, JSON.pretty_generate(builder.build))
|
|
26
|
+
warn "Wrote #{output_path}"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
desc "Write the current /metrics payload to a file. Defaults to pg_reports.metrics in pwd. Override with OUTPUT=."
|
|
30
|
+
task metrics: :environment do
|
|
31
|
+
output_path = ENV.fetch("OUTPUT", "pg_reports.metrics")
|
|
32
|
+
File.write(output_path, PgReports::Grafana::Exporter.render)
|
|
33
|
+
warn "Wrote #{output_path}"
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: pg_reports
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Eldar Avatov
|
|
@@ -148,11 +148,14 @@ files:
|
|
|
148
148
|
- LICENSE.txt
|
|
149
149
|
- README.md
|
|
150
150
|
- app/controllers/pg_reports/dashboard_controller.rb
|
|
151
|
+
- app/controllers/pg_reports/metrics_controller.rb
|
|
151
152
|
- app/views/layouts/pg_reports/application.html.erb
|
|
153
|
+
- app/views/pg_reports/dashboard/_database_selector.html.erb
|
|
152
154
|
- app/views/pg_reports/dashboard/_fake_source_data.html.erb
|
|
153
155
|
- app/views/pg_reports/dashboard/_show_modals.html.erb
|
|
154
156
|
- app/views/pg_reports/dashboard/_show_scripts.html.erb
|
|
155
157
|
- app/views/pg_reports/dashboard/_show_styles.html.erb
|
|
158
|
+
- app/views/pg_reports/dashboard/_target_selector.html.erb
|
|
156
159
|
- app/views/pg_reports/dashboard/index.html.erb
|
|
157
160
|
- app/views/pg_reports/dashboard/show.html.erb
|
|
158
161
|
- config/locales/en.yml
|
|
@@ -163,6 +166,9 @@ files:
|
|
|
163
166
|
- lib/pg_reports/annotation_parser.rb
|
|
164
167
|
- lib/pg_reports/compatibility.rb
|
|
165
168
|
- lib/pg_reports/configuration.rb
|
|
169
|
+
- lib/pg_reports/connection/error_translator.rb
|
|
170
|
+
- lib/pg_reports/connection/registry.rb
|
|
171
|
+
- lib/pg_reports/connection/target.rb
|
|
166
172
|
- lib/pg_reports/dashboard/reports_registry.rb
|
|
167
173
|
- lib/pg_reports/definitions/connections/active_connections.yml
|
|
168
174
|
- lib/pg_reports/definitions/connections/blocking_queries.yml
|
|
@@ -214,6 +220,8 @@ files:
|
|
|
214
220
|
- lib/pg_reports/executor.rb
|
|
215
221
|
- lib/pg_reports/explain_analyzer.rb
|
|
216
222
|
- lib/pg_reports/filter.rb
|
|
223
|
+
- lib/pg_reports/grafana/dashboard_builder.rb
|
|
224
|
+
- lib/pg_reports/grafana/exporter.rb
|
|
217
225
|
- lib/pg_reports/module_generator.rb
|
|
218
226
|
- lib/pg_reports/modules/connections.rb
|
|
219
227
|
- lib/pg_reports/modules/indexes.rb
|
|
@@ -278,6 +286,7 @@ files:
|
|
|
278
286
|
- lib/pg_reports/sql_loader.rb
|
|
279
287
|
- lib/pg_reports/telegram_sender.rb
|
|
280
288
|
- lib/pg_reports/version.rb
|
|
289
|
+
- lib/tasks/pg_reports.rake
|
|
281
290
|
homepage: https://github.com/deadalice/pg_reports
|
|
282
291
|
licenses:
|
|
283
292
|
- MIT
|