From 3d1e954521b017fd89b19e02bfb49ca279661b1a Mon Sep 17 00:00:00 2001 From: Rostislav Dugin Date: Sat, 9 Aug 2025 08:51:24 +0300 Subject: [PATCH] TEMP (metrics): Add part of metrics --- backend/cmd/main.go | 10 ++ .../databases/databases/postgresql/model.go | 92 +++++++++++ .../collectors/db_monitoring_service.go | 3 + .../collectors/system_monitoring_service.go | 3 + .../postgres/metrics/background_service.go | 33 ++++ .../monitoring/postgres/metrics/controller.go | 62 ++++++++ .../monitoring/postgres/metrics/di.go | 35 +++++ .../monitoring/postgres/metrics/dto.go | 14 ++ .../monitoring/postgres/metrics/enums.go | 22 +++ .../monitoring/postgres/metrics/model.go | 20 +++ .../monitoring/postgres/metrics/repository.go | 45 ++++++ .../monitoring/postgres/metrics/service.go | 42 +++++ .../postgres/settings/controller.go | 97 ++++++++++++ .../monitoring/postgres/settings/di.go | 28 ++++ .../monitoring/postgres/settings/model.go | 71 +++++++++ .../postgres/settings/repository.go | 50 ++++++ .../monitoring/postgres/settings/service.go | 148 ++++++++++++++++++ .../usecases/postgresql/restore_backup_uc.go | 21 ++- backend/internal/util/tools/enums.go | 7 + 19 files changed, 800 insertions(+), 3 deletions(-) create mode 100644 backend/internal/features/monitoring/postgres/collectors/db_monitoring_service.go create mode 100644 backend/internal/features/monitoring/postgres/collectors/system_monitoring_service.go create mode 100644 backend/internal/features/monitoring/postgres/metrics/background_service.go create mode 100644 backend/internal/features/monitoring/postgres/metrics/controller.go create mode 100644 backend/internal/features/monitoring/postgres/metrics/di.go create mode 100644 backend/internal/features/monitoring/postgres/metrics/dto.go create mode 100644 backend/internal/features/monitoring/postgres/metrics/enums.go create mode 100644 backend/internal/features/monitoring/postgres/metrics/model.go create mode 100644 backend/internal/features/monitoring/postgres/metrics/repository.go create mode 100644 backend/internal/features/monitoring/postgres/metrics/service.go create mode 100644 backend/internal/features/monitoring/postgres/settings/controller.go create mode 100644 backend/internal/features/monitoring/postgres/settings/di.go create mode 100644 backend/internal/features/monitoring/postgres/settings/model.go create mode 100644 backend/internal/features/monitoring/postgres/settings/repository.go create mode 100644 backend/internal/features/monitoring/postgres/settings/service.go diff --git a/backend/cmd/main.go b/backend/cmd/main.go index 54eaf10..8520174 100644 --- a/backend/cmd/main.go +++ b/backend/cmd/main.go @@ -20,6 +20,8 @@ import ( "postgresus-backend/internal/features/disk" healthcheck_attempt "postgresus-backend/internal/features/healthcheck/attempt" healthcheck_config "postgresus-backend/internal/features/healthcheck/config" + postgres_monitoring_metrics "postgresus-backend/internal/features/monitoring/postgres/metrics" + postgres_monitoring_settings "postgresus-backend/internal/features/monitoring/postgres/settings" "postgresus-backend/internal/features/notifiers" "postgresus-backend/internal/features/restores" "postgresus-backend/internal/features/storages" @@ -147,6 +149,8 @@ func setUpRoutes(r *gin.Engine) { healthcheckAttemptController := healthcheck_attempt.GetHealthcheckAttemptController() diskController := disk.GetDiskController() backupConfigController := backups_config.GetBackupConfigController() + postgresMonitoringSettingsController := postgres_monitoring_settings.GetPostgresMonitoringSettingsController() + postgresMonitoringMetricsController := postgres_monitoring_metrics.GetPostgresMonitoringMetricsController() downdetectContoller.RegisterRoutes(v1) userController.RegisterRoutes(v1) @@ -160,6 +164,8 @@ func setUpRoutes(r *gin.Engine) { healthcheckConfigController.RegisterRoutes(v1) healthcheckAttemptController.RegisterRoutes(v1) backupConfigController.RegisterRoutes(v1) + postgresMonitoringSettingsController.RegisterRoutes(v1) + postgresMonitoringMetricsController.RegisterRoutes(v1) } func setUpDependencies() { @@ -188,6 +194,10 @@ func runBackgroundTasks(log *slog.Logger) { go runWithPanicLogging(log, "healthcheck attempt background service", func() { healthcheck_attempt.GetHealthcheckAttemptBackgroundService().RunBackgroundTasks() }) + + go runWithPanicLogging(log, "postgres monitoring metrics background service", func() { + postgres_monitoring_metrics.GetPostgresMonitoringMetricsBackgroundService().Run() + }) } func runWithPanicLogging(log *slog.Logger, serviceName string, fn func()) { diff --git a/backend/internal/features/databases/databases/postgresql/model.go b/backend/internal/features/databases/databases/postgresql/model.go index 3783082..15abf9e 100644 --- a/backend/internal/features/databases/databases/postgresql/model.go +++ b/backend/internal/features/databases/databases/postgresql/model.go @@ -7,6 +7,7 @@ import ( "log/slog" "postgresus-backend/internal/util/tools" "regexp" + "slices" "time" "github.com/google/uuid" @@ -175,3 +176,94 @@ func buildConnectionStringForDB(p *PostgresqlDatabase, dbName string) string { sslMode, ) } + +func (p *PostgresqlDatabase) InstallExtensions(extensions []tools.PostgresqlExtension) error { + if len(extensions) == 0 { + return nil + } + + if p.Database == nil || *p.Database == "" { + return errors.New("database name is required for installing extensions") + } + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + // Build connection string for the specific database + connStr := buildConnectionStringForDB(p, *p.Database) + + // Connect to database + conn, err := pgx.Connect(ctx, connStr) + if err != nil { + return fmt.Errorf("failed to connect to database '%s': %w", *p.Database, err) + } + defer func() { + if closeErr := conn.Close(ctx); closeErr != nil { + // Log error but don't return it to avoid masking the main error + } + }() + + // Check which extensions are already installed + installedExtensions, err := p.getInstalledExtensions(ctx, conn) + if err != nil { + return fmt.Errorf("failed to check installed extensions: %w", err) + } + + // Install missing extensions + for _, extension := range extensions { + if contains(installedExtensions, string(extension)) { + continue // Extension already installed + } + + if err := p.installExtension(ctx, conn, string(extension)); err != nil { + return fmt.Errorf("failed to install extension '%s': %w", extension, err) + } + } + + return nil +} + +// getInstalledExtensions queries the database for currently installed extensions +func (p *PostgresqlDatabase) getInstalledExtensions(ctx context.Context, conn *pgx.Conn) ([]string, error) { + query := "SELECT extname FROM pg_extension" + + rows, err := conn.Query(ctx, query) + if err != nil { + return nil, fmt.Errorf("failed to query installed extensions: %w", err) + } + defer rows.Close() + + var extensions []string + for rows.Next() { + var extname string + + if err := rows.Scan(&extname); err != nil { + return nil, fmt.Errorf("failed to scan extension name: %w", err) + } + + extensions = append(extensions, extname) + } + + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("error iterating over extension rows: %w", err) + } + + return extensions, nil +} + +// installExtension installs a single PostgreSQL extension +func (p *PostgresqlDatabase) installExtension(ctx context.Context, conn *pgx.Conn, extensionName string) error { + query := fmt.Sprintf("CREATE EXTENSION IF NOT EXISTS %s", extensionName) + + _, err := conn.Exec(ctx, query) + if err != nil { + return fmt.Errorf("failed to execute CREATE EXTENSION: %w", err) + } + + return nil +} + +// contains checks if a string slice contains a specific string +func contains(slice []string, item string) bool { + return slices.Contains(slice, item) +} diff --git a/backend/internal/features/monitoring/postgres/collectors/db_monitoring_service.go b/backend/internal/features/monitoring/postgres/collectors/db_monitoring_service.go new file mode 100644 index 0000000..fe96863 --- /dev/null +++ b/backend/internal/features/monitoring/postgres/collectors/db_monitoring_service.go @@ -0,0 +1,3 @@ +package postgres_monitoring_collectors + +type DbMonitoringBackgroundService struct {} \ No newline at end of file diff --git a/backend/internal/features/monitoring/postgres/collectors/system_monitoring_service.go b/backend/internal/features/monitoring/postgres/collectors/system_monitoring_service.go new file mode 100644 index 0000000..1fa0a35 --- /dev/null +++ b/backend/internal/features/monitoring/postgres/collectors/system_monitoring_service.go @@ -0,0 +1,3 @@ +package postgres_monitoring_collectors + +type SystemMonitoringBackgroundService struct {} \ No newline at end of file diff --git a/backend/internal/features/monitoring/postgres/metrics/background_service.go b/backend/internal/features/monitoring/postgres/metrics/background_service.go new file mode 100644 index 0000000..11ce69e --- /dev/null +++ b/backend/internal/features/monitoring/postgres/metrics/background_service.go @@ -0,0 +1,33 @@ +package postgres_monitoring_metrics + +import ( + "postgresus-backend/internal/config" + "postgresus-backend/internal/util/logger" + "time" +) + +var log = logger.GetLogger() + +type PostgresMonitoringMetricsBackgroundService struct { + metricsRepository *PostgresMonitoringMetricRepository +} + +func (s *PostgresMonitoringMetricsBackgroundService) Run() { + for { + if config.IsShouldShutdown() { + return + } + + s.RemoveOldMetrics() + + time.Sleep(5 * time.Minute) + } +} + +func (s *PostgresMonitoringMetricsBackgroundService) RemoveOldMetrics() { + monthAgo := time.Now().UTC().Add(-3 * 30 * 24 * time.Hour) + + if err := s.metricsRepository.RemoveOlderThan(monthAgo); err != nil { + log.Error("Failed to remove old metrics", "error", err) + } +} diff --git a/backend/internal/features/monitoring/postgres/metrics/controller.go b/backend/internal/features/monitoring/postgres/metrics/controller.go new file mode 100644 index 0000000..40c939e --- /dev/null +++ b/backend/internal/features/monitoring/postgres/metrics/controller.go @@ -0,0 +1,62 @@ +package postgres_monitoring_metrics + +import ( + "net/http" + "postgresus-backend/internal/features/users" + + "github.com/gin-gonic/gin" +) + +type PostgresMonitoringMetricsController struct { + metricsService *PostgresMonitoringMetricService + userService *users.UserService +} + +func (c *PostgresMonitoringMetricsController) RegisterRoutes(router *gin.RouterGroup) { + router.POST("/postgres-monitoring-metrics/get", c.GetMetrics) +} + +// GetMetrics +// @Summary Get postgres monitoring metrics +// @Description Get postgres monitoring metrics for a database within a time range +// @Tags postgres-monitoring-metrics +// @Accept json +// @Produce json +// @Param request body GetMetricsRequest true "Metrics request data" +// @Success 200 {object} []PostgresMonitoringMetric +// @Failure 400 +// @Failure 401 +// @Router /postgres-monitoring-metrics/get [post] +func (c *PostgresMonitoringMetricsController) GetMetrics(ctx *gin.Context) { + var requestDTO GetMetricsRequest + if err := ctx.ShouldBindJSON(&requestDTO); err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + authorizationHeader := ctx.GetHeader("Authorization") + if authorizationHeader == "" { + ctx.JSON(http.StatusUnauthorized, gin.H{"error": "authorization header is required"}) + return + } + + user, err := c.userService.GetUserFromToken(authorizationHeader) + if err != nil { + ctx.JSON(http.StatusUnauthorized, gin.H{"error": "invalid token"}) + return + } + + metrics, err := c.metricsService.GetMetrics( + user, + requestDTO.DatabaseID, + requestDTO.MetricType, + requestDTO.From, + requestDTO.To, + ) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + ctx.JSON(http.StatusOK, metrics) +} diff --git a/backend/internal/features/monitoring/postgres/metrics/di.go b/backend/internal/features/monitoring/postgres/metrics/di.go new file mode 100644 index 0000000..a883fe7 --- /dev/null +++ b/backend/internal/features/monitoring/postgres/metrics/di.go @@ -0,0 +1,35 @@ +package postgres_monitoring_metrics + +import ( + "postgresus-backend/internal/features/databases" + "postgresus-backend/internal/features/users" +) + +var metricsRepository = &PostgresMonitoringMetricRepository{} +var metricsService = &PostgresMonitoringMetricService{ + metricsRepository, + databases.GetDatabaseService(), +} +var metricsController = &PostgresMonitoringMetricsController{ + metricsService, + users.GetUserService(), +} +var metricsBackgroundService = &PostgresMonitoringMetricsBackgroundService{ + metricsRepository, +} + +func GetPostgresMonitoringMetricsController() *PostgresMonitoringMetricsController { + return metricsController +} + +func GetPostgresMonitoringMetricsService() *PostgresMonitoringMetricService { + return metricsService +} + +func GetPostgresMonitoringMetricsRepository() *PostgresMonitoringMetricRepository { + return metricsRepository +} + +func GetPostgresMonitoringMetricsBackgroundService() *PostgresMonitoringMetricsBackgroundService { + return metricsBackgroundService +} diff --git a/backend/internal/features/monitoring/postgres/metrics/dto.go b/backend/internal/features/monitoring/postgres/metrics/dto.go new file mode 100644 index 0000000..26ca872 --- /dev/null +++ b/backend/internal/features/monitoring/postgres/metrics/dto.go @@ -0,0 +1,14 @@ +package postgres_monitoring_metrics + +import ( + "time" + + "github.com/google/uuid" +) + +type GetMetricsRequest struct { + DatabaseID uuid.UUID `json:"databaseId" binding:"required"` + MetricType PostgresMonitoringMetricType `json:"metricType"` + From time.Time `json:"from" binding:"required"` + To time.Time `json:"to" binding:"required"` +} \ No newline at end of file diff --git a/backend/internal/features/monitoring/postgres/metrics/enums.go b/backend/internal/features/monitoring/postgres/metrics/enums.go new file mode 100644 index 0000000..5861e76 --- /dev/null +++ b/backend/internal/features/monitoring/postgres/metrics/enums.go @@ -0,0 +1,22 @@ +package postgres_monitoring_metrics + +type PostgresMonitoringMetricType string + +const ( + // system resources (need extensions) + MetricsTypeSystemCPU PostgresMonitoringMetricType = "SYSTEM_CPU_USAGE" + MetricsTypeSystemRAM PostgresMonitoringMetricType = "SYSTEM_RAM_USAGE" + MetricsTypeSystemROM PostgresMonitoringMetricType = "SYSTEM_ROM_USAGE" + MetricsTypeSystemIO PostgresMonitoringMetricType = "SYSTEM_IO_USAGE" + // db resources (don't need extensions) + MetricsTypeDbRAM PostgresMonitoringMetricType = "DB_RAM_USAGE" + MetricsTypeDbROM PostgresMonitoringMetricType = "DB_ROM_USAGE" + MetricsTypeDbIO PostgresMonitoringMetricType = "DB_IO_USAGE" +) + +type PostgresMonitoringMetricValueType string + +const ( + MetricsValueTypeByte PostgresMonitoringMetricValueType = "BYTE" + MetricsValueTypePercent PostgresMonitoringMetricValueType = "PERCENT" +) diff --git a/backend/internal/features/monitoring/postgres/metrics/model.go b/backend/internal/features/monitoring/postgres/metrics/model.go new file mode 100644 index 0000000..d135054 --- /dev/null +++ b/backend/internal/features/monitoring/postgres/metrics/model.go @@ -0,0 +1,20 @@ +package postgres_monitoring_metrics + +import ( + "time" + + "github.com/google/uuid" +) + +type PostgresMonitoringMetric struct { + ID uuid.UUID `json:"id" gorm:"column:id;primaryKey;type:uuid;default:gen_random_uuid()"` + DatabaseID uuid.UUID `json:"databaseId" gorm:"column:database_id;not null;type:uuid"` + Metric PostgresMonitoringMetricType `json:"metric" gorm:"column:metric;not null"` + ValueType PostgresMonitoringMetricValueType `json:"valueType" gorm:"column:value_type;not null"` + Value float64 `json:"value" gorm:"column:value;not null"` + CreatedAt time.Time `json:"createdAt" gorm:"column:created_at;not null"` +} + +func (p *PostgresMonitoringMetric) TableName() string { + return "postgres_monitoring_metrics" +} diff --git a/backend/internal/features/monitoring/postgres/metrics/repository.go b/backend/internal/features/monitoring/postgres/metrics/repository.go new file mode 100644 index 0000000..da1d2bc --- /dev/null +++ b/backend/internal/features/monitoring/postgres/metrics/repository.go @@ -0,0 +1,45 @@ +package postgres_monitoring_metrics + +import ( + "postgresus-backend/internal/storage" + "time" + + "github.com/google/uuid" +) + +type PostgresMonitoringMetricRepository struct{} + +func (r *PostgresMonitoringMetricRepository) Insert(metrics []PostgresMonitoringMetric) error { + return storage.GetDb().Create(&metrics).Error +} + +func (r *PostgresMonitoringMetricRepository) GetByMetrics( + databaseID uuid.UUID, + metricType PostgresMonitoringMetricType, + from time.Time, + to time.Time, +) ([]PostgresMonitoringMetric, error) { + var metrics []PostgresMonitoringMetric + + query := storage.GetDb(). + Where("database_id = ?", databaseID). + Where("created_at >= ?", from). + Where("created_at <= ?", to). + Where("metric = ?", metricType) + + if err := query. + Order("created_at DESC"). + Find(&metrics).Error; err != nil { + return nil, err + } + + return metrics, nil +} + +func (r *PostgresMonitoringMetricRepository) RemoveOlderThan( + olderThan time.Time, +) error { + return storage.GetDb(). + Where("created_at < ?", olderThan). + Delete(&PostgresMonitoringMetric{}).Error +} \ No newline at end of file diff --git a/backend/internal/features/monitoring/postgres/metrics/service.go b/backend/internal/features/monitoring/postgres/metrics/service.go new file mode 100644 index 0000000..ba148de --- /dev/null +++ b/backend/internal/features/monitoring/postgres/metrics/service.go @@ -0,0 +1,42 @@ +package postgres_monitoring_metrics + +import ( + "errors" + "postgresus-backend/internal/features/databases" + users_models "postgresus-backend/internal/features/users/models" + "time" + + "github.com/google/uuid" +) + +type PostgresMonitoringMetricService struct { + metricsRepository *PostgresMonitoringMetricRepository + databaseService *databases.DatabaseService +} + +func (s *PostgresMonitoringMetricService) Insert(metrics []PostgresMonitoringMetric) error { + if len(metrics) == 0 { + return nil + } + + return s.metricsRepository.Insert(metrics) +} + +func (s *PostgresMonitoringMetricService) GetMetrics( + user *users_models.User, + databaseID uuid.UUID, + metricType PostgresMonitoringMetricType, + from time.Time, + to time.Time, +) ([]PostgresMonitoringMetric, error) { + database, err := s.databaseService.GetDatabaseByID(databaseID) + if err != nil { + return nil, err + } + + if database.UserID != user.ID { + return nil, errors.New("database not found") + } + + return s.metricsRepository.GetByMetrics(databaseID, metricType, from, to) +} diff --git a/backend/internal/features/monitoring/postgres/settings/controller.go b/backend/internal/features/monitoring/postgres/settings/controller.go new file mode 100644 index 0000000..989756e --- /dev/null +++ b/backend/internal/features/monitoring/postgres/settings/controller.go @@ -0,0 +1,97 @@ +package postgres_monitoring_settings + +import ( + "net/http" + "postgresus-backend/internal/features/users" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +type PostgresMonitoringSettingsController struct { + postgresMonitoringSettingsService *PostgresMonitoringSettingsService + userService *users.UserService +} + +func (c *PostgresMonitoringSettingsController) RegisterRoutes(router *gin.RouterGroup) { + router.POST("/postgres-monitoring-settings/save", c.SaveSettings) + router.GET("/postgres-monitoring-settings/database/:id", c.GetSettingsByDbID) +} + +// SaveSettings +// @Summary Save postgres monitoring settings +// @Description Save or update postgres monitoring settings for a database +// @Tags postgres-monitoring-settings +// @Accept json +// @Produce json +// @Param request body PostgresMonitoringSettings true "Postgres monitoring settings data" +// @Success 200 {object} PostgresMonitoringSettings +// @Failure 400 +// @Failure 401 +// @Router /postgres-monitoring-settings/save [post] +func (c *PostgresMonitoringSettingsController) SaveSettings(ctx *gin.Context) { + var requestDTO PostgresMonitoringSettings + if err := ctx.ShouldBindJSON(&requestDTO); err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + authorizationHeader := ctx.GetHeader("Authorization") + if authorizationHeader == "" { + ctx.JSON(http.StatusUnauthorized, gin.H{"error": "authorization header is required"}) + return + } + + user, err := c.userService.GetUserFromToken(authorizationHeader) + if err != nil { + ctx.JSON(http.StatusUnauthorized, gin.H{"error": "invalid token"}) + return + } + + err = c.postgresMonitoringSettingsService.Save(user, &requestDTO) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + ctx.JSON(http.StatusOK, requestDTO) +} + +// GetSettingsByDbID +// @Summary Get postgres monitoring settings by database ID +// @Description Get postgres monitoring settings for a specific database +// @Tags postgres-monitoring-settings +// @Produce json +// @Param id path string true "Database ID" +// @Success 200 {object} PostgresMonitoringSettings +// @Failure 400 +// @Failure 401 +// @Failure 404 +// @Router /postgres-monitoring-settings/database/{id} [get] +func (c *PostgresMonitoringSettingsController) GetSettingsByDbID(ctx *gin.Context) { + dbID := ctx.Param("id") + if dbID == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "database ID is required"}) + return + } + + authorizationHeader := ctx.GetHeader("Authorization") + if authorizationHeader == "" { + ctx.JSON(http.StatusUnauthorized, gin.H{"error": "authorization header is required"}) + return + } + + user, err := c.userService.GetUserFromToken(authorizationHeader) + if err != nil { + ctx.JSON(http.StatusUnauthorized, gin.H{"error": "invalid token"}) + return + } + + settings, err := c.postgresMonitoringSettingsService.GetByDbID(user, uuid.MustParse(dbID)) + if err != nil { + ctx.JSON(http.StatusNotFound, gin.H{"error": "postgres monitoring settings not found"}) + return + } + + ctx.JSON(http.StatusOK, settings) +} diff --git a/backend/internal/features/monitoring/postgres/settings/di.go b/backend/internal/features/monitoring/postgres/settings/di.go new file mode 100644 index 0000000..5c26bba --- /dev/null +++ b/backend/internal/features/monitoring/postgres/settings/di.go @@ -0,0 +1,28 @@ +package postgres_monitoring_settings + +import ( + "postgresus-backend/internal/features/databases" + "postgresus-backend/internal/features/users" +) + +var postgresMonitoringSettingsRepository = &PostgresMonitoringSettingsRepository{} +var postgresMonitoringSettingsService = &PostgresMonitoringSettingsService{ + databases.GetDatabaseService(), + postgresMonitoringSettingsRepository, +} +var postgresMonitoringSettingsController = &PostgresMonitoringSettingsController{ + postgresMonitoringSettingsService, + users.GetUserService(), +} + +func GetPostgresMonitoringSettingsController() *PostgresMonitoringSettingsController { + return postgresMonitoringSettingsController +} + +func GetPostgresMonitoringSettingsService() *PostgresMonitoringSettingsService { + return postgresMonitoringSettingsService +} + +func GetPostgresMonitoringSettingsRepository() *PostgresMonitoringSettingsRepository { + return postgresMonitoringSettingsRepository +} diff --git a/backend/internal/features/monitoring/postgres/settings/model.go b/backend/internal/features/monitoring/postgres/settings/model.go new file mode 100644 index 0000000..103a515 --- /dev/null +++ b/backend/internal/features/monitoring/postgres/settings/model.go @@ -0,0 +1,71 @@ +package postgres_monitoring_settings + +import ( + "postgresus-backend/internal/features/databases" + "postgresus-backend/internal/util/tools" + "strings" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +type PostgresMonitoringSettings struct { + DatabaseID uuid.UUID `json:"databaseId" gorm:"column:database_id;not null"` + Database *databases.Database `json:"database" gorm:"foreignKey:DatabaseID"` + + IsSystemResourcesMonitoringEnabled bool `json:"isSystemResourcesMonitoringEnabled" gorm:"column:is_system_resources_monitoring_enabled;not null"` + IsDbResourcesMonitoringEnabled bool `json:"isDbResourcesMonitoringEnabled" gorm:"column:is_db_resources_monitoring_enabled;not null"` + MonitoringIntervalSeconds int64 `json:"monitoringIntervalSeconds" gorm:"column:monitoring_interval_seconds;not null"` + + InstalledExtensions []tools.PostgresqlExtension `json:"installedExtensions" gorm:"-"` + InstalledExtensionsRaw string `json:"-" gorm:"column:installed_extensions_raw"` +} + +func (p *PostgresMonitoringSettings) TableName() string { + return "postgres_monitoring_settings" +} + +func (p *PostgresMonitoringSettings) AfterFind(tx *gorm.DB) error { + if p.InstalledExtensionsRaw != "" { + rawExtensions := strings.Split(p.InstalledExtensionsRaw, ",") + + p.InstalledExtensions = make([]tools.PostgresqlExtension, len(rawExtensions)) + + for i, ext := range rawExtensions { + p.InstalledExtensions[i] = tools.PostgresqlExtension(ext) + } + } else { + p.InstalledExtensions = []tools.PostgresqlExtension{} + } + + return nil +} + +func (p *PostgresMonitoringSettings) BeforeSave(tx *gorm.DB) error { + extensions := make([]string, len(p.InstalledExtensions)) + + for i, ext := range p.InstalledExtensions { + extensions[i] = string(ext) + } + + p.InstalledExtensionsRaw = strings.Join(extensions, ",") + + return nil +} + +func (p *PostgresMonitoringSettings) AddInstalledExtensions(extensions []tools.PostgresqlExtension) { + for _, ext := range extensions { + exists := false + + for _, existing := range p.InstalledExtensions { + if existing == ext { + exists = true + break + } + } + + if !exists { + p.InstalledExtensions = append(p.InstalledExtensions, ext) + } + } +} diff --git a/backend/internal/features/monitoring/postgres/settings/repository.go b/backend/internal/features/monitoring/postgres/settings/repository.go new file mode 100644 index 0000000..6b1bb7c --- /dev/null +++ b/backend/internal/features/monitoring/postgres/settings/repository.go @@ -0,0 +1,50 @@ +package postgres_monitoring_settings + +import ( + "errors" + "postgresus-backend/internal/storage" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +type PostgresMonitoringSettingsRepository struct{} + +func (r *PostgresMonitoringSettingsRepository) Save(settings *PostgresMonitoringSettings) error { + return storage.GetDb().Save(settings).Error +} + +func (r *PostgresMonitoringSettingsRepository) GetByDbID( + dbID uuid.UUID, +) (*PostgresMonitoringSettings, error) { + var settings PostgresMonitoringSettings + + if err := storage. + GetDb(). + Where("database_id = ?", dbID). + First(&settings).Error; err != nil { + return nil, err + } + + return &settings, nil +} + +func (r *PostgresMonitoringSettingsRepository) GetByDbIDWithRelations( + dbID uuid.UUID, +) (*PostgresMonitoringSettings, error) { + var settings PostgresMonitoringSettings + + if err := storage. + GetDb(). + Preload("Database"). + Where("database_id = ?", dbID). + First(&settings).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, nil + } + + return nil, err + } + + return &settings, nil +} diff --git a/backend/internal/features/monitoring/postgres/settings/service.go b/backend/internal/features/monitoring/postgres/settings/service.go new file mode 100644 index 0000000..57def68 --- /dev/null +++ b/backend/internal/features/monitoring/postgres/settings/service.go @@ -0,0 +1,148 @@ +package postgres_monitoring_settings + +import ( + "errors" + "postgresus-backend/internal/features/databases" + users_models "postgresus-backend/internal/features/users/models" + "postgresus-backend/internal/util/logger" + "postgresus-backend/internal/util/tools" + + "github.com/google/uuid" +) + +var log = logger.GetLogger() + +type PostgresMonitoringSettingsService struct { + databaseService *databases.DatabaseService + postgresMonitoringSettingsRepository *PostgresMonitoringSettingsRepository +} + +func (s *PostgresMonitoringSettingsService) OnDatabaseCreated(dbID uuid.UUID) { + db, err := s.databaseService.GetDatabaseByID(dbID) + if err != nil { + return + } + + if db.Type != databases.DatabaseTypePostgres { + return + } + + settings := &PostgresMonitoringSettings{ + DatabaseID: dbID, + IsSystemResourcesMonitoringEnabled: true, + IsDbResourcesMonitoringEnabled: true, + MonitoringIntervalSeconds: 15, + } + + installedExtensions, err := s.ensureSystemMonitoringExtensionsInstalled(dbID) + if err != nil { + settings.IsSystemResourcesMonitoringEnabled = false + } else { + settings.AddInstalledExtensions(installedExtensions) + } + + err = s.postgresMonitoringSettingsRepository.Save(settings) + if err != nil { + log.Error("failed to save postgres monitoring settings", "error", err) + } +} + +func (s *PostgresMonitoringSettingsService) Save( + user *users_models.User, + settings *PostgresMonitoringSettings, +) error { + db, err := s.databaseService.GetDatabaseByID(settings.DatabaseID) + if err != nil { + return err + } + + if db.UserID != user.ID { + return errors.New("user does not have access to this database") + } + + existingSettings, err := s.postgresMonitoringSettingsRepository.GetByDbID(settings.DatabaseID) + if err != nil { + return err + } + + if existingSettings != nil && + settings.IsSystemResourcesMonitoringEnabled && + !existingSettings.IsSystemResourcesMonitoringEnabled { + extensions, err := s.ensureSystemMonitoringExtensionsInstalled(settings.DatabaseID) + if err != nil { + return err + } + + settings.AddInstalledExtensions(extensions) + } + + return s.postgresMonitoringSettingsRepository.Save(settings) +} + +func (s *PostgresMonitoringSettingsService) GetByDbID( + user *users_models.User, + dbID uuid.UUID, +) (*PostgresMonitoringSettings, error) { + dbSettings, err := s.postgresMonitoringSettingsRepository.GetByDbIDWithRelations(dbID) + if err != nil { + return nil, err + } + + if dbSettings == nil { + dbSettings = &PostgresMonitoringSettings{ + DatabaseID: dbID, + + IsSystemResourcesMonitoringEnabled: false, + IsDbResourcesMonitoringEnabled: false, + MonitoringIntervalSeconds: 15, + + InstalledExtensions: []tools.PostgresqlExtension{}, + InstalledExtensionsRaw: "", + } + + err = s.Save(user, dbSettings) + if err != nil { + return nil, err + } + + return s.GetByDbID(user, dbID) + } + + if dbSettings.Database.UserID != user.ID { + return nil, errors.New("user does not have access to this database") + } + + return dbSettings, nil +} + +func (s *PostgresMonitoringSettingsService) ensureSystemMonitoringExtensionsInstalled( + dbID uuid.UUID, +) ([]tools.PostgresqlExtension, error) { + database, err := s.databaseService.GetDatabaseByID(dbID) + if err != nil { + return nil, err + } + + if database.Type != databases.DatabaseTypePostgres { + return nil, errors.New("database is not a postgres database") + } + + if database.Postgresql == nil { + return nil, errors.New("database is not a postgres database") + } + + if database.Postgresql.Version < tools.PostgresqlVersion16 { + return nil, errors.New("system monitoring extensions supported for postgres 16+") + } + + extensions := []tools.PostgresqlExtension{ + tools.PostgresqlExtensionPgProctab, + } + + err = database.Postgresql.InstallExtensions(extensions) + if err != nil { + return nil, err + } + + return extensions, nil +} diff --git a/backend/internal/features/restores/usecases/postgresql/restore_backup_uc.go b/backend/internal/features/restores/usecases/postgresql/restore_backup_uc.go index 4f93556..5c3ea98 100644 --- a/backend/internal/features/restores/usecases/postgresql/restore_backup_uc.go +++ b/backend/internal/features/restores/usecases/postgresql/restore_backup_uc.go @@ -163,7 +163,7 @@ func (uc *RestorePostgresqlBackupUsecase) restoreFromStorage( // Add the temporary backup file as the last argument to pg_restore args = append(args, tempBackupFile) - return uc.executePgRestore(ctx, pgBin, args, pgpassFile, pgConfig) + return uc.executePgRestore(ctx, pgBin, args, pgpassFile, pgConfig, backup) } // downloadBackupToTempFile downloads backup data from storage to a temporary file @@ -236,6 +236,7 @@ func (uc *RestorePostgresqlBackupUsecase) executePgRestore( args []string, pgpassFile string, pgConfig *pgtypes.PostgresqlDatabase, + backup *backups.Backup, ) error { cmd := exec.CommandContext(ctx, pgBin, args...) uc.logger.Info("Executing PostgreSQL restore command", "command", cmd.String()) @@ -284,7 +285,7 @@ func (uc *RestorePostgresqlBackupUsecase) executePgRestore( return fmt.Errorf("restore cancelled due to shutdown") } - return uc.handlePgRestoreError(waitErr, stderrOutput, pgBin, args) + return uc.handlePgRestoreError(waitErr, stderrOutput, pgBin, args, backup, pgConfig) } return nil @@ -336,6 +337,8 @@ func (uc *RestorePostgresqlBackupUsecase) handlePgRestoreError( stderrOutput []byte, pgBin string, args []string, + backup *backups.Backup, + pgConfig *pgtypes.PostgresqlDatabase, ) error { // Enhanced error handling for PostgreSQL connection and restore issues stderrStr := string(stderrOutput) @@ -404,8 +407,20 @@ func (uc *RestorePostgresqlBackupUsecase) handlePgRestoreError( stderrStr, ) } else if containsIgnoreCase(stderrStr, "database") && containsIgnoreCase(stderrStr, "does not exist") { + backupDbName := "unknown" + if backup.Database != nil && backup.Database.Postgresql != nil && backup.Database.Postgresql.Database != nil { + backupDbName = *backup.Database.Postgresql.Database + } + + targetDbName := "unknown" + if pgConfig.Database != nil { + targetDbName = *pgConfig.Database + } + errorMsg = fmt.Sprintf( - "Target database does not exist. Create the database before restoring. stderr: %s", + "Target database does not exist (backup db %s, not found %s). Create the database before restoring. stderr: %s", + backupDbName, + targetDbName, stderrStr, ) } diff --git a/backend/internal/util/tools/enums.go b/backend/internal/util/tools/enums.go index f3d44eb..ff42308 100644 --- a/backend/internal/util/tools/enums.go +++ b/backend/internal/util/tools/enums.go @@ -5,6 +5,13 @@ import ( "strconv" ) +type PostgresqlExtension string + +const ( + // needed for system monitoring (CPU, RAM) + PostgresqlExtensionPgProctab PostgresqlExtension = "pg_proctab" +) + type PostgresqlVersion string const (