mirror of
https://github.com/tailscale/tailscale.git
synced 2025-07-29 15:23:45 +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
|
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.
|
// 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.
|
// 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.
|
// In case of error, the iterator ends after the pair reporting the error.
|
||||||
|
@ -6,6 +6,7 @@ package cli
|
|||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"cmp"
|
||||||
"context"
|
"context"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
@ -108,6 +109,17 @@ func debugCmd() *ffcli.Command {
|
|||||||
Exec: runDaemonBusEvents,
|
Exec: runDaemonBusEvents,
|
||||||
ShortHelp: "Watch events on the tailscaled bus",
|
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",
|
Name: "metrics",
|
||||||
ShortUsage: "tailscale debug metrics",
|
ShortUsage: "tailscale debug metrics",
|
||||||
@ -807,6 +819,50 @@ func runDaemonBusEvents(ctx context.Context, args []string) error {
|
|||||||
return nil
|
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 {
|
var metricsArgs struct {
|
||||||
watch bool
|
watch bool
|
||||||
}
|
}
|
||||||
|
@ -93,6 +93,7 @@ var handler = map[string]LocalAPIHandler{
|
|||||||
"component-debug-logging": (*Handler).serveComponentDebugLogging,
|
"component-debug-logging": (*Handler).serveComponentDebugLogging,
|
||||||
"debug": (*Handler).serveDebug,
|
"debug": (*Handler).serveDebug,
|
||||||
"debug-bus-events": (*Handler).serveDebugBusEvents,
|
"debug-bus-events": (*Handler).serveDebugBusEvents,
|
||||||
|
"debug-bus-graph": (*Handler).serveEventBusGraph,
|
||||||
"debug-derp-region": (*Handler).serveDebugDERPRegion,
|
"debug-derp-region": (*Handler).serveDebugDERPRegion,
|
||||||
"debug-dial-types": (*Handler).serveDebugDialTypes,
|
"debug-dial-types": (*Handler).serveDebugDialTypes,
|
||||||
"debug-log": (*Handler).serveDebugLog,
|
"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) {
|
func (h *Handler) serveComponentDebugLogging(w http.ResponseWriter, r *http.Request) {
|
||||||
if !h.PermitWrite {
|
if !h.PermitWrite {
|
||||||
http.Error(w, "debug access denied", http.StatusForbidden)
|
http.Error(w, "debug access denied", http.StatusForbidden)
|
||||||
|
@ -195,3 +195,16 @@ type DebugEvent struct {
|
|||||||
To []string
|
To []string
|
||||||
Event any
|
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