package server
import (
"fmt"
"net/http"
"sort"
"strings"
dto "github.com/prometheus/client_model/go"
"github.com/uptrace/bunrouter"
"github.com/Warky-Devs/vecna.git/pkg/metrics"
)
// dashboardHandler returns an HTML metrics dashboard by gathering from the registry.
func dashboardHandler(reg *metrics.Registry) bunrouter.HandlerFunc {
return func(w http.ResponseWriter, req bunrouter.Request) error {
families, err := reg.Prometheus().Gather()
if err != nil {
http.Error(w, "failed to gather metrics", http.StatusInternalServerError)
return nil
}
// Sort families by name for deterministic output.
sort.Slice(families, func(i, j int) bool {
return families[i].GetName() < families[j].GetName()
})
var b strings.Builder
b.WriteString(`
Vecna Metrics Dashboard
Vecna Metrics Dashboard
`)
for _, fam := range families {
name := fam.GetName()
help := fam.GetHelp()
mtype := fam.GetType()
b.WriteString(``)
fmt.Fprintf(&b, "
%s
", htmlEsc(name))
if help != "" {
fmt.Fprintf(&b, `
%s
`, htmlEsc(help))
}
switch mtype {
case dto.MetricType_COUNTER:
renderCounter(&b, fam.GetMetric())
case dto.MetricType_GAUGE:
renderGauge(&b, fam.GetMetric())
case dto.MetricType_HISTOGRAM:
renderHistogram(&b, fam.GetMetric())
default:
renderGeneric(&b, fam.GetMetric())
}
b.WriteString(`
`)
}
b.WriteString("")
w.Header().Set("Content-Type", "text/html; charset=utf-8")
_, err = fmt.Fprint(w, b.String())
return err
}
}
func renderCounter(b *strings.Builder, ms []*dto.Metric) {
b.WriteString("")
if len(ms) > 0 && len(ms[0].GetLabel()) > 0 {
for _, lp := range ms[0].GetLabel() {
fmt.Fprintf(b, "| %s | ", htmlEsc(lp.GetName()))
}
}
b.WriteString("count |
")
for _, m := range ms {
b.WriteString("")
for _, lp := range m.GetLabel() {
val := lp.GetValue()
cls := ""
if lp.GetName() == "status" {
if strings.HasPrefix(val, "2") {
cls = ` class="badge b-ok"`
} else if strings.HasPrefix(val, "4") || strings.HasPrefix(val, "5") {
cls = ` class="badge b-err"`
}
}
if cls != "" {
fmt.Fprintf(b, "| %s | ", cls, htmlEsc(val))
} else {
fmt.Fprintf(b, "%s | ", htmlEsc(val))
}
}
fmt.Fprintf(b, `%.0f | `, m.GetCounter().GetValue())
b.WriteString("
")
}
b.WriteString("
")
}
func renderGauge(b *strings.Builder, ms []*dto.Metric) {
b.WriteString("")
if len(ms) > 0 && len(ms[0].GetLabel()) > 0 {
for _, lp := range ms[0].GetLabel() {
fmt.Fprintf(b, "| %s | ", htmlEsc(lp.GetName()))
}
}
b.WriteString("value |
")
for _, m := range ms {
b.WriteString("")
for _, lp := range m.GetLabel() {
fmt.Fprintf(b, "| %s | ", htmlEsc(lp.GetValue()))
}
fmt.Fprintf(b, `%.4g | `, m.GetGauge().GetValue())
b.WriteString("
")
}
b.WriteString("
")
}
func renderHistogram(b *strings.Builder, ms []*dto.Metric) {
b.WriteString("")
if len(ms) > 0 && len(ms[0].GetLabel()) > 0 {
for _, lp := range ms[0].GetLabel() {
fmt.Fprintf(b, "| %s | ", htmlEsc(lp.GetName()))
}
}
b.WriteString("count | p50 (s) | p95 (s) | p99 (s) |
")
for _, m := range ms {
b.WriteString("")
for _, lp := range m.GetLabel() {
fmt.Fprintf(b, "| %s | ", htmlEsc(lp.GetValue()))
}
h := m.GetHistogram()
count := h.GetSampleCount()
p50 := histogramQuantile(h, 0.50)
p95 := histogramQuantile(h, 0.95)
p99 := histogramQuantile(h, 0.99)
fmt.Fprintf(b, `%d | `, count)
fmt.Fprintf(b, `%s | `, fmtSeconds(p50))
fmt.Fprintf(b, `%s | `, fmtSeconds(p95))
fmt.Fprintf(b, `%s | `, fmtSeconds(p99))
b.WriteString("
")
}
b.WriteString("
")
}
func renderGeneric(b *strings.Builder, ms []*dto.Metric) {
fmt.Fprintf(b, `%d series
`, len(ms))
}
// histogramQuantile computes a linear-interpolated quantile from a cumulative histogram.
func histogramQuantile(h *dto.Histogram, q float64) float64 {
buckets := h.GetBucket()
total := float64(h.GetSampleCount())
if total == 0 || len(buckets) == 0 {
return 0
}
target := q * total
var prevCount float64
var prevBound float64
for _, b := range buckets {
count := float64(b.GetCumulativeCount())
bound := b.GetUpperBound()
if count >= target {
if count == prevCount {
return prevBound
}
// linear interpolation within bucket
return prevBound + (bound-prevBound)*(target-prevCount)/(count-prevCount)
}
prevCount = count
prevBound = bound
}
return prevBound
}
func fmtSeconds(s float64) string {
if s == 0 {
return "—"
}
if s < 0.001 {
return fmt.Sprintf("%.3fms", s*1000)
}
return fmt.Sprintf("%.3fs", s)
}
func htmlEsc(s string) string {
s = strings.ReplaceAll(s, "&", "&")
s = strings.ReplaceAll(s, "<", "<")
s = strings.ReplaceAll(s, ">", ">")
return s
}