diff --git a/enterprise/tailnet/coordinator.go b/enterprise/tailnet/coordinator.go
index 12c3d3ad38bd8..9974f803bd92f 100644
--- a/enterprise/tailnet/coordinator.go
+++ b/enterprise/tailnet/coordinator.go
@@ -5,7 +5,6 @@ import (
"context"
"encoding/json"
"errors"
- "fmt"
"io"
"net"
"net/http"
@@ -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, "
high-availability wireguard coordinator debug
")
- _, _ = fmt.Fprintln(w, "warning: this only provides info from the node that served the request, if there are multiple replicas this data may be incomplete
")
-
- agpl.CoordinatorHTTPDebug(c.agentSockets, c.agentToConnectionSockets, c.agentNameCache)(w, r)
+ agpl.CoordinatorHTTPDebug(true, c.agentSockets, c.agentToConnectionSockets, c.nodes, c.agentNameCache)(w, r)
}
diff --git a/enterprise/tailnet/pgcoord.go b/enterprise/tailnet/pgcoord.go
index cb577fe271354..25396e9c84546 100644
--- a/enterprise/tailnet/pgcoord.go
+++ b/enterprise/tailnet/pgcoord.go
@@ -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"
diff --git a/tailnet/coordinator.go b/tailnet/coordinator.go
index 23e3bad99fc65..a5347f981a210 100644
--- a/tailnet/coordinator.go
+++ b/tailnet/coordinator.go
@@ -5,6 +5,7 @@ import (
"encoding/json"
"errors"
"fmt"
+ "html/template"
"io"
"net"
"net/http"
@@ -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, "in-memory wireguard coordinator debug
")
-
- 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, "# agents: total %d
\n", len(agentSocketsMap))
- _, _ = fmt.Fprintln(w, "")
- 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, "- %s (
%s
): created %v ago, write %v ago, overwrites %d \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, "connections: total %d
\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, "")
- for _, connSocket := range connSockets {
- start, lastWrite := connSocket.conn.Stats()
- _, _ = fmt.Fprintf(w, "- %s (
%s
): created %v ago, write %v ago \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, "
")
- }
+ 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, "
")
+ 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, "# missing agents: total %d
\n", len(missingAgents))
- _, _ = fmt.Fprintln(w, "")
-
- for _, agentConns := range missingAgents {
- agentName, ok := agentNameCache.Get(agentConns.id)
- if !ok {
- agentName = "unknown"
- }
-
- _, _ = fmt.Fprintf(w, "- %s (
%s
): created ? ago, write ? ago, overwrites ? \n",
- agentName,
- agentConns.id.String(),
- )
-
- _, _ = fmt.Fprintf(w, "connections: total %d
\n", len(agentConns.conns))
- _, _ = fmt.Fprintln(w, "")
- for _, agentConn := range agentConns.conns {
- start, lastWrite := agentConn.conn.Stats()
- _, _ = fmt.Fprintf(w, "- %s (
%s
): created %v ago, write %v ago \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, "
")
- }
- _, _ = fmt.Fprintln(w, "
")
+ 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 = `
+
+
+
+
+
+
+ {{- if .HA }}
+ high-availability wireguard coordinator debug
+ warning: this only provides info from the node that served the request, if there are multiple replicas this data may be incomplete
+ {{- else }}
+ in-memory wireguard coordinator debug
+ {{- end }}
+
+ # agents: total {{ len .Agents }}
+
+ {{- range .Agents }}
+ -
+ {{ .Name }} (
{{ .ID }}
): created {{ .CreatedAge }} ago, write {{ .LastWriteAge }} ago, overwrites {{ .Overwrites }}
+ connections: total {{ len .Connections}}
+
+ {{- range .Connections }}
+ - {{ .Name }} (
{{ .ID }}
): created {{ .CreatedAge }} ago, write {{ .LastWriteAge }} ago
+ {{- end }}
+
+
+ {{- end }}
+
+
+ # missing agents: total {{ len .MissingAgents }}
+
+ {{- range .MissingAgents}}
+ - {{ .Name }} (
{{ .ID }}
): created ? ago, write ? ago, overwrites ?
+ connections: total {{ len .Connections }}
+
+ {{- range .Connections }}
+ - {{ .Name }} (
{{ .ID }}
): created {{ .CreatedAge }} ago, write {{ .LastWriteAge }} ago
+ {{- end }}
+
+ {{- end }}
+
+
+ # nodes: total {{ len .Nodes }}
+
+ {{- range .Nodes }}
+ - {{ .Name }} (
{{ .ID }}
):
+ {{ marshal .Node }}
+
+ {{- end }}
+
+
+
+`