mirror of
https://github.com/Warky-Devs/vecna.git
synced 2026-05-05 01:26:58 +00:00
feat(server): add support for extra maps in adapter configuration
* Introduced ExtraMapConfig to allow multiple adapter configurations. * Updated server and handler to utilize extra maps for routing. * Added dashboard handler for metrics visualization.
This commit is contained in:
214
pkg/server/dashboard.go
Normal file
214
pkg/server/dashboard.go
Normal file
@@ -0,0 +1,214 @@
|
||||
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(`<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Vecna Metrics Dashboard</title>
|
||||
<style>
|
||||
*{box-sizing:border-box;margin:0;padding:0}
|
||||
body{font-family:system-ui,sans-serif;background:#0f1117;color:#e2e8f0;padding:1.5rem}
|
||||
h1{font-size:1.4rem;font-weight:700;margin-bottom:1.25rem;color:#7dd3fc}
|
||||
h2{font-size:.85rem;font-weight:600;color:#94a3b8;text-transform:uppercase;letter-spacing:.06em;margin:1.5rem 0 .5rem}
|
||||
table{width:100%;border-collapse:collapse;font-size:.82rem;margin-bottom:.5rem}
|
||||
th{text-align:left;padding:.35rem .6rem;background:#1e2330;color:#64748b;font-weight:600;border-bottom:1px solid #2d3748}
|
||||
td{padding:.3rem .6rem;border-bottom:1px solid #1e2330;color:#cbd5e1}
|
||||
tr:last-child td{border-bottom:none}
|
||||
tr:hover td{background:#1a2035}
|
||||
.val{text-align:right;font-variant-numeric:tabular-nums;color:#7dd3fc}
|
||||
.badge{display:inline-block;padding:.1rem .45rem;border-radius:.25rem;font-size:.75rem;font-weight:600}
|
||||
.b-ok{background:#14532d;color:#86efac}
|
||||
.b-err{background:#7f1d1d;color:#fca5a5}
|
||||
.section{background:#141824;border:1px solid #1e2330;border-radius:.5rem;padding:1rem;margin-bottom:1rem}
|
||||
.help{font-size:.75rem;color:#475569;margin-bottom:.5rem}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Vecna Metrics Dashboard</h1>
|
||||
`)
|
||||
|
||||
for _, fam := range families {
|
||||
name := fam.GetName()
|
||||
help := fam.GetHelp()
|
||||
mtype := fam.GetType()
|
||||
|
||||
b.WriteString(`<div class="section">`)
|
||||
fmt.Fprintf(&b, "<h2>%s</h2>", htmlEsc(name))
|
||||
if help != "" {
|
||||
fmt.Fprintf(&b, `<div class="help">%s</div>`, 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(`</div>`)
|
||||
}
|
||||
|
||||
b.WriteString("</body></html>")
|
||||
|
||||
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("<table><thead><tr>")
|
||||
if len(ms) > 0 && len(ms[0].GetLabel()) > 0 {
|
||||
for _, lp := range ms[0].GetLabel() {
|
||||
fmt.Fprintf(b, "<th>%s</th>", htmlEsc(lp.GetName()))
|
||||
}
|
||||
}
|
||||
b.WriteString("<th style='text-align:right'>count</th></tr></thead><tbody>")
|
||||
for _, m := range ms {
|
||||
b.WriteString("<tr>")
|
||||
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, "<td><span%s>%s</span></td>", cls, htmlEsc(val))
|
||||
} else {
|
||||
fmt.Fprintf(b, "<td>%s</td>", htmlEsc(val))
|
||||
}
|
||||
}
|
||||
fmt.Fprintf(b, `<td class="val">%.0f</td>`, m.GetCounter().GetValue())
|
||||
b.WriteString("</tr>")
|
||||
}
|
||||
b.WriteString("</tbody></table>")
|
||||
}
|
||||
|
||||
func renderGauge(b *strings.Builder, ms []*dto.Metric) {
|
||||
b.WriteString("<table><thead><tr>")
|
||||
if len(ms) > 0 && len(ms[0].GetLabel()) > 0 {
|
||||
for _, lp := range ms[0].GetLabel() {
|
||||
fmt.Fprintf(b, "<th>%s</th>", htmlEsc(lp.GetName()))
|
||||
}
|
||||
}
|
||||
b.WriteString("<th style='text-align:right'>value</th></tr></thead><tbody>")
|
||||
for _, m := range ms {
|
||||
b.WriteString("<tr>")
|
||||
for _, lp := range m.GetLabel() {
|
||||
fmt.Fprintf(b, "<td>%s</td>", htmlEsc(lp.GetValue()))
|
||||
}
|
||||
fmt.Fprintf(b, `<td class="val">%.4g</td>`, m.GetGauge().GetValue())
|
||||
b.WriteString("</tr>")
|
||||
}
|
||||
b.WriteString("</tbody></table>")
|
||||
}
|
||||
|
||||
func renderHistogram(b *strings.Builder, ms []*dto.Metric) {
|
||||
b.WriteString("<table><thead><tr>")
|
||||
if len(ms) > 0 && len(ms[0].GetLabel()) > 0 {
|
||||
for _, lp := range ms[0].GetLabel() {
|
||||
fmt.Fprintf(b, "<th>%s</th>", htmlEsc(lp.GetName()))
|
||||
}
|
||||
}
|
||||
b.WriteString("<th style='text-align:right'>count</th><th style='text-align:right'>p50 (s)</th><th style='text-align:right'>p95 (s)</th><th style='text-align:right'>p99 (s)</th></tr></thead><tbody>")
|
||||
for _, m := range ms {
|
||||
b.WriteString("<tr>")
|
||||
for _, lp := range m.GetLabel() {
|
||||
fmt.Fprintf(b, "<td>%s</td>", 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, `<td class="val">%d</td>`, count)
|
||||
fmt.Fprintf(b, `<td class="val">%s</td>`, fmtSeconds(p50))
|
||||
fmt.Fprintf(b, `<td class="val">%s</td>`, fmtSeconds(p95))
|
||||
fmt.Fprintf(b, `<td class="val">%s</td>`, fmtSeconds(p99))
|
||||
b.WriteString("</tr>")
|
||||
}
|
||||
b.WriteString("</tbody></table>")
|
||||
}
|
||||
|
||||
func renderGeneric(b *strings.Builder, ms []*dto.Metric) {
|
||||
fmt.Fprintf(b, `<div class="help">%d series</div>`, 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
|
||||
}
|
||||
Reference in New Issue
Block a user