This commit is contained in:
@@ -412,6 +412,11 @@ func CreateTableReport(logger *slog.Logger, Database db.Database, ctx context.Co
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
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_")
|
||||
isMonthlySummary := strings.HasPrefix(tableName, "inventory_monthly_summary_")
|
||||
hideInventoryID := isHourlySnapshot || isDailySummary || isMonthlySummary
|
||||
hideRowID := isHourlySnapshot || isDailySummary || isMonthlySummary
|
||||
humanizeTimes := isDailySummary || isMonthlySummary
|
||||
applyTemplateFilter := isHourlySnapshot || isDailySummary || isMonthlySummary
|
||||
type columnSpec struct {
|
||||
Name string
|
||||
SourceIndex int
|
||||
Humanize bool
|
||||
}
|
||||
specs := make([]columnSpec, 0, len(columns)+2)
|
||||
columnIndex := make(map[string]int, len(columns))
|
||||
for i, columnName := range columns {
|
||||
columnIndex[strings.ToLower(columnName)] = i
|
||||
}
|
||||
used := make(map[string]struct{}, len(columns))
|
||||
addSpec := func(columnName string, sourceIndex int) {
|
||||
if hideInventoryID && strings.EqualFold(columnName, "InventoryId") {
|
||||
continue
|
||||
return
|
||||
}
|
||||
specs = append(specs, columnSpec{Name: columnName, SourceIndex: i})
|
||||
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" {
|
||||
specs = append(specs, columnSpec{Name: "CreationTimeReadable", SourceIndex: i, Humanize: true})
|
||||
specs = append(specs, columnSpec{Name: "CreationTimeReadable", SourceIndex: sourceIndex, Humanize: true})
|
||||
}
|
||||
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)
|
||||
if applyTemplateFilter && hasColumn(columns, "IsTemplate") {
|
||||
query = fmt.Sprintf(`%s WHERE %s`, query, templateExclusionFilter())
|
||||
}
|
||||
orderBy := snapshotOrderBy(columns)
|
||||
if 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 {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -500,7 +550,15 @@ func CreateTableReport(logger *slog.Logger, Database db.Database, ctx context.Co
|
||||
return nil, err
|
||||
}
|
||||
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]
|
||||
if spec.Humanize {
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
if index, err := xlsx.GetSheetIndex(sheetName); err == nil {
|
||||
xlsx.SetActiveSheet(index)
|
||||
}
|
||||
|
||||
if err := xlsx.Write(&buffer); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -668,6 +738,133 @@ func snapshotOrderBy(columns []string) string {
|
||||
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) {
|
||||
rawValues := 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") = 'gold' THEN 1 ELSE 0 END), 0) AS gold_total
|
||||
FROM %s
|
||||
`, record.TableName)
|
||||
WHERE %s
|
||||
`, record.TableName, templateExclusionFilter())
|
||||
var row struct {
|
||||
VmCount int64 `db:"vm_count"`
|
||||
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 "Gold" IS NOT NULL THEN "Gold" ELSE 0 END) / 100.0, 0) AS gold_total
|
||||
FROM %s
|
||||
`, record.TableName)
|
||||
WHERE %s
|
||||
`, record.TableName, templateExclusionFilter())
|
||||
var row struct {
|
||||
VmCount int64 `db:"vm_count"`
|
||||
VcpuTotal float64 `db:"vcpu_total"`
|
||||
@@ -808,7 +1007,7 @@ func writeTotalsChart(logger *slog.Logger, xlsx *excelize.File, sheetName string
|
||||
}
|
||||
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 {
|
||||
cell, _ := excelize.CoordinatesToCellName(i+1, 1)
|
||||
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)
|
||||
}
|
||||
|
||||
lastRow := len(points) + 1
|
||||
series := []excelize.ChartSeries{
|
||||
{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)},
|
||||
{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)},
|
||||
{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)},
|
||||
{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!$F$1", sheetName), Categories: fmt.Sprintf("%s!$A$2:$A$%d", sheetName, lastRow), Values: fmt.Sprintf("%s!$F$2:$F$%d", sheetName, lastRow)},
|
||||
{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)},
|
||||
{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)},
|
||||
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)
|
||||
}
|
||||
}
|
||||
chart := excelize.Chart{
|
||||
Type: excelize.Line,
|
||||
Series: series,
|
||||
Legend: excelize.ChartLegend{Position: "bottom"},
|
||||
if err := SetColAutoWidth(xlsx, sheetName); err != nil {
|
||||
logger.Error("Error setting totals auto width", "error", err)
|
||||
}
|
||||
|
||||
if err := xlsx.AddChart(sheetName, "G2", &chart); err != nil {
|
||||
logger.Error("Error adding totals chart", "error", err)
|
||||
lastRow := len(points) + 1
|
||||
sheetRef := quoteSheetName(sheetName)
|
||||
categories := fmt.Sprintf("%s!$A$2:$A$%d", sheetRef, lastRow)
|
||||
buildSeries := func(col string) excelize.ChartSeries {
|
||||
return excelize.ChartSeries{
|
||||
Name: fmt.Sprintf("%s!$%s$1", sheetRef, col),
|
||||
Categories: categories,
|
||||
Values: fmt.Sprintf("%s!$%s$2:$%s$%d", sheetRef, col, col, 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{
|
||||
Type: excelize.Line,
|
||||
Series: series,
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
makeChart("K2", "B", "E")
|
||||
makeChart("K18", "C", "D")
|
||||
makeChart("K34", "F", "G", "H", "I")
|
||||
}
|
||||
|
||||
func formatEpochHuman(value interface{}) string {
|
||||
|
||||
Reference in New Issue
Block a user