mirror of
https://github.com/databasus/databasus.git
synced 2026-04-06 08:41:58 +02:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7fb59bb5d0 | ||
|
|
dc9ddae42e | ||
|
|
a409c8ccb3 | ||
|
|
a018b0c62f | ||
|
|
97d7253dda | ||
|
|
81aadd19e1 | ||
|
|
432bdced3e |
3
.github/workflows/ci-release.yml
vendored
3
.github/workflows/ci-release.yml
vendored
@@ -132,11 +132,12 @@ jobs:
|
||||
TEST_POSTGRES_15_PORT=5003
|
||||
TEST_POSTGRES_16_PORT=5004
|
||||
TEST_POSTGRES_17_PORT=5005
|
||||
TEST_POSTGRES_18_PORT=5006
|
||||
# testing S3
|
||||
TEST_MINIO_PORT=9000
|
||||
TEST_MINIO_CONSOLE_PORT=9001
|
||||
# testing NAS
|
||||
TEST_NAS_PORT=5006
|
||||
TEST_NAS_PORT=7006
|
||||
EOF
|
||||
|
||||
- name: Start test containers
|
||||
|
||||
@@ -85,8 +85,8 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
> /etc/apt/sources.list.d/pgdg.list && \
|
||||
apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
postgresql-17 postgresql-client-13 postgresql-client-14 postgresql-client-15 \
|
||||
postgresql-client-16 postgresql-client-17 && \
|
||||
postgresql-17 postgresql-18 postgresql-client-13 postgresql-client-14 postgresql-client-15 \
|
||||
postgresql-client-16 postgresql-client-17 postgresql-client-18 && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Create postgres user and set up directories
|
||||
|
||||
12
README.md
12
README.md
@@ -1,8 +1,8 @@
|
||||
<div align="center">
|
||||
<img src="assets/logo.svg" style="margin-bottom: 20px;" alt="Postgresus Logo" width="250"/>
|
||||
|
||||
<h3>PostgreSQL monitoring and backup</h3>
|
||||
<p>Free, open source and self-hosted solution for automated PostgreSQL monitoring and backups. With multiple storage options and notifications</p>
|
||||
<h3>PostgreSQL backup</h3>
|
||||
<p>Free, open source and self-hosted solution for automated PostgreSQL backups. With multiple storage options and notifications</p>
|
||||
|
||||
<!-- Badges -->
|
||||
[](LICENSE)
|
||||
@@ -54,7 +54,7 @@
|
||||
|
||||
### 🐘 **PostgreSQL Support**
|
||||
|
||||
- **Multiple versions**: PostgreSQL 13, 14, 15, 16 and 17
|
||||
- **Multiple versions**: PostgreSQL 13, 14, 15, 16, 17 and 18
|
||||
- **SSL support**: Secure connections available
|
||||
- **Easy restoration**: One-click restore from any backup
|
||||
|
||||
@@ -64,12 +64,6 @@
|
||||
- **Privacy-first**: All your data stays on your infrastructure
|
||||
- **Open source**: MIT licensed, inspect every line of code
|
||||
|
||||
### 📊 **Monitoring & Insights**
|
||||
|
||||
- **Real-time metrics**: Track database health
|
||||
- **Historical data**: View trends and patterns over time
|
||||
- **Alert system**: Get notified when issues are detected
|
||||
|
||||
### 📦 Installation
|
||||
|
||||
You have three ways to install Postgresus:
|
||||
|
||||
@@ -22,8 +22,9 @@ TEST_POSTGRES_14_PORT=5002
|
||||
TEST_POSTGRES_15_PORT=5003
|
||||
TEST_POSTGRES_16_PORT=5004
|
||||
TEST_POSTGRES_17_PORT=5005
|
||||
TEST_POSTGRES_18_PORT=5006
|
||||
# testing S3
|
||||
TEST_MINIO_PORT=9000
|
||||
TEST_MINIO_CONSOLE_PORT=9001
|
||||
# testing NAS
|
||||
TEST_NAS_PORT=5006
|
||||
TEST_NAS_PORT=7006
|
||||
@@ -20,7 +20,6 @@ import (
|
||||
"postgresus-backend/internal/features/disk"
|
||||
healthcheck_attempt "postgresus-backend/internal/features/healthcheck/attempt"
|
||||
healthcheck_config "postgresus-backend/internal/features/healthcheck/config"
|
||||
postgres_monitoring_collectors "postgresus-backend/internal/features/monitoring/postgres/collectors"
|
||||
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"
|
||||
@@ -210,10 +209,6 @@ func runBackgroundTasks(log *slog.Logger) {
|
||||
go runWithPanicLogging(log, "postgres monitoring metrics background service", func() {
|
||||
postgres_monitoring_metrics.GetPostgresMonitoringMetricsBackgroundService().Run()
|
||||
})
|
||||
|
||||
go runWithPanicLogging(log, "postgres monitoring collectors background service", func() {
|
||||
postgres_monitoring_collectors.GetDbMonitoringBackgroundService().Run()
|
||||
})
|
||||
}
|
||||
|
||||
func runWithPanicLogging(log *slog.Logger, serviceName string, fn func()) {
|
||||
|
||||
@@ -87,6 +87,17 @@ services:
|
||||
container_name: test-postgres-17
|
||||
shm_size: 1gb
|
||||
|
||||
test-postgres-18:
|
||||
image: postgres:18
|
||||
ports:
|
||||
- "${TEST_POSTGRES_18_PORT}:5432"
|
||||
environment:
|
||||
- POSTGRES_DB=testdb
|
||||
- POSTGRES_USER=testuser
|
||||
- POSTGRES_PASSWORD=testpassword
|
||||
container_name: test-postgres-18
|
||||
shm_size: 1gb
|
||||
|
||||
# Test NAS server (Samba)
|
||||
test-nas:
|
||||
image: dperson/samba:latest
|
||||
|
||||
@@ -38,6 +38,7 @@ type EnvVariables struct {
|
||||
TestPostgres15Port string `env:"TEST_POSTGRES_15_PORT"`
|
||||
TestPostgres16Port string `env:"TEST_POSTGRES_16_PORT"`
|
||||
TestPostgres17Port string `env:"TEST_POSTGRES_17_PORT"`
|
||||
TestPostgres18Port string `env:"TEST_POSTGRES_18_PORT"`
|
||||
|
||||
TestMinioPort string `env:"TEST_MINIO_PORT"`
|
||||
TestMinioConsolePort string `env:"TEST_MINIO_CONSOLE_PORT"`
|
||||
@@ -154,6 +155,10 @@ func loadEnvVariables() {
|
||||
log.Error("TEST_POSTGRES_17_PORT is empty")
|
||||
os.Exit(1)
|
||||
}
|
||||
if env.TestPostgres18Port == "" {
|
||||
log.Error("TEST_POSTGRES_18_PORT is empty")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if env.TestMinioPort == "" {
|
||||
log.Error("TEST_MINIO_PORT is empty")
|
||||
|
||||
@@ -44,6 +44,7 @@ func SetupDependencies() {
|
||||
SetDatabaseStorageChangeListener(backupService)
|
||||
|
||||
databases.GetDatabaseService().AddDbRemoveListener(backupService)
|
||||
databases.GetDatabaseService().AddDbCopyListener(backups_config.GetBackupConfigService())
|
||||
}
|
||||
|
||||
func GetBackupService() *BackupService {
|
||||
|
||||
@@ -90,3 +90,18 @@ func (b *BackupConfig) Validate() error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *BackupConfig) Copy(newDatabaseID uuid.UUID) *BackupConfig {
|
||||
return &BackupConfig{
|
||||
DatabaseID: newDatabaseID,
|
||||
IsBackupsEnabled: b.IsBackupsEnabled,
|
||||
StorePeriod: b.StorePeriod,
|
||||
BackupIntervalID: uuid.Nil,
|
||||
BackupInterval: b.BackupInterval.Copy(),
|
||||
StorageID: b.StorageID,
|
||||
SendNotificationsOn: b.SendNotificationsOn,
|
||||
IsRetryIfFailed: b.IsRetryIfFailed,
|
||||
MaxFailedTriesCount: b.MaxFailedTriesCount,
|
||||
CpuCount: b.CpuCount,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,6 +130,20 @@ func (s *BackupConfigService) GetBackupConfigsWithEnabledBackups() ([]*BackupCon
|
||||
return s.backupConfigRepository.GetWithEnabledBackups()
|
||||
}
|
||||
|
||||
func (s *BackupConfigService) OnDatabaseCopied(originalDatabaseID, newDatabaseID uuid.UUID) {
|
||||
originalConfig, err := s.GetBackupConfigByDbId(originalDatabaseID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
newConfig := originalConfig.Copy(newDatabaseID)
|
||||
|
||||
_, err = s.SaveBackupConfig(newConfig)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (s *BackupConfigService) initializeDefaultConfig(
|
||||
databaseID uuid.UUID,
|
||||
) error {
|
||||
|
||||
@@ -21,6 +21,7 @@ func (c *DatabaseController) RegisterRoutes(router *gin.RouterGroup) {
|
||||
router.GET("/databases", c.GetDatabases)
|
||||
router.POST("/databases/:id/test-connection", c.TestDatabaseConnection)
|
||||
router.POST("/databases/test-connection-direct", c.TestDatabaseConnectionDirect)
|
||||
router.POST("/databases/:id/copy", c.CopyDatabase)
|
||||
router.GET("/databases/notifier/:id/is-using", c.IsNotifierUsing)
|
||||
|
||||
}
|
||||
@@ -325,3 +326,42 @@ func (c *DatabaseController) IsNotifierUsing(ctx *gin.Context) {
|
||||
|
||||
ctx.JSON(http.StatusOK, gin.H{"isUsing": isUsing})
|
||||
}
|
||||
|
||||
// CopyDatabase
|
||||
// @Summary Copy a database
|
||||
// @Description Copy an existing database configuration
|
||||
// @Tags databases
|
||||
// @Produce json
|
||||
// @Param id path string true "Database ID"
|
||||
// @Success 201 {object} Database
|
||||
// @Failure 400
|
||||
// @Failure 401
|
||||
// @Failure 500
|
||||
// @Router /databases/{id}/copy [post]
|
||||
func (c *DatabaseController) CopyDatabase(ctx *gin.Context) {
|
||||
id, err := uuid.Parse(ctx.Param("id"))
|
||||
if err != nil {
|
||||
ctx.JSON(http.StatusBadRequest, gin.H{"error": "invalid database ID"})
|
||||
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
|
||||
}
|
||||
|
||||
copiedDatabase, err := c.databaseService.CopyDatabase(user, id)
|
||||
if err != nil {
|
||||
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusCreated, copiedDatabase)
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ var databaseService = &DatabaseService{
|
||||
logger.GetLogger(),
|
||||
[]DatabaseCreationListener{},
|
||||
[]DatabaseRemoveListener{},
|
||||
[]DatabaseCopyListener{},
|
||||
}
|
||||
|
||||
var databaseController = &DatabaseController{
|
||||
|
||||
@@ -21,3 +21,7 @@ type DatabaseCreationListener interface {
|
||||
type DatabaseRemoveListener interface {
|
||||
OnBeforeDatabaseRemove(databaseID uuid.UUID) error
|
||||
}
|
||||
|
||||
type DatabaseCopyListener interface {
|
||||
OnDatabaseCopied(originalDatabaseID, newDatabaseID uuid.UUID)
|
||||
}
|
||||
|
||||
@@ -35,6 +35,10 @@ func (d *Database) Validate() error {
|
||||
|
||||
switch d.Type {
|
||||
case DatabaseTypePostgres:
|
||||
if d.Postgresql == nil {
|
||||
return errors.New("postgresql database is required")
|
||||
}
|
||||
|
||||
return d.Postgresql.Validate()
|
||||
default:
|
||||
return errors.New("invalid database type: " + string(d.Type))
|
||||
|
||||
@@ -3,6 +3,7 @@ package databases
|
||||
import (
|
||||
"errors"
|
||||
"log/slog"
|
||||
"postgresus-backend/internal/features/databases/databases/postgresql"
|
||||
"postgresus-backend/internal/features/notifiers"
|
||||
users_models "postgresus-backend/internal/features/users/models"
|
||||
"time"
|
||||
@@ -17,6 +18,7 @@ type DatabaseService struct {
|
||||
|
||||
dbCreationListener []DatabaseCreationListener
|
||||
dbRemoveListener []DatabaseRemoveListener
|
||||
dbCopyListener []DatabaseCopyListener
|
||||
}
|
||||
|
||||
func (s *DatabaseService) AddDbCreationListener(
|
||||
@@ -31,6 +33,12 @@ func (s *DatabaseService) AddDbRemoveListener(
|
||||
s.dbRemoveListener = append(s.dbRemoveListener, dbRemoveListener)
|
||||
}
|
||||
|
||||
func (s *DatabaseService) AddDbCopyListener(
|
||||
dbCopyListener DatabaseCopyListener,
|
||||
) {
|
||||
s.dbCopyListener = append(s.dbCopyListener, dbCopyListener)
|
||||
}
|
||||
|
||||
func (s *DatabaseService) CreateDatabase(
|
||||
user *users_models.User,
|
||||
database *Database,
|
||||
@@ -220,6 +228,67 @@ func (s *DatabaseService) SetLastBackupTime(databaseID uuid.UUID, backupTime tim
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *DatabaseService) CopyDatabase(
|
||||
user *users_models.User,
|
||||
databaseID uuid.UUID,
|
||||
) (*Database, error) {
|
||||
existingDatabase, err := s.dbRepository.FindByID(databaseID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if existingDatabase.UserID != user.ID {
|
||||
return nil, errors.New("you have not access to this database")
|
||||
}
|
||||
|
||||
newDatabase := &Database{
|
||||
ID: uuid.Nil,
|
||||
UserID: user.ID,
|
||||
Name: existingDatabase.Name + " (Copy)",
|
||||
Type: existingDatabase.Type,
|
||||
Notifiers: existingDatabase.Notifiers,
|
||||
LastBackupTime: nil,
|
||||
LastBackupErrorMessage: nil,
|
||||
HealthStatus: existingDatabase.HealthStatus,
|
||||
}
|
||||
|
||||
switch existingDatabase.Type {
|
||||
case DatabaseTypePostgres:
|
||||
if existingDatabase.Postgresql != nil {
|
||||
newDatabase.Postgresql = &postgresql.PostgresqlDatabase{
|
||||
ID: uuid.Nil,
|
||||
DatabaseID: nil,
|
||||
Version: existingDatabase.Postgresql.Version,
|
||||
Host: existingDatabase.Postgresql.Host,
|
||||
Port: existingDatabase.Postgresql.Port,
|
||||
Username: existingDatabase.Postgresql.Username,
|
||||
Password: existingDatabase.Postgresql.Password,
|
||||
Database: existingDatabase.Postgresql.Database,
|
||||
IsHttps: existingDatabase.Postgresql.IsHttps,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := newDatabase.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
copiedDatabase, err := s.dbRepository.Save(newDatabase)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, listener := range s.dbCreationListener {
|
||||
listener.OnDatabaseCreated(copiedDatabase.ID)
|
||||
}
|
||||
|
||||
for _, listener := range s.dbCopyListener {
|
||||
listener.OnDatabaseCopied(databaseID, copiedDatabase.ID)
|
||||
}
|
||||
|
||||
return copiedDatabase, nil
|
||||
}
|
||||
|
||||
func (s *DatabaseService) SetHealthStatus(
|
||||
databaseID uuid.UUID,
|
||||
healthStatus *HealthStatus,
|
||||
|
||||
@@ -64,6 +64,16 @@ func (i *Interval) ShouldTriggerBackup(now time.Time, lastBackupTime *time.Time)
|
||||
}
|
||||
}
|
||||
|
||||
func (i *Interval) Copy() *Interval {
|
||||
return &Interval{
|
||||
ID: uuid.Nil,
|
||||
Interval: i.Interval,
|
||||
TimeOfDay: i.TimeOfDay,
|
||||
Weekday: i.Weekday,
|
||||
DayOfMonth: i.DayOfMonth,
|
||||
}
|
||||
}
|
||||
|
||||
// daily trigger: honour the TimeOfDay slot and catch up the previous one
|
||||
func (i *Interval) shouldTriggerDaily(now, lastBackup time.Time) bool {
|
||||
if i.TimeOfDay == nil {
|
||||
|
||||
@@ -73,6 +73,7 @@ func Test_BackupAndRestorePostgresql_RestoreIsSuccesful(t *testing.T) {
|
||||
{"PostgreSQL 15", "15", env.TestPostgres15Port},
|
||||
{"PostgreSQL 16", "16", env.TestPostgres16Port},
|
||||
{"PostgreSQL 17", "17", env.TestPostgres17Port},
|
||||
{"PostgreSQL 18", "18", env.TestPostgres18Port},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
|
||||
@@ -20,6 +20,7 @@ const (
|
||||
PostgresqlVersion15 PostgresqlVersion = "15"
|
||||
PostgresqlVersion16 PostgresqlVersion = "16"
|
||||
PostgresqlVersion17 PostgresqlVersion = "17"
|
||||
PostgresqlVersion18 PostgresqlVersion = "18"
|
||||
)
|
||||
|
||||
type PostgresqlExecutable string
|
||||
@@ -41,6 +42,8 @@ func GetPostgresqlVersionEnum(version string) PostgresqlVersion {
|
||||
return PostgresqlVersion16
|
||||
case "17":
|
||||
return PostgresqlVersion17
|
||||
case "18":
|
||||
return PostgresqlVersion18
|
||||
default:
|
||||
panic(fmt.Sprintf("invalid postgresql version: %s", version))
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ func VerifyPostgresesInstallation(
|
||||
PostgresqlVersion15,
|
||||
PostgresqlVersion16,
|
||||
PostgresqlVersion17,
|
||||
PostgresqlVersion18,
|
||||
}
|
||||
|
||||
requiredCommands := []PostgresqlExecutable{
|
||||
|
||||
@@ -5,7 +5,7 @@ set -e # Exit on any error
|
||||
# Ensure non-interactive mode for apt
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
echo "Installing PostgreSQL client tools versions 13-17 for Linux (Debian/Ubuntu)..."
|
||||
echo "Installing PostgreSQL client tools versions 13-18 for Linux (Debian/Ubuntu)..."
|
||||
echo
|
||||
|
||||
# Check if running on supported system
|
||||
@@ -47,7 +47,7 @@ echo "Updating package list..."
|
||||
$SUDO apt-get update -qq -y
|
||||
|
||||
# Install client tools for each version
|
||||
versions="13 14 15 16 17"
|
||||
versions="13 14 15 16 17 18"
|
||||
|
||||
for version in $versions; do
|
||||
echo "Installing PostgreSQL $version client tools..."
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
set -e # Exit on any error
|
||||
|
||||
echo "Installing PostgreSQL client tools versions 13-17 for MacOS..."
|
||||
echo "Installing PostgreSQL client tools versions 13-18 for MacOS..."
|
||||
echo
|
||||
|
||||
# Check if Homebrew is installed
|
||||
@@ -36,6 +36,7 @@ declare -A PG_URLS=(
|
||||
["15"]="https://ftp.postgresql.org/pub/source/v15.8/postgresql-15.8.tar.gz"
|
||||
["16"]="https://ftp.postgresql.org/pub/source/v16.4/postgresql-16.4.tar.gz"
|
||||
["17"]="https://ftp.postgresql.org/pub/source/v17.0/postgresql-17.0.tar.gz"
|
||||
["18"]="https://ftp.postgresql.org/pub/source/v18.0/postgresql-18.0.tar.gz"
|
||||
)
|
||||
|
||||
# Create temporary build directory
|
||||
@@ -106,7 +107,7 @@ build_postgresql_client() {
|
||||
}
|
||||
|
||||
# Build each version
|
||||
versions="13 14 15 16 17"
|
||||
versions="13 14 15 16 17 18"
|
||||
|
||||
for version in $versions; do
|
||||
url=${PG_URLS[$version]}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
@echo off
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
echo Downloading and installing PostgreSQL versions 13-17 for Windows...
|
||||
echo Downloading and installing PostgreSQL versions 13-18 for Windows...
|
||||
echo.
|
||||
|
||||
:: Create downloads and postgresql directories if they don't exist
|
||||
@@ -22,9 +22,10 @@ set "PG14_URL=%BASE_URL%/postgresql-14.13-1-windows-x64.exe"
|
||||
set "PG15_URL=%BASE_URL%/postgresql-15.8-1-windows-x64.exe"
|
||||
set "PG16_URL=%BASE_URL%/postgresql-16.4-1-windows-x64.exe"
|
||||
set "PG17_URL=%BASE_URL%/postgresql-17.0-1-windows-x64.exe"
|
||||
set "PG18_URL=%BASE_URL%/postgresql-18.0-1-windows-x64.exe"
|
||||
|
||||
:: Array of versions
|
||||
set "versions=13 14 15 16 17"
|
||||
set "versions=13 14 15 16 17 18"
|
||||
|
||||
:: Download and install each version
|
||||
for %%v in (%versions%) do (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
This directory is needed only for development and CI\CD.
|
||||
|
||||
We have to download and install all the PostgreSQL versions from 13 to 17 locally.
|
||||
We have to download and install all the PostgreSQL versions from 13 to 18 locally.
|
||||
This is needed so we can call pg_dump, pg_dumpall, etc. on each version of the PostgreSQL database.
|
||||
|
||||
You do not need to install PostgreSQL fully with all the components.
|
||||
@@ -13,6 +13,7 @@ We have to install the following:
|
||||
- PostgreSQL 15
|
||||
- PostgreSQL 16
|
||||
- PostgreSQL 17
|
||||
- PostgreSQL 18
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -76,6 +77,7 @@ For example:
|
||||
- `./tools/postgresql/postgresql-15/bin/pg_dump`
|
||||
- `./tools/postgresql/postgresql-16/bin/pg_dump`
|
||||
- `./tools/postgresql/postgresql-17/bin/pg_dump`
|
||||
- `./tools/postgresql/postgresql-18/bin/pg_dump`
|
||||
|
||||
## Usage
|
||||
|
||||
|
||||
@@ -72,10 +72,7 @@ If you need to add some explanation, do it in appropriate place in the code. Or
|
||||
Before taking anything more than a couple of lines of code, please write Rostislav via Telegram (@rostislav_dugin) and confirm priority. It is possible that we already have something in the works, it is not needed or it's not project priority.
|
||||
|
||||
Nearsest features:
|
||||
- add system metrics (CPU, RAM, disk, IO) (in progress by Rostislav Dugin)
|
||||
- add copying of databases
|
||||
- add API keys and API actions
|
||||
- add UI component of backups lazy loaded
|
||||
|
||||
Backups flow:
|
||||
|
||||
@@ -102,29 +99,6 @@ Extra:
|
||||
|
||||
Monitoring flow:
|
||||
|
||||
|
||||
- add queries stats (slowest, most frequent, etc. via pg_stat_statements)
|
||||
- add DB size distribution chart (tables, indexes, etc.)
|
||||
- add alerting for slow queries (listen for slow query and if they reach >100ms - send message)
|
||||
- add alerting for high resource usage (listen for high resource usage and if they reach >90% - send message)
|
||||
- add performance test for DB (to compare DBs on different clouds and VPS)
|
||||
- add DB metrics (pg_stat_activity, pg_locks, pg_stat_database)
|
||||
- add chart of connections (from IPs, apps names, etc.)
|
||||
- add chart of transactions (TPS)
|
||||
- deadlocks chart
|
||||
- chart of connection attempts (to see crash loops)
|
||||
- add chart of IDLE transactions VS executing transactions
|
||||
- show queries that take the most IO time (suboptimal indexes)
|
||||
- show chart by top IO / CPU queries usage (see page 90 of the PostgreSQL monitoring book)
|
||||
|
||||
```
|
||||
exec_time | IO | CPU | query
|
||||
105 hrs | 73% | 27% | SELECT * FROM users;
|
||||
```
|
||||
|
||||
- chart of read / update / delete / insert queries
|
||||
- chart with deadlocks, conflicts, rollbacks (see page 115 of the PostgreSQL monitoring book)
|
||||
- stats of buffer usage
|
||||
- status of IO (DB, indexes, sequences)
|
||||
- % of cache hit
|
||||
- replication stats
|
||||
|
||||
@@ -48,6 +48,14 @@ export const databaseApi = {
|
||||
);
|
||||
},
|
||||
|
||||
async copyDatabase(id: string) {
|
||||
const requestOptions: RequestOptions = new RequestOptions();
|
||||
return apiHelper.fetchPostJson<Database>(
|
||||
`${getApplicationServer()}/api/v1/databases/${id}/copy`,
|
||||
requestOptions,
|
||||
);
|
||||
},
|
||||
|
||||
async testDatabaseConnection(id: string) {
|
||||
const requestOptions: RequestOptions = new RequestOptions();
|
||||
return apiHelper.fetchPostJson(
|
||||
|
||||
@@ -4,4 +4,5 @@ export enum PostgresqlVersion {
|
||||
PostgresqlVersion15 = '15',
|
||||
PostgresqlVersion16 = '16',
|
||||
PostgresqlVersion17 = '17',
|
||||
PostgresqlVersion18 = '18',
|
||||
}
|
||||
|
||||
@@ -56,13 +56,6 @@ export const DatabaseComponent = ({
|
||||
>
|
||||
Backups
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`cursor-pointer rounded-tl-md rounded-tr-md px-6 py-2 ${currentTab === 'metrics' ? 'bg-white' : 'bg-gray-200'}`}
|
||||
onClick={() => setCurrentTab('metrics')}
|
||||
>
|
||||
Metrics
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{currentTab === 'config' && (
|
||||
|
||||
@@ -7,10 +7,6 @@ import { ToastHelper } from '../../../shared/toast';
|
||||
import { ConfirmationComponent } from '../../../shared/ui';
|
||||
import { EditBackupConfigComponent, ShowBackupConfigComponent } from '../../backups';
|
||||
import { EditHealthcheckConfigComponent, ShowHealthcheckConfigComponent } from '../../healthcheck';
|
||||
import {
|
||||
EditMonitoringSettingsComponent,
|
||||
ShowMonitoringSettingsComponent,
|
||||
} from '../../monitoring/settings';
|
||||
import { EditDatabaseNotifiersComponent } from './edit/EditDatabaseNotifiersComponent';
|
||||
import { EditDatabaseSpecificDataComponent } from './edit/EditDatabaseSpecificDataComponent';
|
||||
import { ShowDatabaseNotifiersComponent } from './show/ShowDatabaseNotifiersComponent';
|
||||
@@ -39,13 +35,12 @@ export const DatabaseConfigComponent = ({
|
||||
const [isEditBackupConfig, setIsEditBackupConfig] = useState(false);
|
||||
const [isEditNotifiersSettings, setIsEditNotifiersSettings] = useState(false);
|
||||
const [isEditHealthcheckSettings, setIsEditHealthcheckSettings] = useState(false);
|
||||
const [isEditMonitoringSettings, setIsEditMonitoringSettings] = useState(false);
|
||||
|
||||
const [isNameUnsaved, setIsNameUnsaved] = useState(false);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
const [isTestingConnection, setIsTestingConnection] = useState(false);
|
||||
|
||||
const [isCopying, setIsCopying] = useState(false);
|
||||
const [isShowRemoveConfirm, setIsShowRemoveConfirm] = useState(false);
|
||||
const [isRemoving, setIsRemoving] = useState(false);
|
||||
|
||||
@@ -55,6 +50,28 @@ export const DatabaseConfigComponent = ({
|
||||
databaseApi.getDatabase(database.id).then(setDatabase);
|
||||
};
|
||||
|
||||
const copyDatabase = () => {
|
||||
if (!database) return;
|
||||
|
||||
setIsCopying(true);
|
||||
|
||||
databaseApi
|
||||
.copyDatabase(database.id)
|
||||
.then((copiedDatabase) => {
|
||||
ToastHelper.showToast({
|
||||
title: 'Database copied successfully!',
|
||||
description: `"${copiedDatabase.name}" has been created successfully`,
|
||||
});
|
||||
window.location.reload();
|
||||
})
|
||||
.catch((e: Error) => {
|
||||
alert(e.message);
|
||||
})
|
||||
.finally(() => {
|
||||
setIsCopying(false);
|
||||
});
|
||||
};
|
||||
|
||||
const testConnection = () => {
|
||||
if (!database) return;
|
||||
|
||||
@@ -97,16 +114,13 @@ export const DatabaseConfigComponent = ({
|
||||
});
|
||||
};
|
||||
|
||||
const startEdit = (
|
||||
type: 'name' | 'database' | 'backup-config' | 'notifiers' | 'healthcheck' | 'monitoring',
|
||||
) => {
|
||||
const startEdit = (type: 'name' | 'database' | 'backup-config' | 'notifiers' | 'healthcheck') => {
|
||||
setEditDatabase(JSON.parse(JSON.stringify(database)));
|
||||
setIsEditName(type === 'name');
|
||||
setIsEditDatabaseSpecificDataSettings(type === 'database');
|
||||
setIsEditBackupConfig(type === 'backup-config');
|
||||
setIsEditNotifiersSettings(type === 'notifiers');
|
||||
setIsEditHealthcheckSettings(type === 'healthcheck');
|
||||
setIsEditMonitoringSettings(type === 'monitoring');
|
||||
setIsNameUnsaved(false);
|
||||
};
|
||||
|
||||
@@ -344,40 +358,6 @@ export const DatabaseConfigComponent = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-10">
|
||||
<div className="w-[400px]">
|
||||
<div className="mt-5 flex items-center font-bold">
|
||||
<div>Monitoring settings</div>
|
||||
|
||||
{!isEditMonitoringSettings ? (
|
||||
<div className="ml-2 h-4 w-4 cursor-pointer" onClick={() => startEdit('monitoring')}>
|
||||
<img src="/icons/pen-gray.svg" />
|
||||
</div>
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-1 text-sm">
|
||||
{isEditMonitoringSettings ? (
|
||||
<EditMonitoringSettingsComponent
|
||||
database={database}
|
||||
onCancel={() => {
|
||||
setIsEditMonitoringSettings(false);
|
||||
loadSettings();
|
||||
}}
|
||||
onSaved={() => {
|
||||
setIsEditMonitoringSettings(false);
|
||||
loadSettings();
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<ShowMonitoringSettingsComponent database={database} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!isEditDatabaseSpecificDataSettings && (
|
||||
<div className="mt-10">
|
||||
<Button
|
||||
@@ -391,6 +371,17 @@ export const DatabaseConfigComponent = ({
|
||||
Test connection
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="primary"
|
||||
className="mr-1"
|
||||
ghost
|
||||
onClick={copyDatabase}
|
||||
loading={isCopying}
|
||||
disabled={isCopying}
|
||||
>
|
||||
Copy
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="primary"
|
||||
danger
|
||||
|
||||
@@ -13,6 +13,7 @@ interface Props {
|
||||
export const DatabasesComponent = ({ contentHeight }: Props) => {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [databases, setDatabases] = useState<Database[]>([]);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
|
||||
const [isShowAddDatabase, setIsShowAddDatabase] = useState(false);
|
||||
const [selectedDatabaseId, setSelectedDatabaseId] = useState<string | undefined>(undefined);
|
||||
@@ -58,23 +59,46 @@ export const DatabasesComponent = ({ contentHeight }: Props) => {
|
||||
</Button>
|
||||
);
|
||||
|
||||
const filteredDatabases = databases.filter((database) =>
|
||||
database.name.toLowerCase().includes(searchQuery.toLowerCase()),
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex grow">
|
||||
<div
|
||||
className="mx-3 w-[250px] min-w-[250px] overflow-y-auto"
|
||||
className="mx-3 w-[250px] min-w-[250px] overflow-y-auto pr-2"
|
||||
style={{ height: contentHeight }}
|
||||
>
|
||||
{databases.length >= 5 && addDatabaseButton}
|
||||
{databases.length >= 5 && (
|
||||
<>
|
||||
{addDatabaseButton}
|
||||
|
||||
{databases.map((database) => (
|
||||
<DatabaseCardComponent
|
||||
key={database.id}
|
||||
database={database}
|
||||
selectedDatabaseId={selectedDatabaseId}
|
||||
setSelectedDatabaseId={setSelectedDatabaseId}
|
||||
/>
|
||||
))}
|
||||
<div className="mb-2">
|
||||
<input
|
||||
placeholder="Search database"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="w-full border-b border-gray-300 p-1 text-gray-500 outline-none"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{filteredDatabases.length > 0
|
||||
? filteredDatabases.map((database) => (
|
||||
<DatabaseCardComponent
|
||||
key={database.id}
|
||||
database={database}
|
||||
selectedDatabaseId={selectedDatabaseId}
|
||||
setSelectedDatabaseId={setSelectedDatabaseId}
|
||||
/>
|
||||
))
|
||||
: searchQuery && (
|
||||
<div className="mb-4 text-center text-sm text-gray-500">
|
||||
No databases found matching "{searchQuery}"
|
||||
</div>
|
||||
)}
|
||||
|
||||
{databases.length < 5 && addDatabaseButton}
|
||||
|
||||
|
||||
@@ -155,6 +155,7 @@ export const EditDatabaseSpecificDataComponent = ({
|
||||
{ label: '15', value: PostgresqlVersion.PostgresqlVersion15 },
|
||||
{ label: '16', value: PostgresqlVersion.PostgresqlVersion16 },
|
||||
{ label: '17', value: PostgresqlVersion.PostgresqlVersion17 },
|
||||
{ label: '18', value: PostgresqlVersion.PostgresqlVersion18 },
|
||||
]}
|
||||
/>
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ const postgresqlVersionLabels = {
|
||||
[PostgresqlVersion.PostgresqlVersion15]: '15',
|
||||
[PostgresqlVersion.PostgresqlVersion16]: '16',
|
||||
[PostgresqlVersion.PostgresqlVersion17]: '17',
|
||||
[PostgresqlVersion.PostgresqlVersion18]: '18',
|
||||
};
|
||||
|
||||
export const ShowDatabaseSpecificDataComponent = ({ database }: Props) => {
|
||||
|
||||
Reference in New Issue
Block a user