extend average calculations in daily/monthly rollups
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
@@ -37,7 +37,7 @@ steps:
|
||||
- go install github.com/a-h/templ/cmd/templ@latest
|
||||
- go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest
|
||||
- go install github.com/swaggo/swag/cmd/swag@latest
|
||||
- go install github.com/goreleaser/nfpm/v2/cmd/nfpm@latest
|
||||
# - go install github.com/goreleaser/nfpm/v2/cmd/nfpm@latest
|
||||
- sqlc generate
|
||||
- templ generate -path ./components
|
||||
- swag init --exclude "pkg.mod,pkg.build,pkg.tools" -o server/router/docs
|
||||
|
||||
@@ -86,6 +86,7 @@ func (c *CronTask) RunVcenterSnapshotHourly(ctx context.Context, logger *slog.Lo
|
||||
}
|
||||
|
||||
presentSnapshots := make(map[string]inventorySnapshotRow, len(vcVms))
|
||||
totals := snapshotTotals{}
|
||||
for _, vm := range vcVms {
|
||||
if strings.HasPrefix(vm.Name(), "vCLS-") {
|
||||
continue
|
||||
@@ -113,6 +114,11 @@ func (c *CronTask) RunVcenterSnapshotHourly(ctx context.Context, logger *slog.Lo
|
||||
}
|
||||
row.IsPresent = "TRUE"
|
||||
presentSnapshots[vm.Reference().Value] = row
|
||||
|
||||
totals.VmCount++
|
||||
totals.VcpuTotal += nullInt64ToInt(row.InitialVcpus)
|
||||
totals.RamTotal += nullInt64ToInt(row.InitialRam)
|
||||
totals.DiskTotal += nullFloat64ToFloat(row.ProvisionedDisk)
|
||||
}
|
||||
|
||||
for _, row := range presentSnapshots {
|
||||
@@ -137,6 +143,14 @@ func (c *CronTask) RunVcenterSnapshotHourly(ctx context.Context, logger *slog.Lo
|
||||
}
|
||||
|
||||
vc.Logout()
|
||||
|
||||
c.Logger.Info("Hourly snapshot summary",
|
||||
"vcenter", url,
|
||||
"vm_count", totals.VmCount,
|
||||
"vcpu_total", totals.VcpuTotal,
|
||||
"ram_total_mb", totals.RamTotal,
|
||||
"disk_total_gb", totals.DiskTotal,
|
||||
)
|
||||
}
|
||||
|
||||
c.Logger.Debug("Finished hourly vcenter snapshot")
|
||||
@@ -160,17 +174,61 @@ func (c *CronTask) RunVcenterDailyAggregate(ctx context.Context, logger *slog.Lo
|
||||
return err
|
||||
}
|
||||
|
||||
currentTotals, err := snapshotTotalsForTable(ctx, dbConn, sourceTable)
|
||||
if err != nil {
|
||||
c.Logger.Warn("unable to calculate daily totals", "error", err, "table", sourceTable)
|
||||
} else {
|
||||
c.Logger.Info("Daily snapshot totals",
|
||||
"table", sourceTable,
|
||||
"vm_count", currentTotals.VmCount,
|
||||
"vcpu_total", currentTotals.VcpuTotal,
|
||||
"ram_total_mb", currentTotals.RamTotal,
|
||||
"disk_total_gb", currentTotals.DiskTotal,
|
||||
)
|
||||
}
|
||||
|
||||
prevTable, _ := dailyInventoryTableName(targetTime.AddDate(0, 0, -1))
|
||||
if prevTable != "" && tableExists(ctx, dbConn, prevTable) {
|
||||
prevTotals, err := snapshotTotalsForTable(ctx, dbConn, prevTable)
|
||||
if err != nil {
|
||||
c.Logger.Warn("unable to calculate previous day totals", "error", err, "table", prevTable)
|
||||
} else {
|
||||
c.Logger.Info("Daily snapshot comparison",
|
||||
"current_table", sourceTable,
|
||||
"previous_table", prevTable,
|
||||
"vm_delta", currentTotals.VmCount-prevTotals.VmCount,
|
||||
"vcpu_delta", currentTotals.VcpuTotal-prevTotals.VcpuTotal,
|
||||
"ram_delta_mb", currentTotals.RamTotal-prevTotals.RamTotal,
|
||||
"disk_delta_gb", currentTotals.DiskTotal-prevTotals.DiskTotal,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
insertQuery := fmt.Sprintf(`
|
||||
INSERT INTO %s (
|
||||
"InventoryId", "Name", "Vcenter", "VmId", "EventKey", "CloudId", "CreationTime", "DeletionTime",
|
||||
"ResourcePool", "VmType", "Datacenter", "Cluster", "Folder", "ProvisionedDisk", "InitialVcpus",
|
||||
"InitialRam", "IsTemplate", "PoweredOn", "SrmPlaceholder", "VmUuid", "SamplesPresent"
|
||||
"InitialRam", "IsTemplate", "PoweredOn", "SrmPlaceholder", "VmUuid",
|
||||
"SamplesPresent", "AvgVcpus", "AvgRam", "AvgDisk", "AvgIsPresent",
|
||||
"PoolTinPct", "PoolBronzePct", "PoolSilverPct", "PoolGoldPct"
|
||||
)
|
||||
SELECT
|
||||
"InventoryId", "Name", "Vcenter", "VmId", "EventKey", "CloudId", "CreationTime", "DeletionTime",
|
||||
"ResourcePool", "VmType", "Datacenter", "Cluster", "Folder", "ProvisionedDisk", "InitialVcpus",
|
||||
"InitialRam", "IsTemplate", "PoweredOn", "SrmPlaceholder", "VmUuid",
|
||||
SUM(CASE WHEN "IsPresent" = 'TRUE' THEN 1 ELSE 0 END) AS "SamplesPresent"
|
||||
SUM(CASE WHEN "IsPresent" = 'TRUE' THEN 1 ELSE 0 END) AS "SamplesPresent",
|
||||
AVG(CASE WHEN "IsPresent" = 'TRUE' AND "InitialVcpus" IS NOT NULL THEN "InitialVcpus" END) AS "AvgVcpus",
|
||||
AVG(CASE WHEN "IsPresent" = 'TRUE' AND "InitialRam" IS NOT NULL THEN "InitialRam" END) AS "AvgRam",
|
||||
AVG(CASE WHEN "IsPresent" = 'TRUE' AND "ProvisionedDisk" IS NOT NULL THEN "ProvisionedDisk" END) AS "AvgDisk",
|
||||
AVG(CASE WHEN "IsPresent" = 'TRUE' THEN 1 ELSE 0 END) AS "AvgIsPresent",
|
||||
100.0 * SUM(CASE WHEN "IsPresent" = 'TRUE' AND LOWER("ResourcePool") = 'tin' THEN 1 ELSE 0 END)
|
||||
/ NULLIF(SUM(CASE WHEN "IsPresent" = 'TRUE' THEN 1 ELSE 0 END), 0) AS "PoolTinPct",
|
||||
100.0 * SUM(CASE WHEN "IsPresent" = 'TRUE' AND LOWER("ResourcePool") = 'bronze' THEN 1 ELSE 0 END)
|
||||
/ NULLIF(SUM(CASE WHEN "IsPresent" = 'TRUE' THEN 1 ELSE 0 END), 0) AS "PoolBronzePct",
|
||||
100.0 * SUM(CASE WHEN "IsPresent" = 'TRUE' AND LOWER("ResourcePool") = 'silver' THEN 1 ELSE 0 END)
|
||||
/ NULLIF(SUM(CASE WHEN "IsPresent" = 'TRUE' THEN 1 ELSE 0 END), 0) AS "PoolSilverPct",
|
||||
100.0 * SUM(CASE WHEN "IsPresent" = 'TRUE' AND LOWER("ResourcePool") = 'gold' THEN 1 ELSE 0 END)
|
||||
/ NULLIF(SUM(CASE WHEN "IsPresent" = 'TRUE' THEN 1 ELSE 0 END), 0) AS "PoolGoldPct"
|
||||
FROM %s
|
||||
GROUP BY
|
||||
"InventoryId", "Name", "Vcenter", "VmId", "EventKey", "CloudId", "CreationTime", "DeletionTime",
|
||||
@@ -222,12 +280,26 @@ func (c *CronTask) RunVcenterMonthlyAggregate(ctx context.Context, logger *slog.
|
||||
return fmt.Errorf("no valid daily snapshot tables found for %s", targetMonth.Format("2006-01"))
|
||||
}
|
||||
|
||||
monthlyTotals, err := snapshotTotalsForUnion(ctx, dbConn, unionQuery)
|
||||
if err != nil {
|
||||
c.Logger.Warn("unable to calculate monthly totals", "error", err, "month", targetMonth.Format("2006-01"))
|
||||
} else {
|
||||
c.Logger.Info("Monthly snapshot totals",
|
||||
"month", targetMonth.Format("2006-01"),
|
||||
"vm_count", monthlyTotals.VmCount,
|
||||
"vcpu_total", monthlyTotals.VcpuTotal,
|
||||
"ram_total_mb", monthlyTotals.RamTotal,
|
||||
"disk_total_gb", monthlyTotals.DiskTotal,
|
||||
)
|
||||
}
|
||||
|
||||
insertQuery := fmt.Sprintf(`
|
||||
INSERT INTO %s (
|
||||
"InventoryId", "Name", "Vcenter", "VmId", "EventKey", "CloudId", "CreationTime", "DeletionTime",
|
||||
"ResourcePool", "VmType", "Datacenter", "Cluster", "Folder", "ProvisionedDisk", "InitialVcpus",
|
||||
"InitialRam", "IsTemplate", "PoweredOn", "SrmPlaceholder", "VmUuid",
|
||||
"AvgVcpus", "AvgRam", "AvgIsPresent"
|
||||
"AvgVcpus", "AvgRam", "AvgDisk", "AvgIsPresent",
|
||||
"PoolTinPct", "PoolBronzePct", "PoolSilverPct", "PoolGoldPct"
|
||||
)
|
||||
SELECT
|
||||
"InventoryId", "Name", "Vcenter", "VmId", "EventKey", "CloudId", "CreationTime", "DeletionTime",
|
||||
@@ -235,7 +307,16 @@ SELECT
|
||||
"InitialRam", "IsTemplate", "PoweredOn", "SrmPlaceholder", "VmUuid",
|
||||
AVG(CASE WHEN "InitialVcpus" IS NOT NULL THEN "InitialVcpus" END) AS "AvgVcpus",
|
||||
AVG(CASE WHEN "InitialRam" IS NOT NULL THEN "InitialRam" END) AS "AvgRam",
|
||||
AVG(CASE WHEN "IsPresent" = 'TRUE' THEN 1 ELSE 0 END) AS "AvgIsPresent"
|
||||
AVG(CASE WHEN "ProvisionedDisk" IS NOT NULL THEN "ProvisionedDisk" END) AS "AvgDisk",
|
||||
AVG(CASE WHEN "IsPresent" = 'TRUE' THEN 1 ELSE 0 END) AS "AvgIsPresent",
|
||||
100.0 * SUM(CASE WHEN "IsPresent" = 'TRUE' AND LOWER("ResourcePool") = 'tin' THEN 1 ELSE 0 END)
|
||||
/ NULLIF(SUM(CASE WHEN "IsPresent" = 'TRUE' THEN 1 ELSE 0 END), 0) AS "PoolTinPct",
|
||||
100.0 * SUM(CASE WHEN "IsPresent" = 'TRUE' AND LOWER("ResourcePool") = 'bronze' THEN 1 ELSE 0 END)
|
||||
/ NULLIF(SUM(CASE WHEN "IsPresent" = 'TRUE' THEN 1 ELSE 0 END), 0) AS "PoolBronzePct",
|
||||
100.0 * SUM(CASE WHEN "IsPresent" = 'TRUE' AND LOWER("ResourcePool") = 'silver' THEN 1 ELSE 0 END)
|
||||
/ NULLIF(SUM(CASE WHEN "IsPresent" = 'TRUE' THEN 1 ELSE 0 END), 0) AS "PoolSilverPct",
|
||||
100.0 * SUM(CASE WHEN "IsPresent" = 'TRUE' AND LOWER("ResourcePool") = 'gold' THEN 1 ELSE 0 END)
|
||||
/ NULLIF(SUM(CASE WHEN "IsPresent" = 'TRUE' THEN 1 ELSE 0 END), 0) AS "PoolGoldPct"
|
||||
FROM (
|
||||
%s
|
||||
) snapshots
|
||||
@@ -270,6 +351,7 @@ func (c *CronTask) RunSnapshotCleanup(ctx context.Context, logger *slog.Logger)
|
||||
return err
|
||||
}
|
||||
|
||||
removedHourly := 0
|
||||
for _, table := range hourlyTables {
|
||||
if strings.HasPrefix(table, "inventory_daily_summary_") {
|
||||
continue
|
||||
@@ -281,6 +363,8 @@ func (c *CronTask) RunSnapshotCleanup(ctx context.Context, logger *slog.Logger)
|
||||
if tableDate.Before(truncateDate(hourlyCutoff)) {
|
||||
if err := dropSnapshotTable(ctx, dbConn, table); err != nil {
|
||||
c.Logger.Error("failed to drop hourly snapshot table", "error", err, "table", table)
|
||||
} else {
|
||||
removedHourly++
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -289,6 +373,7 @@ func (c *CronTask) RunSnapshotCleanup(ctx context.Context, logger *slog.Logger)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
removedDaily := 0
|
||||
for _, table := range dailyTables {
|
||||
tableDate, ok := parseSnapshotDate(table, "inventory_daily_summary_", "20060102")
|
||||
if !ok {
|
||||
@@ -297,11 +382,18 @@ func (c *CronTask) RunSnapshotCleanup(ctx context.Context, logger *slog.Logger)
|
||||
if tableDate.Before(truncateDate(dailyCutoff)) {
|
||||
if err := dropSnapshotTable(ctx, dbConn, table); err != nil {
|
||||
c.Logger.Error("failed to drop daily snapshot table", "error", err, "table", table)
|
||||
} else {
|
||||
removedDaily++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c.Logger.Debug("Finished snapshot cleanup")
|
||||
c.Logger.Info("Finished snapshot cleanup",
|
||||
"removed_hourly_tables", removedHourly,
|
||||
"removed_daily_tables", removedDaily,
|
||||
"hourly_max_age_days", hourlyMaxDays,
|
||||
"daily_max_age_months", dailyMaxMonths,
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -379,13 +471,33 @@ func ensureDailySummaryTable(ctx context.Context, dbConn *sqlx.DB, tableName str
|
||||
"PoweredOn" TEXT,
|
||||
"SrmPlaceholder" TEXT,
|
||||
"VmUuid" TEXT,
|
||||
"SamplesPresent" BIGINT NOT NULL
|
||||
"SamplesPresent" BIGINT NOT NULL,
|
||||
"AvgVcpus" REAL,
|
||||
"AvgRam" REAL,
|
||||
"AvgDisk" REAL,
|
||||
"AvgIsPresent" REAL,
|
||||
"PoolTinPct" REAL,
|
||||
"PoolBronzePct" REAL,
|
||||
"PoolSilverPct" REAL,
|
||||
"PoolGoldPct" REAL
|
||||
);`, tableName)
|
||||
|
||||
_, err := dbConn.ExecContext(ctx, ddl)
|
||||
if _, err := dbConn.ExecContext(ctx, ddl); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return ensureSnapshotColumns(ctx, dbConn, tableName, []columnDef{
|
||||
{Name: "AvgVcpus", Type: "REAL"},
|
||||
{Name: "AvgRam", Type: "REAL"},
|
||||
{Name: "AvgDisk", Type: "REAL"},
|
||||
{Name: "AvgIsPresent", Type: "REAL"},
|
||||
{Name: "PoolTinPct", Type: "REAL"},
|
||||
{Name: "PoolBronzePct", Type: "REAL"},
|
||||
{Name: "PoolSilverPct", Type: "REAL"},
|
||||
{Name: "PoolGoldPct", Type: "REAL"},
|
||||
})
|
||||
}
|
||||
|
||||
func ensureMonthlySummaryTable(ctx context.Context, dbConn *sqlx.DB, tableName string) error {
|
||||
ddl := fmt.Sprintf(`CREATE TABLE IF NOT EXISTS %s (
|
||||
"InventoryId" BIGINT,
|
||||
@@ -410,13 +522,27 @@ func ensureMonthlySummaryTable(ctx context.Context, dbConn *sqlx.DB, tableName s
|
||||
"VmUuid" TEXT,
|
||||
"AvgVcpus" REAL,
|
||||
"AvgRam" REAL,
|
||||
"AvgIsPresent" REAL
|
||||
"AvgDisk" REAL,
|
||||
"AvgIsPresent" REAL,
|
||||
"PoolTinPct" REAL,
|
||||
"PoolBronzePct" REAL,
|
||||
"PoolSilverPct" REAL,
|
||||
"PoolGoldPct" REAL
|
||||
);`, tableName)
|
||||
|
||||
_, err := dbConn.ExecContext(ctx, ddl)
|
||||
if _, err := dbConn.ExecContext(ctx, ddl); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return ensureSnapshotColumns(ctx, dbConn, tableName, []columnDef{
|
||||
{Name: "AvgDisk", Type: "REAL"},
|
||||
{Name: "PoolTinPct", Type: "REAL"},
|
||||
{Name: "PoolBronzePct", Type: "REAL"},
|
||||
{Name: "PoolSilverPct", Type: "REAL"},
|
||||
{Name: "PoolGoldPct", Type: "REAL"},
|
||||
})
|
||||
}
|
||||
|
||||
func buildUnionQuery(tables []string, columns []string) string {
|
||||
queries := make([]string, 0, len(tables))
|
||||
columnList := strings.Join(columns, ", ")
|
||||
@@ -453,6 +579,121 @@ func dropSnapshotTable(ctx context.Context, dbConn *sqlx.DB, table string) error
|
||||
return err
|
||||
}
|
||||
|
||||
type snapshotTotals struct {
|
||||
VmCount int64
|
||||
VcpuTotal int64
|
||||
RamTotal int64
|
||||
DiskTotal float64
|
||||
}
|
||||
|
||||
type columnDef struct {
|
||||
Name string
|
||||
Type string
|
||||
}
|
||||
|
||||
func ensureSnapshotColumns(ctx context.Context, dbConn *sqlx.DB, tableName string, columns []columnDef) error {
|
||||
if _, err := safeTableName(tableName); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, column := range columns {
|
||||
if err := addColumnIfMissing(ctx, dbConn, tableName, column); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func addColumnIfMissing(ctx context.Context, dbConn *sqlx.DB, tableName string, column columnDef) error {
|
||||
query := fmt.Sprintf(`ALTER TABLE %s ADD COLUMN "%s" %s`, tableName, column.Name, column.Type)
|
||||
if _, err := dbConn.ExecContext(ctx, query); err != nil {
|
||||
errText := strings.ToLower(err.Error())
|
||||
if strings.Contains(errText, "duplicate column") || strings.Contains(errText, "already exists") {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func snapshotTotalsForTable(ctx context.Context, dbConn *sqlx.DB, table string) (snapshotTotals, error) {
|
||||
if _, err := safeTableName(table); err != nil {
|
||||
return snapshotTotals{}, err
|
||||
}
|
||||
query := fmt.Sprintf(`
|
||||
SELECT
|
||||
COUNT(DISTINCT "VmId") AS vm_count,
|
||||
COALESCE(SUM(CASE WHEN "InitialVcpus" IS NOT NULL THEN "InitialVcpus" ELSE 0 END), 0) AS vcpu_total,
|
||||
COALESCE(SUM(CASE WHEN "InitialRam" IS NOT NULL THEN "InitialRam" ELSE 0 END), 0) AS ram_total,
|
||||
COALESCE(SUM(CASE WHEN "ProvisionedDisk" IS NOT NULL THEN "ProvisionedDisk" ELSE 0 END), 0) AS disk_total
|
||||
FROM %s
|
||||
WHERE "IsPresent" = 'TRUE'
|
||||
`, table)
|
||||
|
||||
var totals snapshotTotals
|
||||
if err := dbConn.GetContext(ctx, &totals, query); err != nil {
|
||||
return snapshotTotals{}, err
|
||||
}
|
||||
return totals, nil
|
||||
}
|
||||
|
||||
func snapshotTotalsForUnion(ctx context.Context, dbConn *sqlx.DB, unionQuery string) (snapshotTotals, error) {
|
||||
query := fmt.Sprintf(`
|
||||
SELECT
|
||||
COUNT(DISTINCT "VmId") AS vm_count,
|
||||
COALESCE(SUM(CASE WHEN "InitialVcpus" IS NOT NULL THEN "InitialVcpus" ELSE 0 END), 0) AS vcpu_total,
|
||||
COALESCE(SUM(CASE WHEN "InitialRam" IS NOT NULL THEN "InitialRam" ELSE 0 END), 0) AS ram_total,
|
||||
COALESCE(SUM(CASE WHEN "ProvisionedDisk" IS NOT NULL THEN "ProvisionedDisk" ELSE 0 END), 0) AS disk_total
|
||||
FROM (
|
||||
%s
|
||||
) snapshots
|
||||
WHERE "IsPresent" = 'TRUE'
|
||||
`, unionQuery)
|
||||
|
||||
var totals snapshotTotals
|
||||
if err := dbConn.GetContext(ctx, &totals, query); err != nil {
|
||||
return snapshotTotals{}, err
|
||||
}
|
||||
return totals, nil
|
||||
}
|
||||
|
||||
func tableExists(ctx context.Context, dbConn *sqlx.DB, table string) bool {
|
||||
driver := strings.ToLower(dbConn.DriverName())
|
||||
switch driver {
|
||||
case "sqlite":
|
||||
var count int
|
||||
err := dbConn.GetContext(ctx, &count, `
|
||||
SELECT COUNT(1)
|
||||
FROM sqlite_master
|
||||
WHERE type = 'table' AND name = ?
|
||||
`, table)
|
||||
return err == nil && count > 0
|
||||
case "pgx", "postgres":
|
||||
var count int
|
||||
err := dbConn.GetContext(ctx, &count, `
|
||||
SELECT COUNT(1)
|
||||
FROM pg_catalog.pg_tables
|
||||
WHERE schemaname = 'public' AND tablename = $1
|
||||
`, table)
|
||||
return err == nil && count > 0
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func nullInt64ToInt(value sql.NullInt64) int64 {
|
||||
if value.Valid {
|
||||
return value.Int64
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func nullFloat64ToFloat(value sql.NullFloat64) float64 {
|
||||
if value.Valid {
|
||||
return value.Float64
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func getEnvInt(key string, fallback int) int {
|
||||
raw := strings.TrimSpace(os.Getenv(key))
|
||||
if raw == "" {
|
||||
|
||||
Reference in New Issue
Block a user