enhance database logging
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:
@@ -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)
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
@@ -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
210
server/handler/vmTrace.go
Normal 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")
|
||||
}
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user