Skip to content

chore(tailnet): rewrite coordinator debug using html/template #8752

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jul 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 1 addition & 7 deletions enterprise/tailnet/coordinator.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net"
"net/http"
Expand Down Expand Up @@ -702,13 +701,8 @@ func (c *haCoordinator) formatAgentUpdate(id uuid.UUID, node *agpl.Node) ([]byte
}

func (c *haCoordinator) ServeHTTPDebug(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")

c.mutex.RLock()
defer c.mutex.RUnlock()

_, _ = fmt.Fprintln(w, "<h1>high-availability wireguard coordinator debug</h1>")
_, _ = fmt.Fprintln(w, "<h4 style=\"margin-top:-25px\">warning: this only provides info from the node that served the request, if there are multiple replicas this data may be incomplete</h4>")

agpl.CoordinatorHTTPDebug(c.agentSockets, c.agentToConnectionSockets, c.agentNameCache)(w, r)
agpl.CoordinatorHTTPDebug(true, c.agentSockets, c.agentToConnectionSockets, c.nodes, c.agentNameCache)(w, r)
}
1 change: 0 additions & 1 deletion enterprise/tailnet/pgcoord.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import (
"nhooyr.io/websocket"

"cdr.dev/slog"

"github.com/coder/coder/coderd/database"
"github.com/coder/coder/coderd/database/dbauthz"
"github.com/coder/coder/coderd/database/pubsub"
Expand Down
275 changes: 172 additions & 103 deletions tailnet/coordinator.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"encoding/json"
"errors"
"fmt"
"html/template"
"io"
"net"
"net/http"
Expand Down Expand Up @@ -646,136 +647,204 @@ func (c *coordinator) ServeHTTPDebug(w http.ResponseWriter, r *http.Request) {
}

func (c *core) serveHTTPDebug(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")

c.mutex.RLock()
defer c.mutex.RUnlock()

_, _ = fmt.Fprintln(w, "<h1>in-memory wireguard coordinator debug</h1>")

CoordinatorHTTPDebug(c.agentSockets, c.agentToConnectionSockets, c.agentNameCache)(w, r)
CoordinatorHTTPDebug(false, c.agentSockets, c.agentToConnectionSockets, c.nodes, c.agentNameCache)(w, r)
}

func CoordinatorHTTPDebug(
ha bool,
agentSocketsMap map[uuid.UUID]Queue,
agentToConnectionSocketsMap map[uuid.UUID]map[uuid.UUID]Queue,
nodesMap map[uuid.UUID]*Node,
agentNameCache *lru.Cache[uuid.UUID, string],
) func(w http.ResponseWriter, _ *http.Request) {
return func(w http.ResponseWriter, _ *http.Request) {
now := time.Now()

type idConn struct {
id uuid.UUID
conn Queue
w.Header().Set("Content-Type", "text/html; charset=utf-8")

tmpl, err := template.New("coordinator_debug").Funcs(template.FuncMap{
"marshal": func(v interface{}) template.JS {
a, err := json.MarshalIndent(v, "", " ")
if err != nil {
//nolint:gosec
return template.JS(fmt.Sprintf(`{"err": %q}`, err))
}
//nolint:gosec
return template.JS(a)
},
}).Parse(coordinatorDebugTmpl)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
_, _ = w.Write([]byte(err.Error()))
return
}

{
_, _ = fmt.Fprintf(w, "<h2 id=agents><a href=#agents>#</a> agents: total %d</h2>\n", len(agentSocketsMap))
_, _ = fmt.Fprintln(w, "<ul>")
agentSockets := make([]idConn, 0, len(agentSocketsMap))

for id, conn := range agentSocketsMap {
agentSockets = append(agentSockets, idConn{id, conn})
now := time.Now()
data := htmlDebug{HA: ha}
for id, conn := range agentSocketsMap {
start, lastWrite := conn.Stats()
agent := &htmlAgent{
Name: conn.Name(),
ID: id,
CreatedAge: now.Sub(time.Unix(start, 0)).Round(time.Second),
LastWriteAge: now.Sub(time.Unix(lastWrite, 0)).Round(time.Second),
Overwrites: int(conn.Overwrites()),
}

slices.SortFunc(agentSockets, func(a, b idConn) bool {
return a.conn.Name() < b.conn.Name()
})

for _, agent := range agentSockets {
start, lastWrite := agent.conn.Stats()
_, _ = fmt.Fprintf(w, "<li style=\"margin-top:4px\"><b>%s</b> (<code>%s</code>): created %v ago, write %v ago, overwrites %d </li>\n",
agent.conn.Name(),
agent.id.String(),
now.Sub(time.Unix(start, 0)).Round(time.Second),
now.Sub(time.Unix(lastWrite, 0)).Round(time.Second),
agent.conn.Overwrites(),
)

if conns := agentToConnectionSocketsMap[agent.id]; len(conns) > 0 {
_, _ = fmt.Fprintf(w, "<h3 style=\"margin:0px;font-size:16px;font-weight:400\">connections: total %d</h3>\n", len(conns))

connSockets := make([]idConn, 0, len(conns))
for id, conn := range conns {
connSockets = append(connSockets, idConn{id, conn})
}
slices.SortFunc(connSockets, func(a, b idConn) bool {
return a.id.String() < b.id.String()
})

_, _ = fmt.Fprintln(w, "<ul>")
for _, connSocket := range connSockets {
start, lastWrite := connSocket.conn.Stats()
_, _ = fmt.Fprintf(w, "<li><b>%s</b> (<code>%s</code>): created %v ago, write %v ago </li>\n",
connSocket.conn.Name(),
connSocket.id.String(),
now.Sub(time.Unix(start, 0)).Round(time.Second),
now.Sub(time.Unix(lastWrite, 0)).Round(time.Second),
)
}
_, _ = fmt.Fprintln(w, "</ul>")
}
for id, conn := range agentToConnectionSocketsMap[id] {
start, lastWrite := conn.Stats()
agent.Connections = append(agent.Connections, &htmlClient{
Name: conn.Name(),
ID: id,
CreatedAge: now.Sub(time.Unix(start, 0)).Round(time.Second),
LastWriteAge: now.Sub(time.Unix(lastWrite, 0)).Round(time.Second),
})
}
slices.SortFunc(agent.Connections, func(a, b *htmlClient) bool {
return a.Name < b.Name
})

_, _ = fmt.Fprintln(w, "</ul>")
data.Agents = append(data.Agents, agent)
}
slices.SortFunc(data.Agents, func(a, b *htmlAgent) bool {
return a.Name < b.Name
})

{
type agentConns struct {
id uuid.UUID
conns []idConn
for agentID, conns := range agentToConnectionSocketsMap {
if len(conns) == 0 {
continue
}

missingAgents := []agentConns{}
for agentID, conns := range agentToConnectionSocketsMap {
if len(conns) == 0 {
continue
if _, ok := agentSocketsMap[agentID]; !ok {
agentName, ok := agentNameCache.Get(agentID)
if !ok {
agentName = "unknown"
}

if _, ok := agentSocketsMap[agentID]; !ok {
connsSlice := make([]idConn, 0, len(conns))
for id, conn := range conns {
connsSlice = append(connsSlice, idConn{id, conn})
}
slices.SortFunc(connsSlice, func(a, b idConn) bool {
return a.id.String() < b.id.String()
agent := &htmlAgent{
Name: agentName,
ID: agentID,
}
for id, conn := range conns {
start, lastWrite := conn.Stats()
agent.Connections = append(agent.Connections, &htmlClient{
Name: conn.Name(),
ID: id,
CreatedAge: now.Sub(time.Unix(start, 0)).Round(time.Second),
LastWriteAge: now.Sub(time.Unix(lastWrite, 0)).Round(time.Second),
})

missingAgents = append(missingAgents, agentConns{agentID, connsSlice})
}
slices.SortFunc(agent.Connections, func(a, b *htmlClient) bool {
return a.Name < b.Name
})

data.MissingAgents = append(data.MissingAgents, agent)
}
slices.SortFunc(missingAgents, func(a, b agentConns) bool {
return a.id.String() < b.id.String()
}
slices.SortFunc(data.MissingAgents, func(a, b *htmlAgent) bool {
return a.Name < b.Name
})

for id, node := range nodesMap {
name, _ := agentNameCache.Get(id)
data.Nodes = append(data.Nodes, &htmlNode{
ID: id,
Name: name,
Node: node,
})
}
slices.SortFunc(data.Nodes, func(a, b *htmlNode) bool {
return a.Name+a.ID.String() < b.Name+b.ID.String()
})

_, _ = fmt.Fprintf(w, "<h2 id=missing-agents><a href=#missing-agents>#</a> missing agents: total %d</h2>\n", len(missingAgents))
_, _ = fmt.Fprintln(w, "<ul>")

for _, agentConns := range missingAgents {
agentName, ok := agentNameCache.Get(agentConns.id)
if !ok {
agentName = "unknown"
}

_, _ = fmt.Fprintf(w, "<li style=\"margin-top:4px\"><b>%s</b> (<code>%s</code>): created ? ago, write ? ago, overwrites ? </li>\n",
agentName,
agentConns.id.String(),
)

_, _ = fmt.Fprintf(w, "<h3 style=\"margin:0px;font-size:16px;font-weight:400\">connections: total %d</h3>\n", len(agentConns.conns))
_, _ = fmt.Fprintln(w, "<ul>")
for _, agentConn := range agentConns.conns {
start, lastWrite := agentConn.conn.Stats()
_, _ = fmt.Fprintf(w, "<li><b>%s</b> (<code>%s</code>): created %v ago, write %v ago </li>\n",
agentConn.conn.Name(),
agentConn.id.String(),
now.Sub(time.Unix(start, 0)).Round(time.Second),
now.Sub(time.Unix(lastWrite, 0)).Round(time.Second),
)
}
_, _ = fmt.Fprintln(w, "</ul>")
}
_, _ = fmt.Fprintln(w, "</ul>")
err = tmpl.Execute(w, data)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
_, _ = w.Write([]byte(err.Error()))
return
}
}
}

type htmlDebug struct {
HA bool
Agents []*htmlAgent
MissingAgents []*htmlAgent
Nodes []*htmlNode
}

type htmlAgent struct {
Name string
ID uuid.UUID
CreatedAge time.Duration
LastWriteAge time.Duration
Overwrites int
Connections []*htmlClient
}

type htmlClient struct {
Name string
ID uuid.UUID
CreatedAge time.Duration
LastWriteAge time.Duration
}

type htmlNode struct {
ID uuid.UUID
Name string
Node *Node
}

var coordinatorDebugTmpl = `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
{{- if .HA }}
<h1>high-availability wireguard coordinator debug</h1>
<h4 style="margin-top:-25px">warning: this only provides info from the node that served the request, if there are multiple replicas this data may be incomplete</h4>
{{- else }}
<h1>in-memory wireguard coordinator debug</h1>
{{- end }}

<h2 id=agents> <a href=#agents>#</a> agents: total {{ len .Agents }} </h2>
<ul>
{{- range .Agents }}
<li style="margin-top:4px">
<b>{{ .Name }}</b> (<code>{{ .ID }}</code>): created {{ .CreatedAge }} ago, write {{ .LastWriteAge }} ago, overwrites {{ .Overwrites }}
<h3 style="margin:0px;font-size:16px;font-weight:400"> connections: total {{ len .Connections}} </h3>
<ul>
{{- range .Connections }}
<li><b>{{ .Name }}</b> (<code>{{ .ID }}</code>): created {{ .CreatedAge }} ago, write {{ .LastWriteAge }} ago </li>
{{- end }}
</ul>
</li>
{{- end }}
</ul>

<h2 id=missing-agents><a href=#missing-agents>#</a> missing agents: total {{ len .MissingAgents }}</h2>
<ul>
{{- range .MissingAgents}}
<li style="margin-top:4px"><b>{{ .Name }}</b> (<code>{{ .ID }}</code>): created ? ago, write ? ago, overwrites ? </li>
<h3 style="margin:0px;font-size:16px;font-weight:400"> connections: total {{ len .Connections }} </h3>
<ul>
{{- range .Connections }}
<li><b>{{ .Name }}</b> (<code>{{ .ID }}</code>): created {{ .CreatedAge }} ago, write {{ .LastWriteAge }} ago </li>
{{- end }}
</ul>
{{- end }}
</ul>

<h2 id=nodes><a href=#nodes>#</a> nodes: total {{ len .Nodes }}</h2>
<ul>
{{- range .Nodes }}
<li style="margin-top:4px"><b>{{ .Name }}</b> (<code>{{ .ID }}</code>):
<span style="white-space: pre;"><code>{{ marshal .Node }}</code></span>
</li>
{{- end }}
</ul>
</body>
</html>
`