bugfix reports
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
2026-01-14 13:51:30 +11:00
parent 013ae4568e
commit b9ab34db0a
13 changed files with 404 additions and 115 deletions

View File

@@ -10,7 +10,6 @@ CREATE TABLE IF NOT EXISTS "Inventory" (
"CreationTime" INTEGER, "CreationTime" INTEGER,
"DeletionTime" INTEGER, "DeletionTime" INTEGER,
"ResourcePool" TEXT, "ResourcePool" TEXT,
"VmType" TEXT,
"Datacenter" TEXT, "Datacenter" TEXT,
"Cluster" TEXT, "Cluster" TEXT,
"Folder" TEXT, "Folder" TEXT,

View File

@@ -0,0 +1,9 @@
-- +goose Up
-- +goose StatementBegin
ALTER TABLE "Inventory" DROP COLUMN "VmType";
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
ALTER TABLE "Inventory" ADD COLUMN "VmType" TEXT;
-- +goose StatementEnd

View File

@@ -10,7 +10,6 @@ CREATE TABLE IF NOT EXISTS "Inventory" (
"CreationTime" BIGINT, "CreationTime" BIGINT,
"DeletionTime" BIGINT, "DeletionTime" BIGINT,
"ResourcePool" TEXT, "ResourcePool" TEXT,
"VmType" TEXT,
"Datacenter" TEXT, "Datacenter" TEXT,
"Cluster" TEXT, "Cluster" TEXT,
"Folder" TEXT, "Folder" TEXT,

View File

@@ -0,0 +1,9 @@
-- +goose Up
-- +goose StatementBegin
ALTER TABLE "Inventory" DROP COLUMN IF EXISTS "VmType";
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
ALTER TABLE "Inventory" ADD COLUMN "VmType" TEXT;
-- +goose StatementEnd

View File

@@ -36,7 +36,6 @@ type Inventory struct {
CreationTime sql.NullInt64 `db:"CreationTime" json:"CreationTime"` CreationTime sql.NullInt64 `db:"CreationTime" json:"CreationTime"`
DeletionTime sql.NullInt64 `db:"DeletionTime" json:"DeletionTime"` DeletionTime sql.NullInt64 `db:"DeletionTime" json:"DeletionTime"`
ResourcePool sql.NullString `db:"ResourcePool" json:"ResourcePool"` ResourcePool sql.NullString `db:"ResourcePool" json:"ResourcePool"`
VmType sql.NullString `db:"VmType" json:"VmType"`
Datacenter sql.NullString `db:"Datacenter" json:"Datacenter"` Datacenter sql.NullString `db:"Datacenter" json:"Datacenter"`
Cluster sql.NullString `db:"Cluster" json:"Cluster"` Cluster sql.NullString `db:"Cluster" json:"Cluster"`
Folder sql.NullString `db:"Folder" json:"Folder"` Folder sql.NullString `db:"Folder" json:"Folder"`

View File

@@ -32,9 +32,9 @@ WHERE "CloudId" = ? LIMIT 1;
-- name: CreateInventory :one -- name: CreateInventory :one
INSERT INTO inventory ( INSERT INTO inventory (
"Name", "Vcenter", "VmId", "VmUuid", "EventKey", "CloudId", "CreationTime", "ResourcePool", "VmType", "IsTemplate", "Datacenter", "Cluster", "Folder", "ProvisionedDisk", "InitialVcpus", "InitialRam", "SrmPlaceholder", "PoweredOn" "Name", "Vcenter", "VmId", "VmUuid", "EventKey", "CloudId", "CreationTime", "ResourcePool", "IsTemplate", "Datacenter", "Cluster", "Folder", "ProvisionedDisk", "InitialVcpus", "InitialRam", "SrmPlaceholder", "PoweredOn"
) VALUES( ) VALUES(
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
) )
RETURNING *; RETURNING *;

View File

@@ -101,11 +101,11 @@ func (q *Queries) CreateEvent(ctx context.Context, arg CreateEventParams) (Event
const createInventory = `-- name: CreateInventory :one const createInventory = `-- name: CreateInventory :one
INSERT INTO inventory ( INSERT INTO inventory (
"Name", "Vcenter", "VmId", "VmUuid", "EventKey", "CloudId", "CreationTime", "ResourcePool", "VmType", "IsTemplate", "Datacenter", "Cluster", "Folder", "ProvisionedDisk", "InitialVcpus", "InitialRam", "SrmPlaceholder", "PoweredOn" "Name", "Vcenter", "VmId", "VmUuid", "EventKey", "CloudId", "CreationTime", "ResourcePool", "IsTemplate", "Datacenter", "Cluster", "Folder", "ProvisionedDisk", "InitialVcpus", "InitialRam", "SrmPlaceholder", "PoweredOn"
) VALUES( ) VALUES(
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
) )
RETURNING Iid, Name, Vcenter, VmId, EventKey, CloudId, CreationTime, DeletionTime, ResourcePool, VmType, Datacenter, Cluster, Folder, ProvisionedDisk, InitialVcpus, InitialRam, IsTemplate, PoweredOn, SrmPlaceholder, VmUuid RETURNING Iid, Name, Vcenter, VmId, EventKey, CloudId, CreationTime, DeletionTime, ResourcePool, Datacenter, Cluster, Folder, ProvisionedDisk, InitialVcpus, InitialRam, IsTemplate, PoweredOn, SrmPlaceholder, VmUuid
` `
type CreateInventoryParams struct { type CreateInventoryParams struct {
@@ -117,7 +117,6 @@ type CreateInventoryParams struct {
CloudId sql.NullString `db:"CloudId" json:"CloudId"` CloudId sql.NullString `db:"CloudId" json:"CloudId"`
CreationTime sql.NullInt64 `db:"CreationTime" json:"CreationTime"` CreationTime sql.NullInt64 `db:"CreationTime" json:"CreationTime"`
ResourcePool sql.NullString `db:"ResourcePool" json:"ResourcePool"` ResourcePool sql.NullString `db:"ResourcePool" json:"ResourcePool"`
VmType sql.NullString `db:"VmType" json:"VmType"`
IsTemplate interface{} `db:"IsTemplate" json:"IsTemplate"` IsTemplate interface{} `db:"IsTemplate" json:"IsTemplate"`
Datacenter sql.NullString `db:"Datacenter" json:"Datacenter"` Datacenter sql.NullString `db:"Datacenter" json:"Datacenter"`
Cluster sql.NullString `db:"Cluster" json:"Cluster"` Cluster sql.NullString `db:"Cluster" json:"Cluster"`
@@ -139,7 +138,6 @@ func (q *Queries) CreateInventory(ctx context.Context, arg CreateInventoryParams
arg.CloudId, arg.CloudId,
arg.CreationTime, arg.CreationTime,
arg.ResourcePool, arg.ResourcePool,
arg.VmType,
arg.IsTemplate, arg.IsTemplate,
arg.Datacenter, arg.Datacenter,
arg.Cluster, arg.Cluster,
@@ -161,7 +159,6 @@ func (q *Queries) CreateInventory(ctx context.Context, arg CreateInventoryParams
&i.CreationTime, &i.CreationTime,
&i.DeletionTime, &i.DeletionTime,
&i.ResourcePool, &i.ResourcePool,
&i.VmType,
&i.Datacenter, &i.Datacenter,
&i.Cluster, &i.Cluster,
&i.Folder, &i.Folder,
@@ -281,7 +278,7 @@ func (q *Queries) CreateUpdate(ctx context.Context, arg CreateUpdateParams) (Upd
} }
const getInventoryByName = `-- name: GetInventoryByName :many const getInventoryByName = `-- name: GetInventoryByName :many
SELECT Iid, Name, Vcenter, VmId, EventKey, CloudId, CreationTime, DeletionTime, ResourcePool, VmType, Datacenter, Cluster, Folder, ProvisionedDisk, InitialVcpus, InitialRam, IsTemplate, PoweredOn, SrmPlaceholder, VmUuid FROM inventory SELECT Iid, Name, Vcenter, VmId, EventKey, CloudId, CreationTime, DeletionTime, ResourcePool, Datacenter, Cluster, Folder, ProvisionedDisk, InitialVcpus, InitialRam, IsTemplate, PoweredOn, SrmPlaceholder, VmUuid FROM inventory
WHERE "Name" = ? WHERE "Name" = ?
` `
@@ -304,7 +301,6 @@ func (q *Queries) GetInventoryByName(ctx context.Context, name string) ([]Invent
&i.CreationTime, &i.CreationTime,
&i.DeletionTime, &i.DeletionTime,
&i.ResourcePool, &i.ResourcePool,
&i.VmType,
&i.Datacenter, &i.Datacenter,
&i.Cluster, &i.Cluster,
&i.Folder, &i.Folder,
@@ -330,7 +326,7 @@ func (q *Queries) GetInventoryByName(ctx context.Context, name string) ([]Invent
} }
const getInventoryByVcenter = `-- name: GetInventoryByVcenter :many const getInventoryByVcenter = `-- name: GetInventoryByVcenter :many
SELECT Iid, Name, Vcenter, VmId, EventKey, CloudId, CreationTime, DeletionTime, ResourcePool, VmType, Datacenter, Cluster, Folder, ProvisionedDisk, InitialVcpus, InitialRam, IsTemplate, PoweredOn, SrmPlaceholder, VmUuid FROM inventory SELECT Iid, Name, Vcenter, VmId, EventKey, CloudId, CreationTime, DeletionTime, ResourcePool, Datacenter, Cluster, Folder, ProvisionedDisk, InitialVcpus, InitialRam, IsTemplate, PoweredOn, SrmPlaceholder, VmUuid FROM inventory
WHERE "Vcenter" = ? WHERE "Vcenter" = ?
` `
@@ -353,7 +349,6 @@ func (q *Queries) GetInventoryByVcenter(ctx context.Context, vcenter string) ([]
&i.CreationTime, &i.CreationTime,
&i.DeletionTime, &i.DeletionTime,
&i.ResourcePool, &i.ResourcePool,
&i.VmType,
&i.Datacenter, &i.Datacenter,
&i.Cluster, &i.Cluster,
&i.Folder, &i.Folder,
@@ -379,7 +374,7 @@ func (q *Queries) GetInventoryByVcenter(ctx context.Context, vcenter string) ([]
} }
const getInventoryEventId = `-- name: GetInventoryEventId :one const getInventoryEventId = `-- name: GetInventoryEventId :one
SELECT Iid, Name, Vcenter, VmId, EventKey, CloudId, CreationTime, DeletionTime, ResourcePool, VmType, Datacenter, Cluster, Folder, ProvisionedDisk, InitialVcpus, InitialRam, IsTemplate, PoweredOn, SrmPlaceholder, VmUuid FROM inventory SELECT Iid, Name, Vcenter, VmId, EventKey, CloudId, CreationTime, DeletionTime, ResourcePool, Datacenter, Cluster, Folder, ProvisionedDisk, InitialVcpus, InitialRam, IsTemplate, PoweredOn, SrmPlaceholder, VmUuid FROM inventory
WHERE "CloudId" = ? LIMIT 1 WHERE "CloudId" = ? LIMIT 1
` `
@@ -396,7 +391,6 @@ func (q *Queries) GetInventoryEventId(ctx context.Context, cloudid sql.NullStrin
&i.CreationTime, &i.CreationTime,
&i.DeletionTime, &i.DeletionTime,
&i.ResourcePool, &i.ResourcePool,
&i.VmType,
&i.Datacenter, &i.Datacenter,
&i.Cluster, &i.Cluster,
&i.Folder, &i.Folder,
@@ -412,7 +406,7 @@ func (q *Queries) GetInventoryEventId(ctx context.Context, cloudid sql.NullStrin
} }
const getInventoryVcUrl = `-- name: GetInventoryVcUrl :many const getInventoryVcUrl = `-- name: GetInventoryVcUrl :many
SELECT Iid, Name, Vcenter, VmId, EventKey, CloudId, CreationTime, DeletionTime, ResourcePool, VmType, Datacenter, Cluster, Folder, ProvisionedDisk, InitialVcpus, InitialRam, IsTemplate, PoweredOn, SrmPlaceholder, VmUuid FROM inventory SELECT Iid, Name, Vcenter, VmId, EventKey, CloudId, CreationTime, DeletionTime, ResourcePool, Datacenter, Cluster, Folder, ProvisionedDisk, InitialVcpus, InitialRam, IsTemplate, PoweredOn, SrmPlaceholder, VmUuid FROM inventory
WHERE "Vcenter" = ?1 WHERE "Vcenter" = ?1
` `
@@ -435,7 +429,6 @@ func (q *Queries) GetInventoryVcUrl(ctx context.Context, vc string) ([]Inventory
&i.CreationTime, &i.CreationTime,
&i.DeletionTime, &i.DeletionTime,
&i.ResourcePool, &i.ResourcePool,
&i.VmType,
&i.Datacenter, &i.Datacenter,
&i.Cluster, &i.Cluster,
&i.Folder, &i.Folder,
@@ -461,7 +454,7 @@ func (q *Queries) GetInventoryVcUrl(ctx context.Context, vc string) ([]Inventory
} }
const getInventoryVmId = `-- name: GetInventoryVmId :one const getInventoryVmId = `-- name: GetInventoryVmId :one
SELECT Iid, Name, Vcenter, VmId, EventKey, CloudId, CreationTime, DeletionTime, ResourcePool, VmType, Datacenter, Cluster, Folder, ProvisionedDisk, InitialVcpus, InitialRam, IsTemplate, PoweredOn, SrmPlaceholder, VmUuid FROM inventory SELECT Iid, Name, Vcenter, VmId, EventKey, CloudId, CreationTime, DeletionTime, ResourcePool, Datacenter, Cluster, Folder, ProvisionedDisk, InitialVcpus, InitialRam, IsTemplate, PoweredOn, SrmPlaceholder, VmUuid FROM inventory
WHERE "VmId" = ?1 AND "Datacenter" = ?2 WHERE "VmId" = ?1 AND "Datacenter" = ?2
` `
@@ -483,7 +476,6 @@ func (q *Queries) GetInventoryVmId(ctx context.Context, arg GetInventoryVmIdPara
&i.CreationTime, &i.CreationTime,
&i.DeletionTime, &i.DeletionTime,
&i.ResourcePool, &i.ResourcePool,
&i.VmType,
&i.Datacenter, &i.Datacenter,
&i.Cluster, &i.Cluster,
&i.Folder, &i.Folder,
@@ -499,7 +491,7 @@ func (q *Queries) GetInventoryVmId(ctx context.Context, arg GetInventoryVmIdPara
} }
const getInventoryVmUuid = `-- name: GetInventoryVmUuid :one const getInventoryVmUuid = `-- name: GetInventoryVmUuid :one
SELECT Iid, Name, Vcenter, VmId, EventKey, CloudId, CreationTime, DeletionTime, ResourcePool, VmType, Datacenter, Cluster, Folder, ProvisionedDisk, InitialVcpus, InitialRam, IsTemplate, PoweredOn, SrmPlaceholder, VmUuid FROM inventory SELECT Iid, Name, Vcenter, VmId, EventKey, CloudId, CreationTime, DeletionTime, ResourcePool, Datacenter, Cluster, Folder, ProvisionedDisk, InitialVcpus, InitialRam, IsTemplate, PoweredOn, SrmPlaceholder, VmUuid FROM inventory
WHERE "VmUuid" = ?1 AND "Datacenter" = ?2 WHERE "VmUuid" = ?1 AND "Datacenter" = ?2
` `
@@ -521,7 +513,6 @@ func (q *Queries) GetInventoryVmUuid(ctx context.Context, arg GetInventoryVmUuid
&i.CreationTime, &i.CreationTime,
&i.DeletionTime, &i.DeletionTime,
&i.ResourcePool, &i.ResourcePool,
&i.VmType,
&i.Datacenter, &i.Datacenter,
&i.Cluster, &i.Cluster,
&i.Folder, &i.Folder,
@@ -537,7 +528,7 @@ func (q *Queries) GetInventoryVmUuid(ctx context.Context, arg GetInventoryVmUuid
} }
const getReportInventory = `-- name: GetReportInventory :many const getReportInventory = `-- name: GetReportInventory :many
SELECT Iid, Name, Vcenter, VmId, EventKey, CloudId, CreationTime, DeletionTime, ResourcePool, VmType, Datacenter, Cluster, Folder, ProvisionedDisk, InitialVcpus, InitialRam, IsTemplate, PoweredOn, SrmPlaceholder, VmUuid FROM inventory SELECT Iid, Name, Vcenter, VmId, EventKey, CloudId, CreationTime, DeletionTime, ResourcePool, Datacenter, Cluster, Folder, ProvisionedDisk, InitialVcpus, InitialRam, IsTemplate, PoweredOn, SrmPlaceholder, VmUuid FROM inventory
ORDER BY "CreationTime" ORDER BY "CreationTime"
` `
@@ -560,7 +551,6 @@ func (q *Queries) GetReportInventory(ctx context.Context) ([]Inventory, error) {
&i.CreationTime, &i.CreationTime,
&i.DeletionTime, &i.DeletionTime,
&i.ResourcePool, &i.ResourcePool,
&i.VmType,
&i.Datacenter, &i.Datacenter,
&i.Cluster, &i.Cluster,
&i.Folder, &i.Folder,
@@ -679,7 +669,7 @@ func (q *Queries) GetVmUpdates(ctx context.Context, arg GetVmUpdatesParams) ([]U
const inventoryCleanup = `-- name: InventoryCleanup :exec const inventoryCleanup = `-- name: InventoryCleanup :exec
DELETE FROM inventory DELETE FROM inventory
WHERE "VmId" = ?1 AND "Datacenter" = ?2 WHERE "VmId" = ?1 AND "Datacenter" = ?2
RETURNING Iid, Name, Vcenter, VmId, EventKey, CloudId, CreationTime, DeletionTime, ResourcePool, VmType, Datacenter, Cluster, Folder, ProvisionedDisk, InitialVcpus, InitialRam, IsTemplate, PoweredOn, SrmPlaceholder, VmUuid RETURNING Iid, Name, Vcenter, VmId, EventKey, CloudId, CreationTime, DeletionTime, ResourcePool, Datacenter, Cluster, Folder, ProvisionedDisk, InitialVcpus, InitialRam, IsTemplate, PoweredOn, SrmPlaceholder, VmUuid
` `
type InventoryCleanupParams struct { type InventoryCleanupParams struct {
@@ -695,7 +685,7 @@ func (q *Queries) InventoryCleanup(ctx context.Context, arg InventoryCleanupPara
const inventoryCleanupTemplates = `-- name: InventoryCleanupTemplates :exec const inventoryCleanupTemplates = `-- name: InventoryCleanupTemplates :exec
DELETE FROM inventory DELETE FROM inventory
WHERE "IsTemplate" = 'TRUE' WHERE "IsTemplate" = 'TRUE'
RETURNING Iid, Name, Vcenter, VmId, EventKey, CloudId, CreationTime, DeletionTime, ResourcePool, VmType, Datacenter, Cluster, Folder, ProvisionedDisk, InitialVcpus, InitialRam, IsTemplate, PoweredOn, SrmPlaceholder, VmUuid RETURNING Iid, Name, Vcenter, VmId, EventKey, CloudId, CreationTime, DeletionTime, ResourcePool, Datacenter, Cluster, Folder, ProvisionedDisk, InitialVcpus, InitialRam, IsTemplate, PoweredOn, SrmPlaceholder, VmUuid
` `
func (q *Queries) InventoryCleanupTemplates(ctx context.Context) error { func (q *Queries) InventoryCleanupTemplates(ctx context.Context) error {
@@ -706,7 +696,7 @@ func (q *Queries) InventoryCleanupTemplates(ctx context.Context) error {
const inventoryCleanupVcenter = `-- name: InventoryCleanupVcenter :exec const inventoryCleanupVcenter = `-- name: InventoryCleanupVcenter :exec
DELETE FROM inventory DELETE FROM inventory
WHERE "Vcenter" = ?1 WHERE "Vcenter" = ?1
RETURNING Iid, Name, Vcenter, VmId, EventKey, CloudId, CreationTime, DeletionTime, ResourcePool, VmType, Datacenter, Cluster, Folder, ProvisionedDisk, InitialVcpus, InitialRam, IsTemplate, PoweredOn, SrmPlaceholder, VmUuid RETURNING Iid, Name, Vcenter, VmId, EventKey, CloudId, CreationTime, DeletionTime, ResourcePool, Datacenter, Cluster, Folder, ProvisionedDisk, InitialVcpus, InitialRam, IsTemplate, PoweredOn, SrmPlaceholder, VmUuid
` `
func (q *Queries) InventoryCleanupVcenter(ctx context.Context, vc string) error { func (q *Queries) InventoryCleanupVcenter(ctx context.Context, vc string) error {
@@ -793,7 +783,7 @@ func (q *Queries) ListEvents(ctx context.Context) ([]Event, error) {
} }
const listInventory = `-- name: ListInventory :many const listInventory = `-- name: ListInventory :many
SELECT Iid, Name, Vcenter, VmId, EventKey, CloudId, CreationTime, DeletionTime, ResourcePool, VmType, Datacenter, Cluster, Folder, ProvisionedDisk, InitialVcpus, InitialRam, IsTemplate, PoweredOn, SrmPlaceholder, VmUuid FROM inventory SELECT Iid, Name, Vcenter, VmId, EventKey, CloudId, CreationTime, DeletionTime, ResourcePool, Datacenter, Cluster, Folder, ProvisionedDisk, InitialVcpus, InitialRam, IsTemplate, PoweredOn, SrmPlaceholder, VmUuid FROM inventory
ORDER BY "Name" ORDER BY "Name"
` `
@@ -816,7 +806,6 @@ func (q *Queries) ListInventory(ctx context.Context) ([]Inventory, error) {
&i.CreationTime, &i.CreationTime,
&i.DeletionTime, &i.DeletionTime,
&i.ResourcePool, &i.ResourcePool,
&i.VmType,
&i.Datacenter, &i.Datacenter,
&i.Cluster, &i.Cluster,
&i.Folder, &i.Folder,

View File

@@ -8,7 +8,6 @@ CREATE TABLE IF NOT EXISTS inventory (
"CreationTime" INTEGER, "CreationTime" INTEGER,
"DeletionTime" INTEGER, "DeletionTime" INTEGER,
"ResourcePool" TEXT, "ResourcePool" TEXT,
"VmType" TEXT,
"Datacenter" TEXT, "Datacenter" TEXT,
"Cluster" TEXT, "Cluster" TEXT,
"Folder" TEXT, "Folder" TEXT,

View File

@@ -113,12 +113,10 @@ func CreateInventoryReport(logger *slog.Logger, Database db.Database, ctx contex
} }
// Set column autowidth // Set column autowidth
/*
err = SetColAutoWidth(xlsx, sheetName) err = SetColAutoWidth(xlsx, sheetName)
if err != nil { if err != nil {
fmt.Printf("Error setting auto width : '%s'\n", err) logger.Error("Error setting auto width", "error", err)
} }
*/
// Save the Excel file into a byte buffer // Save the Excel file into a byte buffer
if err := xlsx.Write(&buffer); err != nil { if err := xlsx.Write(&buffer); err != nil {
@@ -226,12 +224,10 @@ func CreateUpdatesReport(logger *slog.Logger, Database db.Database, ctx context.
} }
// Set column autowidth // Set column autowidth
/*
err = SetColAutoWidth(xlsx, sheetName) err = SetColAutoWidth(xlsx, sheetName)
if err != nil { if err != nil {
fmt.Printf("Error setting auto width : '%s'\n", err) logger.Error("Error setting auto width", "error", err)
} }
*/
// Save the Excel file into a byte buffer // Save the Excel file into a byte buffer
if err := xlsx.Write(&buffer); err != nil { if err := xlsx.Write(&buffer); err != nil {

View File

@@ -412,6 +412,11 @@ func CreateTableReport(logger *slog.Logger, Database db.Database, ctx context.Co
} }
dbConn := Database.DB() dbConn := Database.DB()
if strings.HasPrefix(tableName, "inventory_daily_summary_") || strings.HasPrefix(tableName, "inventory_monthly_summary_") {
if err := ensureSummaryReportColumns(ctx, dbConn, tableName); err != nil {
logger.Warn("Unable to ensure summary columns for report", "error", err, "table", tableName)
}
}
columns, err := tableColumns(ctx, dbConn, tableName) columns, err := tableColumns(ctx, dbConn, tableName)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -424,27 +429,68 @@ func CreateTableReport(logger *slog.Logger, Database db.Database, ctx context.Co
isDailySummary := strings.HasPrefix(tableName, "inventory_daily_summary_") isDailySummary := strings.HasPrefix(tableName, "inventory_daily_summary_")
isMonthlySummary := strings.HasPrefix(tableName, "inventory_monthly_summary_") isMonthlySummary := strings.HasPrefix(tableName, "inventory_monthly_summary_")
hideInventoryID := isHourlySnapshot || isDailySummary || isMonthlySummary hideInventoryID := isHourlySnapshot || isDailySummary || isMonthlySummary
hideRowID := isHourlySnapshot || isDailySummary || isMonthlySummary
humanizeTimes := isDailySummary || isMonthlySummary humanizeTimes := isDailySummary || isMonthlySummary
applyTemplateFilter := isHourlySnapshot || isDailySummary || isMonthlySummary
type columnSpec struct { type columnSpec struct {
Name string Name string
SourceIndex int SourceIndex int
Humanize bool Humanize bool
} }
specs := make([]columnSpec, 0, len(columns)+2) specs := make([]columnSpec, 0, len(columns)+2)
columnIndex := make(map[string]int, len(columns))
for i, columnName := range columns { for i, columnName := range columns {
if hideInventoryID && strings.EqualFold(columnName, "InventoryId") { columnIndex[strings.ToLower(columnName)] = i
continue
} }
specs = append(specs, columnSpec{Name: columnName, SourceIndex: i}) used := make(map[string]struct{}, len(columns))
addSpec := func(columnName string, sourceIndex int) {
if hideInventoryID && strings.EqualFold(columnName, "InventoryId") {
return
}
if hideRowID && strings.EqualFold(columnName, "RowId") {
return
}
if strings.EqualFold(columnName, "VmType") {
return
}
if (isDailySummary || isMonthlySummary) && (strings.EqualFold(columnName, "EventKey") || strings.EqualFold(columnName, "CloudId")) {
return
}
if (isDailySummary || isMonthlySummary) && strings.EqualFold(columnName, "Gold") {
return
}
specs = append(specs, columnSpec{Name: columnName, SourceIndex: sourceIndex})
if humanizeTimes && columnName == "CreationTime" { if humanizeTimes && columnName == "CreationTime" {
specs = append(specs, columnSpec{Name: "CreationTimeReadable", SourceIndex: i, Humanize: true}) specs = append(specs, columnSpec{Name: "CreationTimeReadable", SourceIndex: sourceIndex, Humanize: true})
} }
if humanizeTimes && columnName == "DeletionTime" { if humanizeTimes && columnName == "DeletionTime" {
specs = append(specs, columnSpec{Name: "DeletionTimeReadable", SourceIndex: i, Humanize: true}) specs = append(specs, columnSpec{Name: "DeletionTimeReadable", SourceIndex: sourceIndex, Humanize: true})
}
}
if isDailySummary || isMonthlySummary {
for _, columnName := range summaryReportOrder() {
if idx, ok := columnIndex[strings.ToLower(columnName)]; ok {
addSpec(columnName, idx)
used[strings.ToLower(columnName)] = struct{}{}
} else {
logger.Warn("Summary report column missing from table", "table", tableName, "column", columnName)
addSpec(columnName, -1)
}
}
} else {
for i, columnName := range columns {
if _, ok := used[strings.ToLower(columnName)]; ok {
continue
}
addSpec(columnName, i)
} }
} }
query := fmt.Sprintf(`SELECT * FROM %s`, tableName) query := fmt.Sprintf(`SELECT * FROM %s`, tableName)
if applyTemplateFilter && hasColumn(columns, "IsTemplate") {
query = fmt.Sprintf(`%s WHERE %s`, query, templateExclusionFilter())
}
orderBy := snapshotOrderBy(columns) orderBy := snapshotOrderBy(columns)
if orderBy != "" { if orderBy != "" {
query = fmt.Sprintf(`%s ORDER BY "%s" DESC`, query, orderBy) query = fmt.Sprintf(`%s ORDER BY "%s" DESC`, query, orderBy)
@@ -471,7 +517,11 @@ func CreateTableReport(logger *slog.Logger, Database db.Database, ctx context.Co
} }
for i, spec := range specs { for i, spec := range specs {
cell := fmt.Sprintf("%s1", string(rune('A'+i))) cell, err := excelize.CoordinatesToCellName(i+1, 1)
if err != nil {
logger.Error("Error determining header cell", "error", err, "column", i+1)
continue
}
xlsx.SetCellValue(sheetName, cell, spec.Name) xlsx.SetCellValue(sheetName, cell, spec.Name)
} }
@@ -500,7 +550,15 @@ func CreateTableReport(logger *slog.Logger, Database db.Database, ctx context.Co
return nil, err return nil, err
} }
for colIndex, spec := range specs { for colIndex, spec := range specs {
cell := fmt.Sprintf("%s%d", string(rune('A'+colIndex)), rowIndex) cell, err := excelize.CoordinatesToCellName(colIndex+1, rowIndex)
if err != nil {
logger.Error("Error determining data cell", "error", err, "column", colIndex+1, "row", rowIndex)
continue
}
if spec.SourceIndex < 0 || spec.SourceIndex >= len(values) {
xlsx.SetCellValue(sheetName, cell, "")
continue
}
value := values[spec.SourceIndex] value := values[spec.SourceIndex]
if spec.Humanize { if spec.Humanize {
xlsx.SetCellValue(sheetName, cell, formatEpochHuman(value)) xlsx.SetCellValue(sheetName, cell, formatEpochHuman(value))
@@ -528,8 +586,20 @@ func CreateTableReport(logger *slog.Logger, Database db.Database, ctx context.Co
logger.Error("Error freezing top row", "error", err) logger.Error("Error freezing top row", "error", err)
} }
if err := SetColAutoWidth(xlsx, sheetName); err != nil {
logger.Error("Error setting auto width", "error", err)
}
if isDailySummary || isMonthlySummary {
addReportMetadataSheet(logger, xlsx)
}
addTotalsChartSheet(logger, Database, ctx, xlsx, tableName) addTotalsChartSheet(logger, Database, ctx, xlsx, tableName)
if index, err := xlsx.GetSheetIndex(sheetName); err == nil {
xlsx.SetActiveSheet(index)
}
if err := xlsx.Write(&buffer); err != nil { if err := xlsx.Write(&buffer); err != nil {
return nil, err return nil, err
} }
@@ -668,6 +738,133 @@ func snapshotOrderBy(columns []string) string {
return "" return ""
} }
func hasColumn(columns []string, name string) bool {
for _, col := range columns {
if strings.EqualFold(col, name) {
return true
}
}
return false
}
func templateExclusionFilter() string {
return `COALESCE(CAST("IsTemplate" AS TEXT), '') NOT IN ('TRUE', 'true', '1')`
}
func quoteSheetName(name string) string {
escaped := strings.ReplaceAll(name, "'", "''")
return fmt.Sprintf("'%s'", escaped)
}
type columnDef struct {
Name string
Type string
}
func ensureSummaryReportColumns(ctx context.Context, dbConn *sqlx.DB, tableName string) error {
if err := validateTableName(tableName); err != nil {
return err
}
columns, err := tableColumns(ctx, dbConn, tableName)
if err != nil {
return err
}
existing := make(map[string]struct{}, len(columns))
for _, col := range columns {
existing[strings.ToLower(col)] = struct{}{}
}
for _, column := range summaryReportColumns() {
if _, ok := existing[strings.ToLower(column.Name)]; ok {
continue
}
query := fmt.Sprintf(`ALTER TABLE %s ADD COLUMN "%s" %s`, tableName, column.Name, column.Type)
if _, err := dbConn.ExecContext(ctx, query); err != nil {
return err
}
}
return nil
}
func summaryReportColumns() []columnDef {
return []columnDef{
{Name: "InventoryId", Type: "BIGINT"},
{Name: "Name", Type: "TEXT"},
{Name: "Vcenter", Type: "TEXT"},
{Name: "VmId", Type: "TEXT"},
{Name: "EventKey", Type: "TEXT"},
{Name: "CloudId", Type: "TEXT"},
{Name: "CreationTime", Type: "BIGINT"},
{Name: "DeletionTime", Type: "BIGINT"},
{Name: "ResourcePool", Type: "TEXT"},
{Name: "Datacenter", Type: "TEXT"},
{Name: "Cluster", Type: "TEXT"},
{Name: "Folder", Type: "TEXT"},
{Name: "ProvisionedDisk", Type: "REAL"},
{Name: "VcpuCount", Type: "BIGINT"},
{Name: "RamGB", Type: "BIGINT"},
{Name: "IsTemplate", Type: "TEXT"},
{Name: "PoweredOn", Type: "TEXT"},
{Name: "SrmPlaceholder", Type: "TEXT"},
{Name: "VmUuid", Type: "TEXT"},
{Name: "SamplesPresent", Type: "BIGINT"},
{Name: "AvgVcpuCount", Type: "REAL"},
{Name: "AvgRamGB", Type: "REAL"},
{Name: "AvgProvisionedDisk", Type: "REAL"},
{Name: "AvgIsPresent", Type: "REAL"},
{Name: "PoolTinPct", Type: "REAL"},
{Name: "PoolBronzePct", Type: "REAL"},
{Name: "PoolSilverPct", Type: "REAL"},
{Name: "PoolGoldPct", Type: "REAL"},
{Name: "Tin", Type: "REAL"},
{Name: "Bronze", Type: "REAL"},
{Name: "Silver", Type: "REAL"},
{Name: "Gold", Type: "REAL"},
}
}
func summaryReportOrder() []string {
return []string{
"Name",
"Vcenter",
"VmId",
"CreationTime",
"DeletionTime",
"ResourcePool",
"Datacenter",
"Cluster",
"Folder",
"ProvisionedDisk",
"VcpuCount",
"RamGB",
"IsTemplate",
"PoweredOn",
"SrmPlaceholder",
"VmUuid",
"SamplesPresent",
"AvgVcpuCount",
"AvgRamGB",
"AvgProvisionedDisk",
"AvgIsPresent",
"PoolTinPct",
"PoolBronzePct",
"PoolSilverPct",
"PoolGoldPct",
}
}
func addReportMetadataSheet(logger *slog.Logger, xlsx *excelize.File) {
sheetName := "Metadata"
if _, err := xlsx.NewSheet(sheetName); err != nil {
logger.Error("Error creating metadata sheet", "error", err)
return
}
xlsx.SetCellValue(sheetName, "A1", "ReportGeneratedAt")
xlsx.SetCellValue(sheetName, "B1", time.Now().Format(time.RFC3339))
if err := SetColAutoWidth(xlsx, sheetName); err != nil {
logger.Error("Error setting metadata auto width", "error", err)
}
}
func scanRowValues(rows *sqlx.Rows, columnCount int) ([]interface{}, error) { func scanRowValues(rows *sqlx.Rows, columnCount int) ([]interface{}, error) {
rawValues := make([]interface{}, columnCount) rawValues := make([]interface{}, columnCount)
scanArgs := make([]interface{}, columnCount) scanArgs := make([]interface{}, columnCount)
@@ -722,7 +919,8 @@ SELECT
COALESCE(SUM(CASE WHEN "IsPresent" = 'TRUE' AND LOWER("ResourcePool") = 'silver' THEN 1 ELSE 0 END), 0) AS silver_total, COALESCE(SUM(CASE WHEN "IsPresent" = 'TRUE' AND LOWER("ResourcePool") = 'silver' THEN 1 ELSE 0 END), 0) AS silver_total,
COALESCE(SUM(CASE WHEN "IsPresent" = 'TRUE' AND LOWER("ResourcePool") = 'gold' THEN 1 ELSE 0 END), 0) AS gold_total COALESCE(SUM(CASE WHEN "IsPresent" = 'TRUE' AND LOWER("ResourcePool") = 'gold' THEN 1 ELSE 0 END), 0) AS gold_total
FROM %s FROM %s
`, record.TableName) WHERE %s
`, record.TableName, templateExclusionFilter())
var row struct { var row struct {
VmCount int64 `db:"vm_count"` VmCount int64 `db:"vm_count"`
VcpuTotal int64 `db:"vcpu_total"` VcpuTotal int64 `db:"vcpu_total"`
@@ -768,7 +966,8 @@ SELECT
COALESCE(SUM(CASE WHEN "Silver" IS NOT NULL THEN "Silver" ELSE 0 END) / 100.0, 0) AS silver_total, COALESCE(SUM(CASE WHEN "Silver" IS NOT NULL THEN "Silver" ELSE 0 END) / 100.0, 0) AS silver_total,
COALESCE(SUM(CASE WHEN "Gold" IS NOT NULL THEN "Gold" ELSE 0 END) / 100.0, 0) AS gold_total COALESCE(SUM(CASE WHEN "Gold" IS NOT NULL THEN "Gold" ELSE 0 END) / 100.0, 0) AS gold_total
FROM %s FROM %s
`, record.TableName) WHERE %s
`, record.TableName, templateExclusionFilter())
var row struct { var row struct {
VmCount int64 `db:"vm_count"` VmCount int64 `db:"vm_count"`
VcpuTotal float64 `db:"vcpu_total"` VcpuTotal float64 `db:"vcpu_total"`
@@ -808,7 +1007,7 @@ func writeTotalsChart(logger *slog.Logger, xlsx *excelize.File, sheetName string
} }
xlsx.SetActiveSheet(index) xlsx.SetActiveSheet(index)
headers := []string{"Label", "VmCount", "VcpuCount", "RamGB", "PresenceRatio", "Tin", "Bronze", "Silver", "Gold"} headers := []string{"Label", "VmCount", "VcpuCount", "RamGB", "ProratedVmCount", "Tin", "Bronze", "Silver", "Gold"}
for i, header := range headers { for i, header := range headers {
cell, _ := excelize.CoordinatesToCellName(i+1, 1) cell, _ := excelize.CoordinatesToCellName(i+1, 1)
xlsx.SetCellValue(sheetName, cell, header) xlsx.SetCellValue(sheetName, cell, header)
@@ -826,26 +1025,47 @@ func writeTotalsChart(logger *slog.Logger, xlsx *excelize.File, sheetName string
xlsx.SetCellValue(sheetName, fmt.Sprintf("I%d", row), point.GoldTotal) xlsx.SetCellValue(sheetName, fmt.Sprintf("I%d", row), point.GoldTotal)
} }
if endCell, err := excelize.CoordinatesToCellName(len(headers), 1); err == nil {
filterRange := "A1:" + endCell
if err := xlsx.AutoFilter(sheetName, filterRange, nil); err != nil {
logger.Error("Error setting totals autofilter", "error", err)
}
}
if err := SetColAutoWidth(xlsx, sheetName); err != nil {
logger.Error("Error setting totals auto width", "error", err)
}
lastRow := len(points) + 1 lastRow := len(points) + 1
series := []excelize.ChartSeries{ sheetRef := quoteSheetName(sheetName)
{Name: fmt.Sprintf("%s!$B$1", sheetName), Categories: fmt.Sprintf("%s!$A$2:$A$%d", sheetName, lastRow), Values: fmt.Sprintf("%s!$B$2:$B$%d", sheetName, lastRow)}, categories := fmt.Sprintf("%s!$A$2:$A$%d", sheetRef, lastRow)
{Name: fmt.Sprintf("%s!$C$1", sheetName), Categories: fmt.Sprintf("%s!$A$2:$A$%d", sheetName, lastRow), Values: fmt.Sprintf("%s!$C$2:$C$%d", sheetName, lastRow)}, buildSeries := func(col string) excelize.ChartSeries {
{Name: fmt.Sprintf("%s!$D$1", sheetName), Categories: fmt.Sprintf("%s!$A$2:$A$%d", sheetName, lastRow), Values: fmt.Sprintf("%s!$D$2:$D$%d", sheetName, lastRow)}, return excelize.ChartSeries{
{Name: fmt.Sprintf("%s!$E$1", sheetName), Categories: fmt.Sprintf("%s!$A$2:$A$%d", sheetName, lastRow), Values: fmt.Sprintf("%s!$E$2:$E$%d", sheetName, lastRow)}, Name: fmt.Sprintf("%s!$%s$1", sheetRef, col),
{Name: fmt.Sprintf("%s!$F$1", sheetName), Categories: fmt.Sprintf("%s!$A$2:$A$%d", sheetName, lastRow), Values: fmt.Sprintf("%s!$F$2:$F$%d", sheetName, lastRow)}, Categories: categories,
{Name: fmt.Sprintf("%s!$G$1", sheetName), Categories: fmt.Sprintf("%s!$A$2:$A$%d", sheetName, lastRow), Values: fmt.Sprintf("%s!$G$2:$G$%d", sheetName, lastRow)}, Values: fmt.Sprintf("%s!$%s$2:$%s$%d", sheetRef, col, col, lastRow),
{Name: fmt.Sprintf("%s!$H$1", sheetName), Categories: fmt.Sprintf("%s!$A$2:$A$%d", sheetName, lastRow), Values: fmt.Sprintf("%s!$H$2:$H$%d", sheetName, lastRow)}, }
{Name: fmt.Sprintf("%s!$I$1", sheetName), Categories: fmt.Sprintf("%s!$A$2:$A$%d", sheetName, lastRow), Values: fmt.Sprintf("%s!$I$2:$I$%d", sheetName, lastRow)}, }
makeChart := func(anchor string, cols ...string) {
series := make([]excelize.ChartSeries, 0, len(cols))
for _, col := range cols {
series = append(series, buildSeries(col))
} }
chart := excelize.Chart{ chart := excelize.Chart{
Type: excelize.Line, Type: excelize.Line,
Series: series, Series: series,
Legend: excelize.ChartLegend{Position: "bottom"}, Legend: excelize.ChartLegend{Position: "bottom"},
XAxis: excelize.ChartAxis{MajorGridLines: true},
YAxis: excelize.ChartAxis{MajorGridLines: true},
}
if err := xlsx.AddChart(sheetName, anchor, &chart); err != nil {
logger.Error("Error adding totals chart", "error", err, "anchor", anchor)
}
} }
if err := xlsx.AddChart(sheetName, "G2", &chart); err != nil { makeChart("K2", "B", "E")
logger.Error("Error adding totals chart", "error", err) makeChart("K18", "C", "D")
} makeChart("K34", "F", "G", "H", "I")
} }
func formatEpochHuman(value interface{}) string { func formatEpochHuman(value interface{}) string {

View File

@@ -29,7 +29,6 @@ type inventorySnapshotRow struct {
CreationTime sql.NullInt64 CreationTime sql.NullInt64
DeletionTime sql.NullInt64 DeletionTime sql.NullInt64
ResourcePool sql.NullString ResourcePool sql.NullString
VmType sql.NullString
Datacenter sql.NullString Datacenter sql.NullString
Cluster sql.NullString Cluster sql.NullString
Folder sql.NullString Folder sql.NullString
@@ -181,10 +180,10 @@ func (c *CronTask) aggregateDailySummary(ctx context.Context, targetTime time.Ti
} }
unionQuery := buildUnionQuery(hourlyTables, []string{ unionQuery := buildUnionQuery(hourlyTables, []string{
`"InventoryId"`, `"Name"`, `"Vcenter"`, `"VmId"`, `"EventKey"`, `"CloudId"`, `"CreationTime"`, `"InventoryId"`, `"Name"`, `"Vcenter"`, `"VmId"`, `"EventKey"`, `"CloudId"`, `"CreationTime"`,
`"DeletionTime"`, `"ResourcePool"`, `"VmType"`, `"Datacenter"`, `"Cluster"`, `"Folder"`, `"DeletionTime"`, `"ResourcePool"`, `"Datacenter"`, `"Cluster"`, `"Folder"`,
`"ProvisionedDisk"`, `"VcpuCount"`, `"RamGB"`, `"IsTemplate"`, `"PoweredOn"`, `"ProvisionedDisk"`, `"VcpuCount"`, `"RamGB"`, `"IsTemplate"`, `"PoweredOn"`,
`"SrmPlaceholder"`, `"VmUuid"`, `"IsPresent"`, `"SrmPlaceholder"`, `"VmUuid"`, `"SnapshotTime"`, `"IsPresent"`,
}) }, templateExclusionFilter())
currentTotals, err := snapshotTotalsForUnion(ctx, dbConn, unionQuery) currentTotals, err := snapshotTotalsForUnion(ctx, dbConn, unionQuery)
if err != nil { if err != nil {
@@ -209,10 +208,10 @@ func (c *CronTask) aggregateDailySummary(ctx context.Context, targetTime time.Ti
} }
prevUnion := buildUnionQuery(prevTables, []string{ prevUnion := buildUnionQuery(prevTables, []string{
`"InventoryId"`, `"Name"`, `"Vcenter"`, `"VmId"`, `"EventKey"`, `"CloudId"`, `"CreationTime"`, `"InventoryId"`, `"Name"`, `"Vcenter"`, `"VmId"`, `"EventKey"`, `"CloudId"`, `"CreationTime"`,
`"DeletionTime"`, `"ResourcePool"`, `"VmType"`, `"Datacenter"`, `"Cluster"`, `"Folder"`, `"DeletionTime"`, `"ResourcePool"`, `"Datacenter"`, `"Cluster"`, `"Folder"`,
`"ProvisionedDisk"`, `"VcpuCount"`, `"RamGB"`, `"IsTemplate"`, `"PoweredOn"`, `"ProvisionedDisk"`, `"VcpuCount"`, `"RamGB"`, `"IsTemplate"`, `"PoweredOn"`,
`"SrmPlaceholder"`, `"VmUuid"`, `"IsPresent"`, `"SrmPlaceholder"`, `"VmUuid"`, `"SnapshotTime"`, `"IsPresent"`,
}) }, templateExclusionFilter())
prevTotals, err := snapshotTotalsForUnion(ctx, dbConn, prevUnion) prevTotals, err := snapshotTotalsForUnion(ctx, dbConn, prevUnion)
if err != nil { if err != nil {
c.Logger.Warn("unable to calculate previous day totals", "error", err, "date", prevStart.Format("2006-01-02")) c.Logger.Warn("unable to calculate previous day totals", "error", err, "date", prevStart.Format("2006-01-02"))
@@ -231,15 +230,17 @@ func (c *CronTask) aggregateDailySummary(ctx context.Context, targetTime time.Ti
insertQuery := fmt.Sprintf(` insertQuery := fmt.Sprintf(`
INSERT INTO %s ( INSERT INTO %s (
"InventoryId", "Name", "Vcenter", "VmId", "EventKey", "CloudId", "CreationTime", "DeletionTime", "InventoryId", "Name", "Vcenter", "VmId", "EventKey", "CloudId", "CreationTime", "DeletionTime",
"ResourcePool", "VmType", "Datacenter", "Cluster", "Folder", "ProvisionedDisk", "VcpuCount", "ResourcePool", "Datacenter", "Cluster", "Folder", "ProvisionedDisk", "VcpuCount",
"RamGB", "IsTemplate", "PoweredOn", "SrmPlaceholder", "VmUuid", "RamGB", "IsTemplate", "PoweredOn", "SrmPlaceholder", "VmUuid",
"SamplesPresent", "AvgVcpuCount", "AvgRamGB", "AvgProvisionedDisk", "AvgIsPresent", "SamplesPresent", "AvgVcpuCount", "AvgRamGB", "AvgProvisionedDisk", "AvgIsPresent",
"PoolTinPct", "PoolBronzePct", "PoolSilverPct", "PoolGoldPct", "PoolTinPct", "PoolBronzePct", "PoolSilverPct", "PoolGoldPct",
"Tin", "Bronze", "Silver", "Gold" "Tin", "Bronze", "Silver", "Gold"
) )
SELECT SELECT
"InventoryId", "Name", "Vcenter", "VmId", "EventKey", "CloudId", "CreationTime", "DeletionTime", "InventoryId", "Name", "Vcenter", "VmId", "EventKey", "CloudId",
"ResourcePool", "VmType", "Datacenter", "Cluster", "Folder", "ProvisionedDisk", "VcpuCount", COALESCE(NULLIF("CreationTime", 0), MIN(CASE WHEN "IsPresent" = 'TRUE' THEN "SnapshotTime" END), 0) AS "CreationTime",
"DeletionTime",
"ResourcePool", "Datacenter", "Cluster", "Folder", "ProvisionedDisk", "VcpuCount",
"RamGB", "IsTemplate", "PoweredOn", "SrmPlaceholder", "VmUuid", "RamGB", "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 "VcpuCount" IS NOT NULL THEN "VcpuCount" END) AS "AvgVcpuCount", AVG(CASE WHEN "IsPresent" = 'TRUE' AND "VcpuCount" IS NOT NULL THEN "VcpuCount" END) AS "AvgVcpuCount",
@@ -267,7 +268,7 @@ FROM (
) snapshots ) snapshots
GROUP BY GROUP BY
"InventoryId", "Name", "Vcenter", "VmId", "EventKey", "CloudId", "CreationTime", "DeletionTime", "InventoryId", "Name", "Vcenter", "VmId", "EventKey", "CloudId", "CreationTime", "DeletionTime",
"ResourcePool", "VmType", "Datacenter", "Cluster", "Folder", "ProvisionedDisk", "VcpuCount", "ResourcePool", "Datacenter", "Cluster", "Folder", "ProvisionedDisk", "VcpuCount",
"RamGB", "IsTemplate", "PoweredOn", "SrmPlaceholder", "VmUuid"; "RamGB", "IsTemplate", "PoweredOn", "SrmPlaceholder", "VmUuid";
`, summaryTable, unionQuery) `, summaryTable, unionQuery)
@@ -346,10 +347,10 @@ func (c *CronTask) aggregateMonthlySummary(ctx context.Context, targetMonth time
} }
unionQuery := buildUnionQuery(dailyTables, []string{ unionQuery := buildUnionQuery(dailyTables, []string{
`"InventoryId"`, `"Name"`, `"Vcenter"`, `"VmId"`, `"EventKey"`, `"CloudId"`, `"CreationTime"`, `"InventoryId"`, `"Name"`, `"Vcenter"`, `"VmId"`, `"EventKey"`, `"CloudId"`, `"CreationTime"`,
`"DeletionTime"`, `"ResourcePool"`, `"VmType"`, `"Datacenter"`, `"Cluster"`, `"Folder"`, `"DeletionTime"`, `"ResourcePool"`, `"Datacenter"`, `"Cluster"`, `"Folder"`,
`"ProvisionedDisk"`, `"VcpuCount"`, `"RamGB"`, `"IsTemplate"`, `"PoweredOn"`, `"ProvisionedDisk"`, `"VcpuCount"`, `"RamGB"`, `"IsTemplate"`, `"PoweredOn"`,
`"SrmPlaceholder"`, `"VmUuid"`, `"IsPresent"`, `"SrmPlaceholder"`, `"VmUuid"`, `"IsPresent"`,
}) }, templateExclusionFilter())
if strings.TrimSpace(unionQuery) == "" { if strings.TrimSpace(unionQuery) == "" {
return fmt.Errorf("no valid daily snapshot tables found for %s", targetMonth.Format("2006-01")) return fmt.Errorf("no valid daily snapshot tables found for %s", targetMonth.Format("2006-01"))
} }
@@ -370,7 +371,7 @@ func (c *CronTask) aggregateMonthlySummary(ctx context.Context, targetMonth time
insertQuery := fmt.Sprintf(` insertQuery := fmt.Sprintf(`
INSERT INTO %s ( INSERT INTO %s (
"InventoryId", "Name", "Vcenter", "VmId", "EventKey", "CloudId", "CreationTime", "DeletionTime", "InventoryId", "Name", "Vcenter", "VmId", "EventKey", "CloudId", "CreationTime", "DeletionTime",
"ResourcePool", "VmType", "Datacenter", "Cluster", "Folder", "ProvisionedDisk", "VcpuCount", "ResourcePool", "Datacenter", "Cluster", "Folder", "ProvisionedDisk", "VcpuCount",
"RamGB", "IsTemplate", "PoweredOn", "SrmPlaceholder", "VmUuid", "RamGB", "IsTemplate", "PoweredOn", "SrmPlaceholder", "VmUuid",
"AvgVcpuCount", "AvgRamGB", "AvgProvisionedDisk", "AvgIsPresent", "AvgVcpuCount", "AvgRamGB", "AvgProvisionedDisk", "AvgIsPresent",
"PoolTinPct", "PoolBronzePct", "PoolSilverPct", "PoolGoldPct", "PoolTinPct", "PoolBronzePct", "PoolSilverPct", "PoolGoldPct",
@@ -378,7 +379,7 @@ INSERT INTO %s (
) )
SELECT SELECT
"InventoryId", "Name", "Vcenter", "VmId", "EventKey", "CloudId", "CreationTime", "DeletionTime", "InventoryId", "Name", "Vcenter", "VmId", "EventKey", "CloudId", "CreationTime", "DeletionTime",
"ResourcePool", "VmType", "Datacenter", "Cluster", "Folder", "ProvisionedDisk", "VcpuCount", "ResourcePool", "Datacenter", "Cluster", "Folder", "ProvisionedDisk", "VcpuCount",
"RamGB", "IsTemplate", "PoweredOn", "SrmPlaceholder", "VmUuid", "RamGB", "IsTemplate", "PoweredOn", "SrmPlaceholder", "VmUuid",
AVG(CASE WHEN "VcpuCount" IS NOT NULL THEN "VcpuCount" END) AS "AvgVcpuCount", AVG(CASE WHEN "VcpuCount" IS NOT NULL THEN "VcpuCount" END) AS "AvgVcpuCount",
AVG(CASE WHEN "RamGB" IS NOT NULL THEN "RamGB" END) AS "AvgRamGB", AVG(CASE WHEN "RamGB" IS NOT NULL THEN "RamGB" END) AS "AvgRamGB",
@@ -405,7 +406,7 @@ FROM (
) snapshots ) snapshots
GROUP BY GROUP BY
"InventoryId", "Name", "Vcenter", "VmId", "EventKey", "CloudId", "CreationTime", "DeletionTime", "InventoryId", "Name", "Vcenter", "VmId", "EventKey", "CloudId", "CreationTime", "DeletionTime",
"ResourcePool", "VmType", "Datacenter", "Cluster", "Folder", "ProvisionedDisk", "VcpuCount", "ResourcePool", "Datacenter", "Cluster", "Folder", "ProvisionedDisk", "VcpuCount",
"RamGB", "IsTemplate", "PoweredOn", "SrmPlaceholder", "VmUuid"; "RamGB", "IsTemplate", "PoweredOn", "SrmPlaceholder", "VmUuid";
`, monthlyTable, unionQuery) `, monthlyTable, unionQuery)
@@ -530,8 +531,7 @@ func ensureDailyInventoryTable(ctx context.Context, dbConn *sqlx.DB, tableName s
"CloudId" TEXT, "CloudId" TEXT,
"CreationTime" BIGINT, "CreationTime" BIGINT,
"DeletionTime" BIGINT, "DeletionTime" BIGINT,
"ResourcePool" TEXT, "ResourcePool" TEXT TEXT,
"VmType" TEXT,
"Datacenter" TEXT, "Datacenter" TEXT,
"Cluster" TEXT, "Cluster" TEXT,
"Folder" TEXT, "Folder" TEXT,
@@ -556,8 +556,7 @@ func ensureDailyInventoryTable(ctx context.Context, dbConn *sqlx.DB, tableName s
"CloudId" TEXT, "CloudId" TEXT,
"CreationTime" BIGINT, "CreationTime" BIGINT,
"DeletionTime" BIGINT, "DeletionTime" BIGINT,
"ResourcePool" TEXT, "ResourcePool" TEXT TEXT,
"VmType" TEXT,
"Datacenter" TEXT, "Datacenter" TEXT,
"Cluster" TEXT, "Cluster" TEXT,
"Folder" TEXT, "Folder" TEXT,
@@ -601,8 +600,7 @@ func ensureDailySummaryTable(ctx context.Context, dbConn *sqlx.DB, tableName str
"CloudId" TEXT, "CloudId" TEXT,
"CreationTime" BIGINT, "CreationTime" BIGINT,
"DeletionTime" BIGINT, "DeletionTime" BIGINT,
"ResourcePool" TEXT, "ResourcePool" TEXT TEXT,
"VmType" TEXT,
"Datacenter" TEXT, "Datacenter" TEXT,
"Cluster" TEXT, "Cluster" TEXT,
"Folder" TEXT, "Folder" TEXT,
@@ -638,8 +636,7 @@ func ensureDailySummaryTable(ctx context.Context, dbConn *sqlx.DB, tableName str
"CloudId" TEXT, "CloudId" TEXT,
"CreationTime" BIGINT, "CreationTime" BIGINT,
"DeletionTime" BIGINT, "DeletionTime" BIGINT,
"ResourcePool" TEXT, "ResourcePool" TEXT TEXT,
"VmType" TEXT,
"Datacenter" TEXT, "Datacenter" TEXT,
"Cluster" TEXT, "Cluster" TEXT,
"Folder" TEXT, "Folder" TEXT,
@@ -673,6 +670,10 @@ func ensureDailySummaryTable(ctx context.Context, dbConn *sqlx.DB, tableName str
return err return err
} }
if err := ensureSnapshotColumns(ctx, dbConn, tableName, baseSummaryColumns()); err != nil {
return err
}
return ensureSnapshotColumns(ctx, dbConn, tableName, []columnDef{ return ensureSnapshotColumns(ctx, dbConn, tableName, []columnDef{
{Name: "AvgVcpuCount", Type: "REAL"}, {Name: "AvgVcpuCount", Type: "REAL"},
{Name: "AvgRamGB", Type: "REAL"}, {Name: "AvgRamGB", Type: "REAL"},
@@ -704,8 +705,7 @@ func ensureMonthlySummaryTable(ctx context.Context, dbConn *sqlx.DB, tableName s
"CloudId" TEXT, "CloudId" TEXT,
"CreationTime" BIGINT, "CreationTime" BIGINT,
"DeletionTime" BIGINT, "DeletionTime" BIGINT,
"ResourcePool" TEXT, "ResourcePool" TEXT TEXT,
"VmType" TEXT,
"Datacenter" TEXT, "Datacenter" TEXT,
"Cluster" TEXT, "Cluster" TEXT,
"Folder" TEXT, "Folder" TEXT,
@@ -740,8 +740,7 @@ func ensureMonthlySummaryTable(ctx context.Context, dbConn *sqlx.DB, tableName s
"CloudId" TEXT, "CloudId" TEXT,
"CreationTime" BIGINT, "CreationTime" BIGINT,
"DeletionTime" BIGINT, "DeletionTime" BIGINT,
"ResourcePool" TEXT, "ResourcePool" TEXT TEXT,
"VmType" TEXT,
"Datacenter" TEXT, "Datacenter" TEXT,
"Cluster" TEXT, "Cluster" TEXT,
"Folder" TEXT, "Folder" TEXT,
@@ -774,6 +773,10 @@ func ensureMonthlySummaryTable(ctx context.Context, dbConn *sqlx.DB, tableName s
return err return err
} }
if err := ensureSnapshotColumns(ctx, dbConn, tableName, baseSummaryColumns()); err != nil {
return err
}
return ensureSnapshotColumns(ctx, dbConn, tableName, []columnDef{ return ensureSnapshotColumns(ctx, dbConn, tableName, []columnDef{
{Name: "AvgVcpuCount", Type: "REAL"}, {Name: "AvgVcpuCount", Type: "REAL"},
{Name: "AvgRamGB", Type: "REAL"}, {Name: "AvgRamGB", Type: "REAL"},
@@ -790,18 +793,26 @@ func ensureMonthlySummaryTable(ctx context.Context, dbConn *sqlx.DB, tableName s
}) })
} }
func buildUnionQuery(tables []string, columns []string) string { func buildUnionQuery(tables []string, columns []string, whereClause string) string {
queries := make([]string, 0, len(tables)) queries := make([]string, 0, len(tables))
columnList := strings.Join(columns, ", ") columnList := strings.Join(columns, ", ")
for _, table := range tables { for _, table := range tables {
if _, err := safeTableName(table); err != nil { if _, err := safeTableName(table); err != nil {
continue continue
} }
queries = append(queries, fmt.Sprintf("SELECT %s FROM %s", columnList, table)) query := fmt.Sprintf("SELECT %s FROM %s", columnList, table)
if whereClause != "" {
query = fmt.Sprintf("%s WHERE %s", query, whereClause)
}
queries = append(queries, query)
} }
return strings.Join(queries, "\nUNION ALL\n") return strings.Join(queries, "\nUNION ALL\n")
} }
func templateExclusionFilter() string {
return `COALESCE(CAST("IsTemplate" AS TEXT), '') NOT IN ('TRUE', 'true', '1')`
}
func parseSnapshotDate(table string, prefix string, layout string) (time.Time, bool) { func parseSnapshotDate(table string, prefix string, layout string) (time.Time, bool) {
if !strings.HasPrefix(table, prefix) { if !strings.HasPrefix(table, prefix) {
return time.Time{}, false return time.Time{}, false
@@ -860,10 +871,10 @@ func tableHasRows(ctx context.Context, dbConn *sqlx.DB, table string) (bool, err
} }
type snapshotTotals struct { type snapshotTotals struct {
VmCount int64 VmCount int64 `db:"vm_count"`
VcpuTotal int64 VcpuTotal int64 `db:"vcpu_total"`
RamTotal int64 RamTotal int64 `db:"ram_total"`
DiskTotal float64 DiskTotal float64 `db:"disk_total"`
} }
type columnDef struct { type columnDef struct {
@@ -883,6 +894,31 @@ func ensureSnapshotColumns(ctx context.Context, dbConn *sqlx.DB, tableName strin
return nil return nil
} }
func baseSummaryColumns() []columnDef {
return []columnDef{
{Name: "InventoryId", Type: "BIGINT"},
{Name: "Name", Type: "TEXT"},
{Name: "Vcenter", Type: "TEXT"},
{Name: "VmId", Type: "TEXT"},
{Name: "EventKey", Type: "TEXT"},
{Name: "CloudId", Type: "TEXT"},
{Name: "CreationTime", Type: "BIGINT"},
{Name: "DeletionTime", Type: "BIGINT"},
{Name: "ResourcePool", Type: "TEXT"},
{Name: "Datacenter", Type: "TEXT"},
{Name: "Cluster", Type: "TEXT"},
{Name: "Folder", Type: "TEXT"},
{Name: "ProvisionedDisk", Type: "REAL"},
{Name: "VcpuCount", Type: "BIGINT"},
{Name: "RamGB", Type: "BIGINT"},
{Name: "IsTemplate", Type: "TEXT"},
{Name: "PoweredOn", Type: "TEXT"},
{Name: "SrmPlaceholder", Type: "TEXT"},
{Name: "VmUuid", Type: "TEXT"},
{Name: "SamplesPresent", Type: "BIGINT"},
}
}
func addColumnIfMissing(ctx context.Context, dbConn *sqlx.DB, tableName string, column columnDef) error { 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) query := fmt.Sprintf(`ALTER TABLE %s ADD COLUMN "%s" %s`, tableName, column.Name, column.Type)
if _, err := dbConn.ExecContext(ctx, query); err != nil { if _, err := dbConn.ExecContext(ctx, query); err != nil {
@@ -1054,6 +1090,25 @@ func intWithDefault(value int, fallback int) int {
return value return value
} }
func normalizeResourcePool(value string) string {
trimmed := strings.TrimSpace(value)
if trimmed == "" {
return ""
}
switch {
case strings.EqualFold(trimmed, "tin"):
return "Tin"
case strings.EqualFold(trimmed, "bronze"):
return "Bronze"
case strings.EqualFold(trimmed, "silver"):
return "Silver"
case strings.EqualFold(trimmed, "gold"):
return "Gold"
default:
return trimmed
}
}
func snapshotFromVM(vmObject *mo.VirtualMachine, vc *vcenter.Vcenter, snapshotTime time.Time, inv *queries.Inventory) (inventorySnapshotRow, error) { func snapshotFromVM(vmObject *mo.VirtualMachine, vc *vcenter.Vcenter, snapshotTime time.Time, inv *queries.Inventory) (inventorySnapshotRow, error) {
if vmObject == nil { if vmObject == nil {
return inventorySnapshotRow{}, fmt.Errorf("missing VM object") return inventorySnapshotRow{}, fmt.Errorf("missing VM object")
@@ -1071,7 +1126,6 @@ func snapshotFromVM(vmObject *mo.VirtualMachine, vc *vcenter.Vcenter, snapshotTi
row.EventKey = inv.EventKey row.EventKey = inv.EventKey
row.CloudId = inv.CloudId row.CloudId = inv.CloudId
row.DeletionTime = inv.DeletionTime row.DeletionTime = inv.DeletionTime
row.VmType = inv.VmType
} }
if vmObject.Config != nil { if vmObject.Config != nil {
@@ -1112,7 +1166,9 @@ func snapshotFromVM(vmObject *mo.VirtualMachine, vc *vcenter.Vcenter, snapshotTi
} }
if inv != nil { if inv != nil {
row.ResourcePool = inv.ResourcePool if inv.ResourcePool.Valid {
row.ResourcePool = sql.NullString{String: normalizeResourcePool(inv.ResourcePool.String), Valid: true}
}
row.Datacenter = inv.Datacenter row.Datacenter = inv.Datacenter
row.Cluster = inv.Cluster row.Cluster = inv.Cluster
row.Folder = inv.Folder row.Folder = inv.Folder
@@ -1144,7 +1200,7 @@ func snapshotFromVM(vmObject *mo.VirtualMachine, vc *vcenter.Vcenter, snapshotTi
if row.ResourcePool.String == "" { if row.ResourcePool.String == "" {
if rpName, err := vc.GetVmResourcePool(*vmObject); err == nil { if rpName, err := vc.GetVmResourcePool(*vmObject); err == nil {
row.ResourcePool = sql.NullString{String: rpName, Valid: rpName != ""} row.ResourcePool = sql.NullString{String: normalizeResourcePool(rpName), Valid: rpName != ""}
} }
} }
@@ -1179,8 +1235,7 @@ func snapshotFromInventory(inv queries.Inventory, snapshotTime time.Time) invent
CloudId: inv.CloudId, CloudId: inv.CloudId,
CreationTime: inv.CreationTime, CreationTime: inv.CreationTime,
DeletionTime: inv.DeletionTime, DeletionTime: inv.DeletionTime,
ResourcePool: inv.ResourcePool, ResourcePool: sql.NullString{String: normalizeResourcePool(inv.ResourcePool.String), Valid: inv.ResourcePool.Valid},
VmType: inv.VmType,
Datacenter: inv.Datacenter, Datacenter: inv.Datacenter,
Cluster: inv.Cluster, Cluster: inv.Cluster,
Folder: inv.Folder, Folder: inv.Folder,
@@ -1199,10 +1254,10 @@ func insertDailyInventoryRow(ctx context.Context, dbConn *sqlx.DB, tableName str
query := fmt.Sprintf(` query := fmt.Sprintf(`
INSERT INTO %s ( INSERT INTO %s (
"InventoryId", "Name", "Vcenter", "VmId", "EventKey", "CloudId", "CreationTime", "DeletionTime", "InventoryId", "Name", "Vcenter", "VmId", "EventKey", "CloudId", "CreationTime", "DeletionTime",
"ResourcePool", "VmType", "Datacenter", "Cluster", "Folder", "ProvisionedDisk", "VcpuCount", "ResourcePool", "Datacenter", "Cluster", "Folder", "ProvisionedDisk", "VcpuCount",
"RamGB", "IsTemplate", "PoweredOn", "SrmPlaceholder", "VmUuid", "SnapshotTime", "IsPresent" "RamGB", "IsTemplate", "PoweredOn", "SrmPlaceholder", "VmUuid", "SnapshotTime", "IsPresent"
) )
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?); VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
`, tableName) `, tableName)
query = sqlx.Rebind(sqlx.BindType(dbConn.DriverName()), query) query = sqlx.Rebind(sqlx.BindType(dbConn.DriverName()), query)
@@ -1217,7 +1272,6 @@ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
row.CreationTime, row.CreationTime,
row.DeletionTime, row.DeletionTime,
row.ResourcePool, row.ResourcePool,
row.VmType,
row.Datacenter, row.Datacenter,
row.Cluster, row.Cluster,
row.Folder, row.Folder,

View File

@@ -23,8 +23,13 @@ import (
func (h *Handler) SnapshotAggregateForce(w http.ResponseWriter, r *http.Request) { func (h *Handler) SnapshotAggregateForce(w http.ResponseWriter, r *http.Request) {
snapshotType := strings.ToLower(strings.TrimSpace(r.URL.Query().Get("type"))) snapshotType := strings.ToLower(strings.TrimSpace(r.URL.Query().Get("type")))
dateValue := strings.TrimSpace(r.URL.Query().Get("date")) dateValue := strings.TrimSpace(r.URL.Query().Get("date"))
startedAt := time.Now()
if snapshotType == "" || dateValue == "" { if snapshotType == "" || dateValue == "" {
h.Logger.Warn("Snapshot aggregation request missing parameters",
"type", snapshotType,
"date", dateValue,
)
writeJSONError(w, http.StatusBadRequest, "type and date are required") writeJSONError(w, http.StatusBadRequest, "type and date are required")
return return
} }
@@ -40,28 +45,40 @@ func (h *Handler) SnapshotAggregateForce(w http.ResponseWriter, r *http.Request)
case "daily": case "daily":
parsed, err := time.Parse("2006-01-02", dateValue) parsed, err := time.Parse("2006-01-02", dateValue)
if err != nil { if err != nil {
h.Logger.Warn("Snapshot aggregation invalid daily date format", "date", dateValue)
writeJSONError(w, http.StatusBadRequest, "date must be YYYY-MM-DD") writeJSONError(w, http.StatusBadRequest, "date must be YYYY-MM-DD")
return return
} }
h.Logger.Info("Starting daily snapshot aggregation", "date", parsed.Format("2006-01-02"), "force", true)
if err := ct.AggregateDailySummary(ctx, parsed, true); err != nil { if err := ct.AggregateDailySummary(ctx, parsed, true); err != nil {
h.Logger.Error("Daily snapshot aggregation failed", "date", parsed.Format("2006-01-02"), "error", err)
writeJSONError(w, http.StatusInternalServerError, err.Error()) writeJSONError(w, http.StatusInternalServerError, err.Error())
return return
} }
case "monthly": case "monthly":
parsed, err := time.Parse("2006-01", dateValue) parsed, err := time.Parse("2006-01", dateValue)
if err != nil { if err != nil {
h.Logger.Warn("Snapshot aggregation invalid monthly date format", "date", dateValue)
writeJSONError(w, http.StatusBadRequest, "date must be YYYY-MM") writeJSONError(w, http.StatusBadRequest, "date must be YYYY-MM")
return return
} }
h.Logger.Info("Starting monthly snapshot aggregation", "date", parsed.Format("2006-01"), "force", true)
if err := ct.AggregateMonthlySummary(ctx, parsed, true); err != nil { if err := ct.AggregateMonthlySummary(ctx, parsed, true); err != nil {
h.Logger.Error("Monthly snapshot aggregation failed", "date", parsed.Format("2006-01"), "error", err)
writeJSONError(w, http.StatusInternalServerError, err.Error()) writeJSONError(w, http.StatusInternalServerError, err.Error())
return return
} }
default: default:
h.Logger.Warn("Snapshot aggregation invalid type", "type", snapshotType)
writeJSONError(w, http.StatusBadRequest, "type must be daily or monthly") writeJSONError(w, http.StatusBadRequest, "type must be daily or monthly")
return return
} }
h.Logger.Info("Snapshot aggregation completed",
"type", snapshotType,
"date", dateValue,
"duration", time.Since(startedAt),
)
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]string{ json.NewEncoder(w).Encode(map[string]string{

View File

@@ -25,15 +25,14 @@ func (l *LoggingMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
start := time.Now() start := time.Now()
l.handler.ServeHTTP(w, r) l.handler.ServeHTTP(w, r)
query := r.URL.RawQuery requestPath := r.URL.RequestURI()
if query == "" { if requestPath == "" {
query = "-" requestPath = r.URL.Path
} }
l.logger.Debug( l.logger.Debug(
"Request recieved", "Request recieved",
slog.String("method", r.Method), slog.String("method", r.Method),
slog.String("path", r.URL.Path), slog.String("request", requestPath),
slog.String("query", query),
slog.String("remote", r.RemoteAddr), slog.String("remote", r.RemoteAddr),
slog.Duration("duration", time.Since(start)), slog.Duration("duration", time.Since(start)),
) )