lifecycle diagnostics
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
2026-02-09 14:27:41 +11:00
parent 59b16db04f
commit 6dcbb9caef
8 changed files with 498 additions and 134 deletions

View File

@@ -35,6 +35,7 @@ func (h *Handler) VmTrace(w http.ResponseWriter, r *http.Request) {
var entries []views.VmTraceEntry
chart := views.VmTraceChart{}
diagnostics := views.VmTraceDiagnostics{}
queryLabel := firstNonEmpty(vmID, vmUUID, name)
displayQuery := ""
if queryLabel != "" {
@@ -47,10 +48,11 @@ func (h *Handler) VmTrace(w http.ResponseWriter, r *http.Request) {
// Only fetch data when a query is provided; otherwise render empty page with form.
if vmID != "" || vmUUID != "" || name != "" {
h.Logger.Info("vm trace request", "view", viewType, "vm_id", vmID, "vm_uuid", vmUUID, "name", name)
lifecycle, lifeErr := db.FetchVmLifecycle(ctx, h.Database.DB(), vmID, vmUUID, name)
lifecycle, lifecycleDiag, lifeErr := db.FetchVmLifecycleWithDiagnostics(ctx, h.Database.DB(), vmID, vmUUID, name)
if lifeErr != nil {
h.Logger.Warn("failed to fetch VM lifecycle", "error", lifeErr)
}
diagnostics = buildVmTraceDiagnostics(lifecycleDiag)
var (
rows []db.VmTraceRow
err error
@@ -114,7 +116,7 @@ func (h *Handler) VmTrace(w http.ResponseWriter, r *http.Request) {
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
if err := views.VmTracePage(queryLabel, displayQuery, vmID, vmUUID, name, creationLabel, deletionLabel, creationApprox, entries, chart, meta).Render(ctx, w); err != nil {
if err := views.VmTracePage(queryLabel, displayQuery, vmID, vmUUID, name, creationLabel, deletionLabel, creationApprox, entries, chart, meta, diagnostics).Render(ctx, w); err != nil {
http.Error(w, "Failed to render template", http.StatusInternalServerError)
}
}
@@ -155,6 +157,65 @@ func buildVmTraceMeta(viewType, vmID, vmUUID, name string) views.VmTraceMeta {
return meta
}
func buildVmTraceDiagnostics(diag db.VmLifecycleDiagnostics) views.VmTraceDiagnostics {
lines := make([]views.VmTraceDiagnosticLine, 0, 24)
lookup := "-"
if diag.LookupField != "" {
lookup = diag.LookupField + "=" + diag.LookupValue
}
lines = append(lines, views.VmTraceDiagnosticLine{Label: "Lookup", Value: lookup})
lines = append(lines, views.VmTraceDiagnosticLine{Label: "Final First Seen", Value: formatDiagUnix(diag.FinalLifecycle.FirstSeen)})
lines = append(lines, views.VmTraceDiagnosticLine{Label: "Final Last Seen", Value: formatDiagUnix(diag.FinalLifecycle.LastSeen)})
lines = append(lines, views.VmTraceDiagnosticLine{Label: "Final Creation Time", Value: formatDiagUnix(diag.FinalLifecycle.CreationTime)})
lines = append(lines, views.VmTraceDiagnosticLine{Label: "Final Creation Approx", Value: fmt.Sprintf("%t", diag.FinalLifecycle.CreationApprox)})
lines = append(lines, views.VmTraceDiagnosticLine{Label: "Final Deletion Time", Value: formatDiagUnix(diag.FinalLifecycle.DeletionTime)})
lines = append(lines, summarizeLifecycleSource(diag.HourlyCache)...)
lines = append(lines, summarizeLifecycleSource(diag.LifecycleCache)...)
lines = append(lines, summarizeLifecycleSource(diag.SnapshotFallback)...)
return views.VmTraceDiagnostics{
Visible: len(lines) > 0,
Lines: lines,
}
}
func summarizeLifecycleSource(src db.VmLifecycleSourceDiagnostics) []views.VmTraceDiagnosticLine {
prefix := src.Source
if strings.TrimSpace(prefix) == "" {
prefix = "source"
}
out := []views.VmTraceDiagnosticLine{
{Label: prefix + " used", Value: fmt.Sprintf("%t", src.Used)},
{Label: prefix + " error", Value: defaultString(src.Error, "-")},
{Label: prefix + " matched rows", Value: fmt.Sprintf("%d", src.MatchedRows)},
{Label: prefix + " first seen", Value: formatDiagUnix(src.FirstSeen)},
{Label: prefix + " last seen", Value: formatDiagUnix(src.LastSeen)},
{Label: prefix + " creation", Value: formatDiagUnix(src.CreationTime)},
{Label: prefix + " creation approx", Value: fmt.Sprintf("%t", src.CreationApprox)},
{Label: prefix + " deletion rows", Value: fmt.Sprintf("%d", src.DeletionRows)},
{Label: prefix + " deletion min", Value: formatDiagUnix(src.DeletionMin)},
{Label: prefix + " deletion max", Value: formatDiagUnix(src.DeletionMax)},
{Label: prefix + " selected deletion", Value: formatDiagUnix(src.SelectedDeletionTime)},
{Label: prefix + " stale deletion ignored", Value: fmt.Sprintf("%t", src.StaleDeletionIgnored)},
}
return out
}
func formatDiagUnix(ts int64) string {
if ts <= 0 {
return "-"
}
return fmt.Sprintf("%s (%d)", time.Unix(ts, 0).Local().Format("2006-01-02 15:04:05"), ts)
}
func defaultString(value string, fallback string) string {
if strings.TrimSpace(value) == "" {
return fallback
}
return value
}
func buildVmTraceChart(entries []views.VmTraceEntry) views.VmTraceChart {
if len(entries) == 0 {
return views.VmTraceChart{}