Skip to content

Commit db7e7b6

Browse files
authored
Add memberlist API support (grafana/phlare#578)
1 parent fddcf64 commit db7e7b6

File tree

4 files changed

+327
-1
lines changed

4 files changed

+327
-1
lines changed

pkg/api/index.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ const (
2020
ConfigWeight
2121
RuntimeConfigWeight
2222
DefaultWeight
23-
memberlistWeight
23+
MemberlistWeight
2424
dangerousWeight
2525
OpenAPIDefinitionWeight
2626
BuildInfoWeight

pkg/api/memberlist_status.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// SPDX-License-Identifier: AGPL-3.0-only
2+
// Provenance-includes-location: https://github.com/cortexproject/cortex/blob/master/pkg/api/handlers.go
3+
// Provenance-includes-license: Apache-2.0
4+
// Provenance-includes-copyright: The Cortex Authors.
5+
6+
package api
7+
8+
import (
9+
_ "embed"
10+
"html/template"
11+
"net/http"
12+
"path"
13+
"strings"
14+
15+
"github.com/grafana/dskit/kv/memberlist"
16+
)
17+
18+
//go:embed memberlist_status.gohtml
19+
var memberlistStatusPageHTML string
20+
21+
func MemberlistStatusHandler(httpPathPrefix string, kvs *memberlist.KVInitService) http.Handler {
22+
templ := template.New("memberlist_status")
23+
templ.Funcs(map[string]interface{}{
24+
"AddPathPrefix": func(link string) string { return path.Join(httpPathPrefix, link) },
25+
"StringsJoin": strings.Join,
26+
})
27+
template.Must(templ.Parse(memberlistStatusPageHTML))
28+
return memberlist.NewHTTPStatusHandler(kvs, templ)
29+
}

pkg/api/memberlist_status.gohtml

Lines changed: 293 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,293 @@
1+
{{- /*gotype: github.com/grafana/dskit/kv/memberlist.StatusPageData */ -}}
2+
<!DOCTYPE html>
3+
<html class="h-100">
4+
<head>
5+
<meta charset="UTF-8">
6+
<meta http-equiv="X-UA-Compatible" content="IE=edge">
7+
<meta name="viewport" content="width=device-width, initial-scale=1">
8+
9+
<title>Memberlist: Grafana Phlare</title>
10+
11+
<link rel="stylesheet" href="{{ AddPathPrefix "/static/bootstrap-5.1.3.min.css" }}">
12+
<link rel="stylesheet" href="{{ AddPathPrefix "/static/bootstrap-icons-1.8.1.css" }}">
13+
<link rel="stylesheet" href="{{ AddPathPrefix "/static/phlare-styles.css" }}">
14+
<script src="{{ AddPathPrefix "/static/bootstrap-5.1.3.bundle.min.js" }}"></script>
15+
</head>
16+
<body class="d-flex flex-column h-100">
17+
<main class="flex-shrink-0">
18+
<div class="container">
19+
<div class="header row border-bottom py-3 flex-column-reverse flex-sm-row">
20+
<div class="col-12 col-sm-9 text-center text-sm-start">
21+
<h1>Memberlist: Grafana Phlare</h1>
22+
</div>
23+
<div class="col-12 col-sm-3 text-center text-sm-end mb-3 mb-sm-0">
24+
<img alt="Phlare logo" class="phlare-brand" src="{{ AddPathPrefix "/static/phlare-logo.png" }}">
25+
</div>
26+
</div>
27+
<div class="row my-3">
28+
<div class="col-12">
29+
{{ $HealthScore := .Memberlist.GetHealthScore }}
30+
{{ if eq $HealthScore 0 }}
31+
<div class="alert alert-success" role="alert">
32+
Memberlist cluster has <strong>{{ .Memberlist.NumMembers }}</strong> members and it is <strong>healthy</strong>.
33+
</div>
34+
{{ else }}
35+
<div class="alert alert-warning" role="alert">
36+
Memberlist cluster has <strong>{{ .Memberlist.NumMembers }}</strong> members but health score
37+
is {{ $HealthScore }} (lower is better, 0 = healthy).
38+
</div>
39+
{{ end }}
40+
</div>
41+
42+
<h2>KV Store</h2>
43+
<div class="table-responsive">
44+
<table class="table table-bordered table-hover table-striped">
45+
<thead>
46+
<tr>
47+
<th>Key</th>
48+
<th class="fit-width">Codec</th>
49+
<th class="fit-width">
50+
<span class="text-nowrap">
51+
Version
52+
<i class="bi bi-info-circle"
53+
data-bs-toggle="tooltip" data-bs-placement="top"
54+
title="Note that value 'version' is node-specific. It starts with 0 (on restart), and increases on each received update."></i>
55+
</span>
56+
</th>
57+
<th class="fit-width">Actions</th>
58+
</tr>
59+
</thead>
60+
61+
<tbody>
62+
{{ range $k, $v := .Store }}
63+
<tr>
64+
<td class="align-middle font-monospace small">{{ $k }}</td>
65+
<td class="align-middle font-monospace small fit-width">{{ $v.CodecID }}</td>
66+
<td class="align-middle font-monospace small fit-width">{{ $v.Version }}</td>
67+
<td class="fit-width">
68+
<span class="text-nowrap">
69+
<a href="?viewKey={{ $k }}&format=json-pretty" title="JSON pretty" class="text-decoration-none">
70+
<i class="bi bi-filetype-json text-success"></i>
71+
</a>
72+
<a href="?viewKey={{ $k }}&format=json" title="JSON" class="text-decoration-none">
73+
<i class="bi bi-filetype-json"></i>
74+
</a>
75+
<a href="?viewKey={{ $k }}&format=struct" title="Struct" class="text-decoration-none">
76+
<i class="bi bi-file-earmark-code"></i>
77+
</a>
78+
<a href="?downloadKey={{ $k }}" title="Download" class="text-decoration-none">
79+
<i class="bi bi-file-earmark-arrow-down"></i>
80+
</a>
81+
</span>
82+
</td>
83+
</tr>
84+
{{ end }}
85+
</tbody>
86+
</table>
87+
</div>
88+
89+
<h2>Memberlist Cluster Members</h2>
90+
<div class="table-responsive">
91+
<table class="table table-bordered table-hover table-striped">
92+
<thead>
93+
<tr>
94+
<th>Name</th>
95+
<th>Address</th>
96+
<th class="fit-width">
97+
<span class="text-nowrap">
98+
State
99+
<i class="bi bi-info-circle"
100+
data-bs-toggle="tooltip" data-bs-placement="left"
101+
title="State: 0 = Alive, 1 = Suspect, 2 = Dead, 3 = Left"></i>
102+
</span>
103+
</th>
104+
</tr>
105+
</thead>
106+
107+
<tbody>
108+
{{ range .SortedMembers }}
109+
<tr>
110+
<td class="align-middle font-monospace small">{{ .Name }}</td>
111+
<td class="align-middle font-monospace small">{{ .Address }}</td>
112+
<td class="fit-width text-center py-1">
113+
{{ if eq .State 0}}
114+
<span class="badge bg-success">Alive</span>
115+
{{ else if eq .State 1 }}
116+
<span class="badge bg-warning text-dark">Suspect</span>
117+
{{ else if eq .State 2 }}
118+
<span class="badge bg-danger">Dead</span>
119+
{{ else if eq.State 3}}
120+
<span class="badge bg-info">Left</span>
121+
{{ else }}
122+
<span class="badge bg-info">Unknown: {{ .State }}</span>
123+
{{ end }}
124+
</td>
125+
</tr>
126+
{{ end }}
127+
</tbody>
128+
</table>
129+
</div>
130+
131+
<h2>Message History
132+
{{ if .MessageHistoryBufferBytes }}
133+
<a class="btn btn-outline-warning" href="?deleteMessages=true" data-bs-toggle="tooltip"
134+
data-bs-placement="right" title="Delete sent and received messages buffer">Flush</a>
135+
{{ end }}
136+
</h2>
137+
138+
{{ if .MessageHistoryBufferBytes }}
139+
<div class="accordion">
140+
<div class="accordion-item">
141+
<h3 class="accordion-header" id="heading-received-messages">
142+
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
143+
data-bs-target="#collapse-received-messages" aria-expanded="false"
144+
aria-controls="collapse-received-messages">
145+
Received Messages
146+
</button>
147+
</h3>
148+
<div id="collapse-received-messages" class="accordion-collapse collapse"
149+
aria-labelledby="heading-received-messages">
150+
<div class="accordion-body p-0 table-responsive">
151+
{{ if .ReceivedMessages }}
152+
<table class="table table-hover table-striped">
153+
<thead>
154+
<tr>
155+
<th class="fit-width">ID</th>
156+
<th>Time</th>
157+
<th>Key</th>
158+
<th class="fit-width">Size (B)</th>
159+
<th class="fit-width">Codec</th>
160+
<th class="fit-width">
161+
<span class="text-nowrap">
162+
Version
163+
<i class="bi bi-info-circle"
164+
data-bs-toggle="tooltip" data-bs-placement="top"
165+
title="Version after update. 0 = No change."></i>
166+
</span>
167+
</th>
168+
<th>Changes</th>
169+
<th class="fit-width">Actions</th>
170+
</tr>
171+
</thead>
172+
173+
<tbody class="font-monospace small">
174+
{{ range .ReceivedMessages }}
175+
<tr>
176+
<td class="font-monospace small fit-width">{{ .ID }}</td>
177+
<td class="font-monospace small">{{ .Time.Format "15:04:05.000" }}</td>
178+
<td class="font-monospace small">{{ .Pair.Key }}</td>
179+
<td class="font-monospace small fit-width">{{ .Pair.Value | len }}</td>
180+
<td class="font-monospace small fit-width">{{ .Pair.Codec }}</td>
181+
<td class="font-monospace small fit-width">{{ .Version }}</td>
182+
<td class="font-monospace small">{{ StringsJoin .Changes ", " }}</td>
183+
<td class="fit-width">
184+
<span class="text-nowrap">
185+
<a href="?viewMsg={{ .ID }}&format=json-pretty" class="text-decoration-none">
186+
<i class="bi bi-filetype-json text-success"></i>
187+
</a>
188+
<a href="?viewMsg={{ .ID }}&format=json" class="text-decoration-none">
189+
<i class="bi bi-filetype-json"></i>
190+
</a>
191+
<a href="?viewMsg={{ .ID }}&format=struct" class="text-decoration-none">
192+
<i class="bi bi-file-earmark-code"></i>
193+
</a>
194+
</span>
195+
</td>
196+
</tr>
197+
{{ end }}
198+
</tbody>
199+
200+
</table>
201+
{{ else }}
202+
<span><i>There are no received messages.</i></span>
203+
{{ end }}
204+
</div>
205+
</div>
206+
</div>
207+
<div class="accordion-item">
208+
<h3 class="accordion-header" id="heading-sent-messages">
209+
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
210+
data-bs-target="#collapse-sent-messages" aria-expanded="false"
211+
aria-controls="collapse-sent-messages">
212+
Sent Messages
213+
</button>
214+
</h3>
215+
<div id="collapse-sent-messages" class="accordion-collapse collapse"
216+
aria-labelledby="heading-sent-messages">
217+
<div class="accordion-body p-0 table-responsive">
218+
219+
{{ if .SentMessages }}
220+
<table class="table table-hover table-striped">
221+
<thead>
222+
<tr>
223+
<th>ID</th>
224+
<th>Time</th>
225+
<th>Key</th>
226+
<th>Size</th>
227+
<th>Codec</th>
228+
<th>Version</th>
229+
<th>Changes</th>
230+
<th>Actions</th>
231+
</tr>
232+
</thead>
233+
234+
<tbody>
235+
{{ range .SentMessages }}
236+
<tr>
237+
<td class="font-monospace small">{{ .ID }}</td>
238+
<td class="font-monospace small">{{ .Time.Format "15:04:05.000" }}</td>
239+
<td class="font-monospace small">{{ .Pair.Key }}</td>
240+
<td class="font-monospace small">{{ .Pair.Value | len }}</td>
241+
<td class="font-monospace small">{{ .Pair.Codec }}</td>
242+
<td class="font-monospace small">{{ .Version }}</td>
243+
<td class="font-monospace small">{{ StringsJoin .Changes ", " }}</td>
244+
<td>
245+
<span class="text-nowrap">
246+
<a href="?viewMsg={{ .ID }}&format=json-pretty" class="text-decoration-none">
247+
<i class="bi bi-filetype-json text-success"></i>
248+
</a>
249+
<a href="?viewMsg={{ .ID }}&format=json" class="text-decoration-none">
250+
<i class="bi bi-filetype-json"></i>
251+
</a>
252+
<a href="?viewMsg={{ .ID }}&format=struct" class="text-decoration-none">
253+
<i class="bi bi-file-earmark-code"></i>
254+
</a>
255+
</span>
256+
</td>
257+
</tr>
258+
{{ end }}
259+
</tbody>
260+
</table>
261+
262+
{{ else }}
263+
<span><i>There are no sent messages.</i></span>
264+
{{ end }}
265+
</div>
266+
</div>
267+
</div>
268+
</div>
269+
{{ else }}
270+
271+
<div class="col-12">
272+
<div class="alert alert-info" role="alert">
273+
Message history buffer is disabled.
274+
<br />Enable it by setting the <code>-memberlist.message-history-buffer-bytes</code> flag or the corresponding config key.
275+
</div>
276+
</div>
277+
{{ end }}
278+
</div>
279+
</div>
280+
</main>
281+
<footer class="footer mt-auto py-3 bg-light">
282+
<div class="container">
283+
<small class="text-muted">Status @ {{ .Now.Format "2006-01-02 15:04:05.000" }}</small>
284+
</div>
285+
</footer>
286+
<script type="text/javascript">
287+
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'))
288+
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
289+
return new bootstrap.Tooltip(tooltipTriggerEl)
290+
})
291+
</script>
292+
</body>
293+
</html>

pkg/phlare/modules.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,10 @@ func (f *Phlare) initMemberlistKV() (services.Service, error) {
311311
f.Cfg.Frontend.QuerySchedulerDiscovery = f.Cfg.QueryScheduler.ServiceDiscovery
312312
f.Cfg.Worker.QuerySchedulerDiscovery = f.Cfg.QueryScheduler.ServiceDiscovery
313313

314+
f.Server.HTTP.Path("/memberlist").Handler(api.MemberlistStatusHandler("", f.MemberlistKV))
315+
f.IndexPage.AddLinks(api.MemberlistWeight, "Memberlist", []api.IndexPageLink{
316+
{Desc: "Status", Path: "/memberlist"},
317+
})
314318
return f.MemberlistKV, nil
315319
}
316320

0 commit comments

Comments
 (0)