1 package metrics_test 2 3 import ( 4 "context" 5 "testing" 6 7 "github.com/cybertec-postgresql/pgwatch/v6/internal/log" 8 "github.com/cybertec-postgresql/pgwatch/v6/internal/metrics" 9 "github.com/jackc/pgx/v5/pgconn" 10 "github.com/pashagolub/pgxmock/v5" 11 "github.com/stretchr/testify/assert" 12 ) 13 14 var ctx = log.WithLogger(context.Background(), log.NewNoopLogger()) 15 16 func AnyArgs(n int) []any { 17 args := make([]any, n) 18 for i := range args { 19 args[i] = pgxmock.AnyArg() 20 } 21 return args 22 } 23 24 func TestNewPostgresMetricReaderWriter(t *testing.T) { 25 a := assert.New(t) 26 27 t.Run("ConnectionError", func(*testing.T) { 28 pgrw, err := metrics.NewPostgresMetricReaderWriter(ctx, "postgres://user:pass@foohost:5432/db1") 29 a.Error(err) 30 a.Nil(pgrw) 31 }) 32 t.Run("InvalidConnStr", func(*testing.T) { 33 pgrw, err := metrics.NewPostgresMetricReaderWriter(ctx, "invalid_connstr") 34 a.Error(err) 35 a.Nil(pgrw) 36 }) 37 } 38 39 func TestNewPostgresMetricReaderWriterConn(t *testing.T) { 40 df := metrics.GetDefaultMetrics() 41 metricsCount := len(df.MetricDefs) 42 presetsCount := len(df.PresetDefs) 43 44 a := assert.New(t) 45 conn, err := pgxmock.NewPool() 46 a.NoError(err) 47 48 doesntExist := func() *pgxmock.Rows { return pgxmock.NewRows([]string{"exists"}).AddRow(false) } 49 50 t.Run("FullBoostrap", func(*testing.T) { 51 conn.ExpectQuery(`SELECT EXISTS`).WithArgs("pgwatch").WillReturnRows(doesntExist()) 52 conn.ExpectBegin() 53 conn.ExpectExec("CREATE SCHEMA IF NOT EXISTS pgwatch").WillReturnResult(pgxmock.NewResult("CREATE", 1)) 54 conn.ExpectBegin() 55 conn.ExpectExec(`INSERT.+metric`).WithArgs(AnyArgs(8)...).WillReturnResult(pgxmock.NewResult("INSERT", 1)).Times(uint(metricsCount)) 56 conn.ExpectExec(`INSERT.+preset`).WithArgs(AnyArgs(3)...).WillReturnResult(pgxmock.NewResult("INSERT", 1)).Times(uint(presetsCount)) 57 conn.ExpectCommit() 58 conn.ExpectCommit() 59 conn.ExpectPing() 60 61 readerWriter, err := metrics.NewPostgresMetricReaderWriterConn(ctx, conn) 62 a.NoError(err) 63 a.NotNil(readerWriter) 64 a.NoError(conn.ExpectationsWereMet()) 65 }) 66 67 t.Run("SchemaQueryFail", func(*testing.T) { 68 conn.ExpectQuery(`SELECT EXISTS`).WithArgs("pgwatch").WillReturnError(assert.AnError) 69 rw, err := metrics.NewPostgresMetricReaderWriterConn(ctx, conn) 70 a.Error(err) 71 a.Nil(rw) 72 a.NoError(conn.ExpectationsWereMet()) 73 }) 74 75 t.Run("BeginFail", func(*testing.T) { 76 conn.ExpectQuery(`SELECT EXISTS`).WithArgs("pgwatch").WillReturnRows(doesntExist()) 77 conn.ExpectBegin().WillReturnError(assert.AnError) 78 rw, err := metrics.NewPostgresMetricReaderWriterConn(ctx, conn) 79 a.Error(err) 80 a.Nil(rw) 81 a.NoError(conn.ExpectationsWereMet()) 82 }) 83 84 t.Run("CreateSchemaFail", func(*testing.T) { 85 conn.ExpectQuery(`SELECT EXISTS`).WithArgs("pgwatch").WillReturnRows(doesntExist()) 86 conn.ExpectBegin() 87 conn.ExpectExec("CREATE SCHEMA IF NOT EXISTS pgwatch").WillReturnError(assert.AnError) 88 conn.ExpectRollback() 89 rw, err := metrics.NewPostgresMetricReaderWriterConn(ctx, conn) 90 a.Error(err) 91 a.Nil(rw) 92 a.NoError(conn.ExpectationsWereMet()) 93 }) 94 95 t.Run("WriteDefaultMetricsBeginFail", func(*testing.T) { 96 conn.ExpectQuery(`SELECT EXISTS`).WithArgs("pgwatch").WillReturnRows(doesntExist()) 97 conn.ExpectBegin() 98 conn.ExpectExec("CREATE SCHEMA IF NOT EXISTS pgwatch").WillReturnResult(pgxmock.NewResult("CREATE", 1)) 99 conn.ExpectBegin().WillReturnError(assert.AnError) 100 conn.ExpectRollback() 101 rw, err := metrics.NewPostgresMetricReaderWriterConn(ctx, conn) 102 a.Error(err) 103 a.Nil(rw) 104 a.NoError(conn.ExpectationsWereMet()) 105 }) 106 107 t.Run("WriteInsertMetricsFail", func(*testing.T) { 108 conn.ExpectQuery(`SELECT EXISTS`).WithArgs("pgwatch").WillReturnRows(doesntExist()) 109 conn.ExpectBegin() 110 conn.ExpectExec("CREATE SCHEMA IF NOT EXISTS pgwatch").WillReturnResult(pgxmock.NewResult("CREATE", 1)) 111 conn.ExpectBegin() 112 conn.ExpectExec(`INSERT.+metric`).WithArgs(AnyArgs(8)...).WillReturnError(assert.AnError) 113 conn.ExpectRollback() 114 rw, err := metrics.NewPostgresMetricReaderWriterConn(ctx, conn) 115 a.Error(err) 116 a.Nil(rw) 117 a.NoError(conn.ExpectationsWereMet()) 118 }) 119 120 t.Run("WriteInsertPresetsFail", func(*testing.T) { 121 conn.ExpectQuery(`SELECT EXISTS`).WithArgs("pgwatch").WillReturnRows(doesntExist()) 122 conn.ExpectBegin() 123 conn.ExpectExec("CREATE SCHEMA IF NOT EXISTS pgwatch").WillReturnResult(pgxmock.NewResult("CREATE", 1)) 124 conn.ExpectBegin() 125 conn.ExpectExec(`INSERT.+metric`).WithArgs(AnyArgs(8)...).WillReturnResult(pgxmock.NewResult("INSERT", 1)).Times(uint(metricsCount)) 126 conn.ExpectExec(`INSERT.+preset`).WithArgs(AnyArgs(3)...).WillReturnError(assert.AnError) 127 conn.ExpectRollback() 128 rw, err := metrics.NewPostgresMetricReaderWriterConn(ctx, conn) 129 a.Error(err) 130 a.Nil(rw) 131 a.NoError(conn.ExpectationsWereMet()) 132 }) 133 134 t.Run("DefaultMetricsCommitFail", func(*testing.T) { 135 conn.ExpectQuery(`SELECT EXISTS`).WithArgs("pgwatch").WillReturnRows(doesntExist()) 136 conn.ExpectBegin() 137 conn.ExpectExec("CREATE SCHEMA IF NOT EXISTS pgwatch").WillReturnResult(pgxmock.NewResult("CREATE", 1)) 138 conn.ExpectBegin() 139 conn.ExpectExec(`INSERT.+metric`).WithArgs(AnyArgs(8)...).WillReturnResult(pgxmock.NewResult("INSERT", 1)).Times(uint(metricsCount)) 140 conn.ExpectExec(`INSERT.+preset`).WithArgs(AnyArgs(3)...).WillReturnResult(pgxmock.NewResult("INSERT", 1)).Times(uint(presetsCount)) 141 conn.ExpectCommit().WillReturnError(assert.AnError) 142 conn.ExpectRollback() 143 rw, err := metrics.NewPostgresMetricReaderWriterConn(ctx, conn) 144 a.Error(err) 145 a.Nil(rw) 146 a.NoError(conn.ExpectationsWereMet()) 147 }) 148 149 t.Run("CommitFail", func(*testing.T) { 150 conn.ExpectQuery(`SELECT EXISTS`).WithArgs("pgwatch").WillReturnRows(doesntExist()) 151 conn.ExpectBegin() 152 conn.ExpectExec("CREATE SCHEMA IF NOT EXISTS pgwatch").WillReturnResult(pgxmock.NewResult("CREATE", 1)) 153 conn.ExpectBegin() 154 conn.ExpectExec(`INSERT.+metric`).WithArgs(AnyArgs(8)...).WillReturnResult(pgxmock.NewResult("INSERT", 1)).Times(uint(metricsCount)) 155 conn.ExpectExec(`INSERT.+preset`).WithArgs(AnyArgs(3)...).WillReturnResult(pgxmock.NewResult("INSERT", 1)).Times(uint(presetsCount)) 156 conn.ExpectCommit() 157 conn.ExpectCommit().WillReturnError(assert.AnError) 158 rw, err := metrics.NewPostgresMetricReaderWriterConn(ctx, conn) 159 a.Error(err) 160 a.Nil(rw) 161 a.NoError(conn.ExpectationsWereMet()) 162 }) 163 164 t.Run("SchemaExists", func(*testing.T) { 165 conn.ExpectQuery(`SELECT EXISTS`).WithArgs("pgwatch").WillReturnRows(pgxmock.NewRows([]string{"exists"}).AddRow(true)) 166 conn.ExpectPing() 167 rw, err := metrics.NewPostgresMetricReaderWriterConn(ctx, conn) 168 a.NoError(err) 169 a.NotNil(rw) 170 a.NoError(conn.ExpectationsWereMet()) 171 }) 172 } 173 174 func newTestReaderWriter(t *testing.T) (pgxmock.PgxPoolIface, metrics.ReaderWriter) { 175 t.Helper() 176 conn, err := pgxmock.NewPool() 177 if err != nil { 178 t.Fatalf("pgxmock.NewPool: %v", err) 179 } 180 conn.ExpectQuery(`SELECT EXISTS`).WithArgs("pgwatch"). 181 WillReturnRows(pgxmock.NewRows([]string{"exists"}).AddRow(true)) 182 conn.ExpectPing() 183 rw, err := metrics.NewPostgresMetricReaderWriterConn(ctx, conn) 184 if err != nil { 185 t.Fatalf("NewPostgresMetricReaderWriterConn: %v", err) 186 } 187 return conn, rw 188 } 189 190 func TestMetricsToPostgres(t *testing.T) { 191 conn, rw := newTestReaderWriter(t) 192 193 metricsRows := func() *pgxmock.Rows { 194 return pgxmock.NewRows([]string{"name", "sqls", "init_sql", "description", "node_status", "gauges", "is_instance_level", "storage_name"}). 195 AddRow("test", metrics.SQLs{11: "select"}, "init", "desc", "primary", []string{"*"}, true, "storage") 196 } 197 presetRows := func() *pgxmock.Rows { 198 return pgxmock.NewRows([]string{"name", "description", "metrics"}). 199 AddRow("test", "desc", metrics.MetricIntervals{"metric": 30}) 200 } 201 202 t.Run("GetMetrics", func(t *testing.T) { 203 conn.ExpectQuery(`SELECT.+FROM.+metric`).WillReturnRows(metricsRows()) 204 conn.ExpectQuery(`SELECT.+FROM.+preset`).WillReturnRows(presetRows()) 205 m, err := rw.GetMetrics() 206 assert.NoError(t, err) 207 assert.Len(t, m.MetricDefs, 1) 208 }) 209 210 t.Run("GetMetricsFail", func(t *testing.T) { 211 conn.ExpectQuery(`SELECT.+FROM.+metric`).WillReturnError(assert.AnError) 212 _, err := rw.GetMetrics() 213 assert.Error(t, err) 214 }) 215 216 t.Run("GetPresetsFail", func(t *testing.T) { 217 conn.ExpectQuery(`SELECT.+FROM.+metric`).WillReturnRows(metricsRows()) 218 conn.ExpectQuery(`SELECT.+FROM.+preset`).WillReturnError(assert.AnError) 219 _, err := rw.GetMetrics() 220 assert.Error(t, err) 221 }) 222 223 t.Run("GetMetricsScanFail", func(t *testing.T) { 224 conn.ExpectQuery(`SELECT.+FROM.+metric`).WillReturnRows(metricsRows().RowError(0, assert.AnError)) 225 _, err := rw.GetMetrics() 226 assert.Error(t, err) 227 }) 228 229 t.Run("GetPresetsScanFail", func(t *testing.T) { 230 conn.ExpectQuery(`SELECT.+FROM.+metric`).WillReturnRows(metricsRows()) 231 conn.ExpectQuery(`SELECT.+FROM.+preset`).WillReturnRows(presetRows().RowError(0, assert.AnError)) 232 _, err := rw.GetMetrics() 233 assert.Error(t, err) 234 }) 235 236 t.Run("WriteMetrics", func(t *testing.T) { 237 conn.ExpectBegin().WillReturnError(assert.AnError) 238 assert.Error(t, rw.WriteMetrics(&metrics.Metrics{})) 239 }) 240 241 t.Run("DeleteMetric", func(t *testing.T) { 242 conn.ExpectExec(`DELETE.+metric`).WithArgs("test").WillReturnResult(pgxmock.NewResult("DELETE", 1)) 243 assert.NoError(t, rw.DeleteMetric("test")) 244 }) 245 246 t.Run("UpdateMetric", func(t *testing.T) { 247 conn.ExpectExec(`INSERT.+metric`).WithArgs(AnyArgs(8)...).WillReturnResult(pgxmock.NewResult("UPDATE", 1)) 248 assert.NoError(t, rw.UpdateMetric("test", metrics.Metric{})) 249 }) 250 251 t.Run("FailUpdateMetric", func(t *testing.T) { 252 conn.ExpectExec(`INSERT.+metric`).WithArgs(AnyArgs(8)...).WillReturnResult(pgxmock.NewResult("UPDATE", 0)) 253 assert.ErrorIs(t, rw.UpdateMetric("test", metrics.Metric{}), metrics.ErrMetricNotFound) 254 }) 255 256 t.Run("DeletePreset", func(t *testing.T) { 257 conn.ExpectExec(`DELETE.+preset`).WithArgs("test").WillReturnResult(pgxmock.NewResult("DELETE", 1)) 258 assert.NoError(t, rw.DeletePreset("test")) 259 }) 260 261 t.Run("UpdatePreset", func(t *testing.T) { 262 conn.ExpectExec(`INSERT.+preset`).WithArgs(AnyArgs(3)...).WillReturnResult(pgxmock.NewResult("INSERT", 1)) 263 assert.NoError(t, rw.UpdatePreset("test", metrics.Preset{})) 264 }) 265 266 t.Run("FailUpdatePreset", func(t *testing.T) { 267 conn.ExpectExec(`INSERT.+preset`).WithArgs(AnyArgs(3)...).WillReturnResult(pgxmock.NewResult("INSERT", 0)) 268 assert.ErrorIs(t, rw.UpdatePreset("test", metrics.Preset{}), metrics.ErrPresetNotFound) 269 }) 270 271 t.Run("DeleteMetricError", func(t *testing.T) { 272 conn.ExpectExec(`DELETE.+metric`).WithArgs("test").WillReturnError(assert.AnError) 273 assert.Error(t, rw.DeleteMetric("test")) 274 }) 275 276 t.Run("UpdateMetricExecError", func(t *testing.T) { 277 conn.ExpectExec(`INSERT.+metric`).WithArgs(AnyArgs(8)...).WillReturnError(assert.AnError) 278 assert.Error(t, rw.UpdateMetric("test", metrics.Metric{})) 279 }) 280 281 t.Run("DeletePresetError", func(t *testing.T) { 282 conn.ExpectExec(`DELETE.+preset`).WithArgs("test").WillReturnError(assert.AnError) 283 assert.Error(t, rw.DeletePreset("test")) 284 }) 285 286 t.Run("UpdatePresetExecError", func(t *testing.T) { 287 conn.ExpectExec(`INSERT.+preset`).WithArgs(AnyArgs(3)...).WillReturnError(assert.AnError) 288 assert.Error(t, rw.UpdatePreset("test", metrics.Preset{})) 289 }) 290 291 assert.NoError(t, conn.ExpectationsWereMet()) 292 } 293 294 func TestCreateMetric(t *testing.T) { 295 conn, rw := newTestReaderWriter(t) 296 297 t.Run("Success", func(t *testing.T) { 298 conn.ExpectExec(`INSERT.+metric`).WithArgs(AnyArgs(8)...).WillReturnResult(pgxmock.NewResult("INSERT", 1)) 299 assert.NoError(t, rw.CreateMetric("new_metric", metrics.Metric{})) 300 }) 301 302 t.Run("Duplicate", func(t *testing.T) { 303 conn.ExpectExec(`INSERT.+metric`).WithArgs(AnyArgs(8)...).WillReturnError(&pgconn.PgError{Code: "23505"}) 304 assert.ErrorIs(t, rw.CreateMetric("existing_metric", metrics.Metric{}), metrics.ErrMetricExists) 305 }) 306 307 t.Run("ExecError", func(t *testing.T) { 308 conn.ExpectExec(`INSERT.+metric`).WithArgs(AnyArgs(8)...).WillReturnError(assert.AnError) 309 err := rw.CreateMetric("fail_metric", metrics.Metric{}) 310 assert.Error(t, err) 311 assert.NotErrorIs(t, err, metrics.ErrMetricExists) 312 }) 313 314 assert.NoError(t, conn.ExpectationsWereMet()) 315 } 316 317 func TestCreatePreset(t *testing.T) { 318 conn, rw := newTestReaderWriter(t) 319 320 t.Run("Success", func(t *testing.T) { 321 conn.ExpectExec(`INSERT.+preset`).WithArgs(AnyArgs(3)...).WillReturnResult(pgxmock.NewResult("INSERT", 1)) 322 assert.NoError(t, rw.CreatePreset("new_preset", metrics.Preset{})) 323 }) 324 325 t.Run("Duplicate", func(t *testing.T) { 326 conn.ExpectExec(`INSERT.+preset`).WithArgs(AnyArgs(3)...).WillReturnError(&pgconn.PgError{Code: "23505"}) 327 assert.ErrorIs(t, rw.CreatePreset("existing_preset", metrics.Preset{}), metrics.ErrPresetExists) 328 }) 329 330 t.Run("ExecError", func(t *testing.T) { 331 conn.ExpectExec(`INSERT.+preset`).WithArgs(AnyArgs(3)...).WillReturnError(assert.AnError) 332 err := rw.CreatePreset("fail_preset", metrics.Preset{}) 333 assert.Error(t, err) 334 assert.NotErrorIs(t, err, metrics.ErrPresetExists) 335 }) 336 337 assert.NoError(t, conn.ExpectationsWereMet()) 338 } 339