add vcenter totals line graph
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:
225
server/handler/vcenters.go
Normal file
225
server/handler/vcenters.go
Normal file
@@ -0,0 +1,225 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"vctp/components/views"
|
||||
"vctp/db"
|
||||
)
|
||||
|
||||
// VcenterList renders a list of vCenters being monitored.
|
||||
// @Summary List vCenters
|
||||
// @Description Lists all vCenters with recorded snapshot totals.
|
||||
// @Tags vcenters
|
||||
// @Produce text/html
|
||||
// @Success 200 {string} string "HTML page"
|
||||
// @Router /vcenters [get]
|
||||
func (h *Handler) VcenterList(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
_ = db.SyncVcenterTotalsFromSnapshots(ctx, h.Database.DB())
|
||||
vcs, err := db.ListVcenters(ctx, h.Database.DB())
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("failed to list vcenters: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
links := make([]views.VcenterLink, 0, len(vcs))
|
||||
for _, vc := range vcs {
|
||||
links = append(links, views.VcenterLink{
|
||||
Name: vc,
|
||||
Link: "/vcenters/totals?vcenter=" + url.QueryEscape(vc),
|
||||
})
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
if err := views.VcenterList(links).Render(ctx, w); err != nil {
|
||||
http.Error(w, "Failed to render template", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
// VcenterTotals renders totals for a vCenter.
|
||||
// @Summary vCenter totals
|
||||
// @Description Shows per-snapshot totals for a vCenter.
|
||||
// @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)"
|
||||
// @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) {
|
||||
ctx := r.Context()
|
||||
vc := r.URL.Query().Get("vcenter")
|
||||
if vc == "" {
|
||||
http.Error(w, "vcenter is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
viewType := strings.ToLower(r.URL.Query().Get("type"))
|
||||
if viewType == "" {
|
||||
viewType = "hourly"
|
||||
}
|
||||
switch viewType {
|
||||
case "hourly", "daily", "monthly":
|
||||
default:
|
||||
viewType = "hourly"
|
||||
}
|
||||
if viewType == "hourly" {
|
||||
_ = db.SyncVcenterTotalsFromSnapshots(ctx, h.Database.DB())
|
||||
}
|
||||
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
|
||||
}
|
||||
entries := make([]views.VcenterTotalsEntry, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
entries = append(entries, views.VcenterTotalsEntry{
|
||||
Snapshot: time.Unix(row.SnapshotTime, 0).Local().Format("2006-01-02 15:04:05"),
|
||||
RawTime: row.SnapshotTime,
|
||||
VmCount: row.VmCount,
|
||||
VcpuTotal: row.VcpuTotal,
|
||||
RamTotalGB: row.RamTotalGB,
|
||||
})
|
||||
}
|
||||
chart := buildVcenterChart(entries)
|
||||
meta := buildVcenterMeta(vc, viewType)
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
if err := views.VcenterTotalsPage(vc, entries, chart, meta).Render(ctx, w); err != nil {
|
||||
http.Error(w, "Failed to render template", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
func buildVcenterMeta(vcenter string, viewType string) views.VcenterTotalsMeta {
|
||||
active := viewType
|
||||
if active == "" {
|
||||
active = "hourly"
|
||||
}
|
||||
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",
|
||||
}
|
||||
switch active {
|
||||
case "daily":
|
||||
meta.TypeLabel = "Daily"
|
||||
meta.DailyClass = "web3-button active"
|
||||
case "monthly":
|
||||
meta.TypeLabel = "Monthly"
|
||||
meta.MonthlyClass = "web3-button active"
|
||||
default:
|
||||
meta.ViewType = "hourly"
|
||||
meta.HourlyClass = "web3-button active"
|
||||
}
|
||||
return meta
|
||||
}
|
||||
|
||||
func buildVcenterChart(entries []views.VcenterTotalsEntry) views.VcenterChartData {
|
||||
if len(entries) == 0 {
|
||||
return views.VcenterChartData{}
|
||||
}
|
||||
// Plot oldest on the left, newest on the right.
|
||||
plot := make([]views.VcenterTotalsEntry, 0, len(entries))
|
||||
for i := len(entries) - 1; i >= 0; i-- {
|
||||
plot = append(plot, entries[i])
|
||||
}
|
||||
|
||||
width := 1200.0
|
||||
height := 260.0
|
||||
maxVal := float64(0)
|
||||
for _, e := range plot {
|
||||
if float64(e.VmCount) > maxVal {
|
||||
maxVal = float64(e.VmCount)
|
||||
}
|
||||
if float64(e.VcpuTotal) > maxVal {
|
||||
maxVal = float64(e.VcpuTotal)
|
||||
}
|
||||
if float64(e.RamTotalGB) > maxVal {
|
||||
maxVal = float64(e.RamTotalGB)
|
||||
}
|
||||
}
|
||||
if maxVal == 0 {
|
||||
maxVal = 1
|
||||
}
|
||||
stepX := width
|
||||
if len(plot) > 1 {
|
||||
stepX = width / float64(len(plot)-1)
|
||||
}
|
||||
pointsVm := ""
|
||||
pointsVcpu := ""
|
||||
pointsRam := ""
|
||||
for i, e := range plot {
|
||||
x := 40 + float64(i)*stepX
|
||||
yVm := 10 + (1-(float64(e.VmCount)/maxVal))*height
|
||||
yVcpu := 10 + (1-(float64(e.VcpuTotal)/maxVal))*height
|
||||
yRam := 10 + (1-(float64(e.RamTotalGB)/maxVal))*height
|
||||
if i == 0 {
|
||||
pointsVm = fmt.Sprintf("%.1f,%.1f", x, yVm)
|
||||
pointsVcpu = fmt.Sprintf("%.1f,%.1f", x, yVcpu)
|
||||
pointsRam = fmt.Sprintf("%.1f,%.1f", x, yRam)
|
||||
} else {
|
||||
pointsVm = pointsVm + " " + fmt.Sprintf("%.1f,%.1f", x, yVm)
|
||||
pointsVcpu = pointsVcpu + " " + fmt.Sprintf("%.1f,%.1f", x, yVcpu)
|
||||
pointsRam = pointsRam + " " + fmt.Sprintf("%.1f,%.1f", x, yRam)
|
||||
}
|
||||
}
|
||||
gridX := []float64{}
|
||||
if len(plot) > 1 {
|
||||
for i := 0; i < len(plot); i++ {
|
||||
gridX = append(gridX, 40+float64(i)*stepX)
|
||||
}
|
||||
}
|
||||
gridY := []float64{}
|
||||
for i := 0; i <= 4; i++ {
|
||||
gridY = append(gridY, 10+float64(i)*(height/4))
|
||||
}
|
||||
yTicks := []views.ChartTick{}
|
||||
for i := 0; i <= 4; i++ {
|
||||
val := maxVal * float64(4-i) / 4
|
||||
pos := 10 + float64(i)*(height/4)
|
||||
yTicks = append(yTicks, views.ChartTick{Pos: pos, Label: fmt.Sprintf("%.0f", val)})
|
||||
}
|
||||
xTicks := []views.ChartTick{}
|
||||
maxTicks := 6
|
||||
stepIdx := 1
|
||||
if len(plot) > 1 {
|
||||
stepIdx = (len(plot)-1)/maxTicks + 1
|
||||
}
|
||||
for idx := 0; idx < len(plot); idx += stepIdx {
|
||||
x := 40 + float64(idx)*stepX
|
||||
label := time.Unix(plot[idx].RawTime, 0).Local().Format("01-02 15:04")
|
||||
xTicks = append(xTicks, views.ChartTick{Pos: x, Label: label})
|
||||
}
|
||||
if len(plot) > 1 {
|
||||
lastIdx := len(plot) - 1
|
||||
if (lastIdx % stepIdx) != 0 {
|
||||
x := 40 + float64(lastIdx)*stepX
|
||||
label := time.Unix(plot[lastIdx].RawTime, 0).Local().Format("01-02 15:04")
|
||||
xTicks = append(xTicks, views.ChartTick{Pos: x, Label: label})
|
||||
}
|
||||
}
|
||||
return views.VcenterChartData{
|
||||
PointsVm: pointsVm,
|
||||
PointsVcpu: pointsVcpu,
|
||||
PointsRam: pointsRam,
|
||||
Width: int(width),
|
||||
Height: int(height),
|
||||
GridX: gridX,
|
||||
GridY: gridY,
|
||||
YTicks: yTicks,
|
||||
XTicks: xTicks,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user