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,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PgReports
|
|
4
|
+
module Connection
|
|
5
|
+
# Represents a single named PostgreSQL target (host+credentials).
|
|
6
|
+
# Holds a memoized AR-subclass-backed connection per database it has been
|
|
7
|
+
# asked for, so switching between databases on the same target reuses pools.
|
|
8
|
+
#
|
|
9
|
+
# The :primary target wraps ActiveRecord::Base directly when accessed at its
|
|
10
|
+
# default database — we don't open a parallel pool to the host app's DB.
|
|
11
|
+
# For non-default databases we create an isolated AR subclass that has its
|
|
12
|
+
# own pool, so the host application's pool is never affected.
|
|
13
|
+
class Target
|
|
14
|
+
class ConnectionFailed < PgReports::Error; end
|
|
15
|
+
|
|
16
|
+
attr_reader :name
|
|
17
|
+
|
|
18
|
+
def initialize(name, spec)
|
|
19
|
+
@name = name.to_sym
|
|
20
|
+
@spec = normalize_spec(spec)
|
|
21
|
+
@pools = {} # database (string) => AR class
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Configuration hash used as a template for derived databases.
|
|
25
|
+
def spec
|
|
26
|
+
@spec.dup
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def default_database
|
|
30
|
+
@spec[:database]&.to_s
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Resolve the AR class backing the connection for `database` (nil = default).
|
|
34
|
+
# Returns a class responding to `.connection` (ActiveRecord::Base or subclass).
|
|
35
|
+
def ar_class_for(database = nil)
|
|
36
|
+
db = (database || default_database).to_s
|
|
37
|
+
raise ArgumentError, "Cannot resolve connection: target #{name.inspect} has no default database and none was given" if db.empty?
|
|
38
|
+
|
|
39
|
+
@pools[db] ||= build_pool_class(db)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Returns the active PG connection for `database`, opening it if needed.
|
|
43
|
+
def connection_for(database = nil)
|
|
44
|
+
ar_class_for(database).connection
|
|
45
|
+
rescue ActiveRecord::NoDatabaseError, PG::ConnectionBad => e
|
|
46
|
+
raise ConnectionFailed, "Cannot connect to #{name}/#{database || default_database}: #{e.message}"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# List all databases visible on this target's cluster (using pg_database).
|
|
50
|
+
# Result rows: { "name" => String, "size" => String, "current" => Boolean }
|
|
51
|
+
def list_databases(current: nil)
|
|
52
|
+
rows = connection_for.exec_query(<<~SQL).to_a
|
|
53
|
+
SELECT datname AS name,
|
|
54
|
+
pg_size_pretty(pg_database_size(datname)) AS size
|
|
55
|
+
FROM pg_database
|
|
56
|
+
WHERE datistemplate = false AND datallowconn = true
|
|
57
|
+
ORDER BY datname
|
|
58
|
+
SQL
|
|
59
|
+
current_db = (current || default_database).to_s
|
|
60
|
+
rows.each { |r| r["current"] = (r["name"].to_s == current_db) }
|
|
61
|
+
rows
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Close all derived pools we own. The :primary AR::Base pool is never touched.
|
|
65
|
+
def disconnect!
|
|
66
|
+
@pools.each_value do |klass|
|
|
67
|
+
next if klass.equal?(ActiveRecord::Base)
|
|
68
|
+
klass.connection_pool.disconnect! if klass.connection_pool.connected?
|
|
69
|
+
rescue
|
|
70
|
+
# Best-effort cleanup
|
|
71
|
+
end
|
|
72
|
+
@pools.clear
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def normalize_spec(spec)
|
|
78
|
+
hash = case spec
|
|
79
|
+
when Hash
|
|
80
|
+
spec.transform_keys(&:to_sym)
|
|
81
|
+
when ActiveRecord::DatabaseConfigurations::HashConfig
|
|
82
|
+
spec.configuration_hash.transform_keys(&:to_sym)
|
|
83
|
+
else
|
|
84
|
+
raise ArgumentError, "Unsupported spec type: #{spec.class}"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Ensure adapter defaults to postgresql; we only support PG.
|
|
88
|
+
hash[:adapter] ||= "postgresql"
|
|
89
|
+
hash
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def build_pool_class(database)
|
|
93
|
+
# When asked for the primary target's default database, reuse AR::Base
|
|
94
|
+
# so we don't open a parallel pool to the same DB the host app uses.
|
|
95
|
+
if name == :primary && database == default_database
|
|
96
|
+
return ActiveRecord::Base
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
klass = Class.new(ActiveRecord::Base) { self.abstract_class = true }
|
|
100
|
+
const_name = "Pool_#{name}_#{database}".gsub(/\W/, "_")
|
|
101
|
+
if PgReports::Connection.const_defined?(const_name, false)
|
|
102
|
+
PgReports::Connection.send(:remove_const, const_name)
|
|
103
|
+
end
|
|
104
|
+
PgReports::Connection.const_set(const_name, klass)
|
|
105
|
+
|
|
106
|
+
klass.establish_connection(@spec.merge(database: database))
|
|
107
|
+
klass
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
@@ -277,8 +277,8 @@ module PgReports
|
|
|
277
277
|
seq_scans: {name: "Sequential Scans", description: "Tables with high sequential scans"},
|
|
278
278
|
tables_without_pk: {name: "No Primary Key", description: "Tables missing primary keys"},
|
|
279
279
|
recently_modified: {name: "Recently Modified", description: "Tables with recent activity"},
|
|
280
|
-
update_hotspots: {name: "Update Hotspots", description: "Same rows or indexed columns updated repeatedly"
|
|
281
|
-
unused_tables: {name: "Unused Tables", description: "Tables never queried since the last stats reset"
|
|
280
|
+
update_hotspots: {name: "Update Hotspots", description: "Same rows or indexed columns updated repeatedly"},
|
|
281
|
+
unused_tables: {name: "Unused Tables", description: "Tables never queried since the last stats reset"}
|
|
282
282
|
}
|
|
283
283
|
},
|
|
284
284
|
connections: {
|
|
@@ -316,14 +316,19 @@ module PgReports
|
|
|
316
316
|
name: "Schema Analysis",
|
|
317
317
|
icon: "🔍",
|
|
318
318
|
color: "#06b6d4",
|
|
319
|
+
# These reports introspect the host application's ActiveRecord models,
|
|
320
|
+
# which are bound to the default database. Running them against a
|
|
321
|
+
# different database in the cluster returns rows that may not map to
|
|
322
|
+
# any model. The dashboard greys the category out in that case.
|
|
323
|
+
target_constraint: :primary_default_database_only,
|
|
319
324
|
reports: {
|
|
320
325
|
missing_validations: {name: "Missing Validations", description: "Unique indexes without model validations"},
|
|
321
|
-
unused_columns: {name: "Unused Columns", description: "Columns that have only ever held a single value"
|
|
322
|
-
always_null_columns: {name: "Always-NULL Columns", description: "Nullable columns that contain only NULL"
|
|
323
|
-
polymorphic_without_index: {name: "Polymorphic Without Index", description: "Polymorphic associations missing composite index"
|
|
324
|
-
counter_cache_issues: {name: "Counter Cache Issues", description: "counter_cache declarations whose target column is missing"
|
|
325
|
-
soft_delete_without_scope: {name: "Soft Delete Without Scope", description: "Soft-delete columns with no model scope filtering them"
|
|
326
|
-
orphan_tables: {name: "Orphan Tables", description: "DB tables without a corresponding Rails model"
|
|
326
|
+
unused_columns: {name: "Unused Columns", description: "Columns that have only ever held a single value"},
|
|
327
|
+
always_null_columns: {name: "Always-NULL Columns", description: "Nullable columns that contain only NULL"},
|
|
328
|
+
polymorphic_without_index: {name: "Polymorphic Without Index", description: "Polymorphic associations missing composite index"},
|
|
329
|
+
counter_cache_issues: {name: "Counter Cache Issues", description: "counter_cache declarations whose target column is missing"},
|
|
330
|
+
soft_delete_without_scope: {name: "Soft Delete Without Scope", description: "Soft-delete columns with no model scope filtering them"},
|
|
331
|
+
orphan_tables: {name: "Orphan Tables", description: "DB tables without a corresponding Rails model"}
|
|
327
332
|
}
|
|
328
333
|
}
|
|
329
334
|
}.freeze
|
|
@@ -398,6 +403,15 @@ module PgReports
|
|
|
398
403
|
def self.problem_fields(report)
|
|
399
404
|
REPORT_CONFIG.dig(report.to_sym, :problem_fields) || []
|
|
400
405
|
end
|
|
406
|
+
|
|
407
|
+
# Returns the target constraint declared on a category, or nil.
|
|
408
|
+
# Currently the only constraint is :primary_default_database_only, which
|
|
409
|
+
# means "only meaningful when the dashboard is pointing at the host app's
|
|
410
|
+
# primary target on its default database" — used by Schema Analysis,
|
|
411
|
+
# which depends on ActiveRecord::Base.descendants of the host app.
|
|
412
|
+
def self.target_constraint(category)
|
|
413
|
+
REPORTS.dig(category.to_sym, :target_constraint)
|
|
414
|
+
end
|
|
401
415
|
end
|
|
402
416
|
end
|
|
403
417
|
end
|
data/lib/pg_reports/executor.rb
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module PgReports
|
|
4
|
-
# Executes SQL queries and returns results
|
|
4
|
+
# Executes SQL queries and returns results.
|
|
5
|
+
#
|
|
6
|
+
# The connection is resolved lazily on every #execute call so that thread-local
|
|
7
|
+
# context set by PgReports.with_target / with_database is honored even when an
|
|
8
|
+
# Executor instance has been memoized at the module level.
|
|
5
9
|
class Executor
|
|
6
10
|
def initialize(connection: nil)
|
|
7
|
-
@
|
|
11
|
+
@connection_override = connection
|
|
8
12
|
end
|
|
9
13
|
|
|
10
14
|
# Execute SQL from a file and return results as array of hashes
|
|
@@ -16,10 +20,15 @@ module PgReports
|
|
|
16
20
|
# Execute raw SQL and return results as array of hashes
|
|
17
21
|
def execute(sql, **params)
|
|
18
22
|
processed_sql = interpolate_params(sql, params)
|
|
19
|
-
result =
|
|
23
|
+
result = connection.exec_query(processed_sql)
|
|
20
24
|
result.to_a
|
|
21
25
|
end
|
|
22
26
|
|
|
27
|
+
# Resolved on every call: explicit override > thread-local > registry default.
|
|
28
|
+
def connection
|
|
29
|
+
@connection_override || PgReports.config.connection
|
|
30
|
+
end
|
|
31
|
+
|
|
23
32
|
private
|
|
24
33
|
|
|
25
34
|
# Simple parameter interpolation for SQL
|
|
@@ -40,11 +49,11 @@ module PgReports
|
|
|
40
49
|
when Integer, Float
|
|
41
50
|
value.to_s
|
|
42
51
|
when String
|
|
43
|
-
|
|
52
|
+
connection.quote(value)
|
|
44
53
|
when Array
|
|
45
54
|
"(#{value.map { |v| quote_value(v) }.join(", ")})"
|
|
46
55
|
else
|
|
47
|
-
|
|
56
|
+
connection.quote(value.to_s)
|
|
48
57
|
end
|
|
49
58
|
end
|
|
50
59
|
end
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module PgReports
|
|
6
|
+
module Grafana
|
|
7
|
+
# Builds an importable Grafana dashboard JSON from configured favorites.
|
|
8
|
+
# Each report becomes a row with two panels:
|
|
9
|
+
# - "rows" stat (total)
|
|
10
|
+
# - "issues by severity" timeseries (ok / warning / critical)
|
|
11
|
+
# Severity colours are wired so that any warning lights yellow, any critical lights red.
|
|
12
|
+
class DashboardBuilder
|
|
13
|
+
DATASOURCE_INPUT = "DS_PROMETHEUS"
|
|
14
|
+
|
|
15
|
+
DEFAULT_TITLE = "PgReports — PostgreSQL Health"
|
|
16
|
+
DEFAULT_UID = "pg-reports"
|
|
17
|
+
|
|
18
|
+
SEVERITY_COLORS = {
|
|
19
|
+
"ok" => "green",
|
|
20
|
+
"warning" => "yellow",
|
|
21
|
+
"critical" => "red"
|
|
22
|
+
}.freeze
|
|
23
|
+
|
|
24
|
+
GRID_WIDTH = 24
|
|
25
|
+
ROW_HEIGHT = 1
|
|
26
|
+
TIMESERIES_HEIGHT = 8
|
|
27
|
+
TABLE_HEIGHT = 10
|
|
28
|
+
REPORT_BLOCK_HEIGHT = TIMESERIES_HEIGHT + TABLE_HEIGHT
|
|
29
|
+
|
|
30
|
+
def self.build(**opts)
|
|
31
|
+
new(**opts).build
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def initialize(favorites: PgReports.config.grafana_favorites,
|
|
35
|
+
title: DEFAULT_TITLE,
|
|
36
|
+
uid: DEFAULT_UID,
|
|
37
|
+
refresh: "1m",
|
|
38
|
+
time_from: "now-6h")
|
|
39
|
+
@favorites = normalize(favorites)
|
|
40
|
+
@title = title
|
|
41
|
+
@uid = uid
|
|
42
|
+
@refresh = refresh
|
|
43
|
+
@time_from = time_from
|
|
44
|
+
@panel_id = 0
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def build
|
|
48
|
+
if @favorites.empty?
|
|
49
|
+
raise ArgumentError,
|
|
50
|
+
"No favorites configured. Set PgReports.config.grafana_favorites or pass favorites:."
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
{
|
|
54
|
+
"__inputs" => [datasource_input],
|
|
55
|
+
"__requires" => [grafana_require, prometheus_require],
|
|
56
|
+
"annotations" => {"list" => []},
|
|
57
|
+
"editable" => true,
|
|
58
|
+
"graphTooltip" => 1,
|
|
59
|
+
"panels" => build_panels,
|
|
60
|
+
"refresh" => @refresh,
|
|
61
|
+
"schemaVersion" => 38,
|
|
62
|
+
"tags" => ["pg_reports", "postgresql"],
|
|
63
|
+
"templating" => {"list" => []},
|
|
64
|
+
"time" => {"from" => @time_from, "to" => "now"},
|
|
65
|
+
"timezone" => "browser",
|
|
66
|
+
"title" => @title,
|
|
67
|
+
"uid" => @uid,
|
|
68
|
+
"version" => 1,
|
|
69
|
+
"weekStart" => ""
|
|
70
|
+
}
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def to_json(*)
|
|
74
|
+
JSON.pretty_generate(build)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
def normalize(favorites)
|
|
80
|
+
case favorites
|
|
81
|
+
when Hash then favorites.keys.map(&:to_sym)
|
|
82
|
+
when Array then favorites.map(&:to_sym)
|
|
83
|
+
else []
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def build_panels
|
|
88
|
+
panels = []
|
|
89
|
+
y = 0
|
|
90
|
+
|
|
91
|
+
grouped_favorites.each do |category, keys|
|
|
92
|
+
panels << row_panel(category_label(category), y)
|
|
93
|
+
y += ROW_HEIGHT
|
|
94
|
+
|
|
95
|
+
keys.each do |key|
|
|
96
|
+
info = report_info(category, key)
|
|
97
|
+
panels << timeseries_panel(key, info, y)
|
|
98
|
+
panels << table_panel(key, info, y + TIMESERIES_HEIGHT)
|
|
99
|
+
y += REPORT_BLOCK_HEIGHT
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
panels
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def grouped_favorites
|
|
107
|
+
groups = {}
|
|
108
|
+
@favorites.each do |key|
|
|
109
|
+
category = category_for(key)
|
|
110
|
+
next unless category # silently skip unknown keys; exporter logs them
|
|
111
|
+
groups[category] ||= []
|
|
112
|
+
groups[category] << key
|
|
113
|
+
end
|
|
114
|
+
groups
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def category_for(key)
|
|
118
|
+
Dashboard::ReportsRegistry::REPORTS.each do |category, info|
|
|
119
|
+
return category if info[:reports].key?(key)
|
|
120
|
+
end
|
|
121
|
+
nil
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def category_label(category)
|
|
125
|
+
Dashboard::ReportsRegistry::REPORTS.dig(category, :name) || category.to_s.humanize
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def report_info(category, key)
|
|
129
|
+
Dashboard::ReportsRegistry::REPORTS.dig(category, :reports, key) || {name: key.to_s.humanize, description: ""}
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def row_panel(title, y)
|
|
133
|
+
{
|
|
134
|
+
"id" => next_id,
|
|
135
|
+
"type" => "row",
|
|
136
|
+
"title" => title,
|
|
137
|
+
"collapsed" => false,
|
|
138
|
+
"gridPos" => {"h" => ROW_HEIGHT, "w" => GRID_WIDTH, "x" => 0, "y" => y},
|
|
139
|
+
"panels" => []
|
|
140
|
+
}
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def timeseries_panel(key, info, y)
|
|
144
|
+
{
|
|
145
|
+
"id" => next_id,
|
|
146
|
+
"type" => "timeseries",
|
|
147
|
+
"title" => "#{info[:name]} — issues by severity",
|
|
148
|
+
"description" => info[:description].to_s,
|
|
149
|
+
"datasource" => datasource_ref,
|
|
150
|
+
"gridPos" => {"h" => TIMESERIES_HEIGHT, "w" => GRID_WIDTH, "x" => 0, "y" => y},
|
|
151
|
+
"targets" => severity_targets(key),
|
|
152
|
+
"options" => {
|
|
153
|
+
"legend" => {"displayMode" => "table", "placement" => "right", "calcs" => ["lastNotNull"]},
|
|
154
|
+
"tooltip" => {"mode" => "multi", "sort" => "desc"}
|
|
155
|
+
},
|
|
156
|
+
"fieldConfig" => {
|
|
157
|
+
"defaults" => {
|
|
158
|
+
"custom" => {
|
|
159
|
+
"drawStyle" => "bars",
|
|
160
|
+
"stacking" => {"mode" => "normal", "group" => "A"},
|
|
161
|
+
"fillOpacity" => 60,
|
|
162
|
+
"lineWidth" => 1
|
|
163
|
+
},
|
|
164
|
+
"color" => {"mode" => "fixed"},
|
|
165
|
+
"mappings" => []
|
|
166
|
+
},
|
|
167
|
+
"overrides" => severity_color_overrides
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def table_panel(key, info, y)
|
|
173
|
+
{
|
|
174
|
+
"id" => next_id,
|
|
175
|
+
"type" => "table",
|
|
176
|
+
"title" => "#{info[:name]} — current rows",
|
|
177
|
+
"description" => "#{info[:description]}\n\nLatest snapshot of report rows. Each row's columns are unpacked from Prometheus labels via the Labels-to-fields transformation.".strip,
|
|
178
|
+
"datasource" => datasource_ref,
|
|
179
|
+
"gridPos" => {"h" => TABLE_HEIGHT, "w" => GRID_WIDTH, "x" => 0, "y" => y},
|
|
180
|
+
"targets" => [
|
|
181
|
+
{
|
|
182
|
+
"refId" => "A",
|
|
183
|
+
"expr" => %(pg_reports_row{report="#{key}"}),
|
|
184
|
+
"datasource" => datasource_ref,
|
|
185
|
+
"format" => "table",
|
|
186
|
+
"instant" => true,
|
|
187
|
+
"legendFormat" => "__auto"
|
|
188
|
+
}
|
|
189
|
+
],
|
|
190
|
+
"transformations" => [
|
|
191
|
+
{
|
|
192
|
+
"id" => "labelsToFields",
|
|
193
|
+
"options" => {"mode" => "columns"}
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
"id" => "organize",
|
|
197
|
+
"options" => {
|
|
198
|
+
"excludeByName" => {
|
|
199
|
+
"Time" => true,
|
|
200
|
+
"Value" => true,
|
|
201
|
+
"__name__" => true,
|
|
202
|
+
"instance" => true,
|
|
203
|
+
"job" => true,
|
|
204
|
+
"report" => true,
|
|
205
|
+
"row" => true
|
|
206
|
+
},
|
|
207
|
+
"indexByName" => {},
|
|
208
|
+
"renameByName" => {}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
],
|
|
212
|
+
"options" => {
|
|
213
|
+
"showHeader" => true,
|
|
214
|
+
"cellHeight" => "sm",
|
|
215
|
+
"footer" => {"show" => false}
|
|
216
|
+
},
|
|
217
|
+
"fieldConfig" => {
|
|
218
|
+
"defaults" => {
|
|
219
|
+
"custom" => {"align" => "auto", "displayMode" => "auto"},
|
|
220
|
+
"mappings" => []
|
|
221
|
+
},
|
|
222
|
+
"overrides" => []
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def severity_targets(key)
|
|
228
|
+
SEVERITY_COLORS.keys.each_with_index.map do |severity, i|
|
|
229
|
+
{
|
|
230
|
+
"refId" => ("A".ord + i).chr,
|
|
231
|
+
"expr" => %(pg_reports_issues{report="#{key}",severity="#{severity}"}),
|
|
232
|
+
"datasource" => datasource_ref,
|
|
233
|
+
"legendFormat" => severity
|
|
234
|
+
}
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def severity_color_overrides
|
|
239
|
+
SEVERITY_COLORS.map do |severity, color|
|
|
240
|
+
{
|
|
241
|
+
"matcher" => {"id" => "byName", "options" => severity},
|
|
242
|
+
"properties" => [
|
|
243
|
+
{"id" => "color", "value" => {"mode" => "fixed", "fixedColor" => color}}
|
|
244
|
+
]
|
|
245
|
+
}
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def datasource_input
|
|
250
|
+
{
|
|
251
|
+
"name" => DATASOURCE_INPUT,
|
|
252
|
+
"label" => "Prometheus",
|
|
253
|
+
"description" => "Datasource that scrapes /pg_reports/metrics",
|
|
254
|
+
"type" => "datasource",
|
|
255
|
+
"pluginId" => "prometheus",
|
|
256
|
+
"pluginName" => "Prometheus"
|
|
257
|
+
}
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def datasource_ref
|
|
261
|
+
{"type" => "prometheus", "uid" => "${#{DATASOURCE_INPUT}}"}
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def grafana_require
|
|
265
|
+
{"type" => "grafana", "id" => "grafana", "name" => "Grafana", "version" => "9.0.0"}
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def prometheus_require
|
|
269
|
+
{"type" => "datasource", "id" => "prometheus", "name" => "Prometheus", "version" => "1.0.0"}
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def next_id
|
|
273
|
+
@panel_id += 1
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
end
|