avoid vcenter totals pages scanning whole database
Some checks failed
continuous-integration/drone/push Build is failing

This commit is contained in:
2026-02-09 13:44:43 +11:00
parent c66679a71f
commit 5736dc6929
11 changed files with 991 additions and 195 deletions

View File

@@ -1,6 +1,7 @@
package handler
import (
"context"
"fmt"
"net/http"
"net/url"
@@ -11,6 +12,12 @@ import (
"vctp/db"
)
const (
vcenterHourlyDetailWindowDays = 45
vcenterDailyDefaultLimit = 400
vcenterMonthlyDefaultLimit = 200
)
// VcenterList renders a list of vCenters being monitored.
// @Summary List vCenters
// @Description Lists all vCenters with recorded snapshot totals.
@@ -20,9 +27,6 @@ import (
// @Router /vcenters [get]
func (h *Handler) VcenterList(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
if err := db.SyncVcenterTotalsFromSnapshots(ctx, h.Database.DB()); err != nil {
h.Logger.Warn("failed to sync vcenter totals", "error", err)
}
vcs, err := db.ListVcenters(ctx, h.Database.DB())
if err != nil {
http.Error(w, fmt.Sprintf("failed to list vcenters: %v", err), http.StatusInternalServerError)
@@ -32,7 +36,7 @@ func (h *Handler) VcenterList(w http.ResponseWriter, r *http.Request) {
for _, vc := range vcs {
links = append(links, views.VcenterLink{
Name: vc,
Link: "/vcenters/totals?vcenter=" + url.QueryEscape(vc),
Link: "/vcenters/totals/daily?vcenter=" + url.QueryEscape(vc),
})
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
@@ -41,49 +45,117 @@ func (h *Handler) VcenterList(w http.ResponseWriter, r *http.Request) {
}
}
// VcenterTotals renders totals for a vCenter.
// VcenterTotals keeps backward compatibility with the original endpoint and routes to the new pages.
// @Summary vCenter totals
// @Description Shows per-snapshot totals for a vCenter.
// @Description Redirect-style handler for compatibility; use /vcenters/totals/daily or /vcenters/totals/hourly.
// @Tags vcenters
// @Produce text/html
// @Param vcenter query string true "vCenter URL"
// @Param type query string false "hourly|daily|monthly (default: hourly)"
// @Param limit query int false "Limit results (default 200)"
// @Param type query string false "hourly|daily|monthly"
// @Param limit query int false "Limit results"
// @Success 200 {string} string "HTML page"
// @Failure 400 {string} string "Missing vcenter"
// @Router /vcenters/totals [get]
func (h *Handler) VcenterTotals(w http.ResponseWriter, r *http.Request) {
switch strings.ToLower(strings.TrimSpace(r.URL.Query().Get("type"))) {
case "hourly", "hourly-detail", "detail", "detailed":
h.VcenterTotalsHourlyDetailed(w, r)
return
case "monthly":
h.vcenterTotalsLegacyMonthly(w, r)
return
default:
h.VcenterTotalsDaily(w, r)
return
}
}
// VcenterTotalsDaily renders the daily-aggregation totals page for one vCenter.
// @Summary vCenter daily totals
// @Description Shows daily aggregated VM count/vCPU/RAM totals for a vCenter.
// @Tags vcenters
// @Produce text/html
// @Param vcenter query string true "vCenter URL"
// @Param limit query int false "Limit results (default 400)"
// @Success 200 {string} string "HTML page"
// @Failure 400 {string} string "Missing vcenter"
// @Router /vcenters/totals/daily [get]
func (h *Handler) VcenterTotalsDaily(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
vc := r.URL.Query().Get("vcenter")
vc, ok := requiredVcenterParam(w, r)
if !ok {
return
}
limit := parsePositiveLimit(r, vcenterDailyDefaultLimit)
rows, err := db.ListVcenterTotalsByType(ctx, h.Database.DB(), vc, "daily", limit)
if err != nil {
http.Error(w, fmt.Sprintf("failed to list daily totals: %v", err), http.StatusInternalServerError)
return
}
h.renderVcenterTotalsPage(ctx, w, vc, "daily", rows)
}
// VcenterTotalsHourlyDetailed renders a detailed hourly page over the most recent 45 days.
// @Summary vCenter hourly totals (45 days)
// @Description Shows detailed hourly VM count/vCPU/RAM totals for the latest 45 days.
// @Tags vcenters
// @Produce text/html
// @Param vcenter query string true "vCenter URL"
// @Success 200 {string} string "HTML page"
// @Failure 400 {string} string "Missing vcenter"
// @Router /vcenters/totals/hourly [get]
func (h *Handler) VcenterTotalsHourlyDetailed(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
vc, ok := requiredVcenterParam(w, r)
if !ok {
return
}
since := time.Now().AddDate(0, 0, -vcenterHourlyDetailWindowDays)
rows, err := db.ListVcenterHourlyTotalsSince(ctx, h.Database.DB(), vc, since)
if err != nil {
http.Error(w, fmt.Sprintf("failed to list hourly totals: %v", err), http.StatusInternalServerError)
return
}
h.renderVcenterTotalsPage(ctx, w, vc, "hourly45", rows)
}
func (h *Handler) vcenterTotalsLegacyMonthly(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
vc, ok := requiredVcenterParam(w, r)
if !ok {
return
}
limit := parsePositiveLimit(r, vcenterMonthlyDefaultLimit)
rows, err := db.ListVcenterTotalsByType(ctx, h.Database.DB(), vc, "monthly", limit)
if err != nil {
http.Error(w, fmt.Sprintf("failed to list monthly totals: %v", err), http.StatusInternalServerError)
return
}
h.renderVcenterTotalsPage(ctx, w, vc, "monthly", rows)
}
func requiredVcenterParam(w http.ResponseWriter, r *http.Request) (string, bool) {
vc := strings.TrimSpace(r.URL.Query().Get("vcenter"))
if vc == "" {
http.Error(w, "vcenter is required", http.StatusBadRequest)
return
return "", false
}
viewType := strings.ToLower(r.URL.Query().Get("type"))
if viewType == "" {
viewType = "hourly"
return vc, true
}
func parsePositiveLimit(r *http.Request, defaultLimit int) int {
if defaultLimit <= 0 {
defaultLimit = 200
}
switch viewType {
case "hourly", "daily", "monthly":
default:
viewType = "hourly"
}
if viewType == "hourly" {
if err := db.SyncVcenterTotalsFromSnapshots(ctx, h.Database.DB()); err != nil {
h.Logger.Warn("failed to sync vcenter totals", "error", err)
if raw := strings.TrimSpace(r.URL.Query().Get("limit")); raw != "" {
if parsed, err := strconv.Atoi(raw); err == nil && parsed > 0 {
return parsed
}
}
limit := 200
if l := r.URL.Query().Get("limit"); l != "" {
if v, err := strconv.Atoi(l); err == nil && v > 0 {
limit = v
}
}
rows, err := db.ListVcenterTotalsByType(ctx, h.Database.DB(), vc, viewType, limit)
if err != nil {
http.Error(w, fmt.Sprintf("failed to list totals: %v", err), http.StatusInternalServerError)
return
}
return defaultLimit
}
func (h *Handler) renderVcenterTotalsPage(ctx context.Context, w http.ResponseWriter, vc string, viewType string, rows []db.VcenterTotalRow) {
entries := make([]views.VcenterTotalsEntry, 0, len(rows))
for _, row := range rows {
entries = append(entries, views.VcenterTotalsEntry{
@@ -105,28 +177,26 @@ func (h *Handler) VcenterTotals(w http.ResponseWriter, r *http.Request) {
func buildVcenterMeta(vcenter string, viewType string) views.VcenterTotalsMeta {
active := viewType
if active == "" {
active = "hourly"
active = "daily"
}
meta := views.VcenterTotalsMeta{
ViewType: active,
TypeLabel: "Hourly",
HourlyLink: "/vcenters/totals?vcenter=" + url.QueryEscape(vcenter) + "&type=hourly",
DailyLink: "/vcenters/totals?vcenter=" + url.QueryEscape(vcenter) + "&type=daily",
MonthlyLink: "/vcenters/totals?vcenter=" + url.QueryEscape(vcenter) + "&type=monthly",
HourlyClass: "web3-button",
DailyClass: "web3-button",
MonthlyClass: "web3-button",
ViewType: active,
TypeLabel: "Daily",
HourlyLink: "/vcenters/totals/hourly?vcenter=" + url.QueryEscape(vcenter),
DailyLink: "/vcenters/totals/daily?vcenter=" + url.QueryEscape(vcenter),
HourlyClass: "web3-button",
DailyClass: "web3-button",
}
switch active {
case "daily":
meta.TypeLabel = "Daily"
meta.DailyClass = "web3-button active"
case "hourly45", "hourly":
meta.ViewType = "hourly45"
meta.TypeLabel = fmt.Sprintf("Hourly (last %d days)", vcenterHourlyDetailWindowDays)
meta.HourlyClass = "web3-button active"
case "monthly":
meta.TypeLabel = "Monthly"
meta.MonthlyClass = "web3-button active"
default:
meta.ViewType = "hourly"
meta.HourlyClass = "web3-button active"
meta.ViewType = "daily"
meta.DailyClass = "web3-button active"
}
return meta
}