Skip to content

Commit 31685f7

Browse files
FEATURE (metrics): Add metrics
1 parent 9dbcf91 commit 31685f7

File tree

22 files changed

+1467
-3
lines changed

22 files changed

+1467
-3
lines changed

backend/cmd/main.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import (
2020
"postgresus-backend/internal/features/disk"
2121
healthcheck_attempt "postgresus-backend/internal/features/healthcheck/attempt"
2222
healthcheck_config "postgresus-backend/internal/features/healthcheck/config"
23+
postgres_monitoring_metrics "postgresus-backend/internal/features/monitoring/postgres/metrics"
24+
postgres_monitoring_settings "postgresus-backend/internal/features/monitoring/postgres/settings"
2325
"postgresus-backend/internal/features/notifiers"
2426
"postgresus-backend/internal/features/restores"
2527
"postgresus-backend/internal/features/storages"
@@ -158,6 +160,8 @@ func setUpRoutes(r *gin.Engine) {
158160
healthcheckAttemptController := healthcheck_attempt.GetHealthcheckAttemptController()
159161
diskController := disk.GetDiskController()
160162
backupConfigController := backups_config.GetBackupConfigController()
163+
postgresMonitoringSettingsController := postgres_monitoring_settings.GetPostgresMonitoringSettingsController()
164+
postgresMonitoringMetricsController := postgres_monitoring_metrics.GetPostgresMonitoringMetricsController()
161165

162166
downdetectContoller.RegisterRoutes(v1)
163167
userController.RegisterRoutes(v1)
@@ -171,13 +175,16 @@ func setUpRoutes(r *gin.Engine) {
171175
healthcheckConfigController.RegisterRoutes(v1)
172176
healthcheckAttemptController.RegisterRoutes(v1)
173177
backupConfigController.RegisterRoutes(v1)
178+
postgresMonitoringSettingsController.RegisterRoutes(v1)
179+
postgresMonitoringMetricsController.RegisterRoutes(v1)
174180
}
175181

176182
func setUpDependencies() {
177183
backups.SetupDependencies()
178184
backups.SetupDependencies()
179185
restores.SetupDependencies()
180186
healthcheck_config.SetupDependencies()
187+
postgres_monitoring_settings.SetupDependencies()
181188
}
182189

183190
func runBackgroundTasks(log *slog.Logger) {
@@ -199,6 +206,10 @@ func runBackgroundTasks(log *slog.Logger) {
199206
go runWithPanicLogging(log, "healthcheck attempt background service", func() {
200207
healthcheck_attempt.GetHealthcheckAttemptBackgroundService().RunBackgroundTasks()
201208
})
209+
210+
go runWithPanicLogging(log, "postgres monitoring metrics background service", func() {
211+
postgres_monitoring_metrics.GetPostgresMonitoringMetricsBackgroundService().Run()
212+
})
202213
}
203214

204215
func runWithPanicLogging(log *slog.Logger, serviceName string, fn func()) {

backend/internal/features/databases/databases/postgresql/model.go

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"log/slog"
88
"postgresus-backend/internal/util/tools"
99
"regexp"
10+
"slices"
1011
"time"
1112

1213
"github.com/google/uuid"
@@ -175,3 +176,101 @@ func buildConnectionStringForDB(p *PostgresqlDatabase, dbName string) string {
175176
sslMode,
176177
)
177178
}
179+
180+
func (p *PostgresqlDatabase) InstallExtensions(extensions []tools.PostgresqlExtension) error {
181+
if len(extensions) == 0 {
182+
return nil
183+
}
184+
185+
if p.Database == nil || *p.Database == "" {
186+
return errors.New("database name is required for installing extensions")
187+
}
188+
189+
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
190+
defer cancel()
191+
192+
// Build connection string for the specific database
193+
connStr := buildConnectionStringForDB(p, *p.Database)
194+
195+
// Connect to database
196+
conn, err := pgx.Connect(ctx, connStr)
197+
if err != nil {
198+
return fmt.Errorf("failed to connect to database '%s': %w", *p.Database, err)
199+
}
200+
defer func() {
201+
if closeErr := conn.Close(ctx); closeErr != nil {
202+
fmt.Println("failed to close connection: %w", closeErr)
203+
}
204+
}()
205+
206+
// Check which extensions are already installed
207+
installedExtensions, err := p.getInstalledExtensions(ctx, conn)
208+
if err != nil {
209+
return fmt.Errorf("failed to check installed extensions: %w", err)
210+
}
211+
212+
// Install missing extensions
213+
for _, extension := range extensions {
214+
if contains(installedExtensions, string(extension)) {
215+
continue // Extension already installed
216+
}
217+
218+
if err := p.installExtension(ctx, conn, string(extension)); err != nil {
219+
return fmt.Errorf("failed to install extension '%s': %w", extension, err)
220+
}
221+
}
222+
223+
return nil
224+
}
225+
226+
// getInstalledExtensions queries the database for currently installed extensions
227+
func (p *PostgresqlDatabase) getInstalledExtensions(
228+
ctx context.Context,
229+
conn *pgx.Conn,
230+
) ([]string, error) {
231+
query := "SELECT extname FROM pg_extension"
232+
233+
rows, err := conn.Query(ctx, query)
234+
if err != nil {
235+
return nil, fmt.Errorf("failed to query installed extensions: %w", err)
236+
}
237+
defer rows.Close()
238+
239+
var extensions []string
240+
for rows.Next() {
241+
var extname string
242+
243+
if err := rows.Scan(&extname); err != nil {
244+
return nil, fmt.Errorf("failed to scan extension name: %w", err)
245+
}
246+
247+
extensions = append(extensions, extname)
248+
}
249+
250+
if err := rows.Err(); err != nil {
251+
return nil, fmt.Errorf("error iterating over extension rows: %w", err)
252+
}
253+
254+
return extensions, nil
255+
}
256+
257+
// installExtension installs a single PostgreSQL extension
258+
func (p *PostgresqlDatabase) installExtension(
259+
ctx context.Context,
260+
conn *pgx.Conn,
261+
extensionName string,
262+
) error {
263+
query := fmt.Sprintf("CREATE EXTENSION IF NOT EXISTS %s", extensionName)
264+
265+
_, err := conn.Exec(ctx, query)
266+
if err != nil {
267+
return fmt.Errorf("failed to execute CREATE EXTENSION: %w", err)
268+
}
269+
270+
return nil
271+
}
272+
273+
// contains checks if a string slice contains a specific string
274+
func contains(slice []string, item string) bool {
275+
return slices.Contains(slice, item)
276+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
package postgres_monitoring_collectors
2+
3+
type DbMonitoringBackgroundService struct{}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
package postgres_monitoring_collectors
2+
3+
type SystemMonitoringBackgroundService struct{}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package postgres_monitoring_metrics
2+
3+
import (
4+
"postgresus-backend/internal/config"
5+
"postgresus-backend/internal/util/logger"
6+
"time"
7+
)
8+
9+
var log = logger.GetLogger()
10+
11+
type PostgresMonitoringMetricsBackgroundService struct {
12+
metricsRepository *PostgresMonitoringMetricRepository
13+
}
14+
15+
func (s *PostgresMonitoringMetricsBackgroundService) Run() {
16+
for {
17+
if config.IsShouldShutdown() {
18+
return
19+
}
20+
21+
s.RemoveOldMetrics()
22+
23+
time.Sleep(5 * time.Minute)
24+
}
25+
}
26+
27+
func (s *PostgresMonitoringMetricsBackgroundService) RemoveOldMetrics() {
28+
monthAgo := time.Now().UTC().Add(-3 * 30 * 24 * time.Hour)
29+
30+
if err := s.metricsRepository.RemoveOlderThan(monthAgo); err != nil {
31+
log.Error("Failed to remove old metrics", "error", err)
32+
}
33+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package postgres_monitoring_metrics
2+
3+
import (
4+
"net/http"
5+
"postgresus-backend/internal/features/users"
6+
7+
"github.com/gin-gonic/gin"
8+
)
9+
10+
type PostgresMonitoringMetricsController struct {
11+
metricsService *PostgresMonitoringMetricService
12+
userService *users.UserService
13+
}
14+
15+
func (c *PostgresMonitoringMetricsController) RegisterRoutes(router *gin.RouterGroup) {
16+
router.POST("/postgres-monitoring-metrics/get", c.GetMetrics)
17+
}
18+
19+
// GetMetrics
20+
// @Summary Get postgres monitoring metrics
21+
// @Description Get postgres monitoring metrics for a database within a time range
22+
// @Tags postgres-monitoring-metrics
23+
// @Accept json
24+
// @Produce json
25+
// @Param request body GetMetricsRequest true "Metrics request data"
26+
// @Success 200 {object} []PostgresMonitoringMetric
27+
// @Failure 400
28+
// @Failure 401
29+
// @Router /postgres-monitoring-metrics/get [post]
30+
func (c *PostgresMonitoringMetricsController) GetMetrics(ctx *gin.Context) {
31+
var requestDTO GetMetricsRequest
32+
if err := ctx.ShouldBindJSON(&requestDTO); err != nil {
33+
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
34+
return
35+
}
36+
37+
authorizationHeader := ctx.GetHeader("Authorization")
38+
if authorizationHeader == "" {
39+
ctx.JSON(http.StatusUnauthorized, gin.H{"error": "authorization header is required"})
40+
return
41+
}
42+
43+
user, err := c.userService.GetUserFromToken(authorizationHeader)
44+
if err != nil {
45+
ctx.JSON(http.StatusUnauthorized, gin.H{"error": "invalid token"})
46+
return
47+
}
48+
49+
metrics, err := c.metricsService.GetMetrics(
50+
user,
51+
requestDTO.DatabaseID,
52+
requestDTO.MetricType,
53+
requestDTO.From,
54+
requestDTO.To,
55+
)
56+
if err != nil {
57+
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
58+
return
59+
}
60+
61+
ctx.JSON(http.StatusOK, metrics)
62+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package postgres_monitoring_metrics
2+
3+
import (
4+
"postgresus-backend/internal/features/databases"
5+
"postgresus-backend/internal/features/users"
6+
)
7+
8+
var metricsRepository = &PostgresMonitoringMetricRepository{}
9+
var metricsService = &PostgresMonitoringMetricService{
10+
metricsRepository,
11+
databases.GetDatabaseService(),
12+
}
13+
var metricsController = &PostgresMonitoringMetricsController{
14+
metricsService,
15+
users.GetUserService(),
16+
}
17+
var metricsBackgroundService = &PostgresMonitoringMetricsBackgroundService{
18+
metricsRepository,
19+
}
20+
21+
func GetPostgresMonitoringMetricsController() *PostgresMonitoringMetricsController {
22+
return metricsController
23+
}
24+
25+
func GetPostgresMonitoringMetricsService() *PostgresMonitoringMetricService {
26+
return metricsService
27+
}
28+
29+
func GetPostgresMonitoringMetricsRepository() *PostgresMonitoringMetricRepository {
30+
return metricsRepository
31+
}
32+
33+
func GetPostgresMonitoringMetricsBackgroundService() *PostgresMonitoringMetricsBackgroundService {
34+
return metricsBackgroundService
35+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package postgres_monitoring_metrics
2+
3+
import (
4+
"time"
5+
6+
"github.com/google/uuid"
7+
)
8+
9+
type GetMetricsRequest struct {
10+
DatabaseID uuid.UUID `json:"databaseId" binding:"required"`
11+
MetricType PostgresMonitoringMetricType `json:"metricType"`
12+
From time.Time `json:"from" binding:"required"`
13+
To time.Time `json:"to" binding:"required"`
14+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package postgres_monitoring_metrics
2+
3+
type PostgresMonitoringMetricType string
4+
5+
const (
6+
// system resources (need extensions)
7+
MetricsTypeSystemCPU PostgresMonitoringMetricType = "SYSTEM_CPU_USAGE"
8+
MetricsTypeSystemRAM PostgresMonitoringMetricType = "SYSTEM_RAM_USAGE"
9+
MetricsTypeSystemROM PostgresMonitoringMetricType = "SYSTEM_ROM_USAGE"
10+
MetricsTypeSystemIO PostgresMonitoringMetricType = "SYSTEM_IO_USAGE"
11+
// db resources (don't need extensions)
12+
MetricsTypeDbRAM PostgresMonitoringMetricType = "DB_RAM_USAGE"
13+
MetricsTypeDbROM PostgresMonitoringMetricType = "DB_ROM_USAGE"
14+
MetricsTypeDbIO PostgresMonitoringMetricType = "DB_IO_USAGE"
15+
)
16+
17+
type PostgresMonitoringMetricValueType string
18+
19+
const (
20+
MetricsValueTypeByte PostgresMonitoringMetricValueType = "BYTE"
21+
MetricsValueTypePercent PostgresMonitoringMetricValueType = "PERCENT"
22+
)
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package postgres_monitoring_metrics
2+
3+
import (
4+
"time"
5+
6+
"github.com/google/uuid"
7+
)
8+
9+
type PostgresMonitoringMetric struct {
10+
ID uuid.UUID `json:"id" gorm:"column:id;primaryKey;type:uuid;default:gen_random_uuid()"`
11+
DatabaseID uuid.UUID `json:"databaseId" gorm:"column:database_id;not null;type:uuid"`
12+
Metric PostgresMonitoringMetricType `json:"metric" gorm:"column:metric;not null"`
13+
ValueType PostgresMonitoringMetricValueType `json:"valueType" gorm:"column:value_type;not null"`
14+
Value float64 `json:"value" gorm:"column:value;not null"`
15+
CreatedAt time.Time `json:"createdAt" gorm:"column:created_at;not null"`
16+
}
17+
18+
func (p *PostgresMonitoringMetric) TableName() string {
19+
return "postgres_monitoring_metrics"
20+
}

0 commit comments

Comments
 (0)