mirror of
https://github.com/tailscale/tailscale.git
synced 2025-07-28 23:04:10 +00:00
client/local,cmd/tailscale/cli,ipn/localapi: expose eventbus graph (#16597)
Make it possible to dump the eventbus graph as JSON or DOT to both debug and document what is communicated via the bus. Updates #15160 Signed-off-by: Claus Lensbøl <claus@tailscale.com>
This commit is contained in:
parent
93511be044
commit
d334d9ba07
@ -432,6 +432,12 @@ func (lc *Client) TailDaemonLogs(ctx context.Context) (io.Reader, error) {
|
||||
return res.Body, nil
|
||||
}
|
||||
|
||||
// EventBusGraph returns a graph of active publishers and subscribers in the eventbus
|
||||
// as a [eventbus.DebugTopics]
|
||||
func (lc *Client) EventBusGraph(ctx context.Context) ([]byte, error) {
|
||||
return lc.get200(ctx, "/localapi/v0/debug-bus-graph")
|
||||
}
|
||||
|
||||
// StreamBusEvents returns an iterator of Tailscale bus events as they arrive.
|
||||
// Each pair is a valid event and a nil error, or a zero event a non-nil error.
|
||||
// In case of error, the iterator ends after the pair reporting the error.
|
||||
|
@ -6,6 +6,7 @@ package cli
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"cmp"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
@ -108,6 +109,17 @@ func debugCmd() *ffcli.Command {
|
||||
Exec: runDaemonBusEvents,
|
||||
ShortHelp: "Watch events on the tailscaled bus",
|
||||
},
|
||||
{
|
||||
Name: "daemon-bus-graph",
|
||||
ShortUsage: "tailscale debug daemon-bus-graph",
|
||||
Exec: runDaemonBusGraph,
|
||||
ShortHelp: "Print graph for the tailscaled bus",
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := newFlagSet("debug-bus-graph")
|
||||
fs.StringVar(&daemonBusGraphArgs.format, "format", "json", "output format [json/dot]")
|
||||
return fs
|
||||
})(),
|
||||
},
|
||||
{
|
||||
Name: "metrics",
|
||||
ShortUsage: "tailscale debug metrics",
|
||||
@ -807,6 +819,50 @@ func runDaemonBusEvents(ctx context.Context, args []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
var daemonBusGraphArgs struct {
|
||||
format string
|
||||
}
|
||||
|
||||
func runDaemonBusGraph(ctx context.Context, args []string) error {
|
||||
graph, err := localClient.EventBusGraph(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if format := daemonBusGraphArgs.format; format != "json" && format != "dot" {
|
||||
return fmt.Errorf("unrecognized output format %q", format)
|
||||
}
|
||||
if daemonBusGraphArgs.format == "dot" {
|
||||
var topics eventbus.DebugTopics
|
||||
if err := json.Unmarshal(graph, &topics); err != nil {
|
||||
return fmt.Errorf("unable to parse json: %w", err)
|
||||
}
|
||||
fmt.Print(generateDOTGraph(topics.Topics))
|
||||
} else {
|
||||
fmt.Print(string(graph))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// generateDOTGraph generates the DOT graph format based on the events
|
||||
func generateDOTGraph(topics []eventbus.DebugTopic) string {
|
||||
var sb strings.Builder
|
||||
sb.WriteString("digraph event_bus {\n")
|
||||
|
||||
for _, topic := range topics {
|
||||
// If no subscribers, still ensure the topic is drawn
|
||||
if len(topic.Subscribers) == 0 {
|
||||
topic.Subscribers = append(topic.Subscribers, "no-subscribers")
|
||||
}
|
||||
for _, subscriber := range topic.Subscribers {
|
||||
fmt.Fprintf(&sb, "\t%q -> %q [label=%q];\n",
|
||||
topic.Publisher, subscriber, cmp.Or(topic.Name, "???"))
|
||||
}
|
||||
}
|
||||
|
||||
sb.WriteString("}\n")
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
var metricsArgs struct {
|
||||
watch bool
|
||||
}
|
||||
|
@ -93,6 +93,7 @@ var handler = map[string]LocalAPIHandler{
|
||||
"component-debug-logging": (*Handler).serveComponentDebugLogging,
|
||||
"debug": (*Handler).serveDebug,
|
||||
"debug-bus-events": (*Handler).serveDebugBusEvents,
|
||||
"debug-bus-graph": (*Handler).serveEventBusGraph,
|
||||
"debug-derp-region": (*Handler).serveDebugDERPRegion,
|
||||
"debug-dial-types": (*Handler).serveDebugDialTypes,
|
||||
"debug-log": (*Handler).serveDebugLog,
|
||||
@ -1004,6 +1005,55 @@ func (h *Handler) serveDebugBusEvents(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// serveEventBusGraph taps into the event bus and dumps out the active graph of
|
||||
// publishers and subscribers. It does not represent anything about the messages
|
||||
// exchanged.
|
||||
func (h *Handler) serveEventBusGraph(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != httpm.GET {
|
||||
http.Error(w, "GET required", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
bus, ok := h.LocalBackend().Sys().Bus.GetOK()
|
||||
if !ok {
|
||||
http.Error(w, "event bus not running", http.StatusPreconditionFailed)
|
||||
return
|
||||
}
|
||||
|
||||
debugger := bus.Debugger()
|
||||
clients := debugger.Clients()
|
||||
|
||||
graph := map[string]eventbus.DebugTopic{}
|
||||
|
||||
for _, client := range clients {
|
||||
for _, pub := range debugger.PublishTypes(client) {
|
||||
topic, ok := graph[pub.Name()]
|
||||
if !ok {
|
||||
topic = eventbus.DebugTopic{Name: pub.Name()}
|
||||
}
|
||||
topic.Publisher = client.Name()
|
||||
graph[pub.Name()] = topic
|
||||
}
|
||||
for _, sub := range debugger.SubscribeTypes(client) {
|
||||
topic, ok := graph[sub.Name()]
|
||||
if !ok {
|
||||
topic = eventbus.DebugTopic{Name: sub.Name()}
|
||||
}
|
||||
topic.Subscribers = append(topic.Subscribers, client.Name())
|
||||
graph[sub.Name()] = topic
|
||||
}
|
||||
}
|
||||
|
||||
// The top level map is not really needed for the client, convert to a list.
|
||||
topics := eventbus.DebugTopics{}
|
||||
for _, v := range graph {
|
||||
topics.Topics = append(topics.Topics, v)
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(topics)
|
||||
}
|
||||
|
||||
func (h *Handler) serveComponentDebugLogging(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.PermitWrite {
|
||||
http.Error(w, "debug access denied", http.StatusForbidden)
|
||||
|
@ -195,3 +195,16 @@ type DebugEvent struct {
|
||||
To []string
|
||||
Event any
|
||||
}
|
||||
|
||||
// DebugTopics provides the JSON encoding as a wrapper for a collection of [DebugTopic].
|
||||
type DebugTopics struct {
|
||||
Topics []DebugTopic
|
||||
}
|
||||
|
||||
// DebugTopic provides the JSON encoding of publishers and subscribers for a
|
||||
// given topic.
|
||||
type DebugTopic struct {
|
||||
Name string
|
||||
Publisher string
|
||||
Subscribers []string
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user