enhance database logging
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
2026-01-16 14:28:26 +11:00
parent 588a552e4c
commit ab01c0fc4d
14 changed files with 1406 additions and 68 deletions

View File

@@ -19,14 +19,3 @@ type Handler struct {
Secret *secrets.Secrets
Settings *settings.Settings
}
/*
func (h *Handler) html(ctx context.Context, w http.ResponseWriter, status int, t templ.Component) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(status)
if err := t.Render(ctx, w); err != nil {
h.Logger.Error("Failed to render component", "error", err)
}
}
*/

View File

@@ -20,7 +20,9 @@ import (
// @Router /vcenters [get]
func (h *Handler) VcenterList(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
_ = db.SyncVcenterTotalsFromSnapshots(ctx, h.Database.DB())
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)
@@ -67,7 +69,9 @@ func (h *Handler) VcenterTotals(w http.ResponseWriter, r *http.Request) {
viewType = "hourly"
}
if viewType == "hourly" {
_ = db.SyncVcenterTotalsFromSnapshots(ctx, h.Database.DB())
if err := db.SyncVcenterTotalsFromSnapshots(ctx, h.Database.DB()); err != nil {
h.Logger.Warn("failed to sync vcenter totals", "error", err)
}
}
limit := 200
if l := r.URL.Query().Get("limit"); l != "" {

210
server/handler/vmTrace.go Normal file
View File

@@ -0,0 +1,210 @@
package handler
import (
"fmt"
"net/http"
"strings"
"time"
"vctp/components/views"
"vctp/db"
)
// VmTrace shows per-snapshot details for a VM across all snapshots.
// @Summary Trace VM history
// @Description Shows VM resource history across snapshots, with chart and table.
// @Tags vm
// @Produce text/html
// @Param vm_id query string false "VM ID"
// @Param vm_uuid query string false "VM UUID"
// @Param name query string false "VM name"
// @Success 200 {string} string "HTML page"
// @Failure 400 {string} string "Missing identifier"
// @Router /vm/trace [get]
func (h *Handler) VmTrace(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
vmID := r.URL.Query().Get("vm_id")
vmUUID := r.URL.Query().Get("vm_uuid")
name := r.URL.Query().Get("name")
var entries []views.VmTraceEntry
chart := views.VmTraceChart{}
queryLabel := firstNonEmpty(vmID, vmUUID, name)
displayQuery := ""
if queryLabel != "" {
displayQuery = " for " + queryLabel
}
// Only fetch data when a query is provided; otherwise render empty page with form.
if vmID != "" || vmUUID != "" || name != "" {
h.Logger.Info("vm trace request", "vm_id", vmID, "vm_uuid", vmUUID, "name", name)
rows, err := db.FetchVmTrace(ctx, h.Database.DB(), vmID, vmUUID, name)
if err != nil {
h.Logger.Error("failed to fetch VM trace", "error", err)
http.Error(w, fmt.Sprintf("failed to fetch VM trace: %v", err), http.StatusInternalServerError)
return
}
h.Logger.Info("vm trace results", "row_count", len(rows))
entries = make([]views.VmTraceEntry, 0, len(rows))
for _, row := range rows {
creation := int64(0)
if row.CreationTime.Valid {
creation = row.CreationTime.Int64
}
deletion := int64(0)
if row.DeletionTime.Valid {
deletion = row.DeletionTime.Int64
}
entries = append(entries, views.VmTraceEntry{
Snapshot: time.Unix(row.SnapshotTime, 0).Local().Format("2006-01-02 15:04:05"),
RawTime: row.SnapshotTime,
Name: row.Name,
VmId: row.VmId,
VmUuid: row.VmUuid,
Vcenter: row.Vcenter,
ResourcePool: row.ResourcePool,
VcpuCount: row.VcpuCount,
RamGB: row.RamGB,
ProvisionedDisk: row.ProvisionedDisk,
CreationTime: formatMaybeTime(creation),
DeletionTime: formatMaybeTime(deletion),
})
}
chart = buildVmTraceChart(entries)
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
if err := views.VmTracePage(queryLabel, displayQuery, vmID, vmUUID, name, entries, chart).Render(ctx, w); err != nil {
http.Error(w, "Failed to render template", http.StatusInternalServerError)
}
}
func buildVmTraceChart(entries []views.VmTraceEntry) views.VmTraceChart {
if len(entries) == 0 {
return views.VmTraceChart{}
}
width := 1200.0
height := 220.0
plotWidth := width - 60.0
startX := 40.0
maxVal := float64(0)
for _, e := range entries {
if float64(e.VcpuCount) > maxVal {
maxVal = float64(e.VcpuCount)
}
if float64(e.RamGB) > maxVal {
maxVal = float64(e.RamGB)
}
}
if maxVal == 0 {
maxVal = 1
}
stepX := plotWidth
if len(entries) > 1 {
stepX = plotWidth / float64(len(entries)-1)
}
scale := height / maxVal
var ptsVcpu, ptsRam, ptsTin, ptsBronze, ptsSilver, ptsGold string
appendPt := func(s string, x, y float64) string {
if s == "" {
return fmt.Sprintf("%.1f,%.1f", x, y)
}
return s + " " + fmt.Sprintf("%.1f,%.1f", x, y)
}
for i, e := range entries {
x := startX + float64(i)*stepX
yVcpu := 10 + height - float64(e.VcpuCount)*scale
yRam := 10 + height - float64(e.RamGB)*scale
ptsVcpu = appendPt(ptsVcpu, x, yVcpu)
ptsRam = appendPt(ptsRam, x, yRam)
poolY := map[string]float64{
"tin": 10 + height - scale*maxVal,
"bronze": 10 + height - scale*maxVal*0.9,
"silver": 10 + height - scale*maxVal*0.8,
"gold": 10 + height - scale*maxVal*0.7,
}
lower := strings.ToLower(e.ResourcePool)
if lower == "tin" {
ptsTin = appendPt(ptsTin, x, poolY["tin"])
} else {
ptsTin = appendPt(ptsTin, x, 10+height)
}
if lower == "bronze" {
ptsBronze = appendPt(ptsBronze, x, poolY["bronze"])
} else {
ptsBronze = appendPt(ptsBronze, x, 10+height)
}
if lower == "silver" {
ptsSilver = appendPt(ptsSilver, x, poolY["silver"])
} else {
ptsSilver = appendPt(ptsSilver, x, 10+height)
}
if lower == "gold" {
ptsGold = appendPt(ptsGold, x, poolY["gold"])
} else {
ptsGold = appendPt(ptsGold, x, 10+height)
}
}
gridY := []float64{}
for i := 0; i <= 4; i++ {
gridY = append(gridY, 10+float64(i)*(height/4))
}
gridX := []float64{}
for i := 0; i < len(entries); i++ {
gridX = append(gridX, startX+float64(i)*stepX)
}
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 := 8
stepIdx := 1
if len(entries) > 1 {
stepIdx = (len(entries)-1)/maxTicks + 1
}
for idx := 0; idx < len(entries); idx += stepIdx {
x := startX + float64(idx)*stepX
label := time.Unix(entries[idx].RawTime, 0).Local().Format("01-02 15:04")
xTicks = append(xTicks, views.ChartTick{Pos: x, Label: label})
}
if len(entries) > 1 {
lastIdx := len(entries) - 1
xLast := startX + float64(lastIdx)*stepX
labelLast := time.Unix(entries[lastIdx].RawTime, 0).Local().Format("01-02 15:04")
if len(xTicks) == 0 || xTicks[len(xTicks)-1].Pos != xLast {
xTicks = append(xTicks, views.ChartTick{Pos: xLast, Label: labelLast})
}
}
return views.VmTraceChart{
PointsVcpu: ptsVcpu,
PointsRam: ptsRam,
PointsTin: ptsTin,
PointsBronze: ptsBronze,
PointsSilver: ptsSilver,
PointsGold: ptsGold,
Width: int(width),
Height: int(height),
GridX: gridX,
GridY: gridY,
XTicks: xTicks,
YTicks: yTicks,
}
}
func firstNonEmpty(vals ...string) string {
for _, v := range vals {
if v != "" {
return v
}
}
return ""
}
func formatMaybeTime(ts int64) string {
if ts == 0 {
return ""
}
return time.Unix(ts, 0).Local().Format("2006-01-02 15:04:05")
}

View File

@@ -921,6 +921,52 @@ const docTemplate = `{
}
}
}
},
"/vm/trace": {
"get": {
"description": "Shows VM resource history across snapshots, with chart and table.",
"produces": [
"text/html"
],
"tags": [
"vm"
],
"summary": "Trace VM history",
"parameters": [
{
"type": "string",
"description": "VM ID",
"name": "vm_id",
"in": "query"
},
{
"type": "string",
"description": "VM UUID",
"name": "vm_uuid",
"in": "query"
},
{
"type": "string",
"description": "VM name",
"name": "name",
"in": "query"
}
],
"responses": {
"200": {
"description": "HTML page",
"schema": {
"type": "string"
}
},
"400": {
"description": "Missing identifier",
"schema": {
"type": "string"
}
}
}
}
}
},
"definitions": {

View File

@@ -910,6 +910,52 @@
}
}
}
},
"/vm/trace": {
"get": {
"description": "Shows VM resource history across snapshots, with chart and table.",
"produces": [
"text/html"
],
"tags": [
"vm"
],
"summary": "Trace VM history",
"parameters": [
{
"type": "string",
"description": "VM ID",
"name": "vm_id",
"in": "query"
},
{
"type": "string",
"description": "VM UUID",
"name": "vm_uuid",
"in": "query"
},
{
"type": "string",
"description": "VM name",
"name": "name",
"in": "query"
}
],
"responses": {
"200": {
"description": "HTML page",
"schema": {
"type": "string"
}
},
"400": {
"description": "Missing identifier",
"schema": {
"type": "string"
}
}
}
}
}
},
"definitions": {

View File

@@ -764,4 +764,34 @@ paths:
summary: vCenter totals
tags:
- vcenters
/vm/trace:
get:
description: Shows VM resource history across snapshots, with chart and table.
parameters:
- description: VM ID
in: query
name: vm_id
type: string
- description: VM UUID
in: query
name: vm_uuid
type: string
- description: VM name
in: query
name: name
type: string
produces:
- text/html
responses:
"200":
description: HTML page
schema:
type: string
"400":
description: Missing identifier
schema:
type: string
summary: Trace VM history
tags:
- vm
swagger: "2.0"

View File

@@ -66,6 +66,7 @@ func New(logger *slog.Logger, database db.Database, buildTime string, sha1ver st
mux.HandleFunc("/api/snapshots/hourly/force", h.SnapshotForceHourly)
mux.HandleFunc("/api/snapshots/migrate", h.SnapshotMigrate)
mux.HandleFunc("/api/snapshots/regenerate-hourly-reports", h.SnapshotRegenerateHourlyReports)
mux.HandleFunc("/vm/trace", h.VmTrace)
mux.HandleFunc("/vcenters", h.VcenterList)
mux.HandleFunc("/vcenters/totals", h.VcenterTotals)
mux.HandleFunc("/metrics", h.Metrics)