1 package metrics
2
3 import (
4 "context"
5 _ "embed"
6 "fmt"
7
8 "github.com/cybertec-postgresql/pgwatch/v6/internal/db"
9 "github.com/cybertec-postgresql/pgwatch/v6/internal/log"
10 migrator "github.com/cybertec-postgresql/pgx-migrator"
11 "github.com/jackc/pgx/v5"
12 )
13
14
15 var sqlConfigSchema string
16
17 var initSchema = func(ctx context.Context, conn db.PgxIface) (err error) {
18 var exists bool
19 if exists, err = db.DoesSchemaExist(ctx, conn, "pgwatch"); err != nil || exists {
20 return err
21 }
22 tx, err := conn.Begin(ctx)
23 if err != nil {
24 return err
25 }
26 defer func() { _ = tx.Rollback(ctx) }()
27 if _, err := tx.Exec(ctx, sqlConfigSchema); err != nil {
28 return err
29 }
30 if err := writeMetricsToPostgres(ctx, tx, GetDefaultMetrics()); err != nil {
31 return err
32 }
33 if err := tx.Commit(ctx); err != nil {
34 return err
35 }
36 return nil
37 }
38
39 var initMigrator = func(ctx context.Context) (*migrator.Migrator, error) {
40 return migrator.New(
41 migrator.TableName("pgwatch.migration"),
42 migrator.SetNotice(func(s string) {
43 log.GetLogger(ctx).Info(s)
44 }),
45 migrations(),
46 )
47 }
48
49
50
51 func EnsureConfigSchema(ctx context.Context, conn db.PgxIface) error {
52 return initSchema(ctx, conn)
53 }
54
55
56 func MigrateConfigSchema(ctx context.Context, conn db.PgxIface) error {
57 m, err := initMigrator(ctx)
58 if err != nil {
59 return fmt.Errorf("cannot initialize migration: %w", err)
60 }
61 return m.Migrate(ctx, conn)
62 }
63
64
65 func NeedsConfigSchemaMigration(ctx context.Context, conn db.PgxIface) (bool, error) {
66 m, err := initMigrator(ctx)
67 if err != nil {
68 return false, err
69 }
70 return m.NeedUpgrade(ctx, conn)
71 }
72
73
74 func (dmrw *dbMetricReaderWriter) Migrate() error {
75 return MigrateConfigSchema(dmrw.ctx, dmrw.configDb)
76 }
77
78
79 func (dmrw *dbMetricReaderWriter) NeedsMigration() (bool, error) {
80 return NeedsConfigSchemaMigration(dmrw.ctx, dmrw.configDb)
81 }
82
83
84 const MigrationsCount = 3
85
86
87 var migrations func() migrator.Option = func() migrator.Option {
88 return migrator.Migrations(
89 &migrator.Migration{
90 Name: "00179 Apply metrics migrations for v3",
91 Func: func(context.Context, pgx.Tx) error {
92
93 return nil
94 },
95 },
96
97 &migrator.Migration{
98 Name: "00824 Refactor recommendations",
99 Func: func(ctx context.Context, tx pgx.Tx) error {
100 _, err := tx.Exec(ctx, `
101 -- 1. Update all reco_ metrics to use metric_storage_name = 'recommendations'
102 UPDATE pgwatch.metric
103 SET storage_name = 'recommendations'
104 WHERE name LIKE 'reco_%' AND COALESCE(storage_name, '') = '';
105
106 -- 2. Remove the placeholder 'recommendations' metric if it exists
107 DELETE FROM pgwatch.metric WHERE name = 'recommendations';
108
109 -- 3. Update 'exhaustive' and 'full' presets to replace 'recommendations' with individual reco_ metrics
110 UPDATE pgwatch.preset
111 SET metrics = metrics - 'recommendations' || $reco_metrics${
112 "reco_add_index": 43200,
113 "reco_default_public_schema": 50400,
114 "reco_disabled_triggers": 57600,
115 "reco_drop_index": 64800,
116 "reco_nested_views": 72000,
117 "reco_partial_index_candidates": 79200,
118 "reco_sprocs_wo_search_path": 86400,
119 "reco_superusers": 93600
120 }$reco_metrics$::jsonb
121 WHERE name IN ('exhaustive', 'full') AND metrics ? 'recommendations';
122
123 -- 4. Insert new 'recommendations' preset if it doesn't exist
124 INSERT INTO pgwatch.preset (name, description, metrics)
125 VALUES ('recommendations', 'performance and security recommendations',
126 $reco_metrics${
127 "reco_add_index": 43200,
128 "reco_default_public_schema": 50400,
129 "reco_disabled_triggers": 57600,
130 "reco_drop_index": 64800,
131 "reco_nested_views": 72000,
132 "reco_partial_index_candidates": 79200,
133 "reco_sprocs_wo_search_path": 86400,
134 "reco_superusers": 93600
135 }$reco_metrics$::jsonb)
136 ON CONFLICT (name) DO NOTHING;
137
138 -- 5. Update source configs to replace 'recommendations' with individual reco_ metrics
139 UPDATE pgwatch.source
140 SET config = config - 'recommendations' ||
141 $reco_metrics${
142 "reco_add_index": 43200,
143 "reco_default_public_schema": 50400,
144 "reco_disabled_triggers": 57600,
145 "reco_drop_index": 64800,
146 "reco_nested_views": 72000,
147 "reco_partial_index_candidates": 79200,
148 "reco_sprocs_wo_search_path": 86400,
149 "reco_superusers": 93600
150 }$reco_metrics$::jsonb
151 WHERE config ? 'recommendations';
152
153 -- 6. Update source standby configs to replace 'recommendations' with individual reco_ metrics
154 UPDATE pgwatch.source
155 SET config_standby = config_standby - 'recommendations' ||
156 $reco_metrics${
157 "reco_add_index": 43200,
158 "reco_default_public_schema": 50400,
159 "reco_disabled_triggers": 57600,
160 "reco_drop_index": 64800,
161 "reco_nested_views": 72000,
162 "reco_partial_index_candidates": 79200,
163 "reco_sprocs_wo_search_path": 86400,
164 "reco_superusers": 93600
165 }$reco_metrics$::jsonb
166 WHERE config_standby ? 'recommendations';
167 `)
168 return err
169 },
170 },
171
172 &migrator.Migration{
173 Name: "01405 Add prometheus to source dbtype check constraint",
174 Func: func(ctx context.Context, tx pgx.Tx) error {
175 _, err := tx.Exec(ctx, `
176 ALTER TABLE pgwatch.source
177 DROP CONSTRAINT IF EXISTS source_dbtype_check,
178 ADD CHECK (dbtype IN ('postgres', 'pgbouncer', 'postgres-continuous-discovery', 'patroni', 'pgpool', 'prometheus'));
179 `)
180 return err
181 },
182 },
183
184
185
186
187
188
189
190
191
192 )
193 }
194