Implement target selection, autodiscovery via aggregator, implement listTargets

This commit is contained in:
2026-03-15 05:04:46 +01:00
parent afa65a2b29
commit 7f93466645
16 changed files with 507 additions and 57 deletions

View File

@@ -76,6 +76,7 @@ type PageData struct {
Breadcrumbs []Crumb
Windows []Tab
GroupBys []Tab
Targets []Tab // source/target picker; empty when only one target available
RefreshSecs int
Error string
FilterExpr string // current filter serialised to mini-language for the input box
@@ -340,6 +341,38 @@ func buildGroupByTabs(p QueryParams) []Tab {
return tabs
}
// buildTargetTabs builds the source/target picker tabs from a ListTargets response.
// Returns nil (hide picker) when only one endpoint is reachable.
func (h *Handler) buildTargetTabs(p QueryParams, lt *pb.ListTargetsResponse) []Tab {
// "all" always points at the configured aggregator default.
allTab := Tab{
Label: "all",
URL: p.buildURL(map[string]string{"target": h.defaultTarget}),
Active: p.Target == h.defaultTarget,
}
var collectorTabs []Tab
if lt != nil {
for _, t := range lt.Targets {
addr := t.Addr
if addr == "" {
addr = p.Target // collector reporting itself; addr is the current target
}
collectorTabs = append(collectorTabs, Tab{
Label: t.Name,
URL: p.buildURL(map[string]string{"target": addr}),
Active: p.Target == addr,
})
}
}
// Only render the picker when there is more than one choice.
if len(collectorTabs) == 0 {
return nil
}
return append([]Tab{allTab}, collectorTabs...)
}
func buildTableRows(entries []*pb.TopNEntry, p QueryParams) ([]TableRow, int64) {
if len(entries) == 0 {
return nil, 0
@@ -410,6 +443,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
topNCh := make(chan topNResult, 1)
trendCh := make(chan trendResult, 1)
ltCh := make(chan *pb.ListTargetsResponse, 1)
go func() {
resp, err := client.TopN(ctx, &pb.TopNRequest{
@@ -427,9 +461,18 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
})
trendCh <- trendResult{resp, err}
}()
go func() {
resp, err := client.ListTargets(ctx, &pb.ListTargetsRequest{})
if err != nil {
ltCh <- nil
} else {
ltCh <- resp
}
}()
tn := <-topNCh
tr := <-trendCh
lt := <-ltCh
if tn.err != nil {
h.render(w, http.StatusBadGateway, h.errorPage(params,
@@ -459,6 +502,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
Breadcrumbs: buildCrumbs(params),
Windows: buildWindowTabs(params),
GroupBys: buildGroupByTabs(params),
Targets: h.buildTargetTabs(params, lt),
RefreshSecs: h.refreshSecs,
FilterExpr: filterExprInput,
FilterErr: filterErr,

View File

@@ -34,6 +34,8 @@ a:hover { text-decoration: underline; }
.error { color: #c00; border: 1px solid #fbb; background: #fff5f5; padding: 0.7em 1em; margin: 1em 0; border-radius: 3px; }
.nodata { color: #999; margin: 2em 0; font-style: italic; }
footer { margin-top: 2em; padding-top: 0.6em; border-top: 1px solid #e0e0e0; font-size: 0.8em; color: #999; }
.tabs-targets { margin-top: -0.4em; }
.tabs-label { font-size: 0.85em; color: #888; margin-right: 0.2em; align-self: center; }
.filter-form { display: flex; gap: 0.4em; align-items: center; margin-bottom: 0.7em; }
.filter-input { flex: 1; font-family: monospace; font-size: 13px; padding: 0.25em 0.5em; border: 1px solid #aaa; }
.filter-form button { padding: 0.25em 0.8em; border: 1px solid #aaa; background: #f4f4f4; cursor: pointer; font-family: monospace; }

View File

@@ -13,6 +13,13 @@
{{- end}}
</div>
{{if .Targets}}<div class="tabs tabs-targets">
<span class="tabs-label">source:</span>
{{- range .Targets}}
<a href="{{.URL}}"{{if .Active}} class="active"{{end}}>{{.Label}}</a>
{{- end}}
</div>{{end}}
<form class="filter-form" method="get" action="/">
<input type="hidden" name="target" value="{{.Params.Target}}">
<input type="hidden" name="w" value="{{.Params.WindowS}}">