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:
Claus Lensbøl
2025-07-18 10:55:17 -04:00
committed by GitHub
parent 93511be044
commit d334d9ba07
4 changed files with 125 additions and 0 deletions

View File

@@ -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
}